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:
@@ -0,0 +1,52 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* probablePitchers — today's MLB probable starters (Session 46).
|
||||
*
|
||||
* The ESPN schedule (`scheduleService`) doesn't carry probable pitchers, so MLB
|
||||
* game cards never showed them. This wraps `mlbStatsAdapter.getScheduleWithPitchers`
|
||||
* (FREE statsapi.mlb.com) and best-effort enriches each starter's season ERA.
|
||||
* Everything is graceful + injectable. `shapePitcherGames` is pure/unit-tested.
|
||||
*/
|
||||
|
||||
const mlbStats = require('./adapters/mlbStatsAdapter');
|
||||
|
||||
/** Map the adapter's schedule shape → a compact { home, away } pitcher record. */
|
||||
function shapePitcherGames(scheduleGames) {
|
||||
return (scheduleGames || [])
|
||||
.map((g) => ({
|
||||
home: { team: g.home?.team || null, pitcher: g.home?.probablePitcher?.name || null, pitcherId: g.home?.probablePitcher?.id || null, era: null },
|
||||
away: { team: g.away?.team || null, pitcher: g.away?.probablePitcher?.name || null, pitcherId: g.away?.probablePitcher?.id || null, era: null },
|
||||
}))
|
||||
.filter((x) => x.home.pitcher || x.away.pitcher);
|
||||
}
|
||||
|
||||
async function getProbablePitchers(date, opts = {}) {
|
||||
const adapter = opts.mlbAdapter || mlbStats;
|
||||
let games;
|
||||
try {
|
||||
games = await adapter.getScheduleWithPitchers(date);
|
||||
} catch (e) {
|
||||
console.warn('[probablePitchers] schedule fetch failed:', e.message);
|
||||
return [];
|
||||
}
|
||||
const shaped = shapePitcherGames(games);
|
||||
|
||||
if (opts.withEra !== false) {
|
||||
const eraLookup = opts.eraLookup || (async (id) => {
|
||||
const s = await adapter.getSeasonAverages(id, undefined, 'pitching').catch(() => null);
|
||||
const era = s && parseFloat(s.era);
|
||||
return Number.isFinite(era) ? era : null;
|
||||
});
|
||||
await Promise.all(
|
||||
shaped.flatMap((g) => [g.home, g.away]).map(async (side) => {
|
||||
if (side.pitcherId) {
|
||||
try { side.era = await eraLookup(side.pitcherId); } catch { side.era = null; }
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
return shaped;
|
||||
}
|
||||
|
||||
module.exports = { getProbablePitchers, shapePitcherGames };
|
||||
Reference in New Issue
Block a user