26b276fbfb
Ran the manual regrade with the internal key (thanks). Results are mixed
and the honest half matters more.
CONFIRMED WORKING — the probability layer is fully alive in production.
After POST /api/internal/snapshot/{mlb,wnba}: p_win, ev_pct, model_odds
and value are present on 32/32 live grades (mlb 7/7, wnba 25/25), up from
0/8 before. That fix is done.
NOT WORKING — the grade-range half did not land, and I am not going to
claim it did. The live distribution is unchanged (wnba B17/C8 before AND
after; mlb B4/C3), no A, no D, same four confidence values. Diagnosis:
matchup_grade is 0/25 on the live board, i.e. opp_rank_stat is still
null, so engine1's +/-1.0 opponent factor still never fires and the
ceiling is still +3.0 against the +4.5 an A requires.
Two distinct causes, both verified against the live ESPN feed:
1. refreshTeamStats CRASHED on every team — "buckets is not iterable",
captured 0 / errored 15. ESPN's current shape is results.stats =
an OBJECT with categories[], not an array. The old parser did for...of
on it. This was invisible until S63 gave the function its first
production caller. FIXED here (now captured 15 / errored 0) with a
regression test covering the current shape, the legacy array shape,
and empty payloads.
2. Even parsed correctly, the endpoint does not carry a
defensive-strength metric at all: defensive_rating, opponent_ppg,
pace and opponent_fg_pct all normalize to null — it returns only a
team's OWN stats. So defensive_rank_normalized cannot be computed and
opp_rank_stat remains underivable from this source. A test documents
the gap and will fail if that ever changes.
Consequence: A STILL DOES NOT EMIT, so the A-RATED marketing hold STAYS.
Reviving the opponent factor needs a different derivation (opponent
points allowed from scoreboard/schedule, or a different ESPN endpoint) —
logged as the concrete next item, not hand-waved as done.
Suite 278/3305 green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SmNjJAwEnqHPtXbvSZR8kA
56 lines
2.1 KiB
JavaScript
56 lines
2.1 KiB
JavaScript
/**
|
|
* Session 64 — ESPN team-statistics shape regression.
|
|
*
|
|
* `results.stats` is an OBJECT carrying `categories[]`, not an array. The old
|
|
* parser did `for...of` on it and threw "buckets is not iterable" for EVERY
|
|
* team (verified live: captured 0, errored 15/15). It was invisible because
|
|
* refreshTeamStats had no production callers until S63 wired it in.
|
|
*/
|
|
const { __internals } = require('../../src/services/intelligence/teamStatsCache');
|
|
const { normalize } = __internals;
|
|
|
|
// The real ESPN shape, trimmed.
|
|
const CURRENT_SHAPE = {
|
|
results: {
|
|
stats: {
|
|
categories: [
|
|
{ name: 'general', stats: [{ name: 'avgRebounds', value: 33.8 }] },
|
|
{ name: 'offensive', stats: [{ name: 'fieldGoalPct', value: 47.35 }, { name: 'avgPoints', value: 84.2 }] },
|
|
{ name: 'defensive', stats: [{ name: 'avgBlocks', value: 4.1 }] },
|
|
],
|
|
},
|
|
},
|
|
};
|
|
|
|
// The legacy array shape must keep working.
|
|
const LEGACY_SHAPE = {
|
|
results: { stats: [{ stats: [{ name: 'fieldGoalPct', value: 44.0 }] }] },
|
|
};
|
|
|
|
describe('ESPN team-statistics parsing', () => {
|
|
test('does not throw on the CURRENT object+categories shape', () => {
|
|
expect(() => normalize('wnba', CURRENT_SHAPE)).not.toThrow();
|
|
});
|
|
|
|
test('extracts stats that exist in the current shape', () => {
|
|
expect(normalize('wnba', CURRENT_SHAPE).team_fg_pct).toBeCloseTo(47.35, 1);
|
|
});
|
|
|
|
test('still parses the legacy array shape', () => {
|
|
expect(normalize('wnba', LEGACY_SHAPE).team_fg_pct).toBeCloseTo(44.0, 1);
|
|
});
|
|
|
|
test('missing/empty payloads degrade to nulls, never throw', () => {
|
|
expect(() => normalize('wnba', {})).not.toThrow();
|
|
expect(() => normalize('wnba', null)).not.toThrow();
|
|
});
|
|
|
|
test('DOCUMENTS THE GAP: the endpoint carries no defensive-strength metric, so opponent rank stays underivable', () => {
|
|
const n = normalize('wnba', CURRENT_SHAPE);
|
|
expect(n.defensive_rating).toBeNull();
|
|
expect(n.opponent_ppg).toBeNull();
|
|
// If this test ever fails because these populate, the opponent-rank factor
|
|
// can be revived — see specs/audit-data/grade-collapse.md.
|
|
});
|
|
});
|