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:
Kev
2026-06-18 23:56:26 -04:00
parent f8b120c0aa
commit c8fc9f577e
20 changed files with 608 additions and 32 deletions
+19
View File
@@ -30,6 +30,25 @@ router.use(createRateLimit({ windowMs: 60_000, max: 60 }));
const MISSION_HEADER = { 'X-VYNDR-Mission': 'The slate is never empty' };
// GET /api/schedule/:sport/pitchers (Session 46) — today's MLB probable starters
// (statsapi.mlb.com; the ESPN schedule above doesn't carry them). MLB only.
router.get('/:sport/pitchers', async (req, res) => {
const sport = String(req.params.sport || '').toLowerCase();
if (sport !== 'mlb') {
return res.set(MISSION_HEADER).json({ sport, date: null, games: [] });
}
const date = req.query.date || scheduleService.todayET();
try {
const { getProbablePitchers } = require('../services/probablePitchers');
const games = await getProbablePitchers(date);
res.set('Cache-Control', 'public, max-age=300');
return res.set(MISSION_HEADER).json({ sport, date, games });
} catch (err) {
console.error('[schedule/pitchers]', err.message);
return res.set(MISSION_HEADER).json({ sport, date, games: [] });
}
});
router.get('/:sport', async (req, res) => {
const sport = String(req.params.sport || '').toLowerCase();
const date = req.query.date || scheduleService.todayET();