'use strict'; /** * The portable chain + the calibration gate that guards it. * * The failure these exist to prevent is specific: compounding probabilities that * are individually survivable and jointly catastrophic. A 4-leg ticket of "90%" * legs whose realized rate is 63% is a 4.4x overstatement, in the direction the * user pays for. */ const chain = require('../../src/services/model/chain'); const cal = require('../../src/services/model/calibration'); const atom = (id, p, extra = {}) => ({ id, p, calibrated: true, gameId: `g${id}`, ...extra }); describe('CHAIN ACROSS — the calibration gate is structural', () => { it('REFUSES to compound an uncalibrated atom', () => { const out = chain.chainAcross([atom('a', 0.9), { ...atom('b', 0.9), calibrated: false }]); expect(out.ok).toBe(false); expect(out.reason).toBe('uncalibrated_atoms'); expect(out.uncalibrated).toEqual(['b']); }); it('the refusal is the feature — this is the 4.4x case, made unbuildable', () => { // Model 0.91^4 = 0.686; realized 0.63^4 = 0.157. const legs = ['a', 'b', 'c', 'd'].map((k) => ({ ...atom(k, 0.91), calibrated: false })); expect(chain.chainAcross(legs).ok).toBe(false); // And the same legs, once genuinely calibrated, DO compound. const ok = chain.chainAcross(legs.map((l) => ({ ...l, calibrated: true }))); expect(ok.ok).toBe(true); expect(ok.compound_probability).toBeCloseTo(0.91 ** 4, 3); }); it('independent cross-game legs multiply', () => { const out = chain.chainAcross([atom('a', 0.8), atom('b', 0.5)]); expect(out.independent_probability).toBeCloseTo(0.4, 6); expect(out.cross_game_legs).toBe(2); }); it('correlated same-game legs are NOT treated as independent', () => { const legs = [atom('a', 0.7, { gameId: 'G1' }), atom('b', 0.7, { gameId: 'G1' })]; const indep = chain.chainAcross(legs); const corr = chain.chainAcross(legs, { correlation: () => 0.6 }); // Shared pitcher/park/weather makes them land together more often than // independence implies — and independence is the FLATTERING error here. expect(corr.compound_probability).toBeGreaterThan(indep.compound_probability); expect(corr.cross_game_legs).toBe(1); }); it('an unreadable atom is dropped, never counted as probability zero', () => { const out = chain.chainAcross([atom('a', 0.8), { id: 'b', p: null, calibrated: true }]); expect(out.ok).toBe(true); expect(out.legs).toBe(1); // a p=0 leg would have zeroed the ticket expect(out.compound_probability).toBeCloseTo(0.8, 6); }); it('no usable atoms is an explicit refusal, not a zero', () => { expect(chain.chainAcross([]).ok).toBe(false); expect(chain.chainAcross([{ id: 'x', p: null }]).reason).toBe('no_usable_atoms'); }); }); 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)]); expect(out.expected_value).toBeCloseTo(1.2, 6); expect(out.contributors).toBe(3); }); it('respects weights, and an absent weight contributes ONCE not zero', () => { const out = chain.chainUp([atom('a', 0.5, { weight: 4 }), atom('b', 0.5)]); expect(out.expected_value).toBeCloseTo(2.5, 6); }); it('the archetype-redistribution HOOK is dormant unless supplied', () => { const legs = [atom('star', 0.6), atom('bench', 0.1)]; expect(chain.chainUp(legs).redistributed).toBe(false); }); it('and LIVE when a sport supplies it — a blowout fades the star, feeds the bench', () => { // Dormant in baseball (a nine-run lead does not change who bats next); // this is the basketball case the hook exists for. const legs = [atom('star', 0.6), atom('bench', 0.1)]; const out = chain.chainUp(legs, { context: { blowout: true }, redistribute: (as, ctx) => (ctx.blowout ? as.map((a) => (a.id === 'star' ? { ...a, p: a.p * 0.6 } : { ...a, p: a.p * 2 })) : as), }); expect(out.redistributed).toBe(true); expect(out.expected_value).toBeCloseTo(0.6 * 0.6 + 0.1 * 2, 6); }); }); describe('SELF-CHECK — the model flagging its own suspect calls', () => { it('flags an internal inconsistency as LOW CONFIDENCE, without guessing which side is wrong', () => { const out = chain.selfCheck({ perEntity: [{ p: 0.5 }, { p: 0.5 }, { p: 0.5 }], // sums to 1.5 teamRead: 4.0, }); expect(out.confidence).toBe('LOW'); expect(out.flags.map((f) => f.flag)).toContain('INTERNAL_INCONSISTENCY'); expect(out.flags[0].consequence).toMatch(/do not know which/); }); it('agreement is NORMAL confidence', () => { const out = chain.selfCheck({ perEntity: [{ p: 1.0 }, { p: 1.1 }], teamRead: 2.05 }); expect(out.confidence).toBe('NORMAL'); expect(out.flags).toEqual([]); }); it('market divergence FLAGS the script but never claims we are right', () => { const out = chain.selfCheck({ perEntity: [{ p: 5.5 }], teamRead: 5.5, marketRead: 3.5 }); const f = out.flags.find((x) => x.flag === 'SCRIPT_DIVERGES_FROM_MARKET'); expect(f).toBeTruthy(); expect(f.consequence).toMatch(/either the best or the worst/); // Divergence is not an error signal, so it does not downgrade confidence. expect(out.confidence).toBe('NORMAL'); }); }); describe('PROPAGATION — one settled result improves every reading that shares the atom', () => { it('moves a thin atom a lot and a heavy atom barely at all', () => { const thin = chain.propagate({ id: 'a', p: 0.5, n: 4 }, { won: 1 }); const heavy = chain.propagate({ id: 'a', p: 0.5, n: 400 }, { won: 1 }); expect(thin.p - 0.5).toBeGreaterThan((heavy.p - 0.5) * 10); // That gap is the difference between learning and chasing noise. expect(heavy.n).toBe(401); }); it('an unreadable observation changes nothing', () => { const before = { id: 'a', p: 0.5, n: 10 }; expect(chain.propagate(before, { won: null })).toEqual(before); expect(chain.propagate(before, null)).toEqual(before); }); }); describe('CALIBRATION — the gate itself', () => { const rows = (spec) => spec.flatMap(([p, n, hitRate]) => Array.from({ length: n }, (_, i) => ({ p, won: i < Math.round(n * hitRate) ? 1 : 0 }))); it('passes a model whose stated probabilities are its realized rates', () => { const out = cal.isCalibrated(rows([[0.3, 100, 0.30], [0.5, 100, 0.50], [0.9, 100, 0.90]])); expect(out.calibrated).toBe(true); }); it('FAILS the real hits curve — over-confident exactly where a parlay stacks', () => { const out = cal.isCalibrated(rows([[0.45, 152, 0.49], [0.75, 152, 0.605], [0.91, 100, 0.63]])); expect(out.calibrated).toBe(false); expect(out.reason).toBe('bin_error_exceeds_tolerance'); expect(out.worst_high_confidence_bin.error).toBeGreaterThan(0.15); }); it('refuses to judge on too little data rather than guessing', () => { expect(cal.isCalibrated(rows([[0.5, 20, 0.5]])).reason).toBe('insufficient_sample'); }); it('isotonic fitting corrects the numbers while PRESERVING the ordering', () => { const map = cal.fitIsotonic(rows([[0.45, 152, 0.49], [0.75, 152, 0.605], [0.91, 100, 0.63]])); expect(map).not.toBeNull(); const lo = cal.applyIsotonic(map, 0.45); const hi = cal.applyIsotonic(map, 0.91); expect(hi).toBeGreaterThanOrEqual(lo); // ordering survives expect(hi).toBeLessThan(0.75); // the 0.91 claim is corrected down hard }); });