'use strict'; /** * What a letter is allowed to mean. * * The failure these guard against is the one the whole programme keeps circling: * a grade that LOOKS like it read tonight's matchup while reading nothing. Here * that would be a band labelled factor-informed on an archetype where no factor * ever proved — which is the state of every archetype today. */ const gb = require('../../src/services/model/gradeBands'); /** n rows whose outcome rate genuinely tracks p, interleaved (never front-loaded). */ function rows(specs) { const out = []; for (const [p, n, rate] of specs) { for (let i = 0; i < n; i += 1) { const won = Math.floor((i + 1) * rate) > Math.floor(i * rate) ? 1 : 0; out.push({ p, won }); } } return out; } describe('the two-bar rule is structural', () => { const data = rows([[0.75, 120, 0.75], [0.6, 120, 0.6], [0.45, 120, 0.45]]); it('with NOTHING proven — the state today — no band can be factor-informed', () => { const out = gb.buildBands(data, { archetype: 'GHOST' }); expect(out.basis).toBe('base_rate'); expect(out.two_bar.factor_informed_allowed).toBe(false); expect(out.two_bar.reason).toMatch(/no factor has passed the gate/); expect(out.bands.every((b) => b.basis === 'base_rate')).toBe(true); }); it('PROVEN but not calibrated is still a base-rate read, and names why', () => { // Both bars, or neither claim. A proven factor whose numbers are not // certified honest cannot carry a letter that asserts a rate. const out = gb.buildBands(data, { archetype: 'GHOST', proven: true }); expect(out.basis).toBe('base_rate'); expect(out.two_bar.reason).toMatch(/not certified calibrated/); }); it('CALIBRATED but not proven is still a base-rate read', () => { const out = gb.buildBands(data, { archetype: 'GHOST', calibrated: true }); expect(out.basis).toBe('base_rate'); expect(out.two_bar.reason).toMatch(/no factor has passed the gate/); }); it('only BOTH unlocks factor-informed', () => { const out = gb.buildBands(data, { archetype: 'GHOST', proven: true, calibrated: true }); expect(out.basis).toBe('factor_informed'); expect(out.bands.every((b) => b.basis === 'factor_informed')).toBe(true); }); }); describe('lift is measured against the archetype\'s OWN base rate', () => { it('a band is only credited when its interval clears the base rate', () => { const out = gb.buildBands(rows([[0.8, 200, 0.80], [0.5, 200, 0.50], [0.3, 200, 0.30]]), { archetype: 'GHOST', targetBands: 3, }); const top = out.bands[0]; expect(top.shows_lift).toBe(true); expect(top.separation).toBe('above_base_rate'); expect(top.lift_vs_archetype_base).toBeGreaterThan(0); const bottom = out.bands[out.bands.length - 1]; expect(bottom.shows_deficit).toBe(true); }); it('the same realized rate is lift for one archetype and not for another', () => { // 62% is a real read for a profile that hits 45%, and slightly under water // for one that hits 68%. A raw-rate band would call both the same letter. const lowBase = gb.buildBands(rows([[0.62, 300, 0.62], [0.4, 300, 0.40]]), { archetype: 'LOW', targetBands: 2 }); const highBase = gb.buildBands(rows([[0.62, 300, 0.62], [0.75, 300, 0.75]]), { archetype: 'HIGH', targetBands: 2 }); expect(lowBase.bands[0].shows_lift).toBe(true); expect(highBase.bands.find((b) => b.p_range[0] === 0.62).shows_deficit).toBe(true); }); it('a band indistinguishable from the base rate claims NO lift', () => { const flat = gb.buildBands(rows([[0.7, 150, 0.55], [0.5, 150, 0.55], [0.3, 150, 0.55]]), { archetype: 'FLAT' }); for (const b of flat.bands) { expect(b.shows_lift).toBe(false); expect(b.separation).toBe('indistinguishable_from_base_rate'); } expect(flat.bands_showing_lift).toBe(0); }); }); describe('bands the ledger cannot stand behind', () => { it('indistinguishable neighbours are MERGED, not published as different letters', () => { // If we cannot tell two bands apart, calling one A and one B is a // distinction we have not measured. const out = gb.buildBands(rows([[0.7, 200, 0.60], [0.65, 200, 0.60], [0.6, 200, 0.60]]), { archetype: 'SAME', targetBands: 5, }); expect(out.bands.length).toBe(1); }); it('thin bands are PROVISIONAL, not silently dropped', () => { // "Still counting" and "nothing here" are different claims. const out = gb.buildBands(rows([[0.8, 20, 0.8], [0.3, 20, 0.3]]), { archetype: 'THIN', targetBands: 2 }); expect(out.bands.some((b) => b.provisional)).toBe(true); }); it('too few outcomes → no bands at all, with the refusal stated', () => { const out = gb.buildBands(rows([[0.6, 5, 0.6]]), { archetype: 'TINY' }); expect(out.bands).toEqual([]); expect(out.refused).toMatch(/insufficient settled outcomes/); }); it('the cumulative correction WIDENS every interval', () => { const data = rows([[0.75, 150, 0.75], [0.45, 150, 0.45]]); const one = gb.buildBands(data, { archetype: 'X', cumulativeTests: 1, targetBands: 2 }); const many = gb.buildBands(data, { archetype: 'X', cumulativeTests: 99, targetBands: 2 }); const width = (o) => o.bands[0].ci[1] - o.bands[0].ci[0]; expect(width(many)).toBeGreaterThan(width(one)); }); it('the interval never runs past 0 or 1', () => { const out = gb.buildBands(rows([[0.99, 60, 1.0], [0.01, 60, 0.0]]), { archetype: 'EDGE', targetBands: 2 }); for (const b of out.bands) { expect(b.ci[0]).toBeGreaterThanOrEqual(0); expect(b.ci[1]).toBeLessThanOrEqual(1); } }); }); describe('the reason attached to a grade is true or absent', () => { it('a base-rate band SAYS it is a base-rate read', () => { const out = gb.buildBands(rows([[0.75, 120, 0.75], [0.45, 120, 0.45]]), { archetype: 'BOMBER', targetBands: 2 }); const text = gb.reasoning(out.bands[0], { archetype: 'BOMBER' }); expect(text).toMatch(/base-rate read/); expect(text).toMatch(/no matchup factor is proven/); // The thing it must never do is imply a matchup was read. expect(text).not.toMatch(/matchup edge|favourable matchup/i); }); it('a factor-informed band with NO named proven factors returns nothing', () => { // Better to say nothing than to invent the why. const out = gb.buildBands(rows([[0.75, 120, 0.75], [0.45, 120, 0.45]]), { archetype: 'GHOST', proven: true, calibrated: true, targetBands: 2 }); expect(gb.reasoning(out.bands[0], { archetype: 'GHOST', provenFactors: [] })).toBeNull(); }); it('a factor-informed band names the factors that actually proved', () => { const out = gb.buildBands(rows([[0.75, 120, 0.75], [0.45, 120, 0.45]]), { archetype: 'GHOST', proven: true, calibrated: true, targetBands: 2 }); const text = gb.reasoning(out.bands[0], { archetype: 'GHOST', provenFactors: ['defense_by_direction'] }); expect(text).toMatch(/GHOST: defense_by_direction/); expect(text).toMatch(/proved for this profile/); }); });