'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/); }); });