Files
vyndr/tests/unit/baseRateWindow.test.js
T
builtbykev 55b210cb95 Fix the dormant basketball window-bug before it ships; guard the class
PHASE 0 — audit. espnStatsAdapter's slice(0,20) was already fixed at
494c83c, so the ESPN branch of getStatRows inherits a full log and is
CORRECT. MLB's l20_avg is a real seasonTotal/games aggregate, verified,
also CORRECT. TWO basketball defects remained:

  DEFECTIVE  nbaGameLogFeatures   m20 = avg(vals.slice(0, 20))
             -- l20_avg is the season reference projectionFor reads, and
             slicing to 20 made it a twenty-game average wearing a season
             label. The MLB l20 bug, unfixed for basketball.
  DEFECTIVE  getStatRows python   getGameLogs(playerName, sp, 20)

PHASE 1 — both fixed via a named SEASON_LOG_DEPTH = 100, past an 82-game
season so a request can never truncate one. API COST: ZERO. The count is a
request parameter, so asking for a season is the same single call. No
extra request, no extra quota.

MEASUREMENT DEFERRED, EXPLICITLY: basketball is offline, there are no
settled basketball rows, and resolution before/after CANNOT be measured
now. This is a code fix, not a measured improvement -- exactly like the
deferred MLB sibling paths.

PHASE 2 — the window guard asserts the property on source: no fixed N may
stand in for a season. It immediately caught TWO MORE instances I had
missed in Phase 1 -- a hardcoded 20 at featureCache:377 and
gameLogService's own `count = 20` DEFAULT, which would have handed a
twenty-game window to any caller that omitted the argument. That is a
seventh path, found by the guard rather than by me.

Retro-confirmed: run unchanged against 981a05c it goes 3 failed / 6
passed, flagging the basketball slice, the game-log request and the
season-reference check.

The class in full, now six paths across two sports, every one of which
looked like ordinary code. `slice(0, 20)` is unremarkable; what made it a
defect was the QUESTION it answered -- "what is this player's season
rate?" -- and no test could see that mismatch because the value produced
was always a plausible number.

PHASE 3 — THIS CLOSES THE NON-ACCRUAL ARC. Everything buildable without
settled rows is built: push unblocked (it was the wrong remote, not the
firewall), grade surface honest and rendering, reachability guarded,
champion repaired across every path including dormant basketball, and the
window class guarded so it cannot return.

The program is now correctly IDLE on modeling. Today's count: 0 eligible
dates, all four re-audit items WAITING. FIRST TRIGGER: 10 eligible
calibration dates on the repaired champion, at which point the resumption
order is calibration re-fit, hits factor lift, prior verdict re-audit,
rbi lineup-slot gate.

No basketball measurement claimed. No NBA chain/archetype build. MLB
serving verified byte-identical.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W1sivYNqY2TS5ftykmHBU9
2026-08-07 15:00:24 -04:00

107 lines
4.4 KiB
JavaScript

'use strict';
/**
* THE WINDOW GUARD — a fixed N must never stand in for a season.
*
* This class has now cost six paths across two sports:
*
* MLB getStatRows res.last10 -> a 10-game "season rate"
* MLB mlbGameLogFeatures res.last10 -> l20_avg was 10 games
* NBA espnStatsAdapter rows.slice(0, 20) -> every downstream rate capped at 20
* NBA nbaGameLogFeatures vals.slice(0, 20) -> l20_avg was 20 games
* NBA getStatRows (python) getGameLogs(.., 20) -> a 20-game base rate
* NBA gameLogFeatures getGameLogs(.., 20) -> same
*
* Every one looked correct in isolation. `slice(0, 20)` is unremarkable code;
* what made it a defect was the QUESTION it was answering — "what is this
* player's season rate?" — and no test could see that mismatch, because the
* value it produced was always a plausible number.
*
* So this asserts the property directly on the source: the base-rate and
* season-reference paths must not narrow to a fixed window. Basketball is
* OFFLINE, which is exactly why it is guarded now — a regression there would
* otherwise surface only when it had already served grades.
*/
const fs = require('fs');
const path = require('path');
const ROOT = path.join(__dirname, '..', '..');
const read = (rel) => fs.readFileSync(path.join(ROOT, rel), 'utf8');
/** Body of a named function, for targeted assertions. */
function fnBody(src, name) {
const i = src.indexOf(`function ${name}(`);
if (i < 0) return null;
let depth = 0; let started = false;
for (let j = i; j < src.length; j += 1) {
if (src[j] === '{') { depth += 1; started = true; }
else if (src[j] === '}') { depth -= 1; if (started && depth === 0) return src.slice(i, j + 1); }
}
return null;
}
describe('no fixed window may stand in for a season', () => {
const featureCache = read('src/services/intelligence/featureCache.js');
it('the season log depth is a named constant past a full season', () => {
// A literal 20 buried in a call is how every instance of this bug looked.
expect(featureCache).toMatch(/const SEASON_LOG_DEPTH = (\d+);/);
const depth = Number(/const SEASON_LOG_DEPTH = (\d+);/.exec(featureCache)[1]);
expect(depth).toBeGreaterThanOrEqual(82); // a basketball season
});
it('no game-log request asks for a hardcoded small count', () => {
const calls = [...featureCache.matchAll(/getGameLogs\([^)]*\)/g)].map((m) => m[0]);
expect(calls.length).toBeGreaterThan(0);
for (const c of calls) {
const literal = /,\s*(\d+)\s*\)/.exec(c);
if (literal) expect(Number(literal[1])).toBeGreaterThanOrEqual(82);
}
});
it('the BASKETBALL season reference (l20_avg) is not a 20-game slice', () => {
const body = fnBody(featureCache, 'nbaGameLogFeatures');
expect(body).not.toBeNull();
// The exact pre-fix line. It read as ordinary code and was the defect.
expect(body).not.toMatch(/m20\s*=\s*avg\(vals\.slice\(0,\s*20\)\)/);
expect(body).toMatch(/m20\s*=\s*avg\(vals\)/);
});
it('the MLB season reference is a real season aggregate, not a slice', () => {
const body = fnBody(featureCache, 'mlbGameLogFeatures');
expect(body).not.toBeNull();
expect(body).toMatch(/l20_avg\s*=\s*seasonTotal\s*\/\s*games/);
});
it('the MLB base-rate path reads the full log, not last10', () => {
const body = fnBody(featureCache, 'getStatRows');
expect(body).toMatch(/res\.fullLog/);
});
it('the ESPN adapter does not cap the parsed game log', () => {
const adapter = read('src/services/adapters/espnStatsAdapter.js');
const body = fnBody(adapter, 'parseGameLog');
expect(body).not.toBeNull();
// `return rows.slice(0, 20)` capped every downstream basketball rate.
expect(body).not.toMatch(/return\s+rows\.slice\(/);
});
});
describe('the guard would have caught the real defects', () => {
it('flags the exact pre-fix basketball slice', () => {
const preFix = 'const m20 = avg(vals.slice(0, 20));';
expect(/m20\s*=\s*avg\(vals\.slice\(0,\s*20\)\)/.test(preFix)).toBe(true);
});
it('flags the exact pre-fix ESPN cap', () => {
expect(/return\s+rows\.slice\(/.test(' return rows.slice(0, 20);')).toBe(true);
});
it('flags a hardcoded small game-log request', () => {
const preFix = 'await gameLogs.getGameLogs(playerName, sp, 20);';
const literal = /,\s*(\d+)\s*\)/.exec(/getGameLogs\([^)]*\)/.exec(preFix)[0]);
expect(Number(literal[1])).toBeLessThan(82); // i.e. would fail the guard
});
});