f61ec6b391
Seven orders of measurement-first repair. The served grade does not move. A0/A1 — the unordered page walk returned the right COUNT and the wrong ROWS: 410-617 of 2,490 duplicated with an equal number never returned, while rows.length matched the server exactly. safePaginate orders on a real unique key, verifies the tuple at runtime, and THROWS on a query error instead of treating it as end-of-data. Both hits PROVES are withdrawn: they were drawn through that reader, and defense_by_direction's distinct-n was likely below the gate floor all along. A2/A2b — rolled across every reader: 11 FAIL -> 0. Composite keys pulled from pg_index (the context tables are dated-composite and had no single unique column). The unordered helper is deleted, not parked. A3 — ledgerService and retentionService defaulted the SAME env var to DIFFERENT versions, so no ledger row ever carried the marker eligibility requires. One source now. model_snapshots settlement moved onto the cron: 15,484 -> 28,894 settled, repaired-champion 0 -> 7,556. A4 — hitsFactorContext takes an as-of cutoff. Refusal over reconstruction: no row at-or-before the date means the factor does not apply, never the nearest row. Live path unchanged, proven 400/400 on real rows. A5 — factor_inputs freezes what the factor READ, never the multiplier, so an audit can recompute and check. It also recorded the finding: the three hits factors have NEVER fired. prop.opponent and prop.opposing_pitcher are read by the resolver and written by nothing. A6/A7 — matchupKeys resolves those keys from the posted lineup plus the schedule's probable pitchers, and fires the factors into a SHADOW freeze: 248 fires on 308 props, 245 of which would move the grade. The served forecast is untouched. specs/a8-shadow-factor-gate.md pre-registers the test that decides whether they ever go live. Nothing is turned on. CALIBRATION_DEPLOYED stays []. Both verdicts stay withdrawn. 4,772 tests / 371 suites green, web build exit 0, read-integrity harness 34/34. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
173 lines
7.2 KiB
JavaScript
173 lines
7.2 KiB
JavaScript
'use strict';
|
|
|
|
/**
|
|
* A7 — does the shadow EVIDENCE actually accrue, on the scheduled path?
|
|
*
|
|
* A6 measured the shadow firing in a hand-run script. That proves the resolver
|
|
* works; it does not prove a scheduled snapshot writes anything. This drives the
|
|
* REAL `gradeAndCacheSlate` -> onGraded -> rowsFromSides chain with injected
|
|
* deps and asserts a `would_fire` block lands on the retention row.
|
|
*
|
|
* The distinction matters: A5 found factors that were built, correct, and never
|
|
* invoked. Evidence that is generated but never persisted is the same failure
|
|
* one layer along.
|
|
*/
|
|
|
|
const gradeSlate = require('../../src/services/gradeSlateService');
|
|
const retention = require('../../src/services/retentionService');
|
|
const ff = require('../../src/services/model/factorFreeze');
|
|
const hf = require('../../src/services/model/hitsFactors');
|
|
const { nameKey } = require('../../src/utils/playerName');
|
|
|
|
const JUDGE = nameKey('Aaron Judge');
|
|
|
|
const SPRAY = {
|
|
as_of_date: '2026-08-10', player_key: JUDGE,
|
|
pull_gb: 0.30, straight_gb: 0.20, oppo_gb: 0.10,
|
|
pull_air: 0.20, straight_air: 0.10, oppo_air: 0.10,
|
|
};
|
|
const OAA = {
|
|
'3B': { oaa: 2 }, SS: { oaa: 1 }, LF: { oaa: 0 }, '1B': { oaa: 0 },
|
|
'2B': { oaa: 1 }, RF: { oaa: -1 }, CF: { oaa: 0 },
|
|
};
|
|
const SPLITS = { vl: { pa: 120, atBats: 110, hits: 33 }, vr: { pa: 300, atBats: 280, hits: 70 } };
|
|
|
|
/** The LIVE context: no opponent, no pitcher — the measured production state. */
|
|
const liveCtx = () => ({
|
|
spray: SPRAY, positionOaa: null, bats: 'R', throws: null,
|
|
pitcherHardHit: null, platoonSplits: SPLITS, archetype: 'BOMBER', as_of: null,
|
|
});
|
|
/** The SHADOW context: same indexes, joined through the resolved keys. */
|
|
const shadowCtx = () => ({ ...liveCtx(), positionOaa: OAA, throws: 'L', pitcherHardHit: 0.45 });
|
|
|
|
const KEYS = {
|
|
opponent: 'Boston Red Sox', opposing_pitcher: 'Chris Sale',
|
|
source: 'lineup_context+schedule', refused: null,
|
|
};
|
|
|
|
/** A resolver with A6's signature: keys omitted = live, keys supplied = shadow. */
|
|
const resolver = (prop, sport, keys) => (keys ? shadowCtx() : liveCtx());
|
|
|
|
const PROP = {
|
|
player: 'Aaron Judge', stat_type: 'hits', line: 0.5, book: 'draftkings',
|
|
over_odds: -120, under_odds: 100, game_time: '2026-08-10T23:05:00Z',
|
|
};
|
|
|
|
/** A grader that runs the REAL engine freeze/shadow logic in miniature. */
|
|
function fakeGrade(base) {
|
|
const legacy = {
|
|
player: base.player, stat_type: base.stat_type, line: base.line,
|
|
direction: base.direction, grade: 'B', confidence: 70, p_win: 0.55,
|
|
};
|
|
// This mirrors analyzeViaEngine1's A5/A6 block exactly.
|
|
if (String(base.stat_type).toLowerCase() === 'hits' && base.factor_context) {
|
|
const live = hf.adjustProbability(0.55, base.factor_context);
|
|
if (live.p_adjusted != null && live.factors_fired > 0) legacy.p_win = live.p_adjusted;
|
|
let shadow = null;
|
|
const keys = base.matchup_keys || null;
|
|
if (keys && (keys.opponent || keys.opposing_pitcher) && typeof base.factor_context_resolver === 'function') {
|
|
const sc = base.factor_context_resolver(base, base.sport, keys);
|
|
if (sc) shadow = ff.shadowBlock(sc, hf.hitsFactorMultiplier(sc), keys);
|
|
}
|
|
legacy.factor_inputs = ff.freeze(base.factor_context, base, shadow);
|
|
}
|
|
return legacy;
|
|
}
|
|
|
|
async function runSlate(opts = {}) {
|
|
const collected = [];
|
|
await gradeSlate.gradeAndCacheSlate('mlb', [PROP], {
|
|
grade: fakeGrade,
|
|
factorContext: resolver,
|
|
matchupKeys: opts.matchupKeys !== undefined ? opts.matchupKeys : (() => KEYS),
|
|
cacheSet: async () => {},
|
|
onGraded: (base, sides) => {
|
|
collected.push(...retention.rowsFromSides(base, sides, {
|
|
snapshotId: 's1', capturedAt: '2026-08-10T14:00:00Z', sport: 'mlb', gameDate: '2026-08-10',
|
|
}));
|
|
},
|
|
...opts.slate,
|
|
});
|
|
return collected;
|
|
}
|
|
|
|
describe('the shadow accrues through the REAL slate path', () => {
|
|
it('a graded hits row lands with would_fire on it', async () => {
|
|
const rows = await runSlate();
|
|
expect(rows.length).toBeGreaterThan(0);
|
|
const row = rows.find((r) => r.factor_inputs);
|
|
expect(row).toBeDefined();
|
|
expect(row.factor_inputs.would_fire).not.toBeNull();
|
|
expect(row.factor_inputs.would_fire.factors_fired).toBeGreaterThan(0);
|
|
expect(row.factor_inputs.would_fire.keys.opponent).toBe('Boston Red Sox');
|
|
});
|
|
|
|
it('the row also carries the LIVE refusal — both states, side by side', async () => {
|
|
const rows = await runSlate();
|
|
const fi = rows.find((r) => r.factor_inputs).factor_inputs;
|
|
expect(fi.position_oaa).toBeNull(); // what the grade saw
|
|
expect(fi.throws).toBeNull();
|
|
expect(fi.would_fire.shadow_inputs.throws).toBe('L'); // what it would have
|
|
});
|
|
|
|
it('would_fire stays RE-DERIVABLE from what is stored beside it', async () => {
|
|
const rows = await runSlate();
|
|
const fi = rows.find((r) => r.factor_inputs).factor_inputs;
|
|
const re = hf.hitsFactorMultiplier({
|
|
spray: fi.spray, bats: fi.bats,
|
|
platoonSplits: fi.platoon
|
|
? { vl: { pa: fi.platoon.vl.pa, atBats: fi.platoon.vl.ab, hits: fi.platoon.vl.hits },
|
|
vr: { pa: fi.platoon.vr.pa, atBats: fi.platoon.vr.ab, hits: fi.platoon.vr.hits } }
|
|
: null,
|
|
positionOaa: fi.would_fire.shadow_inputs.position_oaa,
|
|
throws: fi.would_fire.shadow_inputs.throws,
|
|
pitcherHardHit: fi.would_fire.shadow_inputs.pitcher_hard_hit,
|
|
});
|
|
expect(re.multiplier).toBe(fi.would_fire.multiplier);
|
|
});
|
|
|
|
it('THE SERVED GRADE IS UNTOUCHED — p_win is the unadjusted forecast', async () => {
|
|
const rows = await runSlate();
|
|
for (const r of rows) expect(Number(r.p_win)).toBe(0.55);
|
|
});
|
|
|
|
it('no matchupKeys resolver => no would_fire, and the grade is the same', async () => {
|
|
const rows = await runSlate({ matchupKeys: null });
|
|
const fi = rows.find((r) => r.factor_inputs).factor_inputs;
|
|
expect(fi.would_fire).toBeNull();
|
|
for (const r of rows) expect(Number(r.p_win)).toBe(0.55);
|
|
});
|
|
|
|
it('refused keys => no shadow fire, never a guess', async () => {
|
|
const refused = { opponent: null, opposing_pitcher: null, source: null, refused: 'no_lineup_row' };
|
|
const rows = await runSlate({ matchupKeys: () => refused });
|
|
expect(rows.find((r) => r.factor_inputs).factor_inputs.would_fire).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('the scheduled path carries the shadow (not just a hand-run script)', () => {
|
|
const src = require('fs').readFileSync(
|
|
require('path').join(__dirname, '..', '..', 'src/services/snapshotService.js'), 'utf8');
|
|
|
|
it('runSnapshot builds the key index and passes it to the grader', () => {
|
|
expect(src).toMatch(/matchupKeys/);
|
|
expect(src).toMatch(/gradeAndCacheSlate\(sp, props, \{\s*\n\s*factorContext,\s*\n\s*matchupKeys,/);
|
|
});
|
|
|
|
it('the index is built BEFORE the grade call, not after', () => {
|
|
expect(src.indexOf('FIX A6 — THE JOIN KEYS')).toBeLessThan(src.indexOf('await deps.gradeAndCacheSlate('));
|
|
});
|
|
|
|
it('runAllSnapshots (the cron entrypoint) routes through runSnapshot', () => {
|
|
expect(src).toMatch(/async function runAllSnapshots/);
|
|
expect(src).toMatch(/runSnapshot\(/);
|
|
});
|
|
|
|
it('a key-resolve failure degrades to no-shadow, never breaking the slate', () => {
|
|
const i = src.indexOf('FIX A6 — THE JOIN KEYS');
|
|
const block = src.slice(i, i + 1600);
|
|
expect(block).toMatch(/catch \(e\)/);
|
|
expect(block).toMatch(/matchupKeys = null|no key index/);
|
|
});
|
|
});
|