'use strict'; /** * Standing re-ablation + the structural bar on promotion. * * Together these close the hole that makes a retroactive "re-adjudicate * everything" pass necessary: a promotion cannot use a laxer correction than * the programme has earned, and a proven feature is re-checked against the * cumulative bar rather than the one it was promoted under. */ const ra = require('../../src/services/model/reAblation'); const reg = require('../../src/services/model/featureRegistry'); afterEach(() => reg.__reset()); const promoted = (p, tests) => ({ key: 'mlb|hits|BOMBER|barrel_x_park', archetype: 'BOMBER', stat: 'hits', evidence: { p_value: p, bonferroni_tests: tests, n: 600 }, }); describe('re-adjudication against the CUMULATIVE bar', () => { it('demotes a feature that cleared the old bar but not the grown one', () => { // Promoted at alpha 0.05/20 = 0.0025 with p = 0.002. The programme has since // tested 60 hypotheses, so the bar is 0.05/60 = 0.00083. const v = ra.readjudicate(promoted(0.002, 20), { p_value: 0.002, n: 700 }, 60); expect(v.verdict).toBe('DEMOTE'); expect(v.original_corrected_alpha).toBeCloseTo(0.0025, 6); expect(v.cumulative_corrected_alpha).toBeCloseTo(0.05 / 60, 8); // Auditable: BOTH p-values and BOTH test counts are on the record. expect(v.original_p_value).toBe(0.002); expect(v.current_p_value).toBe(0.002); expect(v.original_bonferroni_tests).toBe(20); expect(v.cumulative_bonferroni_tests).toBe(60); expect(v.reason).toMatch(/no longer clears/); }); it('keeps a feature strong enough to clear the grown bar', () => { const v = ra.readjudicate(promoted(0.00001, 20), { p_value: 0.00002, n: 900 }, 60); expect(v.verdict).toBe('SURVIVES'); }); it('NO fresh measurement is NOT a demotion — absence is not evidence', () => { // Demoting here would punish whichever stat happens to be off-season. const v = ra.readjudicate(promoted(0.001, 20), null, 60); expect(v.verdict).toBe('PENDING_RETEST'); const v2 = ra.readjudicate(promoted(0.001, 20), { n: 900 }, 60); expect(v2.verdict).toBe('PENDING_RETEST'); }); it('an empty proven set re-adjudicates to nothing, and says so plainly', () => { const out = ra.readjudicateAll([], {}, 38); expect(out.proven_entries_examined).toBe(0); expect(out.demoted).toBe(0); expect(out.summary).toMatch(/NOTHING TO RE-ADJUDICATE/); expect(out.cumulative_corrected_alpha).toBeCloseTo(0.05 / 38, 8); }); it('reports per-slot, so a feature can survive for one archetype and die for another', () => { const strong = { ...promoted(0.00001, 20), key: 'k1', archetype: 'GHOST' }; const weak = { ...promoted(0.002, 20), key: 'k2', archetype: 'BOMBER' }; const out = ra.readjudicateAll([strong, weak], { k1: { p_value: 0.00002, n: 800 }, k2: { p_value: 0.002, n: 800 }, }, 60); expect(out.survived).toBe(1); expect(out.demoted).toBe(1); expect(out.verdicts.find((v) => v.key === 'k1').archetype).toBe('GHOST'); expect(out.verdicts.find((v) => v.key === 'k2').verdict).toBe('DEMOTE'); }); }); describe('promotion cannot use a laxer correction than the programme earned', () => { const base = { n: 600, lift: 0.04, ci95: [0.01, 0.07] }; it('refuses evidence with NO correction recorded at all', () => { expect(reg.promote('mlb', 'batter_barrel_pct', base).ok).toBe(false); }); it('refuses a PER-SESSION correction when the cumulative count is higher', () => { const r = reg.promote('mlb', 'batter_barrel_pct', { ...base, bonferroni_tests: 8 }, null, { cumulativeTests: 38 }); expect(r.ok).toBe(false); expect(r.reason).toBe('insufficient_evidence'); }); it('refuses a p-value that does not clear the corrected alpha', () => { expect(reg.promote('mlb', 'batter_barrel_pct', { ...base, bonferroni_tests: 38, p_value: 0.01 }, null, { cumulativeTests: 38 }).ok).toBe(false); }); it('accepts evidence corrected cumulatively AND significant under it', () => { const r = reg.promote('mlb', 'batter_barrel_pct', { ...base, bonferroni_tests: 38, p_value: 0.0005 }, null, { cumulativeTests: 38 }); expect(r.ok).toBe(true); expect(reg.isLive('mlb', 'batter_barrel_pct')).toBe(true); }); it('the same rule guards a PROVEN conditioning entry', () => { const bad = reg.recordConditioning({ sport: 'mlb', archetype: 'GHOST', stat: 'hits', interaction: 'defense_x_contact', skill: 'CONTACT', status: reg.STATUS.PROVEN, evidence: { ...base, bonferroni_tests: 8 }, }, { cumulativeTests: 38 }); expect(bad.ok).toBe(false); expect(bad.reason).toBe('insufficient_evidence_for_proven'); }); });