c8fc9f577e
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>
69 lines
3.0 KiB
JavaScript
69 lines
3.0 KiB
JavaScript
// Session 46 — Phase 3: MLB starting pitchers.
|
|
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const WEB = path.join(__dirname, '..', '..', 'web', 'src');
|
|
const svc = require('../../src/services/probablePitchers');
|
|
const slate = require('../../web/src/lib/slateAdapter');
|
|
|
|
const scheduleGames = [
|
|
{ home: { team: 'Philadelphia Phillies', probablePitcher: { id: 1, name: 'Zack Wheeler' } }, away: { team: 'Atlanta Braves', probablePitcher: { id: 2, name: 'Spencer Strider' } } },
|
|
{ home: { team: 'New York Yankees', probablePitcher: null }, away: { team: 'Boston Red Sox', probablePitcher: { id: 3, name: 'Brayan Bello' } } },
|
|
];
|
|
|
|
describe('shapePitcherGames', () => {
|
|
it('extracts pitcher names + team, drops games with no probables', () => {
|
|
const shaped = svc.shapePitcherGames(scheduleGames);
|
|
expect(shaped).toHaveLength(2);
|
|
expect(shaped[0].away.pitcher).toBe('Spencer Strider');
|
|
expect(shaped[0].home.pitcher).toBe('Zack Wheeler');
|
|
expect(svc.shapePitcherGames([{ home: {}, away: {} }])).toHaveLength(0);
|
|
});
|
|
});
|
|
|
|
describe('getProbablePitchers (injected adapter)', () => {
|
|
it('returns shaped games with best-effort ERA', async () => {
|
|
const games = await svc.getProbablePitchers('2026-06-18', {
|
|
mlbAdapter: { getScheduleWithPitchers: async () => scheduleGames },
|
|
eraLookup: async (id) => (id === 1 ? 2.89 : id === 2 ? 3.21 : null),
|
|
});
|
|
const phi = games[0];
|
|
expect(phi.home.era).toBe(2.89);
|
|
expect(phi.away.era).toBe(3.21);
|
|
});
|
|
it('degrades to [] when the adapter throws', async () => {
|
|
const games = await svc.getProbablePitchers('x', { mlbAdapter: { getScheduleWithPitchers: async () => { throw new Error('down'); } } });
|
|
expect(games).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe('slateAdapter pitcher mapping', () => {
|
|
const shaped = svc.shapePitcherGames(scheduleGames).map((g) => ({
|
|
home: { ...g.home, era: 2.89 }, away: { ...g.away, era: 3.21 },
|
|
}));
|
|
it('builds a team→pitcher map and resolves a game by team name', () => {
|
|
const map = slate.buildPitcherMap(shaped);
|
|
const p = slate.pitchersForGameTeams('Atlanta Braves', 'Philadelphia Phillies', map);
|
|
expect(p.away.name).toBe('Spencer Strider');
|
|
expect(p.away.era).toBe('3.21');
|
|
expect(p.home.name).toBe('Zack Wheeler');
|
|
});
|
|
it('matches by mascot when the full name differs slightly', () => {
|
|
const map = slate.buildPitcherMap(shaped);
|
|
const p = slate.pitchersForGameTeams('Braves', 'Phillies', map);
|
|
expect(p.away.name).toBe('Spencer Strider');
|
|
});
|
|
it('returns undefined when no pitchers match', () => {
|
|
expect(slate.pitchersForGameTeams('Dodgers', 'Giants', slate.buildPitcherMap(shaped))).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe('Slate wires MLB pitchers', () => {
|
|
const src = fs.readFileSync(path.join(WEB, 'components', 'Slate.tsx'), 'utf8');
|
|
it('fetches /api/schedule/mlb/pitchers and attaches pitchers for MLB', () => {
|
|
expect(src).toContain('/api/schedule/mlb/pitchers');
|
|
expect(src).toContain('buildPitcherMap');
|
|
expect(src).toContain('pitchersForGameTeams');
|
|
});
|
|
});
|