Files
vyndr/tests/unit/chainShadowAccrual.test.js
builtbykev 352016790a MLB canonical event identity, impossible-binding refusal, event-aware dedupe, publication commit
Release-isolated slice built from 41ba38e. Ships ONLY the event-integrity +
publication + lineage-canary closure; the 90-path development tree stays
undeployed.

- canonical MLB event identity from statsapi gamePk (mlb:gamepk:<pk>), with
  event_identity_source/method/version recorded. The id is canonical; the
  binding is derived and says so.
- IMPOSSIBLE-BINDING REFUSAL. Verified in prod 2026-08-26: Joe Mack (Marlins)
  was bound to Dodgers@Braves and Yandy Diaz (Rays) to Rangers@WhiteSox, both
  from one book in the 01:00/03:01 UTC cycles after their own games began. Root
  cause is source market data, not the binder. A prop whose player's team is not
  an event participant now refuses; unknown team preserves uncertainty.
- event-aware dedupe: books still collapse, events no longer do. An unresolved
  MLB event fractures rather than falling back to the collision-prone
  date+teams key.
- publication commit moved AFTER the authoritative Redis slate write, with
  exact parity-gap identity when the product publishes and the record does not.
- lineage dual-write behind LINEAGE_CANARY_SPORTS, DISABLED for this deploy.

Excluded deliberately: WNBA feed/chain, market ontology, PerformanceDistribution,
calibration certification, truth diagnostics, applyRevision Phase-1, and the
analyzeViaEngine1 confidence-rounding change (a served field).

Suite 380/5,040/0 from this worktree; web tsc exit 0; champion output identical
to production.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CQJeAG8vcDoL5zkiaJyVb8
2026-08-27 16:29:38 -04:00

488 lines
21 KiB
JavaScript

'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', () => {
// The EVALUATOR names the column and must: `gradeFreeze` exists to verify
// that the shadow reaches nothing served, and a verifier that cannot say
// the name of the thing it verifies is not a verifier. It is excluded from
// the writer list and held to the stricter rule below — it may NAME the
// column, never assign to it.
const EVALUATOR = 'src/services/evaluator/';
const writers = walk('src')
.filter((f) => /chain_shadow|chainShadow/.test(read(f)))
.filter((f) => !f.startsWith(EVALUATOR));
expect(writers.sort()).toEqual([
'src/services/model/chainShadow.js',
'src/services/retentionService.js',
'src/services/snapshotService.js',
]);
});
it('the evaluator NAMES the shadow column but never assigns to it', () => {
// Excluding it above is only safe if this holds. Proven, not assumed.
const dir = 'src/services/evaluator/';
const files = walk('src').filter((f) => f.startsWith(dir));
expect(files.length).toBeGreaterThan(0);
for (const f of files) {
const src = read(f);
expect(src).not.toMatch(/chain_shadow\s*[:=][^=]/);
expect(src).not.toMatch(/\.chain_shadow\s*=/);
}
});
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'\)/);
});
});