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
+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)]);