'use strict'; /** * CHAIN v1 — does the shadow accrue on the SCHEDULED path, and does it leak? * * Two questions, and the second one is the gate. * * A5's whole finding was a factor that was built, correct, and never invoked. * A6's answer was to drive the real path rather than read the code. Same here: * this runs the REAL `snapshotService.runSnapshot` with injected deps and checks * that a `chain_shadow` block lands on a retention row — because evidence that * is generated and never persisted is that same failure one layer along. * * Then it proves the thing that actually matters: the served `p_win` is * BYTE-IDENTICAL with the shadow on and off. A shadow that moves a served number * is not a shadow, and no amount of labelling fixes it. */ const fs = require('fs'); const path = require('path'); const svc = require('../../src/services/snapshotService'); const retention = require('../../src/services/retentionService'); const cs = require('../../src/services/model/chainShadow'); const { nameKey } = require('../../src/utils/playerName'); const ROOT = path.join(__dirname, '..', '..'); /** A chainable Supabase stub — the context builders are gated on a client. */ function fakeSbTop() { const q = {}; for (const m of ['select', 'eq', 'lte', 'gte', 'in', 'order', 'not', 'is']) q[m] = () => q; q.range = async () => ({ data: [], error: null }); return { from: () => q }; } function memCache() { const store = {}; return { store, cacheGet: async (k) => (k in store ? store[k] : null), cacheSet: async (k, v) => { store[k] = v; }, }; } const PROPS = [ { player: 'Mookie Betts', stat_type: 'hits', line: 0.5, over_odds: -130, under_odds: 100, book: 'dk' }, { player: 'Aaron Judge', stat_type: 'hits', line: 0.5, over_odds: -140, under_odds: 110, book: 'dk' }, ]; /** Served grades, with the counter's p_win — the number that must not move. */ const GRADES = [ { player: 'Mookie Betts', stat_type: 'hits', line: 0.5, direction: 'over', grade: 'B', confidence: 61, p_win: 0.61, game_id: 'mlb:2026-08-12:LAD@SFG', }, { player: 'Aaron Judge', stat_type: 'hits', line: 0.5, direction: 'under', grade: 'C', confidence: 42, p_win: 0.42, game_id: 'mlb:2026-08-12:NYY@BOS', }, ]; const statcastRow = (key, over = {}) => ({ player_key: key, role: 'batter', k_pct: 20.0, bb_pct: 9.0, barrel_pct: 10.0, hard_hit_pct: 43.0, avg_exit_velo: 90.5, avg_launch_angle: 13.0, bats: 'R', sample_pa: 420, ...over, }); const STATCAST = new Map([ [nameKey('Mookie Betts'), statcastRow(nameKey('Mookie Betts'))], [nameKey('Aaron Judge'), statcastRow(nameKey('Aaron Judge'), { barrel_pct: 16.0 })], ]); /** Retention stub that captures rows instead of writing them. */ function captureRetention(sink) { return { ...retention, persist: async (rows) => { sink.push(...rows); return { attempted: rows.length, written: rows.length }; }, }; } /** * The challenger pass fetches park/weather and probable pitchers over the * network. Stubbed to no-ops so this suite is hermetic — the shadow reuses the * SAME statcast handle either way, which is the coupling under test. */ const inertChallengerDeps = () => ({ notify: async () => {}, gameBinder: { attachGameTimes: async () => ({ bound: 0, alreadyHad: 2, unresolved: 0, ambiguous: 0 }) }, environmentContext: { buildContext: async () => ({ _internals: { oppPitcherByTeam: new Map() }, contextFor: () => null, stats: {}, }), }, challenger: { attachChallenger: async (g) => g }, archetypeAxes: { classifyPlayer: () => null }, contactChallenger: { buildRefs: () => ({}), attachContactChallenger: async (g) => g }, projectionChallenger: { attachProjection: async (g) => g }, loadArsenals: async () => new Map(), mlbAdapter: { getPlayerGameLog: async () => [] }, lineupContext: { refreshContext: async () => ({ lineups: 0, opportunity: 0 }) }, ledger: { recordPipelineGrades: async () => ({ written: 0 }), captureClosing: async () => {} }, buildEspnIndex: async () => ({}), captureBookPrices: () => ({}), lockLineCapture: { buildLockRows: () => [], persist: async () => ({ retired: true, attempted: 0 }) }, }); function deps(cache, sink, over = {}) { return { ...inertChallengerDeps(), getOdds: async () => ({ sport: 'mlb', props: PROPS, provider: 'propline' }), gradeAndCacheSlate: async (_sport, props, opts) => { // The REAL collector path: rowsFromSides is what writes model_snapshots. for (const base of props) { const sides = GRADES.filter((g) => g.player === base.player) .map((g) => ({ ...g, stat_type: g.stat_type, line: g.line })); if (opts.onGraded) opts.onGraded({ ...base, game_id: sides[0] && sides[0].game_id }, sides); } await opts.cacheSet('grades:x', { grades: GRADES, updated_at: opts.now(), source: 'test' }); }, resolveStats: async () => ({ found: true, classifierInput: { hr: 30, avg: 0.29, ops: 0.93, k_rate: 20 } }), classify: require('../../src/services/archetypeService').classify, retention: captureRetention(sink), loadStatcast: async () => STATCAST, cacheGet: cache.cacheGet, cacheSet: cache.cacheSet, now: () => '2026-08-12T18:00:00Z', nowMs: () => 1000, ...over, }; } describe('the chain shadow accrues on the REAL scheduled path', () => { it('a graded hits row lands with chain_shadow on it', async () => { const sink = []; await svc.runSnapshot('mlb', deps(memCache(), sink)); const row = sink.find((r) => r.chain_shadow); expect(row).toBeDefined(); expect(row.chain_shadow.chain_p).toBeGreaterThan(0); expect(row.chain_shadow.chain_p).toBeLessThan(1); }); it('THE TRIPLE: chain_p and counter_p on the same row, both side-aligned', async () => { const sink = []; await svc.runSnapshot('mlb', deps(memCache(), sink)); for (const r of sink.filter((x) => x.chain_shadow)) { expect(r.chain_shadow.side).toBe(r.side); expect(r.chain_shadow.counter_p).toBe(r.p_win); expect(r.chain_shadow.divergence) .toBeCloseTo(r.chain_shadow.chain_p - r.chain_shadow.counter_p, 4); } // `outcome` is the third leg and is written by the ordinary settle pass — // it is null here because these games have not been played. expect(sink.every((r) => r.outcome === undefined || r.outcome === null)).toBe(true); }); it('an UNDER row carries 1 - p_over, so no later comparison inverts', async () => { const sink = []; await svc.runSnapshot('mlb', deps(memCache(), sink)); const pair = sink.filter((r) => r.player_key === nameKey('Aaron Judge') && r.chain_shadow); const over = pair.find((r) => r.side === 'over'); const under = pair.find((r) => r.side === 'under'); if (over && under) expect(over.chain_shadow.chain_p + under.chain_shadow.chain_p).toBeCloseTo(1, 4); }); it('every stored block declares itself UN-SERVABLE', async () => { const sink = []; await svc.runSnapshot('mlb', deps(memCache(), sink)); for (const r of sink.filter((x) => x.chain_shadow)) { expect(r.chain_shadow.servable).toBe(false); expect(r.chain_shadow.status).toBe('UN-SERVABLE'); } }); it('the run reports its own counts, so a shadow reading NOTHING is visible', async () => { const r = await svc.runSnapshot('mlb', deps(memCache(), [])); expect(r.chainShadow).toBeTruthy(); expect(r.chainShadow.atoms).toBeGreaterThan(0); expect(r.chainShadow.servable).toBe(false); }); it('no statcast profiles => no shadow, and the snapshot still succeeds', async () => { const sink = []; const r = await svc.runSnapshot('mlb', deps(memCache(), sink, { loadStatcast: async () => new Map() })); expect(r.status).toBe('ok'); expect(sink.every((x) => !x.chain_shadow)).toBe(true); }); it('a THROWING shadow never breaks the snapshot', async () => { const sink = []; const r = await svc.runSnapshot('mlb', deps(memCache(), sink, { chainShadow: { runShadow: () => { throw new Error('boom'); } }, })); expect(r.status).toBe('ok'); expect(r.gradeCount).toBe(2); expect(r.chainShadow).toBeNull(); }); }); describe('CHAIN v2 — the hand split reaches the shadow through the REAL path', () => { const SPLIT = { vl: { pa: 220, atBats: 200, hits: 57 }, vr: { pa: 480, atBats: 430, hits: 95 } }; /** * The A4 context resolver and the A6 key resolver, in miniature. The shadow * composes them: keys give the opposing starter's hand, the context gives the * hitter's own split. WITHOUT the keys the resolver's `throws` is null — the * A5 finding — and the split silently never fires, which is the exact failure * this order exists inside. */ const factorContext = (prop, sport, keys) => ({ spray: null, positionOaa: null, bats: 'R', throws: keys && keys.opposing_pitcher ? 'L' : null, pitcherHardHit: null, platoonSplits: SPLIT, as_of: '2026-08-12', }); const KEYS = { opponent: 'Boston Red Sox', opposing_pitcher: 'Chris Sale', source: 'lineup_context+schedule', refused: null }; /** A chainable Supabase stub — the context builders are gated on a client. */ const fakeSb = () => { const q = {}; for (const m of ['select', 'eq', 'lte', 'gte', 'in', 'order', 'not', 'is']) q[m] = () => q; q.range = async () => ({ data: [], error: null }); q.then = undefined; return { from: () => q }; }; it('FIRES when both the context and the join keys are present', async () => { const sink = []; await svc.runSnapshot('mlb', deps(memCache(), sink, { supabase: fakeSb(), hitsFactorContext: { build: async () => factorContext }, matchupKeys: { build: async () => Object.assign(() => KEYS, { stats: {} }) }, })); const row = sink.find((r) => r.chain_shadow); expect(row).toBeDefined(); expect(row.chain_shadow.platoon_applied).toBe(true); expect(row.chain_shadow.platoon_multiplier).toBeGreaterThan(0); expect(row.chain_shadow.platoon_reason).toBeNull(); }); it('WITHOUT the join keys it falls to a SEASON read — and says which', async () => { // This is the A5 shape reproduced deliberately: the context loads, the // hitter's own split is right there, and the pitcher hand is missing, so // the matchup conditioner cannot run. It must be visible, not silent. const sink = []; await svc.runSnapshot('mlb', deps(memCache(), sink, { supabase: fakeSb(), hitsFactorContext: { build: async () => factorContext }, matchupKeys: { build: async () => null }, })); const row = sink.find((r) => r.chain_shadow); expect(row.chain_shadow.platoon_applied).toBe(false); expect(row.chain_shadow.platoon_reason).toBe('no_pitcher_hand'); }); it('the run reports the matchup rate SEPARATELY from the fire rate', async () => { const r = await svc.runSnapshot('mlb', deps(memCache(), [], { supabase: fakeSb(), hitsFactorContext: { build: async () => factorContext }, matchupKeys: { build: async () => Object.assign(() => KEYS, { stats: {} }) }, })); // Two different questions: did the chain produce a probability at all, and // did it read tonight's matchup or this season's average? expect(r.chainShadow.readable).toBeGreaterThan(0); expect(r.chainShadow.platoon_applied).toBeGreaterThan(0); }); it('THE HAND SPLIT MOVES THE NUMBER — otherwise plumbing it changed nothing', async () => { const withSplit = []; await svc.runSnapshot('mlb', deps(memCache(), withSplit, { supabase: fakeSb(), hitsFactorContext: { build: async () => factorContext }, matchupKeys: { build: async () => Object.assign(() => KEYS, { stats: {} }) }, })); const seasonOnly = []; await svc.runSnapshot('mlb', deps(memCache(), seasonOnly, { supabase: fakeSb(), hitsFactorContext: { build: async () => null }, matchupKeys: { build: async () => null }, })); const a = withSplit.find((r) => r.chain_shadow).chain_shadow.chain_p; const b = seasonOnly.find((r) => r.chain_shadow).chain_shadow.chain_p; expect(a).not.toBeCloseTo(b, 3); }); }); describe('CHAIN v3 — the opportunity term is REALLY fed, not a constant 4.1', () => { const KEYS_WITH_SLOT = (slot) => ({ opponent: 'Boston Red Sox', opposing_pitcher: 'Chris Sale', batting_order: slot, source: 'lineup_context+schedule', refused: null, }); const ctxFor = (prop, sport, keys) => ({ spray: null, positionOaa: null, bats: 'R', throws: keys && keys.opposing_pitcher ? 'L' : null, pitcherHardHit: null, platoonSplits: null, as_of: '2026-08-12', }); const withSlot = (slot, sink) => svc.runSnapshot('mlb', deps(memCache(), sink, { supabase: fakeSbTop(), hitsFactorContext: { build: async () => ctxFor }, matchupKeys: { build: async () => Object.assign(() => KEYS_WITH_SLOT(slot), { stats: {} }) }, })); it('a POSTED lineup slot reaches the chain and sets E[PA]', async () => { const sink = []; await withSlot(1, sink); const row = sink.find((r) => r.chain_shadow); expect(row.chain_shadow.opportunity_source).toBe('lineup_slot'); expect(row.chain_shadow.expected_pa).toBe(4.65); // leadoff, not 4.1 }); it('LEADOFF and the NINE HOLE do not get the same probability', async () => { // This is the v2 defect made a test: without the slot every hitter ran on // DEFAULT_PA (4.1), so the opportunity half of `rate x opportunity` was a // constant across the entire lineup. const lead = []; const nine = []; await withSlot(1, lead); await withSlot(9, nine); const a = lead.find((r) => r.chain_shadow).chain_shadow; const b = nine.find((r) => r.chain_shadow).chain_shadow; expect(a.expected_pa).toBeGreaterThan(b.expected_pa); expect(a.chain_p).not.toBeCloseTo(b.chain_p, 3); }); it('an UNPOSTED lineup falls to a regular AND SAYS SO', async () => { const sink = []; await svc.runSnapshot('mlb', deps(memCache(), sink, { supabase: fakeSbTop(), hitsFactorContext: { build: async () => ctxFor }, matchupKeys: { build: async () => Object.assign(() => KEYS_WITH_SLOT(null), { stats: {} }) }, })); const row = sink.find((r) => r.chain_shadow); expect(row.chain_shadow.opportunity_source).toBe('default_regular'); // 4.1 is "a regular", which is a claim about the league, not about him. It // must never be reported as if the lineup had been read. expect(row.chain_shadow.expected_pa).toBeNull(); }); it('the run reports posted-slot coverage separately from the fire rate', async () => { const r = await svc.runSnapshot('mlb', deps(memCache(), [], { supabase: fakeSbTop(), hitsFactorContext: { build: async () => ctxFor }, matchupKeys: { build: async () => Object.assign(() => KEYS_WITH_SLOT(3), { stats: {} }) }, })); expect(r.chainShadow.opportunity_posted).toBeGreaterThan(0); expect(r.chainShadow.props).toBeGreaterThan(0); }); it('matchupKeys carries batting_order on EVERY return shape, including refusals', () => { // A field present only on the happy path is a field consumers cannot rely // on; the refusal shapes must carry it as an explicit null. const src = fs.readFileSync(path.join(ROOT, 'src/services/model/matchupKeys.js'), 'utf8'); const returns = src.slice(src.indexOf('function resolve(prop)')); expect(returns).toMatch(/const empty = \{[^}]*batting_order: null/); expect((returns.match(/batting_order/g) || []).length).toBeGreaterThanOrEqual(4); }); }); describe('SERVED-GRADE-UNCHANGED — the gate this whole order sits behind', () => { /** Everything a user can see, pulled from what the snapshot actually wrote. */ const servedShape = (store) => JSON.stringify({ grades: (store['grades:mlb'] || {}).grades, snapshot: (store['snapshot:mlb:latest'] || {}).grades, }); it('the served payload is BYTE-IDENTICAL with the shadow on and off', async () => { const on = memCache(); await svc.runSnapshot('mlb', deps(on, [])); const off = memCache(); // `chainShadow: null` cannot be injected (the ?? default would re-require // it), so the OFF arm is a shadow service that reads and returns nothing. await svc.runSnapshot('mlb', deps(off, [], { chainShadow: { runShadow: () => ({ version: 'off', byKey: new Map(), summary: { atoms: 0, readable: 0, refused: 0, games: 0, games_read: 0, per_game: [] } }) }, })); expect(servedShape(on)).toBe(servedShape(off)); }); it('p_win on every served grade is exactly the counter value', async () => { const cache = memCache(); await svc.runSnapshot('mlb', deps(cache, [])); const byPlayer = Object.fromEntries(GRADES.map((g) => [g.player, g.p_win])); for (const g of cache.store['grades:mlb'].grades) { expect(g.p_win).toBe(byPlayer[g.player]); } }); it('NO served grade carries a chain field at all', async () => { const cache = memCache(); await svc.runSnapshot('mlb', deps(cache, [])); for (const g of cache.store['grades:mlb'].grades) { for (const k of Object.keys(g)) { expect(k).not.toMatch(/^chain/); } } }); }); describe('0 LEAKS — measured against the source, not asserted in prose', () => { const read = (p) => fs.readFileSync(path.join(ROOT, p), 'utf8'); const walk = (dir, out = []) => { for (const e of fs.readdirSync(path.join(ROOT, dir), { withFileTypes: true })) { const rel = `${dir}/${e.name}`; if (e.isDirectory()) { if (e.name !== 'node_modules' && e.name !== '.next') walk(rel, out); } else if (/\.(js|ts|tsx)$/.test(e.name)) out.push(rel); } return out; }; it('nothing under src/routes reads the chain shadow', () => { for (const f of walk('src/routes')) { const src = read(f); expect(src).not.toMatch(/chain_shadow|chainShadow|chain_p\b/); } }); it('nothing in the web app reads the chain shadow', () => { for (const f of walk('web/src')) { const src = read(f); expect(src).not.toMatch(/chain_shadow|chainShadow|chain_p\b/); } }); it('the ONLY writers are the shadow service, retention and the snapshot', () => { const writers = walk('src').filter((f) => /chain_shadow|chainShadow/.test(read(f))); expect(writers.sort()).toEqual([ 'src/services/model/chainShadow.js', 'src/services/retentionService.js', 'src/services/snapshotService.js', ]); }); it('the shadow runs with requireCalibrated FALSE and says so where it is stored', () => { expect(read('src/services/model/chainShadow.js')).toMatch(/requireCalibrated:\s*false/); expect(cs.SHADOW_VERSION).toBe('chain-shadow@1'); }); it('CALIBRATION_DEPLOYED is still empty — nothing is served calibrated', () => { expect(svc.CALIBRATION_DEPLOYED).toEqual([]); expect(Object.isFrozen(svc.CALIBRATION_DEPLOYED)).toBe(true); }); it('the counter still serves: probabilityEstimator is what analyzeViaEngine1 calls', () => { const engine = read('src/services/intelligence/analyzeViaEngine1.js'); expect(engine).toMatch(/require\('\.\/probabilityEstimator'\)/); // And the engine knows nothing about the chain — the shadow is downstream. expect(engine).not.toMatch(/chainShadow|baseballChain/); }); it('A8 and the factor freeze are untouched — factor_inputs still lands', async () => { const rows = require('../../src/services/retentionService').rowsFromSides( { player: 'X', stat_type: 'hits', line: 0.5 }, [{ player: 'X', stat_type: 'hits', line: 0.5, direction: 'over', grade: 'B', factor_inputs: { v: 'hits-factors@1' } }], { snapshotId: 's', capturedAt: 't', sport: 'mlb', gameDate: '2026-08-12' }, ); expect(rows[0].factor_inputs).toEqual({ v: 'hits-factors@1' }); expect(rows[0].chain_shadow).toBeNull(); // declared, so batches keep one shape }); it('WNBA is untouched — the shadow is MLB-gated', async () => { const src = read('src/services/snapshotService.js'); const i = src.indexOf('CHAIN v1 — THE SHADOW READ'); const block = src.slice(i, src.indexOf('LINEUP + BASERUNNER CONTEXT', i)); expect(block).toMatch(/if \(sp === 'mlb'\)/); }); });