'use strict'; /** * reAblation — THE SECOND LINE OF DEFENCE. * * The gate decides whether a feature earns its place. This decides whether it * KEEPS it. Those are different questions and only the first has ever been asked * here. * * WHY A PROVEN FEATURE CAN STOP BEING TRUE. Three ways, all of them real and * none of them a bug: * * 1. IT WAS NEVER TRUE. It cleared the bar on a lucky draw. More data is the * only thing that reveals this, and the cumulative correction makes it * rarer without making it impossible. * 2. THE GAME CHANGED. Baseball is not stationary — a league-wide shift in how * pitchers are used, or a rule change, can retire a real effect. * 3. THE BAR ROSE. The cumulative denominator only grows, so a feature proved * at alpha 0.05/20 is being held to 0.05/60 a year later. A feature that * cleared the old bar and not the new one is not being punished unfairly — * it is being held to what the programme has since earned the right to ask. * * A ledger that tightens its own standard and demotes its own features is more * credible than one that only ever adds. So the demotion is recorded with BOTH * p-values and the test count each was corrected against — anyone can see * exactly why, and re-derive it. * * PURE AND INJECTABLE: it takes evidence in and returns verdicts. It performs no * measurement itself and reaches no database, so the decision rule is testable * without a network and cannot quietly drift from the rule the gate uses. */ const { BASE_ALPHA } = require('./featureRegistry'); /** * Re-adjudicate ONE proven entry against the current cumulative denominator. * * @param {object} entry what was promoted, and on what evidence * @param {object} current the fresh measurement (may be absent) * @param {number} cumulativeTests the programme-lifetime distinct test count * @returns {object} an auditable verdict — never a bare boolean */ function readjudicate(entry, current, cumulativeTests) { const tests = Number(cumulativeTests); const correctedAlpha = Number.isFinite(tests) && tests >= 1 ? BASE_ALPHA / tests : BASE_ALPHA; const originalTests = Number(entry && entry.evidence && entry.evidence.bonferroni_tests); const originalAlpha = Number.isFinite(originalTests) && originalTests >= 1 ? BASE_ALPHA / originalTests : null; const originalP = Number(entry && entry.evidence && entry.evidence.p_value); const base = { key: entry && entry.key, archetype: entry && entry.archetype, stat: entry && entry.stat, original_p_value: Number.isFinite(originalP) ? originalP : null, original_bonferroni_tests: Number.isFinite(originalTests) ? originalTests : null, original_corrected_alpha: originalAlpha, cumulative_bonferroni_tests: Number.isFinite(tests) ? tests : null, cumulative_corrected_alpha: correctedAlpha, }; // NO FRESH MEASUREMENT — deliberately NOT a demotion. Absence of a re-test is // not evidence a feature stopped working, and demoting on it would punish // whichever stat happens to be off-season. if (!current || !Number.isFinite(Number(current.p_value))) { return { ...base, verdict: 'PENDING_RETEST', reason: 'no fresh measurement available' }; } const p = Number(current.p_value); const n = Number(current.n); const survives = p < correctedAlpha; return { ...base, current_p_value: p, current_n: Number.isFinite(n) ? n : null, verdict: survives ? 'SURVIVES' : 'DEMOTE', reason: survives ? `p ${p} < cumulative alpha ${correctedAlpha}` : `p ${p} no longer clears the cumulative alpha ${correctedAlpha}` + (originalAlpha !== null ? ` (it cleared ${originalAlpha} when promoted)` : ''), }; } /** * Re-adjudicate a whole registry. Returns the verdicts plus a summary that * states plainly what happened, including the case that matters most right now: * an empty proven set has nothing to re-adjudicate, and saying so is the honest * result rather than a no-op to be glossed. */ function readjudicateAll(provenEntries, measurements, cumulativeTests) { const entries = provenEntries || []; const verdicts = entries.map((e) => readjudicate(e, (measurements || {})[e.key], cumulativeTests)); return { cumulative_bonferroni_tests: cumulativeTests, cumulative_corrected_alpha: Number.isFinite(Number(cumulativeTests)) && cumulativeTests >= 1 ? BASE_ALPHA / cumulativeTests : BASE_ALPHA, proven_entries_examined: entries.length, survived: verdicts.filter((v) => v.verdict === 'SURVIVES').length, demoted: verdicts.filter((v) => v.verdict === 'DEMOTE').length, pending_retest: verdicts.filter((v) => v.verdict === 'PENDING_RETEST').length, verdicts, summary: entries.length === 0 ? 'NOTHING TO RE-ADJUDICATE — the proven set is empty.' : `${verdicts.filter((v) => v.verdict === 'DEMOTE').length} demoted of ${entries.length}.`, }; } module.exports = { readjudicate, readjudicateAll };