/* ============================================================ Session 71 — CHAMPION / CHALLENGER. The champion serves and grades users, unchanged. The challenger runs live and is measured. These lock the three properties that make the A/B worth running: isolation, interpretability, and identity-where-there-is-no-signal. ============================================================ */ const ch = require('../../src/services/challengerProjection'); const axes = require('../../src/services/archetypeAxes'); const JUDGE = axes.classifyPlayer({ role: 'batter', bats: 'R', sample_pa: 261, k_pct: 27.6, bb_pct: 16.1, chase_pct: 25.8, barrel_pct: 21.7, hard_hit_pct: 57.3, avg_launch_angle: 14.6, sweet_spot_pct: 33.6, }); const BELL = axes.classifyPlayer({ role: 'batter', bats: 'S', sample_pa: 387, k_pct: 21.7, bb_pct: 7.5, chase_pct: 30.6, barrel_pct: 10.3, hard_hit_pct: 43.4, avg_launch_angle: 13.9, sweet_spot_pct: 33.8, }); const SKUBAL = axes.classifyPlayer({ role: 'pitcher', role_detail: 'starter', throws: 'L', sample_ip: 82.2, k_pct: 30.5, bb_pct: 3.4, chase_pct: 36.7, barrel_pct: 6.7, gb_pct: 49, fb_pct: 22.6, arm_angle: 46.9, pitch_mix: [{ type: 'FF', velo: 96.7 }], }); const THIN = axes.classifyPlayer({ role: 'batter', sample_pa: 12, barrel_pct: 30 }); describe('identical where there is no signal — the clean-experiment property', () => { it.each([ ['unremarkable player', BELL, 'no_distinctive_axis_or_environment'], ['thin sample', THIN, 'archetype_absent_or_thin'], ['no classification', null, 'archetype_absent_or_thin'], ])('%s → challenger === champion, exactly', (_n, cls, reason) => { const r = ch.adjust({ pWin: 0.42, direction: 'over', statType: 'home_runs', classification: cls }); expect(r.p_win_challenger).toBe(0.42); expect(r.delta).toBe(0); expect(r.adjustments).toEqual([]); expect(r.reason).toBe(reason); }); it('an unmapped stat leaves the champion untouched — no speculative mapping', () => { const r = ch.adjust({ pWin: 0.5, direction: 'over', statType: 'nonsense_stat', classification: JUDGE }); expect(r.p_win_challenger).toBe(0.5); expect(r.reason).toBe('stat_not_mapped'); }); it('no champion probability → nothing to adjust', () => { for (const p of [null, undefined, 0, 1]) { expect(ch.adjust({ pWin: p, direction: 'over', statType: 'hits', classification: JUDGE }).reason) .toBe('no_champion_probability'); } }); }); describe('interpretable — every difference is attributable', () => { it('Judge HR: BOMBER lifts, WHIFF RISK offsets, and both are named', () => { const r = ch.adjust({ pWin: 0.42, direction: 'over', statType: 'home_runs', classification: JUDGE }); const labels = r.adjustments.map((a) => a.label); expect(labels).toContain('BOMBER'); expect(labels).toContain('WHIFF RISK'); expect(r.adjustments.find((a) => a.label === 'BOMBER').nudge).toBeGreaterThan(0); expect(r.adjustments.find((a) => a.label === 'WHIFF RISK').nudge).toBeLessThan(0); expect(r.p_win_challenger).toBeGreaterThan(0.42); }); it('Skubal strikeouts: WHIFF + TRAPDOOR + CANNON all push the same way', () => { const r = ch.adjust({ pWin: 0.6, direction: 'over', statType: 'strikeouts', classification: SKUBAL }); expect(r.p_win_challenger).toBeGreaterThan(0.6); expect(r.adjustments.every((a) => a.nudge > 0)).toBe(true); }); it('a strikeout arm makes hits-allowed LESS likely — sign is per stat', () => { const r = ch.adjust({ pWin: 0.5, direction: 'over', statType: 'hits_allowed', classification: SKUBAL }); expect(r.p_win_challenger).toBeLessThan(0.5); }); it('opposing signals can cancel to exactly zero — an honest "no lean"', () => { const r = ch.adjust({ pWin: 0.55, direction: 'over', statType: 'strikeouts', classification: JUDGE }); expect(r.delta).toBe(0); expect(r.adjustments.length).toBe(2); // both still recorded }); }); describe('direction and bounds', () => { it('UNDER mirrors OVER exactly', () => { const over = ch.adjust({ pWin: 0.42, direction: 'over', statType: 'home_runs', classification: JUDGE }); const under = ch.adjust({ pWin: 0.58, direction: 'under', statType: 'home_runs', classification: JUDGE }); expect(under.delta).toBeCloseTo(-over.delta, 3); }); it('log-odds space keeps every output a valid probability', () => { for (const p of [0.02, 0.5, 0.98]) { const r = ch.adjust({ pWin: p, direction: 'over', statType: 'strikeouts', classification: SKUBAL }); expect(r.p_win_challenger).toBeGreaterThan(0); expect(r.p_win_challenger).toBeLessThan(1); } }); it('the total nudge is capped — a stack of axes cannot re-forecast', () => { const r = ch.adjust({ pWin: 0.5, direction: 'over', statType: 'strikeouts', classification: SKUBAL }); expect(Math.abs(r.delta)).toBeLessThan(0.12); // a lean, not a re-forecast }); }); describe('isolation — the champion is never touched', () => { it('adjust() is pure: same input, same output, no shared state', () => { const a = ch.adjust({ pWin: 0.42, direction: 'over', statType: 'home_runs', classification: JUDGE }); const b = ch.adjust({ pWin: 0.42, direction: 'over', statType: 'home_runs', classification: JUDGE }); expect(a).toEqual(b); }); it('attachChallenger preserves p_win byte-for-byte on every grade', async () => { const grades = [ { player: 'Aaron Judge', stat_type: 'home_runs', direction: 'over', p_win: 0.42, grade: 'B' }, { player: 'Josh Bell', stat_type: 'home_runs', direction: 'over', p_win: 0.33, grade: 'C' }, ]; const out = await ch.attachChallenger(grades, (n) => (n === 'Aaron Judge' ? JUDGE : BELL)); expect(out[0].p_win).toBe(0.42); expect(out[1].p_win).toBe(0.33); expect(out[0].p_win_challenger).toBeGreaterThan(0.42); // moved expect(out[1].p_win_challenger).toBe(0.33); // identical expect(out[1].challenger_delta).toBe(0); }); it('stamps a version so a future adjustment is distinguishable', async () => { const out = await ch.attachChallenger([{ player: 'x', stat_type: 'hits', p_win: 0.5 }], () => null); expect(out[0].challenger_version).toBe(ch.CHALLENGER_VERSION); }); }); describe('retention + serving contract', () => { const fs = require('fs'); it('the ledger retains the challenger beside the champion on one row', () => { const src = fs.readFileSync(require.resolve('../../src/services/ledgerService'), 'utf8'); expect(src).toMatch(/p_win_challenger: numOrNull\(g\.p_win_challenger\)/); expect(src).toMatch(/challenger_adjustments/); }); it('the snapshot writes the CHALLENGER-carrying grades but serves `enriched`', () => { const src = fs.readFileSync(require.resolve('../../src/services/snapshotService'), 'utf8'); expect(src).toMatch(/recordPipelineGrades\(sp, withChallenger, props/); // the served snapshot payload is still the untouched champion object expect(src).toMatch(/grades: enriched/); }); it('a challenger failure can never break the pipeline it is measured inside', () => { const src = fs.readFileSync(require.resolve('../../src/services/snapshotService'), 'utf8'); expect(src).toMatch(/\[challenger\] \$\{sp\} skipped/); }); });