Session 46: Grade card intel + name normalization + pitchers (2122 tests)
Three focused P1 fixes on the Session-45 snapshot model.
- Grade card intel ROOT CAUSE: gameLogService is NBA/WNBA-only (offline Python),
so MLB props never got l5_avg/l20_avg and buildIntelFields returned {}. Wired
MLB game logs into featureCache.gameLogFeatures via mlbStatsAdapter.getPlayerStats
(pure mlbGameLogFeatures + MLB stat_type->field map). buildIntelFields gained
playerStats/projection fallbacks for partial intel.
- Player name normalization: src/utils/playerName.js (+ web/src/lib copy):
normalizeName -> {display,key}. Strips periods, de-dots suffix, accent-folds
the key. Applied in snapshotService grouping, slateAdapter grade index +
player-strip merge (variants collapse, longest name shown), and
playerIntelService. "A.J. Ewing"/"AJ Ewing" + "Jazz Chisholm"/"Jr." now merge.
- MLB starting pitchers: new GET /api/schedule/:sport/pitchers (probablePitchers
service wrapping mlbStatsAdapter.getScheduleWithPitchers + best-effort ERA).
Slate fetches it, builds a team->pitcher map (full name + mascot match),
attaches pitchers to MLB GameCardData. + Next proxy.
Backend 2100 -> 2122 tests (+22), 176 suites. Web build clean (exit 0).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -76,7 +76,66 @@ function daysBetween(aIso, bIso) {
|
||||
return Math.floor(ms / (1000 * 60 * 60 * 24));
|
||||
}
|
||||
|
||||
// MLB stat_type → the per-game field name in a statsapi.mlb.com game-log row.
|
||||
const MLB_LOG_FIELD = {
|
||||
total_bases: 'totalBases', home_runs: 'homeRuns', hits: 'hits', rbi: 'rbi',
|
||||
runs: 'runs', stolen_bases: 'stolenBases', walks: 'baseOnBalls',
|
||||
strikeouts: 'strikeOuts', earned_runs: 'earnedRuns', hits_allowed: 'hits',
|
||||
innings_pitched: 'inningsPitched',
|
||||
};
|
||||
|
||||
function mlbStatValue(statObj, statType) {
|
||||
const f = MLB_LOG_FIELD[statType];
|
||||
if (!f || !statObj) return null;
|
||||
const n = parseFloat(statObj[f]);
|
||||
return Number.isFinite(n) ? n : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Session 46 — derive recent/season averages from a real MLB game log
|
||||
* (mlbStatsAdapter.getPlayerStats result). PURE so it's unit-testable without
|
||||
* the network. Produces l5_avg / l10_avg (recent form) + l20_avg (the season
|
||||
* per-game reference) — exactly the fields buildIntelFields consumes, which the
|
||||
* NBA/WNBA-only Python game-log path never populated for MLB.
|
||||
*/
|
||||
function mlbGameLogFeatures(res, statType) {
|
||||
if (!res || !res.found) return {};
|
||||
const out = {};
|
||||
const logs = Array.isArray(res.last10) ? res.last10 : [];
|
||||
const vals = logs.map((g) => mlbStatValue(g.stat, statType)).filter((v) => v != null);
|
||||
if (vals.length) {
|
||||
const m5 = avg(vals.slice(-5)); // game logs are chronological (recent last)
|
||||
const m10 = avg(vals.slice(-10));
|
||||
const s10 = stddev(vals.slice(-10));
|
||||
if (m5 != null) out.l5_avg = m5;
|
||||
if (m10 != null) out.l10_avg = m10;
|
||||
if (s10 != null) out.l10_stddev = s10;
|
||||
}
|
||||
const seasonTotal = mlbStatValue(res.season, statType);
|
||||
const games = parseFloat(res.season && (res.season.gamesPlayed ?? res.season.gamesStarted ?? res.season.gamesPitched));
|
||||
if (seasonTotal != null && Number.isFinite(games) && games > 0) {
|
||||
out.l20_avg = seasonTotal / games;
|
||||
} else if (out.l10_avg != null) {
|
||||
out.l20_avg = out.l10_avg; // baseline so form has a reference
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
async function gameLogFeatures(playerName, sport, statType) {
|
||||
// MLB game logs come from the FREE statsapi.mlb.com (Session 46) — the Python
|
||||
// gameLogService only covers NBA/WNBA, so MLB props had no recent/season
|
||||
// averages and the grade card's intel sections stayed empty.
|
||||
if (sport === 'mlb') {
|
||||
try {
|
||||
const mlbStats = require('../adapters/mlbStatsAdapter');
|
||||
const res = await mlbStats.getPlayerStats(playerName);
|
||||
return mlbGameLogFeatures(res, statType);
|
||||
} catch (e) {
|
||||
console.warn('[featureCache] MLB game-log features failed:', e.message);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
const logs = await gameLogs.getGameLogs(playerName, sport, 20);
|
||||
if (!logs || logs.length === 0) return {};
|
||||
|
||||
@@ -261,6 +320,8 @@ module.exports = {
|
||||
coachFeatures,
|
||||
lineupFeatures,
|
||||
statFromGameLog,
|
||||
mlbGameLogFeatures,
|
||||
mlbStatValue,
|
||||
avg,
|
||||
stddev,
|
||||
daysBetween,
|
||||
|
||||
Reference in New Issue
Block a user