checkpoint: chain shadow, WNBA possession feed, baseball chain

Backup commit of uncommitted working-tree state found during Legion
recon (Tony resurrection, STEP 0). This work existed only on the
laptop disk.

- chain shadow accrual + probe script (038_chain_shadow.sql)
- WNBA possession feed: ESPN adapter, usage service, verify script
  (039_wnba_player_game.sql)
- baseball chain
- retention/snapshot service updates, tableKeys, matchupKeys
- specs: chain-v1, wnba-possession-feed, wnba-source-survey
- unit tests for the above

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QnvJAkC3h5QGmb6dipoiWn
This commit is contained in:
Kev
2026-08-14 16:53:37 -04:00
parent 0657b71d18
commit 6c34af3414
22 changed files with 4958 additions and 32 deletions
+394
View File
@@ -0,0 +1,394 @@
'use strict';
/**
* BASEBALL'S chainFn + the shadow that runs it on the live board.
*
* Two things are being protected here. First, that the chain actually COMPUTES
* a probability from base events rather than counting how often the player has
* cleared this number — that is the whole difference between the chain and the
* incumbent counter. Second, that none of it can reach a served grade.
*/
const bc = require('../../src/services/model/baseballChain');
const cs = require('../../src/services/model/chainShadow');
const chain = require('../../src/services/model/chain');
const sk = require('../../src/services/model/skillProjection');
/**
* A real-shaped `statcast_aggregates` row. UNITS MATTER: the table stores
* PERCENTAGES (0-100), and feeding fractions in is the mistake that made
* `bip = 1 - k - bb` come out wrong and refused 568 of 576 rows on the first
* Stage A run.
*/
const batterRow = (over = {}) => ({
player_key: 'test batter',
role: 'batter',
k_pct: 22.2,
bb_pct: 8.5,
barrel_pct: 9.5,
hard_hit_pct: 44.0,
avg_exit_velo: 91.2,
avg_launch_angle: 14.0,
bats: 'R',
sample_pa: 400,
...over,
});
const pitcherRow = (over = {}) => ({
player_key: 'test pitcher',
role: 'pitcher',
source_id: 4242,
k_pct: 29.6,
bb_pct: 6.1,
hard_hit_pct: 33.0,
sample_ip: 120,
...over,
});
describe('BASEBALL chainFn — rate x opportunity, and opportunity is a LOOKUP', () => {
it('produces a probability by chaining the PA rate over expected PA', () => {
const out = bc.chainFn({ statType: 'hits', line: 0.5, batterRow: batterRow(), archetype: 'TORCH' });
expect(out).not.toBeNull();
expect(out.p).toBeGreaterThan(0);
expect(out.p).toBeLessThan(1);
// It is a CHAINED read, not a frequency table: the PA tree is on the row.
expect(out.chain_per_pa.p_hit_per_pa).toBeGreaterThan(0);
expect(out.chain_family).toBe('pa_outcome_tree_binomial');
});
it('the probability is genuinely the binomial over the PA distribution', () => {
// Recomputed independently from the same atoms — if these diverge, the
// chainFn is doing something other than what it claims.
const atom = { statType: 'hits', line: 0.5, batterRow: batterRow(), archetype: 'TORCH', expectedPa: 4 };
const out = bc.chainFn(atom);
const pa = sk.paOutcome({ batter: sk.fromStatcastRow(batterRow()), pitcher: null, park: 1, archetype: 'TORCH' });
const pmf = sk.binomialPmf(4, pa.p_hit_per_pa);
expect(out.p).toBeCloseTo(sk.atLeast(pmf, 1), 2);
});
it('LINEUP SLOT is the opportunity term — leadoff outranks the nine hole', () => {
const at = (slot) => bc.chainFn({
statType: 'hits', line: 0.5, batterRow: batterRow(), archetype: 'TORCH', lineupSlot: slot,
});
const lead = at(1);
const nine = at(9);
expect(lead.chain_expected_pa).toBeGreaterThan(nine.chain_expected_pa);
expect(lead.p).toBeGreaterThan(nine.p);
expect(lead.chain_opportunity_source).toBe('lineup_slot');
});
it('an unposted lineup falls to a REGULAR, and says which it used', () => {
const out = bc.chainFn({ statType: 'hits', line: 0.5, batterRow: batterRow(), archetype: 'TORCH' });
expect(out.chain_opportunity_source).toBe('default_regular');
const explicit = bc.chainFn({ statType: 'hits', line: 0.5, batterRow: batterRow(), expectedPa: 5 });
expect(explicit.chain_opportunity_source).toBe('explicit');
});
it('the slot table is monotone — the decline is the documented structure', () => {
const slots = Object.keys(bc.PA_BY_SLOT).map(Number).sort((a, b) => a - b);
for (let i = 1; i < slots.length; i += 1) {
expect(bc.PA_BY_SLOT[slots[i]]).toBeLessThan(bc.PA_BY_SLOT[slots[i - 1]]);
}
expect(bc.expectedPaForSlot(null)).toBeNull();
expect(bc.expectedPaForSlot(12)).toBeNull();
});
it('ARCHETYPE SELECTS THE FEATURES — the same hitter reads differently', () => {
// A BOMBER's hits ride on barrels; a GHOST's ride on beating out grounders.
// If these agreed, the archetype would be a label rather than a selector.
const row = batterRow({ barrel_pct: 16.0, avg_launch_angle: 20.0 });
const bomber = bc.chainFn({ statType: 'hits', line: 0.5, batterRow: row, archetype: 'BOMBER' });
const ghost = bc.chainFn({ statType: 'hits', line: 0.5, batterRow: row, archetype: 'GHOST' });
expect(Math.abs(bomber.p - ghost.p)).toBeGreaterThan(0.01);
});
it('the OPPOSING PITCHER moves the read, and its absence leaves it untouched', () => {
const solo = bc.chainFn({ statType: 'hits', line: 0.5, batterRow: batterRow(), archetype: 'TORCH' });
const vsAce = bc.chainFn({
statType: 'hits', line: 0.5, batterRow: batterRow(), archetype: 'TORCH', pitcherRow: pitcherRow(),
});
expect(solo.chain_pitcher_applied).toBe(false);
expect(vsAce.chain_pitcher_applied).toBe(true);
// A high-K, contact-suppressing arm lowers it.
expect(vsAce.p).toBeLessThan(solo.p);
});
it('NO BATTER PROFILE IS A REFUSAL, never a league hitter', () => {
expect(bc.chainFn({ statType: 'hits', line: 0.5, batterRow: null })).toBeNull();
expect(bc.chainFn({ statType: 'hits', line: 0.5, batterRow: {} })).toBeNull();
});
it('refuses every stat but hits, so a muddy verdict is not widened into', () => {
expect(bc.chainFn({ statType: 'total_bases', line: 1.5, batterRow: batterRow() })).toBeNull();
expect(bc.chainFn({ statType: 'rbi', line: 0.5, batterRow: batterRow() })).toBeNull();
expect(bc.chainFn({ statType: 'hits', line: null, batterRow: batterRow() })).toBeNull();
});
it('plugs into the core as a chainFn with no special-casing in chain.js', () => {
const atoms = [
{ id: 'a', statType: 'hits', line: 0.5, batterRow: batterRow(), gameId: 'G1', calibrated: false },
{ id: 'b', statType: 'hits', line: 0.5, batterRow: null, gameId: 'G1', calibrated: false },
];
const out = chain.chainAcross(atoms, { chainFn: bc.chainFn, requireCalibrated: false });
expect(out.ok).toBe(true);
expect(out.legs).toBe(1); // the unreadable atom was dropped, not zeroed
expect(out.chain_fn_refused).toBe(1);
});
it('redistribution is DORMANT in baseball — a nine-run lead changes no batting order', () => {
const legs = [{ id: 'a', p: 0.5 }];
expect(bc.redistribute(legs)).toBe(legs);
});
});
describe('THE HAND SPLIT — a matchup read, not a season read', () => {
/** A real split: .284 vs LHP, .221 vs RHP, both sides well over the floor. */
const SPLIT = {
vl: { pa: 220, atBats: 200, hits: 57 }, // .285
vr: { pa: 480, atBats: 430, hits: 95 }, // .221
};
const base = (over = {}) => ({
statType: 'hits', line: 0.5, batterRow: batterRow(), archetype: 'TORCH', expectedPa: 4, ...over,
});
it('WITHOUT a split the chain is arithmetically identical to projectSkill', () => {
// The chain is written out in baseballChain so the split can enter between
// the rate and the opportunity term. This is what stops that restructuring
// from quietly becoming a second, different model.
for (const archetype of ['TORCH', 'BOMBER', 'GHOST', null]) {
for (const pa of [3, 4, 5]) {
const mine = bc.chainFn(base({ archetype, expectedPa: pa }));
const theirs = sk.projectSkill({
batter: sk.fromStatcastRow(batterRow()), pitcher: null, park: 1,
archetype, statType: 'hits', line: 0.5, expectedPa: pa,
});
expect(mine.p).toBeCloseTo(theirs.p_over_line, 3);
expect(mine.chain_projected_value).toBeCloseTo(theirs.projected_value, 3);
}
}
});
it('FIRES when bats + throws + a thick split are all present', () => {
const out = bc.chainFn(base({ bats: 'R', throws: 'L', platoonSplits: SPLIT }));
expect(out.chain_platoon_applied).toBe(true);
expect(out.chain_platoon_reason).toBeNull();
expect(out.chain_platoon_multiplier).toBeGreaterThan(1); // RHB vs LHP = the edge
expect(out.chain_platoon_observed_split).toBeGreaterThan(0);
});
it('it enters at the RATE, not at the output probability', () => {
// p_hit_per_pa moves; the season rate is preserved beside it so the
// adjustment stays re-derivable.
const out = bc.chainFn(base({ bats: 'R', throws: 'L', platoonSplits: SPLIT }));
const season = out.chain_per_pa.p_hit_per_pa_season;
const applied = out.chain_per_pa.p_hit_per_pa;
expect(applied).not.toBeCloseTo(season, 6);
expect(applied).toBeCloseTo(season * out.chain_platoon_multiplier, 3);
});
it('and it runs in the DIRECTION tonight\'s matchup runs', () => {
const edge = bc.chainFn(base({ bats: 'R', throws: 'L', platoonSplits: SPLIT }));
const wrongSide = bc.chainFn(base({ bats: 'R', throws: 'R', platoonSplits: SPLIT }));
expect(edge.p).toBeGreaterThan(wrongSide.p);
});
it('REFUSES on a thin split — with the reason, not a league-typical guess', () => {
const thin = { vl: { pa: 40, atBats: 36, hits: 11 }, vr: { pa: 480, atBats: 430, hits: 95 } };
const out = bc.chainFn(base({ bats: 'R', throws: 'L', platoonSplits: thin }));
expect(out.chain_platoon_applied).toBe(false);
expect(out.chain_platoon_reason).toBe('insufficient_split_sample');
// The season rate stands EXACTLY untouched — a refusal is not a nudge.
expect(out.chain_per_pa.p_hit_per_pa).toBeCloseTo(out.chain_per_pa.p_hit_per_pa_season, 6);
expect(out.p).toBeCloseTo(bc.chainFn(base()).p, 6); // a refusal is EXACT, not approximate
});
it('a switch hitter is UNREADABLE, never credited with an automatic edge', () => {
const out = bc.chainFn(base({ bats: 'S', throws: 'L', platoonSplits: SPLIT }));
expect(out.chain_platoon_applied).toBe(false);
expect(out.chain_platoon_reason).toBe('switch_hitter_side_value_unknown');
});
it('names WHICH input is missing, so the fix is diagnosable', () => {
const r = (over) => bc.chainFn(base(over)).chain_platoon_reason;
expect(r({})).toBe('no_splits');
expect(r({ platoonSplits: SPLIT, bats: 'R' })).toBe('no_pitcher_hand');
expect(r({ platoonSplits: SPLIT, throws: 'L' })).toBe('no_batter_hand');
});
it('THERE IS NO FALLBACK PATH — a refused read is dropped, never a season rate', () => {
// The failure this guards is subtle: if an unreadable atom silently became a
// season-rate read, the shadow would agree with the counter for a reason
// that LOOKS like agreement, and the whole comparison would be worthless.
expect(bc.chainFn(base({ batterRow: null }))).toBeNull();
expect(bc.chainFn(base({ statType: 'rbi' }))).toBeNull();
});
it('refusals are REPORTED to the caller with a reason', () => {
const seen = [];
const ctx = { onRefusal: (id, reason) => seen.push([id, reason]) };
bc.chainFn({ ...base({ batterRow: null }), id: 'a' }, ctx);
bc.chainFn({ ...base({ statType: 'rbi' }), id: 'b' }, ctx);
bc.chainFn({ ...base({ line: null }), id: 'c' }, ctx);
expect(seen).toEqual([['a', 'no_batter_profile'], ['b', 'stat_not_chained'], ['c', 'no_line']]);
});
it('a throwing onRefusal never breaks the read', () => {
expect(() => bc.chainFn({ ...base({ batterRow: null }) }, {
onRefusal: () => { throw new Error('boom'); },
})).not.toThrow();
});
});
describe('SHADOW — fire rate and matchup rate are DIFFERENT questions', () => {
const SPLIT = { vl: { pa: 220, atBats: 200, hits: 57 }, vr: { pa: 480, atBats: 430, hits: 95 } };
const grade = (over = {}) => ({
player: 'Test Batter', stat_type: 'hits', direction: 'over', p_win: 0.62,
game_id: 'G1', team: 'New York Yankees', archetype: 'TORCH',
gradedAt: { line: 0.5 }, ...over,
});
const deps = (over = {}) => ({
statcastByKey: new Map([['test batter', batterRow()]]),
pitcherRowFor: () => pitcherRow({ throws: 'L' }),
...over,
});
it('counts the hand split separately from the fire rate', () => {
const withSplit = cs.runShadow([grade()], deps({
handSplitFor: () => ({ bats: 'R', throws: 'L', platoonSplits: SPLIT }),
}));
expect(withSplit.summary.readable).toBe(1);
expect(withSplit.summary.platoon_applied).toBe(1);
const without = cs.runShadow([grade()], deps());
expect(without.summary.readable).toBe(1); // it still FIRES...
expect(without.summary.platoon_applied).toBe(0); // ...but on the SEASON rate
expect(without.summary.platoon_reasons.no_splits).toBe(1);
});
it('a refused atom is counted ONCE, not once per reading', () => {
// chainAcross, chainUp and prepareAtoms each apply the chainFn. Running raw
// atoms through all three would report a 3x refusal count.
const out = cs.runShadow([grade({ player: 'Nobody' })], deps());
expect(out.summary.refused).toBe(1);
expect(out.summary.refusal_reasons).toEqual({ no_batter_profile: 1 });
});
it('the stored block records the split state either way', () => {
const on = [...cs.runShadow([grade()], deps({
handSplitFor: () => ({ bats: 'R', throws: 'L', platoonSplits: SPLIT }),
})).byKey.values()][0];
expect(on.platoon_applied).toBe(true);
expect(on.platoon_multiplier).toBeGreaterThan(1);
const off = [...cs.runShadow([grade()], deps()).byKey.values()][0];
expect(off.platoon_applied).toBe(false);
expect(off.platoon_reason).toBe('no_splits');
expect(off.platoon_multiplier).toBeNull();
});
it('a throwing handSplitFor degrades to a season read, never a broken slate', () => {
const out = cs.runShadow([grade()], deps({
handSplitFor: () => { throw new Error('boom'); },
}));
expect(out.summary.readable).toBe(1);
expect(out.summary.platoon_applied).toBe(0);
});
});
describe('CHAIN SHADOW — runs on the board, reaches nothing', () => {
const grade = (over = {}) => ({
player: 'Test Batter',
stat_type: 'hits',
direction: 'over',
p_win: 0.62,
grade: 'B',
game_id: 'mlb:2026-08-12:NYY@BOS',
team: 'New York Yankees',
archetype: 'TORCH',
gradedAt: { line: 0.5, odds: -130, timestamp: '2026-08-12T18:00:00Z' },
...over,
});
const deps = () => ({
statcastByKey: new Map([['test batter', batterRow()]]),
pitcherRowFor: () => pitcherRow(),
});
it('produces the (chain_p, counter_p) pair the triple needs', () => {
const out = cs.runShadow([grade()], deps());
const block = [...out.byKey.values()][0];
expect(block).toBeTruthy();
const aligned = cs.alignToSide(block, 'over', 0.62);
expect(aligned.chain_p).toBeGreaterThan(0);
expect(aligned.counter_p).toBeCloseTo(0.62, 6);
expect(aligned.divergence).toBeCloseTo(aligned.chain_p - 0.62, 4);
});
it('EVERY block declares itself UN-SERVABLE in the data, not just in a comment', () => {
const out = cs.runShadow([grade()], deps());
for (const block of out.byKey.values()) {
expect(block.servable).toBe(false);
expect(block.status).toBe('UN-SERVABLE');
expect(block.servable_reason).toMatch(/calibration gate/);
}
});
it('SIDE ALIGNMENT: an under row carries 1 - p_over, or every later read inverts', () => {
const out = cs.runShadow([grade({ direction: 'under', p_win: 0.38 })], deps());
const block = [...out.byKey.values()][0];
const over = cs.alignToSide(block, 'over', 0.62);
const under = cs.alignToSide(block, 'under', 0.38);
expect(over.chain_p + under.chain_p).toBeCloseTo(1, 4);
expect(under.side).toBe('under');
});
it('scores against the LOCKED line, not the current one', () => {
// Scoring the chain on a line the counter never saw would compare two
// forecasts of two different questions.
const g = grade({ line: 1.5, gradedAt: { line: 0.5 } });
const atoms = cs.buildAtoms([g], deps());
expect(atoms[0].line).toBe(0.5);
});
it('ignores every stat but hits, and any row with no locked line', () => {
const rows = [
grade({ stat_type: 'total_bases' }),
grade({ stat_type: 'rbi' }),
grade({ gradedAt: null, line: null }),
];
expect(cs.buildAtoms(rows, deps())).toHaveLength(0);
});
it('a player with no statcast profile yields NO BLOCK — absent, not zero', () => {
const out = cs.runShadow([grade({ player: 'Unknown Guy' })], deps());
expect(out.byKey.size).toBe(0);
expect(out.summary.refused).toBe(1);
expect(out.summary.atoms).toBe(1); // it was attempted, and that is visible
});
it('an absent block never fabricates a triple', () => {
expect(cs.alignToSide(null, 'over', 0.5)).toBeNull();
expect(cs.alignToSide({ chain_p_over: null }, 'over', 0.5)).toBeNull();
expect(cs.alignToSide({ chain_p_over: 0.4 }, '', 0.5)).toBeNull();
// An absent counter is absent — not zero, which would read as a 40pt edge.
expect(cs.alignToSide({ chain_p_over: 0.4 }, 'over', null).counter_p).toBeNull();
expect(cs.alignToSide({ chain_p_over: 0.4 }, 'over', null).divergence).toBeNull();
});
it('runs BOTH readings and reports the self-check as VACUOUS today', () => {
// Both readings derive from the same atoms because no independent team read
// exists. Agreement is arithmetic. Labelling it is the honest move.
const out = cs.runShadow([grade(), grade({ player: 'Test Batter', stat_type: 'hits' })], deps());
const block = [...out.byKey.values()][0];
expect(block.game.up_expected_hits).toBeGreaterThan(0);
expect(block.game.across_compound).toBeGreaterThan(0);
expect(block.game.self_check.vacuous).toBe(true);
expect(block.game.self_check.vacuous_reason).toMatch(/same atoms/);
});
it('an empty slate is an empty result, never a thrown snapshot', () => {
const out = cs.runShadow([], deps());
expect(out.byKey.size).toBe(0);
expect(out.summary.atoms).toBe(0);
});
});
+161
View File
@@ -61,6 +61,167 @@ describe('CHAIN ACROSS — the calibration gate is structural', () => {
});
});
describe('THE chainFn SLOT — the stage the header described and the code lacked', () => {
it('defaults to identity-on-p, so every pre-existing caller is unchanged', () => {
const out = chain.chainAcross([atom('a', 0.8), atom('b', 0.5)]);
expect(out.independent_probability).toBeCloseTo(0.4, 6);
expect(out.chain_fn_applied).toBe(false);
});
it('computes the per-entity probability from the atom + context when supplied', () => {
// The sport-specific work is an INPUT, not a code path: rate x opportunity.
const atoms = [
{ id: 'a', rate: 0.25, pa: 4, calibrated: true, gameId: 'G1' },
{ id: 'b', rate: 0.30, pa: 4, calibrated: true, gameId: 'G2' },
];
const chainFn = (at) => 1 - (1 - at.rate) ** at.pa;
const out = chain.chainAcross(atoms, { chainFn });
expect(out.chain_fn_applied).toBe(true);
const pa = 1 - 0.75 ** 4;
const pb = 1 - 0.70 ** 4;
expect(out.independent_probability).toBeCloseTo(pa * pb, 4);
});
it('reads context, so slate-level modifiers reach the atom', () => {
const chainFn = (at, ctx) => at.rate * (ctx.parkBoost || 1);
const legs = [{ id: 'a', rate: 0.4, calibrated: true }];
const plain = chain.chainUp(legs, { chainFn });
const boosted = chain.chainUp(legs, { chainFn, context: { parkBoost: 1.5 } });
expect(plain.expected_value).toBeCloseTo(0.4, 6);
expect(boosted.expected_value).toBeCloseTo(0.6, 6);
});
it('an atom the chainFn cannot read is DROPPED and COUNTED, never p=0', () => {
// A zero leg would zero an entire ticket, and "we could not read him" is not
// "he cannot do it". The count is what stops a silently-failing chainFn from
// looking like a thin slate.
const chainFn = (at) => (at.id === 'b' ? null : 0.8);
const out = chain.chainAcross([atom('a', 0), atom('b', 0)], { chainFn });
expect(out.legs).toBe(1);
expect(out.chain_fn_refused).toBe(1);
expect(out.compound_probability).toBeCloseTo(0.8, 6);
});
it('a THROWING chainFn refuses that atom rather than breaking the read', () => {
const chainFn = (at) => { if (at.id === 'b') throw new Error('no profile'); return 0.5; };
const out = chain.chainAcross([atom('a', 0), atom('b', 0)], { chainFn });
expect(out.ok).toBe(true);
expect(out.chain_fn_refused).toBe(1);
});
it('a chainFn returning an object merges its metadata onto the atom', () => {
const out = chain.chainUp([{ id: 'a', calibrated: true }], {
chainFn: () => ({ p: 0.3, weight: 2 }),
});
expect(out.expected_value).toBeCloseTo(0.6, 6);
});
it('refusing EVERY atom is an explicit refusal carrying the count', () => {
const out = chain.chainAcross([atom('a', 0.5), atom('b', 0.5)], { chainFn: () => null });
expect(out.ok).toBe(false);
expect(out.reason).toBe('no_usable_atoms');
expect(out.chain_fn_refused).toBe(2);
});
});
describe('CORRELATION IS SIGNED — basketball is not baseball with a hook', () => {
const pair = (p) => [atom('a', p, { gameId: 'G1' }), atom('b', p, { gameId: 'G1' })];
it('zero correlation is exactly the independent product', () => {
const out = chain.chainAcross(pair(0.7), { correlation: () => 0 });
expect(out.compound_probability).toBeCloseTo(0.49, 4);
expect(out.correlation_direction).toBe('independent');
});
it('POSITIVE moves the joint toward the weakest leg — unchanged from before', () => {
const out = chain.chainAcross(pair(0.7), { correlation: () => 0.6 });
expect(out.compound_probability).toBeGreaterThan(0.49);
// Fréchet upper bound: two co-monotone 0.7s land together at most 0.7.
expect(out.compound_probability).toBeLessThanOrEqual(0.7);
expect(out.correlation_direction).toBe('toward_joint');
});
it('NEGATIVE moves the joint APART — the case the old clamp made inexpressible', () => {
// Teammates competing for finite possessions: one player's shot is another
// player's non-shot, so they land together LESS often than independence says.
const out = chain.chainAcross(pair(0.7), { correlation: () => -0.6 });
expect(out.compound_probability).toBeLessThan(0.49);
expect(out.correlation_direction).toBe('apart');
});
it('the extremes are the FRÉCHET BOUNDS, which is why the direction is principled', () => {
const hi = chain.chainAcross(pair(0.7), { correlation: () => 1 });
const lo = chain.chainAcross(pair(0.7), { correlation: () => -1 });
expect(hi.compound_probability).toBeCloseTo(0.7, 4); // min(p_i)
expect(lo.compound_probability).toBeCloseTo(0.4, 4); // max(0, Sum p - (n-1))
});
it('the lower bound never goes below zero', () => {
const out = chain.chainAcross(
[atom('a', 0.2, { gameId: 'G1' }), atom('b', 0.3, { gameId: 'G1' })],
{ correlation: () => -1 },
);
expect(out.compound_probability).toBe(0);
});
it('a correlation outside [-1,1] is clamped, not trusted', () => {
const wild = chain.chainAcross(pair(0.7), { correlation: () => -50 });
const unit = chain.chainAcross(pair(0.7), { correlation: () => -1 });
expect(wild.compound_probability).toBeCloseTo(unit.compound_probability, 6);
});
});
describe('REDISTRIBUTE REACHES BOTH READINGS — or selfCheck lies about itself', () => {
const blowout = (as, ctx) => (ctx.blowout
? as.map((a) => (a.id === 'star' ? { ...a, p: a.p * 0.6 } : { ...a, p: a.p * 2 }))
: as);
it('chainAcross now takes the hook chainUp always had', () => {
const legs = [atom('star', 0.6, { gameId: 'G1' }), atom('bench', 0.1, { gameId: 'G1' })];
const out = chain.chainAcross(legs, { redistribute: blowout, context: { blowout: true } });
expect(out.redistributed).toBe(true);
expect(out.independent_probability).toBeCloseTo(0.36 * 0.2, 6);
});
it('and stays dormant unless a sport supplies it', () => {
const legs = [atom('star', 0.6, { gameId: 'G1' }), atom('bench', 0.1, { gameId: 'G1' })];
expect(chain.chainAcross(legs).redistributed).toBe(false);
});
it('THE POINT: a redistribution applied to both readings does not false-flag', () => {
// Before, redistribute reached only the team read. The across-read would
// then be built from pre-redistribution atoms, and selfCheck would report an
// INTERNAL_INCONSISTENCY the model had itself just manufactured.
const legs = [atom('star', 0.6, { gameId: 'G1' }), atom('bench', 0.1, { gameId: 'G1' })];
const opts = { redistribute: blowout, context: { blowout: true } };
const prep = chain.prepareAtoms(legs, opts);
const up = chain.chainUp(legs, opts);
const check = chain.selfCheck({ perEntity: prep.legs, teamRead: up.expected_value });
expect(prep.redistributed).toBe(true);
expect(check.confidence).toBe('NORMAL');
expect(check.flags).toEqual([]);
});
it('prepareAtoms is the shared preparation — same legs, both readings', () => {
const legs = [atom('star', 0.6), atom('bench', 0.1)];
const opts = { redistribute: blowout, context: { blowout: true } };
const prep = chain.prepareAtoms(legs, opts);
expect(prep.legs.map((l) => l.p)).toEqual([0.36, 0.2]);
// chainAcross and chainUp must see EXACTLY these.
expect(chain.chainUp(legs, opts).expected_value).toBeCloseTo(0.56, 6);
expect(chain.chainAcross(legs, opts).independent_probability).toBeCloseTo(0.072, 6);
});
it('a redistribute that returns nothing usable leaves the legs alone', () => {
const legs = [atom('a', 0.5)];
const out = chain.chainUp(legs, { redistribute: () => [] });
expect(out.redistributed).toBe(false);
expect(out.expected_value).toBeCloseTo(0.5, 6);
});
});
describe('CHAIN UP — same atoms, team reading', () => {
it('sums atoms into an expected value', () => {
const out = chain.chainUp([atom('a', 0.3), atom('b', 0.4), atom('c', 0.5)]);
+467
View File
@@ -0,0 +1,467 @@
'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'\)/);
});
});
+319
View File
@@ -0,0 +1,319 @@
'use strict';
/**
* The WNBA possession/usage feed — the source, the derivations, the as-of rule.
*
* Two failures this suite exists to prevent. First, a derived rate that is not
* actually the identity it claims to be: usage and possessions feed the chain's
* opportunity term directly, so a wrong constant or a transposed term would be
* invisible downstream and wrong everywhere. Second — and this is the one that
* has bitten this codebase repeatedly — a read that quietly includes the game it
* is trying to predict.
*/
const adapter = require('../../src/services/adapters/espnWnbaAdapter');
const usage = require('../../src/services/wnbaUsageService');
/** A real-shaped ESPN summary payload, trimmed to what the parser reads. */
const PLAYER_KEYS = [
'minutes', 'points', 'fieldGoalsMade-fieldGoalsAttempted',
'threePointFieldGoalsMade-threePointFieldGoalsAttempted',
'freeThrowsMade-freeThrowsAttempted', 'rebounds', 'assists', 'turnovers',
'steals', 'blocks', 'offensiveRebounds', 'defensiveRebounds', 'fouls', 'plusMinus',
];
const athlete = (id, name, stats, over = {}) => ({
athlete: { id, displayName: name }, starter: true, stats, ...over,
});
const teamStats = (fg, ft, tov, oreb) => [
{ name: 'fieldGoalsMade-fieldGoalsAttempted', displayValue: fg },
{ name: 'freeThrowsMade-freeThrowsAttempted', displayValue: ft },
{ name: 'totalTurnovers', displayValue: String(tov) },
{ name: 'offensiveRebounds', displayValue: String(oreb) },
];
function summary(over = {}) {
return {
header: {
id: '401857134',
season: { year: 2026 },
competitions: [{
date: '2026-08-11T23:30Z',
status: { type: { name: 'STATUS_FINAL' } },
competitors: [
{ homeAway: 'home', score: '106', team: { id: '5', abbreviation: 'IND' } },
{ homeAway: 'away', score: '92', team: { id: '9', abbreviation: 'NY' } },
],
}],
...over.header,
},
boxscore: {
players: [
{
team: { id: '9', abbreviation: 'NY' },
statistics: [{
keys: PLAYER_KEYS,
athletes: [
// MIN PTS FG 3PT FT REB AST TO STL BLK OREB DREB PF +/-
athlete('1', 'Breanna Stewart', ['30', '19', '8-21', '1-5', '2-2', '7', '5', '0', '1', '0', '2', '5', '2', '-2']),
athlete('2', 'Sabrina Ionescu', ['34', '22', '7-18', '4-9', '4-4', '4', '6', '3', '2', '0', '0', '4', '1', '-5']),
athlete('3', 'Deep Bench', null, { didNotPlay: true, stats: [] }),
],
}],
},
],
teams: [
{ team: { id: '9', abbreviation: 'NY' }, statistics: teamStats('35-76', '11-13', 13, 13) },
{ team: { id: '5', abbreviation: 'IND' }, statistics: teamStats('37-69', '16-19', 11, 4) },
],
},
...over,
};
}
describe('THE SOURCE — ESPN returns components, and the parser reads them', () => {
it('parses a real-shaped box score into per-player rows', () => {
const { rows, game } = adapter.parseSummary(summary());
expect(game.game_id).toBe('401857134');
expect(rows).toHaveLength(2);
const s = rows.find((r) => r.player_name === 'Breanna Stewart');
expect(s.minutes).toBe(30);
expect(s.fga).toBe(21);
expect(s.fta).toBe(2);
expect(s.tov).toBe(0);
expect(s.team).toBe('NY');
expect(s.opponent).toBe('IND');
expect(s.home_away).toBe('away');
});
it('a DNP is OMITTED, not recorded as a zero line', () => {
// A zero-minute row asserts he was available and produced nothing. That is a
// different fact from "he did not play", and the usage denominator would
// divide by his zero minutes anyway.
const { rows } = adapter.parseSummary(summary());
expect(rows.find((r) => r.player_name === 'Deep Bench')).toBeUndefined();
});
it('ET dating — a 23:30Z tip is the SAME ET evening, not the next day', () => {
// Using the UTC date would file half of every slate a day late.
const { rows } = adapter.parseSummary(summary());
expect(rows[0].game_date).toBe('2026-08-11');
});
it('an UNFINISHED game is refused — a partial box score is a moving denominator', () => {
const live = summary();
live.header.competitions[0].status.type.name = 'STATUS_IN_PROGRESS';
const out = adapter.parseSummary(live);
expect(out.rows).toHaveLength(0);
expect(out.skipped).toMatch(/not_final/);
});
it('a malformed made-attempted pair is ABSENT, never zero', () => {
expect(adapter.madeAtt('8-21')).toEqual({ made: 8, att: 21 });
expect(adapter.madeAtt('--')).toEqual({ made: null, att: null });
expect(adapter.madeAtt(null)).toEqual({ made: null, att: null });
});
});
describe('THE DERIVATIONS — exact identities, not proxies', () => {
const t = { fga: 76, fta: 13, tov: 13, oreb: 13 };
it('possessions = FGA - OREB + TOV + 0.44*FTA', () => {
expect(adapter.possessions(t)).toBeCloseTo(76 - 13 + 13 + 0.44 * 13, 6);
});
it('a missing term makes possessions ABSENT — never a partial sum', () => {
// Treating an absent turnover count as zero understates possessions and
// inflates every usage rate computed against it.
expect(adapter.possessions({ ...t, tov: null })).toBeNull();
expect(adapter.possessions(null)).toBeNull();
});
it('usage matches the standard identity, recomputed independently', () => {
const p = { fga: 21, fta: 2, tov: 0, minutes: 30 };
const teamMinutes = 200;
const expected = 100 * ((21 + 0.44 * 2 + 0) * (teamMinutes / 5))
/ (30 * (76 + 0.44 * 13 + 13));
expect(adapter.usageRate(p, t, teamMinutes)).toBeCloseTo(expected, 9);
});
it('usage is ABSENT on a zero-minute or missing-term row, never Infinity or 0', () => {
expect(adapter.usageRate({ fga: 5, fta: 0, tov: 1, minutes: 0 }, t, 200)).toBeNull();
expect(adapter.usageRate({ fga: null, fta: 0, tov: 1, minutes: 20 }, t, 200)).toBeNull();
expect(adapter.usageRate({ fga: 5, fta: 0, tov: 1, minutes: 20 }, t, 0)).toBeNull();
});
it('pace handles OVERTIME through team minutes, with no special case', () => {
// 5 players x 40 min = 200 team-minutes in regulation; an OT game has more,
// and the divisor follows rather than being branched on.
const reg = adapter.pace(100, 200);
const ot = adapter.pace(100, 225);
expect(reg).toBeCloseTo(100, 6); // 100 poss per 40 min
expect(ot).toBeLessThan(reg); // same possessions over more time
});
it('true shooting and eFG are absent without attempts', () => {
expect(adapter.trueShooting({ points: 19, fga: 21, fta: 2 })).toBeCloseTo(19 / (2 * (21 + 0.88)), 9);
expect(adapter.trueShooting({ points: 0, fga: 0, fta: 0 })).toBeNull();
expect(adapter.efgPct({ fgm: 8, fg3m: 1, fga: 21 })).toBeCloseTo(8.5 / 21, 9);
expect(adapter.efgPct({ fgm: 0, fg3m: 0, fga: 0 })).toBeNull();
});
it('END TO END: the stored rate equals the identity recomputed from the stored components', () => {
// This is the factorFreeze rule applied to a feed: every component is stored
// beside its derived value, so the rate is checkable rather than asserted.
const { rows } = adapter.parseSummary(summary());
for (const r of rows) {
const expected = 100 * ((r.fga + 0.44 * r.fta + r.tov) * (r.team_minutes / 5))
/ (r.minutes * (r.team_fga + 0.44 * r.team_fta + r.team_tov));
expect(r.usage_rate).toBeCloseTo(expected, 3);
expect(r.team_possessions)
.toBeCloseTo(r.team_fga - r.team_oreb + r.team_tov + 0.44 * r.team_fta, 3);
}
});
it('carries the game state the redistribute hook reads', () => {
const { rows } = adapter.parseSummary(summary());
const ny = rows[0];
expect(ny.team_score).toBe(92);
expect(ny.opp_score).toBe(106);
expect(ny.final_margin).toBe(-14); // a blowout the hook can see
expect(ny.starter).toBe(true);
});
});
describe('LEAGUE MEMBERSHIP — exhibitions and All-Star are not league games', () => {
const teamsPayload = {
sports: [{ leagues: [{ teams: [{ team: { abbreviation: 'NY' } }, { team: { abbreviation: 'IND' } }] }] }],
};
const fetchImpl = (url) => Promise.resolve(url.includes('/teams') ? teamsPayload : summary());
it('a league game passes through', async () => {
const out = await adapter.getGameRows('401857134', { fetchImpl, force: true });
expect(out.rows.length).toBe(2);
});
it('a game against a NON-LEAGUE side is refused, and names who', async () => {
// The season pull surfaced exactly two: an exhibition vs Nigeria and the
// All-Star game. Their usage/pace context is meaningless for a forward
// projection, so counting them would pollute every profile spanning them.
const exhibition = summary();
exhibition.header.competitions[0].competitors[0].team.abbreviation = 'NIGER';
exhibition.boxscore.teams[1].team.abbreviation = 'NIGER';
const f = (url) => Promise.resolve(url.includes('/teams') ? teamsPayload : exhibition);
const out = await adapter.getGameRows('401867794', { fetchImpl: f, force: true });
expect(out.rows).toHaveLength(0);
expect(out.skipped).toMatch(/^not_a_league_game:/);
expect(out.skipped).toContain('NIGER');
});
it('an EMPTY teams response does not silently filter the whole league away', async () => {
// A failed feed that degrades to an empty index looks exactly like an honest
// absence — the fielding_oaa lesson. Unknown membership means NO filtering,
// never "nothing is in the league".
const empty = { sports: [{ leagues: [{ teams: [] }] }] };
const f = (url) => Promise.resolve(url.includes('/teams') ? empty : summary());
const out = await adapter.getGameRows('401857134', { fetchImpl: f, force: true });
expect(out.rows.length).toBe(2);
});
});
describe('THE AS-OF RULE — point-in-time is structural here', () => {
const games = [
{ game_date: '2026-08-01', minutes: 30, usage_rate: 24, team_pace: 96, team_possessions: 80, ts_pct: 0.55, efg_pct: 0.5, starter: true, final_margin: 5, team: 'NY', game_id: 'g1', source_id: '1' },
{ game_date: '2026-08-04', minutes: 34, usage_rate: 28, team_pace: 98, team_possessions: 82, ts_pct: 0.58, efg_pct: 0.52, starter: true, final_margin: -3, team: 'NY', game_id: 'g2', source_id: '1' },
{ game_date: '2026-08-07', minutes: 20, usage_rate: 30, team_pace: 94, team_possessions: 78, ts_pct: 0.50, efg_pct: 0.48, starter: false, final_margin: 12, team: 'NY', game_id: 'g3', source_id: '1' },
{ game_date: '2026-08-11', minutes: 32, usage_rate: 26, team_pace: 99, team_possessions: 83, ts_pct: 0.60, efg_pct: 0.55, starter: true, final_margin: -14, team: 'NY', game_id: 'g4', source_id: '1' },
].map((g) => ({ sport: 'wnba', player_key: 'test player', season: 2026, ...g }));
/** A stub client driving the REAL paginate + read path. */
const client = (rows) => ({
from() {
const st = { rows: rows.slice(), order: [] };
const q = {
select: () => q,
eq: (c, v) => { st.rows = st.rows.filter((r) => r[c] === v); return q; },
lt: (c, v) => { st.rows = st.rows.filter((r) => String(r[c]) < String(v)); return q; },
order: (c, o) => { st.order.push([c, !o || o.ascending !== false]); return q; },
range: async (a, b) => {
const sorted = st.rows.slice().sort((x, y) => {
for (const [c, asc] of st.order) {
const cmp = String(x[c]).localeCompare(String(y[c]));
if (cmp) return asc ? cmp : -cmp;
}
return 0;
});
return { data: sorted.slice(a, b + 1), error: null };
},
};
return q;
},
});
it('reads STRICTLY before the cutoff — a game ON the date is excluded', async () => {
// The leak this prevents: a 2026-08-11 game may tip after grade time, so
// counting it feeds the evening being predicted into the prediction. Same
// rule as snapshotSettlementService.isPreGame.
const p = await usage.profileAsOf(client(games), { playerKey: 'test player', asOf: '2026-08-11' });
expect(p.games).toBe(3);
expect(p.last_game_date).toBe('2026-08-07');
});
it('a later cutoff sees more games — the profile MOVES with the date', async () => {
const early = await usage.profileAsOf(client(games), { playerKey: 'test player', asOf: '2026-08-08' });
const late = await usage.profileAsOf(client(games), { playerKey: 'test player', asOf: '2026-08-12' });
expect(early.games).toBe(3);
expect(late.games).toBe(4);
// The whole point of a point-in-time read: the same player reads differently
// on different dates, because the history behind him is different.
expect(early.usage_rate).not.toBe(late.usage_rate);
expect(early.last_game_date).toBe('2026-08-07');
expect(late.last_game_date).toBe('2026-08-11');
});
it('REFUSES on thin history rather than returning a league-average player', async () => {
// Usage feeds the chain's opportunity term directly; a stand-in profile
// would assert a usage rate about someone never observed.
expect(await usage.profileAsOf(client(games), { playerKey: 'test player', asOf: '2026-08-04' })).toBeNull();
expect(await usage.profileAsOf(client(games), { playerKey: 'nobody', asOf: '2026-12-01' })).toBeNull();
});
it('usage is MINUTES-WEIGHTED, not a flat mean across games', async () => {
// The lineup-K-rate lesson: an unweighted aggregate counts a 20-minute
// cameo like a 34-minute start, and unweighted HURT that model.
const p = await usage.profileAsOf(client(games), { playerKey: 'test player', asOf: '2026-12-01' });
const flat = (24 + 28 + 30 + 26) / 4;
const weighted = (24 * 30 + 28 * 34 + 30 * 20 + 26 * 32) / (30 + 34 + 20 + 32);
expect(p.usage_rate).toBeCloseTo(weighted, 3);
expect(p.usage_rate).not.toBeCloseTo(flat, 3);
});
it('delivers every chainFn input, and the redistribute game-state', async () => {
const p = await usage.profileAsOf(client(games), { playerKey: 'test player', asOf: '2026-12-01' });
for (const f of ['usage_rate', 'minutes_per_game', 'team_pace', 'team_possessions', 'ts_pct', 'efg_pct']) {
expect(p[f]).not.toBeNull();
}
expect(p.starter_rate).toBeCloseTo(0.75, 3);
expect(p.avg_final_margin).toBeCloseTo(0, 3);
expect(p.as_of).toBe('2026-12-01'); // the cutoff is reported, not implied
});
});
describe('the feed is registered and scoped', () => {
it('its unique key is in tableKeys, so safePaginate can walk it', () => {
const { uniqueKeyFor } = require('../../src/utils/tableKeys');
expect(uniqueKeyFor('wnba_player_game')).toEqual(['game_id', 'source_id']);
});
it('MLB and the chain shadow are untouched by this order', () => {
const fs = require('fs');
const path = require('path');
const root = path.join(__dirname, '..', '..');
// The WNBA feed must not have reached into the MLB engine or the served path.
for (const f of ['src/services/model/chainShadow.js', 'src/services/model/baseballChain.js',
'src/services/intelligence/analyzeViaEngine1.js']) {
expect(fs.readFileSync(path.join(root, f), 'utf8')).not.toMatch(/wnbaUsage|espnWnbaAdapter|wnba_player_game/);
}
});
});