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
+38
View File
@@ -0,0 +1,38 @@
'use strict';
/**
* Player-name normalization (Session 46) — the ONE source of truth for comparing
* and de-duplicating player names. PropLine sends variants ("A.J. Ewing" vs
* "AJ Ewing", "Jazz Chisholm" vs "Jazz Chisholm Jr.") as different players; this
* collapses them.
*
* normalizeName("A.J. Ewing") → { display: "AJ Ewing", key: "aj ewing" }
* normalizeName("Jazz Chisholm Jr.")→ { display: "Jazz Chisholm Jr", key: "jazz chisholm" }
* normalizeName("Jazz Chisholm") → { display: "Jazz Chisholm", key: "jazz chisholm" }
* normalizeName("Ronald Acuña Jr.") → { display: "Ronald Acuña Jr", key: "ronald acuna" }
*
* `display` keeps proper casing + accents (periods stripped, suffix de-dotted).
* `key` is accent-folded, lowercased, suffix-stripped for comparison.
*
* NOTE: an identical copy lives at web/src/lib/playerName.js for the frontend
* (the Next bundle can't import from src/). A test cross-checks they agree.
*/
const SUFFIXES = new Set(['jr', 'sr', 'ii', 'iii', 'iv', 'v']);
function normalizeName(raw) {
const display = String(raw == null ? '' : raw)
.replace(/\./g, '') // "A.J." → "AJ", "Jr." → "Jr"
.replace(/\s+/g, ' ')
.trim();
const folded = display.normalize('NFD').replace(/[̀-ͯ]/g, '').toLowerCase();
const key = folded.split(' ').filter((t) => t && !SUFFIXES.has(t)).join(' ');
return { display, key };
}
/** Comparison key only (the common case). */
function nameKey(raw) {
return normalizeName(raw).key;
}
module.exports = { normalizeName, nameKey, SUFFIXES };