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>
72 lines
3.0 KiB
JavaScript
72 lines
3.0 KiB
JavaScript
// Session 46 — Phase 1: MLB game-log features (root cause of the empty grade
|
|
// card intel) + buildIntelFields resilience.
|
|
|
|
const { __internals: fc } = require('../../src/services/intelligence/featureCache');
|
|
const { __internals: eng } = require('../../src/services/intelligence/analyzeViaEngine1');
|
|
|
|
const judgeStats = {
|
|
found: true, group: 'hitting',
|
|
season: { totalBases: 180, homeRuns: 34, hits: 95, rbi: 87, gamesPlayed: 92 },
|
|
last10: [
|
|
{ stat: { totalBases: 2, homeRuns: 0 } }, { stat: { totalBases: 4, homeRuns: 1 } },
|
|
{ stat: { totalBases: 1, homeRuns: 0 } }, { stat: { totalBases: 3, homeRuns: 1 } },
|
|
{ stat: { totalBases: 0, homeRuns: 0 } }, { stat: { totalBases: 5, homeRuns: 1 } },
|
|
{ stat: { totalBases: 2, homeRuns: 0 } }, { stat: { totalBases: 3, homeRuns: 1 } },
|
|
{ stat: { totalBases: 1, homeRuns: 0 } }, { stat: { totalBases: 4, homeRuns: 1 } },
|
|
],
|
|
};
|
|
|
|
describe('mlbGameLogFeatures (root-cause fix)', () => {
|
|
it('derives l5/l10/l20 averages for an MLB stat from the real game log', () => {
|
|
const f = fc.mlbGameLogFeatures(judgeStats, 'total_bases');
|
|
expect(f.l5_avg).toBeGreaterThan(0);
|
|
expect(f.l10_avg).toBeGreaterThan(0);
|
|
// season per-game = 180 / 92 ≈ 1.96
|
|
expect(f.l20_avg).toBeCloseTo(180 / 92, 2);
|
|
});
|
|
|
|
it('returns {} for an unfound player or unmapped stat (graceful)', () => {
|
|
expect(fc.mlbGameLogFeatures({ found: false }, 'hits')).toEqual({});
|
|
expect(fc.mlbGameLogFeatures(judgeStats, 'not_a_stat')).toEqual({});
|
|
});
|
|
|
|
it('mlbStatValue maps stat_type → MLB game-log field', () => {
|
|
expect(fc.mlbStatValue({ homeRuns: 2 }, 'home_runs')).toBe(2);
|
|
expect(fc.mlbStatValue({ totalBases: 3 }, 'total_bases')).toBe(3);
|
|
expect(fc.mlbStatValue({}, 'home_runs')).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('buildIntelFields — feature-populated + resilient', () => {
|
|
it('produces season + last10 + form from MLB-derived features', () => {
|
|
const f = fc.mlbGameLogFeatures(judgeStats, 'total_bases');
|
|
const intel = eng.buildIntelFields(f);
|
|
expect(intel.season_avg).toBeDefined();
|
|
expect(intel.last10_avg).toBeDefined();
|
|
expect(intel.form).toBeDefined();
|
|
});
|
|
|
|
it('falls back to playerStats when the feature vector is empty', () => {
|
|
const intel = eng.buildIntelFields({}, { playerStats: { season_avg: 26.9, last10_avg: 28.4, form: 92, usage: '31%' } });
|
|
expect(intel.season_avg).toBe(26.9);
|
|
expect(intel.last10_avg).toBe(28.4);
|
|
expect(intel.form).toBe(92);
|
|
expect(intel.usage).toBe('31%');
|
|
});
|
|
|
|
it('falls back to the model projection for season when nothing else exists', () => {
|
|
const intel = eng.buildIntelFields({}, { projection: 1.9 });
|
|
expect(intel.season_avg).toBe(1.9);
|
|
});
|
|
|
|
it('still returns {} when there is genuinely nothing (backward compatible)', () => {
|
|
expect(eng.buildIntelFields({})).toEqual({});
|
|
});
|
|
|
|
it('produces partial output (matchup only) when only opp rank exists', () => {
|
|
const intel = eng.buildIntelFields({ opp_rank_stat: 0.8 });
|
|
expect(intel.matchup_grade).toBe('A');
|
|
expect(intel.season_avg).toBeUndefined();
|
|
});
|
|
});
|