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:
@@ -296,18 +296,31 @@ function matchupGradeFromRank(rank) {
|
||||
* yield the fallback. The archetype strip lights up once the snapshot pipeline
|
||||
* (Session 44) feeds per-player season lines into the grade response.
|
||||
*/
|
||||
function buildIntelFields(features = {}) {
|
||||
const firstFinite = (...vals) => vals.find((v) => Number.isFinite(v));
|
||||
|
||||
function buildIntelFields(features = {}, opts = {}) {
|
||||
const out = {};
|
||||
const round1 = (n) => Math.round(n * 10) / 10;
|
||||
if (Number.isFinite(features.l20_avg)) out.season_avg = round1(features.l20_avg);
|
||||
else if (Number.isFinite(features.season_avg)) out.season_avg = round1(features.season_avg);
|
||||
if (Number.isFinite(features.l10_avg)) out.last10_avg = round1(features.l10_avg);
|
||||
else if (Number.isFinite(features.l5_avg)) out.last10_avg = round1(features.l5_avg);
|
||||
// Resilience (Session 46): fall back to a caller-supplied playerStats bundle
|
||||
// and the model projection when the feature vector is sparse. Partial intel
|
||||
// beats none — we add only the fields we can actually back with a number.
|
||||
const ps = opts.playerStats || {};
|
||||
const proj = Number.isFinite(opts.projection) ? opts.projection : undefined;
|
||||
|
||||
const form = computeFormScore(features);
|
||||
const seasonAvg = firstFinite(features.l20_avg, features.season_avg, ps.season_avg, proj);
|
||||
if (seasonAvg != null) out.season_avg = round1(seasonAvg);
|
||||
|
||||
const last10 = firstFinite(features.l10_avg, features.l5_avg, ps.last10_avg);
|
||||
if (last10 != null) out.last10_avg = round1(last10);
|
||||
|
||||
let form = computeFormScore(features);
|
||||
if (form == null && Number.isFinite(ps.form)) form = Math.round(ps.form);
|
||||
if (form != null) out.form = form;
|
||||
|
||||
if (Number.isFinite(features.usage_rate)) out.usage = `${round1(features.usage_rate)}%`;
|
||||
else if (Number.isFinite(features.minutes_per_game)) out.usage = `${Math.round(features.minutes_per_game)} min`;
|
||||
else if (ps.usage) out.usage = String(ps.usage);
|
||||
|
||||
const matchup = matchupGradeFromRank(features.opp_rank_stat);
|
||||
if (matchup) out.matchup_grade = matchup;
|
||||
if (Number.isFinite(features.rest_days)) out.rest = `${features.rest_days}d rest`;
|
||||
|
||||
Reference in New Issue
Block a user