'use strict'; /** * The cumulative Bonferroni denominator, asserted as behaviour. * * The failure this prevents is silent and slow: correcting by 8 in a session * that tries 8 hypotheses, forever, while the programme as a whole has tried * sixty. Nothing looks wrong in any single run. */ const tl = require('../../src/services/model/testLedger'); const H = (o) => ({ sport: 'mlb', target: 'counter_residual', ...o }); describe('the denominator counts DISTINCT hypotheses across the lifetime', () => { it('accumulates across separate runs — it does not reset per session', async () => { const store = tl.memoryStore(); const run1 = await tl.recordAndCount(store, [ H({ stat: 'hits', archetype: 'BOMBER', interaction: 'barrel_x_park' }), H({ stat: 'hits', archetype: 'BOMBER', interaction: 'launch_x_gb' }), ]); expect(run1.cumulative_tests).toBe(2); const run2 = await tl.recordAndCount(store, [ H({ stat: 'hits', archetype: 'GHOST', interaction: 'defense_x_contact' }), ]); // A per-session correction would have said 1 here. The whole point is 3. expect(run2.cumulative_tests).toBe(3); expect(run2.new_this_run).toBe(1); }); it('RE-TESTING the same hypothesis does not inflate the denominator', async () => { const store = tl.memoryStore(); const h = H({ stat: 'hits', archetype: 'BOMBER', interaction: 'barrel_x_park' }); await tl.recordAndCount(store, [h]); const again = await tl.recordAndCount(store, [h]); // Waiting for more sample and re-asking is the SAME question, and must not // be punished — that discipline is what the programme depends on. expect(again.cumulative_tests).toBe(1); expect(again.repeat_this_run).toBe(1); expect(again.new_this_run).toBe(0); }); it('the current batch IS included — a run is corrected for its own tests', async () => { const store = tl.memoryStore(); const out = await tl.recordAndCount(store, [ H({ stat: 'rbi', interaction: 'a' }), H({ stat: 'rbi', interaction: 'b' }), ]); expect(out.cumulative_tests).toBe(2); }); it('the same interaction on a DIFFERENT stat/archetype/target is a NEW shot on goal', async () => { const store = tl.memoryStore(); await tl.recordAndCount(store, [H({ stat: 'hits', archetype: 'BOMBER', interaction: 'x' })]); const out = await tl.recordAndCount(store, [ H({ stat: 'total_bases', archetype: 'BOMBER', interaction: 'x' }), // new stat H({ stat: 'hits', archetype: 'GHOST', interaction: 'x' }), // new archetype H({ stat: 'hits', archetype: 'BOMBER', interaction: 'x', target: 'outcome' }), // new target ]); expect(out.cumulative_tests).toBe(4); expect(out.new_this_run).toBe(3); }); it('the corrected alpha only ever gets HARDER as the programme runs', async () => { const cv = require('../../src/services/model/correlateValidator'); const store = tl.memoryStore(); const a = await tl.recordAndCount(store, [H({ stat: 's', interaction: 'i1' })]); const b = await tl.recordAndCount(store, [H({ stat: 's', interaction: 'i2' })]); const alphaA = cv.VALIDATION_REQUIREMENTS.max_p_value / a.cumulative_tests; const alphaB = cv.VALIDATION_REQUIREMENTS.max_p_value / b.cumulative_tests; expect(alphaB).toBeLessThan(alphaA); }); it('the key is stable and case/shape insensitive', () => { expect(tl.testKey({ sport: 'MLB', stat: 'Hits', archetype: 'bomber', interaction: 'x', target: 'outcome' })) .toBe(tl.testKey({ sport: 'mlb', stat: 'hits', archetype: 'BOMBER', interaction: 'x', target: 'outcome' })); }); it('a missing archetype is a real pooled test, keyed distinctly from a per-archetype one', () => { expect(tl.testKey(H({ stat: 'hits', interaction: 'x' }))) .not.toBe(tl.testKey(H({ stat: 'hits', archetype: 'BOMBER', interaction: 'x' }))); }); });