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
+7 -2
View File
@@ -13,6 +13,7 @@
*/
const { classify } = require('./archetypeService');
const { normalizeName, nameKey } = require('../utils/playerName');
const toNum = (v) => {
const n = parseFloat(v);
@@ -31,14 +32,18 @@ const fmt3 = (v) => {
function sanitizePlayerName(raw) {
let decoded = String(raw == null ? '' : raw);
try { decoded = decodeURIComponent(decoded); } catch { /* malformed % — use raw */ }
return decoded
const cleaned = decoded
.replace(/[^\p{L}\p{N}\s.'-]/gu, '')
.replace(/\s+/g, ' ')
.trim()
.slice(0, 60);
// Session 46 — normalize periods/suffix for display ("A.J. Ewing" → "AJ Ewing").
return normalizeName(cleaned).display;
}
const normName = (n) => String(n == null ? '' : n).toLowerCase().replace(/[^a-z0-9]/g, '');
// Session 46 — match by the normalized name key so name variants resolve to the
// same player (snapshot grades, profile lookups).
const normName = (n) => nameKey(n);
async function loadPlayerGrades(sport, name, cacheGetFn) {
const env = await cacheGetFn(`grades:${sport}`);