1f40014256
routed as date-driven PHASE 0 — threshold derived BLIND, before any stat was re-read. A reversal is informative only if that date's Brier delta is distinguishable from zero at its row count. Per-row Brier difference d_i = (pc-y)^2 - (p-y)^2, so SE(n) = SD(d)/sqrt(n) and n* = (SD(d)/|effect|)^2. Pooled across all four stats so no single stat's verdict could shape the threshold deciding it: pooled rows 3,417 | SD(d) 0.09816 | |effect| 0.01175 n* = (0.09816/0.01175)^2 = 69.8 -> 70 The hand-chosen 20 sat at 0.54 SE -- a coin flip. That is the defect this removes, and why the previous verdict moved with the number. Committed as calibrationRegistry.LODO_MIN_HELD_ROWS = 70 with LODO_THRESHOLD_BASIS; a test recomputes (SD/effect)^2 and asserts it equals the constant, so it cannot drift from its own justification. The derivation script prints no stat verdict, no date and no reversal. PHASE 1 — LODO at n*, applied cold: hits 5 informative drops, 0 reversals PASS total_bases 4 informative drops, 0 reversals PASS rbi reverses 2026-08-01 (n=99) FAIL runs reverses 08-01 (n=86), 08-05 (244) FAIL hits held-out deltas -0.0041/-0.0080/-0.0192/-0.0140/-0.0139 across 123-272 row dates, favourite sign holding on every testable drop. THIS IS THE INSTRUMENT FINALLY POWERED, NOT VINDICATION OF A PREDICTION -- the withdrawal at6ae11f1was correct on the instrument available then, which admitted 20- and 25-row dates as evidence. Nothing about hits changed; the threshold stopped being chosen. PHASE 2 — both failures are DATE-DRIVEN, not underpowered. Every reversal sits above n*=70 (99, 86, 244), so no threshold and no further accrual rescues either: isotonic is fitting day-structure. Routed to the low-parameter calibrator queue (Platt/beta), not built here. PHASE 3 — CALIBRATION_DEPLOYED is now ['hits','total_bases'], frozen and tested, both PROVISIONAL with auto-demotion armed and the >=40 date-cluster promotion bar unchanged. hits stackability for chain.chainAcross is RESTORED, and the record shows it returned through the powered gate rather than by fiat. hits bands rebuilt on p_win_calibrated (765 eval rows): every archetype still one band, still base_rate -- calibrated YES, proven-per-archetype NO. PHASE 4 logged: the deploy set is now set by a power-derived, pre-committed, tested constant rather than an operator-chosen number. At6ae11f1that rule moved the live path AGAINST the operator; it has now moved it back on the same evidence because the instrument changed. Both directions are the rule working. And calibrated p_win separates within archetype no better than raw across 13 archetype slots on two deployed stats -- per-archetype separation will come from proven factors or not at all. p_win never mutated; no Bonferroni slot consumed; counter and frozen clusters verified byte-identical file by file. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W1sivYNqY2TS5ftykmHBU9
153 lines
6.2 KiB
JavaScript
153 lines
6.2 KiB
JavaScript
'use strict';
|
|
|
|
/**
|
|
* Which stats may serve a calibrated number.
|
|
*
|
|
* The thing these protect is the meaning of PROVISIONAL: a provisional deploy
|
|
* that cannot be taken away is just a deploy.
|
|
*/
|
|
|
|
const { createRegistry, STATUS, PROMOTION_DATE_CLUSTERS } = require('../../src/services/model/calibrationRegistry');
|
|
|
|
const MAP = [{ lo: 0.5, hi: 0.7, value: 0.55, n: 300 }];
|
|
const GOOD = { lodo_pass: true, ci: [-0.0061, -0.0045], map: MAP, certified_bands: [[0.6, 0.8]], date_clusters: 7, at: '2026-08-06' };
|
|
|
|
describe('deploy needs BOTH gates', () => {
|
|
it('deploys when LODO passes and the interval excludes zero', () => {
|
|
const r = createRegistry();
|
|
expect(r.deploy('total_bases', GOOD).status).toBe(STATUS.PROVISIONAL);
|
|
});
|
|
|
|
it('refuses on a LODO failure however good the interval', () => {
|
|
const r = createRegistry();
|
|
const out = r.deploy('runs', { ...GOOD, lodo_pass: false });
|
|
expect(out.ok).toBe(false);
|
|
expect(out.reason).toMatch(/date-driven/);
|
|
});
|
|
|
|
it('refuses when the interval spans zero however clean the LODO', () => {
|
|
const r = createRegistry();
|
|
const out = r.deploy('hits', { ...GOOD, ci: [-0.01, 0.002] });
|
|
expect(out.ok).toBe(false);
|
|
expect(out.reason).toMatch(/does not exclude zero/);
|
|
});
|
|
|
|
it('refuses without a map — there is nothing to serve', () => {
|
|
const r = createRegistry();
|
|
expect(r.deploy('hits', { ...GOOD, map: null }).ok).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('auto-demotion is what makes provisional honest', () => {
|
|
it('demotes on the first date where the interval stops excluding zero', () => {
|
|
const r = createRegistry();
|
|
r.deploy('total_bases', GOOD);
|
|
const out = r.reverify('total_bases', { ci: [-0.004, 0.001], date: '2026-08-07' });
|
|
expect(out.status).toBe(STATUS.NONE);
|
|
expect(out.reason).toBe('ci_no_longer_excludes_zero');
|
|
expect(out.breaking_date).toBe('2026-08-07');
|
|
expect(r.serves('total_bases', 0.65).serve).toBe(false);
|
|
});
|
|
|
|
it('demotes when the favourite over-prediction flips sign', () => {
|
|
// A flip means the correction is now pushing the wrong way.
|
|
const r = createRegistry();
|
|
r.deploy('total_bases', GOOD);
|
|
const out = r.reverify('total_bases', { ci: [-0.006, -0.004], favourite_bias: -0.03, date: '2026-08-08' });
|
|
expect(out.status).toBe(STATUS.NONE);
|
|
expect(out.reason).toBe('favourite_bias_flipped');
|
|
});
|
|
|
|
it('logs the demotion with its breaking date', () => {
|
|
const r = createRegistry();
|
|
r.deploy('total_bases', GOOD);
|
|
r.reverify('total_bases', { ci: [0.001, 0.004], date: '2026-08-09' });
|
|
const ev = r.log().find((e) => e.event === 'auto_demoted');
|
|
expect(ev).toMatchObject({ stat: 'total_bases', at: '2026-08-09' });
|
|
});
|
|
|
|
it('stays deployed while both conditions hold', () => {
|
|
const r = createRegistry();
|
|
r.deploy('total_bases', GOOD);
|
|
const out = r.reverify('total_bases', { ci: [-0.007, -0.003], favourite_bias: 0.17, date: '2026-08-07' });
|
|
expect(out.status).toBe(STATUS.PROVISIONAL);
|
|
expect(out.changed).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('the >=40 date-cluster bar is the PROMOTION bar, not the deploy bar', () => {
|
|
it('does not block deployment', () => {
|
|
const r = createRegistry();
|
|
expect(r.deploy('total_bases', { ...GOOD, date_clusters: 7 }).ok).toBe(true);
|
|
});
|
|
|
|
it('promotes out of provisional once it is met', () => {
|
|
const r = createRegistry();
|
|
r.deploy('total_bases', GOOD);
|
|
const out = r.reverify('total_bases', { ci: [-0.006, -0.004], date_clusters: PROMOTION_DATE_CLUSTERS, date: '2026-09-15' });
|
|
expect(out.status).toBe(STATUS.PROMOTED);
|
|
});
|
|
|
|
it('does not promote while the interval has stopped holding', () => {
|
|
const r = createRegistry();
|
|
r.deploy('total_bases', GOOD);
|
|
const out = r.reverify('total_bases', { ci: [-0.001, 0.003], date_clusters: 60, date: '2026-09-15' });
|
|
expect(out.status).toBe(STATUS.NONE);
|
|
});
|
|
});
|
|
|
|
describe('serving is band-limited', () => {
|
|
it('serves inside the certified band and refuses outside it', () => {
|
|
const r = createRegistry();
|
|
r.deploy('total_bases', GOOD);
|
|
expect(r.serves('total_bases', 0.65).serve).toBe(true);
|
|
expect(r.serves('total_bases', 0.65).provisional).toBe(true);
|
|
expect(r.serves('total_bases', 0.95).serve).toBe(false);
|
|
expect(r.serves('total_bases', 0.95).reason).toMatch(/outside the certified band/);
|
|
});
|
|
|
|
it('an undeployed stat never serves', () => {
|
|
const r = createRegistry();
|
|
expect(r.serves('hits', 0.6).serve).toBe(false);
|
|
expect(r.serves('hits', 0.6).reason).toBe('not deployed');
|
|
});
|
|
|
|
it('a missing p_win serves nothing', () => {
|
|
const r = createRegistry();
|
|
r.deploy('total_bases', GOOD);
|
|
expect(r.serves('total_bases', null).serve).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('the LODO held-row threshold is power-derived, not operator-chosen', () => {
|
|
const { LODO_MIN_HELD_ROWS, LODO_THRESHOLD_BASIS } = require('../../src/services/model/calibrationRegistry');
|
|
|
|
it('is the value its own stated derivation produces', () => {
|
|
// n* = (SD(d) / |effect|)^2 -- recomputed here so the constant cannot drift
|
|
// away from the basis that justifies it.
|
|
const { per_row_brier_diff_sd: sd, pooled_effect_abs_mean: eff } = LODO_THRESHOLD_BASIS;
|
|
expect(Math.ceil((sd / eff) ** 2)).toBe(LODO_MIN_HELD_ROWS);
|
|
});
|
|
|
|
it('carries its derivation basis, and was derived blind', () => {
|
|
expect(LODO_THRESHOLD_BASIS.rule).toBe('n* = (SD(d) / |effect|)^2');
|
|
expect(LODO_THRESHOLD_BASIS.derived_blind).toBe(true);
|
|
expect(LODO_THRESHOLD_BASIS.pooled_rows).toBeGreaterThan(1000);
|
|
});
|
|
|
|
it('rejects the thresholds that were previously chosen by hand', () => {
|
|
// 20 was the operator-chosen value whose verdict moved with it; anything
|
|
// below n* cannot distinguish a reversal from a coin flip.
|
|
for (const weak of [10, 20, 25, 30, 50]) {
|
|
const se = LODO_THRESHOLD_BASIS.per_row_brier_diff_sd / Math.sqrt(weak);
|
|
expect(LODO_THRESHOLD_BASIS.pooled_effect_abs_mean).toBeLessThan(se);
|
|
expect(weak).toBeLessThan(LODO_MIN_HELD_ROWS);
|
|
}
|
|
});
|
|
|
|
it('is informative at the committed value', () => {
|
|
const se = LODO_THRESHOLD_BASIS.per_row_brier_diff_sd / Math.sqrt(LODO_MIN_HELD_ROWS);
|
|
expect(LODO_THRESHOLD_BASIS.pooled_effect_abs_mean).toBeGreaterThanOrEqual(se * 0.99);
|
|
});
|
|
});
|