55b210cb95
PHASE 0 — audit. espnStatsAdapter's slice(0,20) was already fixed at494c83c, 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 against981a05cit 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
107 lines
4.4 KiB
JavaScript
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
|
|
});
|
|
});
|