Files
vyndr/tests/unit/teamStatsShape.test.js
T
builtbykev 26b276fbfb Fix the ESPN team-stats parser + report: opponent rank is still underivable
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
2026-07-19 20:31:15 -04:00

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.
});
});