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:
@@ -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)]);
|
||||
|
||||
Reference in New Issue
Block a user