diff --git a/scripts/check-adr-anchors.mjs b/scripts/check-adr-anchors.mjs index 43d0d06ceb..26dba8fc10 100644 --- a/scripts/check-adr-anchors.mjs +++ b/scripts/check-adr-anchors.mjs @@ -238,6 +238,47 @@ import { join } from 'node:path'; import { ANCHORS_DIR, assembleAnchors, loadAnchors, shardNameFor } from './adr-anchors.mjs'; +// ── The self-test's own battery roster and floor (#13489) ────────────────── +// +// `failures.length === 0` used to be this self-test's ONLY success condition, so +// "every case held" and "the cases never ran" printed the same line. Closed the +// way PR #13487 validated on check-doc-authoring: what is pinned is the +// registered NAMES, not a number. Every section opens with `battery('')`, +// every assertion is attributed to the battery most recently opened, and the +// floor requires the OPENED set to equal the DECLARED set with each battery at +// or above its own count. +// +// ⛔ A pinned TOTAL is not the repair: a battery dropping from 9 cases to 3 +// keeps a total "right" the moment a sibling grows. +// +// The counts are a FLOOR, not an equality — adding cases is ordinary work and +// must not red. A battery BELOW its floor means cases stopped running; the +// remedy is to find what stopped registering. +const SELF_TEST_BATTERIES = Object.freeze({ + 'the base directory audit — collisions, allowlists and unparseable names': 14, + 'A tombstone resolves citations but is not anchorable (#7329)': 15, + 'Cited numbers resolve (#6634)': 15, + 'Cited decision LETTERS resolve (#9592)': 26, + // ⛔ NOT today's count. This battery runs one case per `KNOWN_NUMBER_COLLISIONS` + // row (3 today) on top of 20 structural cases, and entries only ever LEAVE that + // list: a resolved collision is meant to be deleted, and the gate already fails + // a stale entry. A floor at 23 would redden every legitimate removal and train + // the next author to edit the floor, which is the one habit these floors exist + // to prevent. Pinned instead is the part that does not move with the list: the + // 20 structural cases ran AND at least one row was actually audited. + 'Live tree: green as shipped, red under ablation': 21, + 'the sharded registry\'s assembly (#6957)': 13, +}); + +// DELETING an entry silences that battery's floor exactly as effectively as +// zeroing it, so the roster's own size is pinned too. +const SELF_TEST_BATTERY_FLOOR = 6; + +// The key an assertion is filed under when no battery is open. It is not a +// declared battery, so it reds by the same set difference rather than silently +// inflating whichever battery happened to run last. +const UNATTRIBUTED_BATTERY = '(no battery open)'; + const ROOT = process.cwd(); /** Where the registry lives. One shard per anchor — see `scripts/adr-anchors.mjs`. */ const MAP_PATH = ANCHORS_DIR; @@ -1130,13 +1171,28 @@ if (ambiguous.length) { * has failed at its one job. */ function selfTest() { + // The battery ledger this self-test's floor is evaluated against (#13489). + // `battery()` opens a battery; every assertion below is attributed to the one + // most recently opened, so a section that stops running stops registering and + // names ITSELF at the floor rather than going quiet. + const seen = new Map(); + let openBattery = null; + const battery = (name) => { + openBattery = name; + }; + const registerCase = () => { + const b = openBattery ?? UNATTRIBUTED_BATTERY; + seen.set(b, (seen.get(b) ?? 0) + 1); + }; const failures = []; let checked = 0; /** @param {string} name @param {boolean} cond @param {string} detail */ const assert = (name, cond, detail) => { + registerCase(); checked++; if (!cond) failures.push(`${name} — ${detail}`); }; + battery('the base directory audit — collisions, allowlists and unparseable names'); const audit = (files, allowlist = []) => auditAdrDirectory(files, allowlist); const joined = (errs) => errs.join('\n'); @@ -1236,6 +1292,7 @@ function selfTest() { // exactly why one `records` set was not enough — so both are pinned, and a // test of only the red half would pass on an implementation that broke the // citation resolution the tombstone exists for. + battery('A tombstone resolves citations but is not anchorable (#7329)'); { const TOMB = [...BASE, '0003-withdrawn-something.md']; const { errors: e, records, nonDecisions } = audit(TOMB); @@ -1335,6 +1392,7 @@ function selfTest() { // the tracked tree the real scan reads, so a literal `ADR-` + four digits in // a fixture would be collected as a genuine citation and fail the gate it is // testing. `id('0202')` keeps the token out of the source. + battery('Cited numbers resolve (#6634)'); { const id = (n) => 'ADR-' + n; const RECORDS = new Set(['0090', '0107']); @@ -1446,6 +1504,7 @@ function selfTest() { // citations (measured: without the sub-decision rule, ADR-0120's lettered // gates and ADR-0020's numbered steps — 20 live citations — read as bad // letters); one too loose passes anything. Both halves are asserted. + battery('Cited decision LETTERS resolve (#9592)'); { const id = (n) => 'ADR-' + n; const idx = decisionIndexFor; @@ -1613,6 +1672,7 @@ function selfTest() { } // ── Live tree: green as shipped, red under ablation ────────────────────── + battery('Live tree: green as shipped, red under ablation'); let liveFiles = null; try { liveFiles = readdirSync(join(ROOT, ADR_DIR)); @@ -1825,6 +1885,7 @@ function selfTest() { // Two of these are the properties the split was adopted for, and they pull in // OPPOSITE directions — so testing only the happy one would leave a layout // that merges everything cleanly, including the two edits that must not. + battery('the sharded registry\'s assembly (#6957)'); { const shard = (file) => [shardNameFor(file), JSON.stringify({ file, adrs: ['ADR-0001'], invariant: 'x' })]; const from = (pairs) => { @@ -1917,6 +1978,51 @@ function selfTest() { failures.push(`self-test threw before finishing — ${e && e.stack ? e.stack : e}`); } + // ── The floor: every declared battery RAN, and ran its cases (#13489) ─── + // + // Evaluated after every battery has had its chance and BEFORE the verdict, so + // the success line below can only be printed by a run in which the set of + // batteries that registered assertions EQUALS the set declared. A set + // difference names WHICH battery stopped; a count says only that something did. + const floorFailure = (message) => { + failures.push(message); + }; + const declaredBatteries = Object.keys(SELF_TEST_BATTERIES); + let floorBreached = false; + if (declaredBatteries.length < SELF_TEST_BATTERY_FLOOR) { + floorBreached = true; + floorFailure( + `SELF_TEST_BATTERIES declares ${declaredBatteries.length} batteries, below the pinned ` + + `${SELF_TEST_BATTERY_FLOOR} — a battery deleted from the roster takes its own floor with it.`, + ); + } + for (const [name, count] of seen) { + if (declaredBatteries.includes(name)) continue; + floorBreached = true; + floorFailure( + `self-test battery "${name}" registered ${count} case(s) but is not declared in ` + + 'SELF_TEST_BATTERIES — an assertion attributed to no declared battery is one nothing floors.', + ); + } + for (const name of declaredBatteries) { + const count = seen.get(name) ?? 0; + if (count >= SELF_TEST_BATTERIES[name]) continue; + floorBreached = true; + floorFailure( + count === 0 + ? `self-test battery "${name}" DID NOT RUN — 0 cases registered, ${SELF_TEST_BATTERIES[name]} pinned. ` + + 'The verdict below would have claimed those cases hold.' + : `self-test battery "${name}" registered ${count} case(s), below its pinned floor of ` + + `${SELF_TEST_BATTERIES[name]} — cases that used to run no longer do.`, + ); + } + if (floorBreached) { + floorFailure( + 'A battery at or below its floor means cases STOPPED RUNNING — the battery is the bug, not the ' + + 'number. Find what stopped registering (an early return, a deleted block, a guard that now ' + + 'skips) and restore it.', + ); + } if (failures.length) { console.error(`✗ check-adr-anchors --self-test — ${failures.length} failure(s) of ${checked} assertion(s)\n`); for (const f of failures) console.error(' • ' + f + '\n'); diff --git a/scripts/check-agent-test-spelling.mjs b/scripts/check-agent-test-spelling.mjs index b4993d562e..b75408ebd2 100644 --- a/scripts/check-agent-test-spelling.mjs +++ b/scripts/check-agent-test-spelling.mjs @@ -818,25 +818,82 @@ function baseFixtureFiles(extra = {}) { // handshake is a flag rather than a returned sentinel. let selfTestReachedVerdict = false; +// ── The self-test's own battery roster and floor (#13489) ────────────────── +// +// `failures.length === 0` used to be this self-test's ONLY success condition, so +// "every case held" and "the cases never ran" printed the same line. Closed the +// way PR #13487 validated on check-doc-authoring: what is pinned is the +// registered NAMES, not a number. Every section opens with `battery('')`, +// every assertion is attributed to the battery most recently opened, and the +// floor requires the OPENED set to equal the DECLARED set with each battery at +// or above its own count. +// +// ⛔ A pinned TOTAL is not the repair: a battery dropping from 9 cases to 3 +// keeps a total "right" the moment a sibling grows. +// +// The counts are a FLOOR, not an equality — adding cases is ordinary work and +// must not red. A battery BELOW its floor means cases stopped running; the +// remedy is to find what stopped registering. +const SELF_TEST_BATTERIES = Object.freeze({ + 'classifier — forms that MUST be refused': 10, + 'classifier — forms that MUST be allowed (a false red here is worse than no gate)': 18, + 'the multi-line spelling a line-at-a-time reader misses': 2, + 'the command word is what pnpm would resolve': 5, + 'measured stripper carve-outs, both named': 2, + 'the SWEEP over a real temp tree — the walk, not the predicate': 5, + 'the escape hatch works — a declared counter-example is not a violation': 2, + 'the same tree WITHOUT the plant is green — the red above is the plant, not the fixture': 3, + 'anti-vacuity — every way a broken selector could wear a pass is a REFUSAL': 4, + 'the declared lists cannot quietly become mute buttons': 4, + 'the dispatch-gates declaration — both directions, derived from the scan roots': 8, + 'the derivation reads THIS workspace, and reads it non-empty': 4, +}); + +// DELETING an entry silences that battery's floor exactly as effectively as +// zeroing it, so the roster's own size is pinned too. +const SELF_TEST_BATTERY_FLOOR = 12; + +// The key an assertion is filed under when no battery is open. It is not a +// declared battery, so it reds by the same set difference rather than silently +// inflating whichever battery happened to run last. +const UNATTRIBUTED_BATTERY = '(no battery open)'; + function selfTest() { + // The battery ledger this self-test's floor is evaluated against (#13489). + // `battery()` opens a battery; every assertion below is attributed to the one + // most recently opened, so a section that stops running stops registering and + // names ITSELF at the floor rather than going quiet. + const seen = new Map(); + let openBattery = null; + const battery = (name) => { + openBattery = name; + }; + const registerCase = () => { + const b = openBattery ?? UNATTRIBUTED_BATTERY; + seen.set(b, (seen.get(b) ?? 0) + 1); + }; const failures = []; const t = (name, actual, expected) => { + registerCase(); const ok = JSON.stringify(actual) === JSON.stringify(expected); if (!ok) failures.push(`${name}\n expected ${JSON.stringify(expected)}\n actual ${JSON.stringify(actual)}`); console.log(` ${ok ? '✓' : '✗'} ${name}`); }; console.log('classifier — forms that MUST be refused'); + battery('classifier — forms that MUST be refused'); for (const line of RED_CASES) { t(line, scanLine(line, VITEST_SCRIPTS).findings.length > 0, true); } console.log('classifier — forms that MUST be allowed (a false red here is worse than no gate)'); + battery('classifier — forms that MUST be allowed (a false red here is worse than no gate)'); for (const line of GREEN_CASES) { t(line, scanLine(line, VITEST_SCRIPTS).findings.length, 0); } console.log('the multi-line spelling a line-at-a-time reader misses'); + battery('the multi-line spelling a line-at-a-time reader misses'); t( 'continuation join', scanLine(logicalLines('pnpm --filter test \\\n -- --maxWorkers=2 \n')[0].text, VITEST_SCRIPTS).findings.length, @@ -846,6 +903,7 @@ function selfTest() { t('continuation keeps the FIRST line number', logicalLines('a \\\nb\nc\n').map((l) => l.line), [1, 3, 4]); console.log('the command word is what pnpm would resolve'); + battery('the command word is what pnpm would resolve'); t('--filter consumes its value', commandWord(['pnpm', '--filter', 'test', 'build']), 'build'); t('run keyword is skipped', commandWord(['pnpm', 'run', 'test']), 'test'); t('bare script', commandWord(['pnpm', 'test']), 'test'); @@ -853,10 +911,12 @@ function selfTest() { t('no command word', commandWord(['pnpm']), null); console.log('measured stripper carve-outs, both named'); + battery('measured stripper carve-outs, both named'); t('turbo', judgeRun(['pnpm', 'turbo', 'run', 'test'], VITEST_SCRIPTS).bound, false); t('npm', judgeRun(['pnpm', 'npm', 'run', 'test'], VITEST_SCRIPTS).bound, false); console.log('the SWEEP over a real temp tree — the walk, not the predicate'); + battery('the SWEEP over a real temp tree — the walk, not the predicate'); const redTree = makeFixtureTree( baseFixtureFiles({ '.claude/agents/bad.md': 'Run `pnpm --filter @objectstack/spec test -- --maxWorkers=2 `.\n' }), ); @@ -870,6 +930,7 @@ function selfTest() { t('the message names the escape hatch', joined.includes('COUNTER_EXAMPLE_FILES'), true); console.log('the escape hatch works — a declared counter-example is not a violation'); + battery('the escape hatch works — a declared counter-example is not a violation'); const exempted = sweep(redTree, { counterExamples: [{ path: '.claude/agents/bad.md', reason: 'quotes the broken form as a warning' }], }); @@ -880,6 +941,7 @@ function selfTest() { } console.log('the same tree WITHOUT the plant is green — the red above is the plant, not the fixture'); + battery('the same tree WITHOUT the plant is green — the red above is the plant, not the fixture'); const greenTree = makeFixtureTree(baseFixtureFiles()); try { const lines = []; @@ -896,6 +958,7 @@ function selfTest() { } console.log('anti-vacuity — every way a broken selector could wear a pass is a REFUSAL'); + battery('anti-vacuity — every way a broken selector could wear a pass is a REFUSAL'); const noRoot = makeFixtureTree({ 'AGENTS.md': 'x\n' }); try { t('a missing declared root refuses', run(noRoot, () => {}), EXIT_REFUSED); @@ -946,6 +1009,7 @@ function selfTest() { } console.log('the declared lists cannot quietly become mute buttons'); + battery('the declared lists cannot quietly become mute buttons'); t('exactly one rule-owning file', RULE_OWNING_FILES.length, 1); t('and it is this file', RULE_OWNING_FILES[0], 'scripts/check-agent-test-spelling.mjs'); t('every counter-example carries a reason', COUNTER_EXAMPLE_FILES.every((e) => typeof e.reason === 'string' && e.reason.trim().length > 0), true); @@ -972,6 +1036,7 @@ function selfTest() { // The live `hintCovers` results are recorded in the docblock as a // MEASUREMENT, taken at the command line where it costs nothing. console.log('the dispatch-gates declaration — both directions, derived from the scan roots'); + battery('the dispatch-gates declaration — both directions, derived from the scan roots'); { const scanRoots = [...INSTRUCTION_ROOTS, ...EXECUTED_ROOTS]; // A root with no separator is refused by the extractor as too generic, so @@ -1009,12 +1074,58 @@ function selfTest() { } console.log('the derivation reads THIS workspace, and reads it non-empty'); + battery('the derivation reads THIS workspace, and reads it non-empty'); const derived = deriveVitestScripts(REPO_ROOT); t('derives a non-empty script set', derived.names.size > 0, true); t('derives `test`', derived.names.has('test'), true); t('does NOT derive `dev` — the documented dev-server spelling is safe by measurement', derived.names.has('dev'), false); t('does NOT derive `dev:crm`', derived.names.has('dev:crm'), false); + // ── The floor: every declared battery RAN, and ran its cases (#13489) ─── + // + // Evaluated after every battery has had its chance and BEFORE the verdict, so + // the success line below can only be printed by a run in which the set of + // batteries that registered assertions EQUALS the set declared. A set + // difference names WHICH battery stopped; a count says only that something did. + const floorFailure = (message) => { + failures.push(message); + }; + const declaredBatteries = Object.keys(SELF_TEST_BATTERIES); + let floorBreached = false; + if (declaredBatteries.length < SELF_TEST_BATTERY_FLOOR) { + floorBreached = true; + floorFailure( + `SELF_TEST_BATTERIES declares ${declaredBatteries.length} batteries, below the pinned ` + + `${SELF_TEST_BATTERY_FLOOR} — a battery deleted from the roster takes its own floor with it.`, + ); + } + for (const [name, count] of seen) { + if (declaredBatteries.includes(name)) continue; + floorBreached = true; + floorFailure( + `self-test battery "${name}" registered ${count} case(s) but is not declared in ` + + 'SELF_TEST_BATTERIES — an assertion attributed to no declared battery is one nothing floors.', + ); + } + for (const name of declaredBatteries) { + const count = seen.get(name) ?? 0; + if (count >= SELF_TEST_BATTERIES[name]) continue; + floorBreached = true; + floorFailure( + count === 0 + ? `self-test battery "${name}" DID NOT RUN — 0 cases registered, ${SELF_TEST_BATTERIES[name]} pinned. ` + + 'The verdict below would have claimed those cases hold.' + : `self-test battery "${name}" registered ${count} case(s), below its pinned floor of ` + + `${SELF_TEST_BATTERIES[name]} — cases that used to run no longer do.`, + ); + } + if (floorBreached) { + floorFailure( + 'A battery at or below its floor means cases STOPPED RUNNING — the battery is the bug, not the ' + + 'number. Find what stopped registering (an early return, a deleted block, a guard that now ' + + 'skips) and restore it.', + ); + } if (failures.length > 0) { console.error(`\n✗ check-agent-test-spelling --self-test -- ${failures.length} failure(s)\n`); for (const failure of failures) console.error(` ${failure}`); diff --git a/scripts/check-aggregator-roster.mjs b/scripts/check-aggregator-roster.mjs index 40c8b7cfc0..3a3a8920a2 100644 --- a/scripts/check-aggregator-roster.mjs +++ b/scripts/check-aggregator-roster.mjs @@ -379,10 +379,61 @@ async function main() { // as one that passed (#13798). const SELF_TEST_VERDICT = 'check-aggregator-roster self-test reached its verdict'; +// ── The self-test's own battery roster and floor (#13489) ────────────────── +// +// `failures.length === 0` used to be this self-test's ONLY success condition, so +// "every case held" and "the cases never ran" printed the same line. Closed the +// way PR #13487 validated on check-doc-authoring: what is pinned is the +// registered NAMES, not a number. Every section opens with `battery('')`, +// every assertion is attributed to the battery most recently opened, and the +// floor requires the OPENED set to equal the DECLARED set with each battery at +// or above its own count. +// +// ⛔ A pinned TOTAL is not the repair: a battery dropping from 9 cases to 3 +// keeps a total "right" the moment a sibling grows. +// +// The counts are a FLOOR, not an equality — adding cases is ordinary work and +// must not red. A battery BELOW its floor means cases stopped running; the +// remedy is to find what stopped registering. +const SELF_TEST_BATTERIES = Object.freeze({ + '(1) BASELINE: the checked-in tree is green, and says what it read': 8, + '(2) POSITIVE CONTROL A, per aggregator': 9, + '(3) POSITIVE CONTROL B, per aggregator': 9, + '(4) A declared member that is not a job in the workflow': 2, + '(5) REFUSALS -- the most important assertions in this file': 9, + '(6) The declaration cannot drift from how the gate actually counts': 2, + '(7) Malformed declarations fail loudly rather than silently shrinking': 2, + '(8) `filter` may not be laundered into the roster to silence a red': 2, + '(9) WIRING: the gate and its self-test really run in CI': 2, +}); + +// DELETING an entry silences that battery's floor exactly as effectively as +// zeroing it, so the roster's own size is pinned too. +const SELF_TEST_BATTERY_FLOOR = 9; + +// The key an assertion is filed under when no battery is open. It is not a +// declared battery, so it reds by the same set difference rather than silently +// inflating whichever battery happened to run last. +const UNATTRIBUTED_BATTERY = '(no battery open)'; + async function selfTest() { + // The battery ledger this self-test's floor is evaluated against (#13489). + // `battery()` opens a battery; every assertion below is attributed to the one + // most recently opened, so a section that stops running stops registering and + // names ITSELF at the floor rather than going quiet. + const seen = new Map(); + let openBattery = null; + const battery = (name) => { + openBattery = name; + }; + const registerCase = () => { + const b = openBattery ?? UNATTRIBUTED_BATTERY; + seen.set(b, (seen.get(b) ?? 0) + 1); + }; const failures = []; let checked = 0; const assert = (condition, description) => { + registerCase(); checked += 1; if (!condition) failures.push(description); }; @@ -423,6 +474,7 @@ async function selfTest() { }; // ── (1) BASELINE: the checked-in tree is green, and says what it read ───── + battery('(1) BASELINE: the checked-in tree is green, and says what it read'); const baseline = judge({ workflows: await readWorkflows(root) }); assert(baseline.problems.length === 0, `the checked-in workflows pass -- got ${JSON.stringify(baseline.problems)}`); assert( @@ -440,6 +492,7 @@ async function selfTest() { // ── (2) POSITIVE CONTROL A, per aggregator ─────────────────────────────── // A declared member dropped from `needs:` -- the card's exact failure. // Predicted direction: RED, naming the aggregator and the orphaned lane. + battery('(2) POSITIVE CONTROL A, per aggregator'); const dropped = [ { label: 'typecheck loses a lane from needs:', file: 'lint.yml', member: 'typecheck-consumers', from: ' - typecheck-consumers\n', to: '' }, { label: 'test-gate loses its matrix from needs:', file: 'ci.yml', member: 'test', from: ' needs: [test, filter]\n', to: ' needs: [filter]\n' }, @@ -467,6 +520,7 @@ async function selfTest() { // A `needs:` entry naming a job that does not exist. Two findings at once, // both true and both wanted: the roster does not account for it, and the // workflow has no such job. + battery('(3) POSITIVE CONTROL B, per aggregator'); const phantom = [ { label: 'typecheck needs a job that was deleted', file: 'lint.yml', from: ' - typecheck-consumers\n', to: ' - typecheck-consumers\n - typecheck-ghost\n', ghost: 'typecheck-ghost' }, { label: 'test-gate needs a job that was deleted', file: 'ci.yml', from: ' needs: [test, filter]\n', to: ' needs: [test, filter, test-ghost]\n', ghost: 'test-ghost' }, @@ -491,6 +545,7 @@ async function selfTest() { } // ── (4) A declared member that is not a job in the workflow ────────────── + battery('(4) A declared member that is not a job in the workflow'); const ghostMember = fixture('typecheck declares a member that does not exist', 'lint.yml', (s) => s.replace(`${MEMBERS_KEY}: typecheck-source-gates`, `${MEMBERS_KEY}: typecheck-phantom typecheck-source-gates`), ); @@ -505,6 +560,7 @@ async function selfTest() { // problems" over an aggregator it never read (#4690). // 5a. The declaration is unfindable: the whole env block is gone. + battery('(5) REFUSALS -- the most important assertions in this file'); const noDeclaration = fixture('typecheck loses its roster declaration', 'lint.yml', (s) => s.replace(new RegExp(`\\n env:\\n ${MEMBERS_KEY}: [^\\n]*\\n`), '\n'), ); @@ -551,6 +607,7 @@ async function selfTest() { ); // ── (6) The declaration cannot drift from how the gate actually counts ──── + battery('(6) The declaration cannot drift from how the gate actually counts'); const legDrift = fixture('dogfood-gate stops counting a declared member', 'ci.yml', (s) => s.replace(" --leg \"dogfood-verify/1:$OS_VERIFY_RESULT\"\n", ''), ); @@ -560,6 +617,7 @@ async function selfTest() { ); // ── (7) Malformed declarations fail loudly rather than silently shrinking ─ + battery('(7) Malformed declarations fail loudly rather than silently shrinking'); const commaSeparated = fixture('typecheck separates its roster with commas', 'lint.yml', (s) => s.replace( `${MEMBERS_KEY}: typecheck-source-gates typecheck-workspace`, @@ -572,6 +630,7 @@ async function selfTest() { ); // ── (8) `filter` may not be laundered into the roster to silence a red ──── + battery('(8) `filter` may not be laundered into the roster to silence a red'); const launderedFilter = fixture('test-gate calls filter a member', 'ci.yml', (s) => s.replace(`${MEMBERS_KEY}: test\n ${NON_MEMBERS_KEY}: filter`, `${MEMBERS_KEY}: test filter`), ); @@ -583,6 +642,7 @@ async function selfTest() { // ── (9) WIRING: the gate and its self-test really run in CI ─────────────── // A gate that exists and is not scheduled is the #10682 shape this card is a // sibling of. Asserted against the workflow text, not remembered. + battery('(9) WIRING: the gate and its self-test really run in CI'); { const lint = sources['lint.yml']; const self = 'scripts/check-aggregator-roster.mjs'; @@ -590,6 +650,51 @@ async function selfTest() { assert(lint.includes(`node ${self} --self-test`), 'wiring: lint.yml runs the --self-test half too'); } + // ── The floor: every declared battery RAN, and ran its cases (#13489) ─── + // + // Evaluated after every battery has had its chance and BEFORE the verdict, so + // the success line below can only be printed by a run in which the set of + // batteries that registered assertions EQUALS the set declared. A set + // difference names WHICH battery stopped; a count says only that something did. + const floorFailure = (message) => { + failures.push(message); + }; + const declaredBatteries = Object.keys(SELF_TEST_BATTERIES); + let floorBreached = false; + if (declaredBatteries.length < SELF_TEST_BATTERY_FLOOR) { + floorBreached = true; + floorFailure( + `SELF_TEST_BATTERIES declares ${declaredBatteries.length} batteries, below the pinned ` + + `${SELF_TEST_BATTERY_FLOOR} — a battery deleted from the roster takes its own floor with it.`, + ); + } + for (const [name, count] of seen) { + if (declaredBatteries.includes(name)) continue; + floorBreached = true; + floorFailure( + `self-test battery "${name}" registered ${count} case(s) but is not declared in ` + + 'SELF_TEST_BATTERIES — an assertion attributed to no declared battery is one nothing floors.', + ); + } + for (const name of declaredBatteries) { + const count = seen.get(name) ?? 0; + if (count >= SELF_TEST_BATTERIES[name]) continue; + floorBreached = true; + floorFailure( + count === 0 + ? `self-test battery "${name}" DID NOT RUN — 0 cases registered, ${SELF_TEST_BATTERIES[name]} pinned. ` + + 'The verdict below would have claimed those cases hold.' + : `self-test battery "${name}" registered ${count} case(s), below its pinned floor of ` + + `${SELF_TEST_BATTERIES[name]} — cases that used to run no longer do.`, + ); + } + if (floorBreached) { + floorFailure( + 'A battery at or below its floor means cases STOPPED RUNNING — the battery is the bug, not the ' + + 'number. Find what stopped registering (an early return, a deleted block, a guard that now ' + + 'skips) and restore it.', + ); + } if (failures.length > 0) { console.error(`✗ check-aggregator-roster --self-test — ${failures.length} failure(s)\n`); for (const failure of failures) console.error(` • ${failure}`); diff --git a/scripts/check-bash32-floor.mjs b/scripts/check-bash32-floor.mjs index 29fc9cfccc..f453895ba7 100644 --- a/scripts/check-bash32-floor.mjs +++ b/scripts/check-bash32-floor.mjs @@ -704,11 +704,70 @@ function fixtureRepo(files) { // handshake is a flag rather than a returned sentinel. let selfTestReachedVerdict = false; +// ── The self-test's own battery roster and floor (#13489) ────────────────── +// +// `failures.length === 0` used to be this self-test's ONLY success condition, so +// "every case held" and "the cases never ran" printed the same line. Closed the +// way PR #13487 validated on check-doc-authoring: what is pinned is the +// registered NAMES, not a number. Every section opens with `battery('')`, +// every assertion is attributed to the battery most recently opened, and the +// floor requires the OPENED set to equal the DECLARED set with each battery at +// or above its own count. +// +// ⛔ A pinned TOTAL is not the repair: a battery dropping from 9 cases to 3 +// keeps a total "right" the moment a sibling grows. +// +// The counts are a FLOOR, not an equality — adding cases is ordinary work and +// must not red. A battery BELOW its floor means cases stopped running; the +// remedy is to find what stopped registering. +const SELF_TEST_BATTERIES = Object.freeze({ + 'the table itself': 2, + '⭐ the pattern is not vacuous, and it is not greedy': 38, + '⭐ and the probes are real shell, not plausible-looking text': 19, + 'E1: full-line comments are prose, trailing comments are not': 3, + 'E2: variables are read through a sigil, and a guarded read is the fix': 8, + 'E3: a builtin only executes in command position': 11, + 'the near-neighbours that are NOT bash 4, so must never redden': 8, + '⭐ a COVERAGE FLOOR: deleting a row must redden this self-test': 7, + '⭐ the two `case` terminators stay DISJOINT': 2, + '⭐ the `-v` unary: three spellings, one release, one bracket trap': 21, + '⭐ the 3.2 replacements the new rows point at must stay GREEN': 7, + 'E2 again, for the row the sweep added': 6, + 'population membership': 5, + '⭐ the declaration, and the two obligations it makes unreachable': 5, + '⭐ end to end, through the real discovery path': 5, + '⭐ the instrument is real: the flagged construct really does break': 4, + 'the real tree': 2, +}); + +// DELETING an entry silences that battery's floor exactly as effectively as +// zeroing it, so the roster's own size is pinned too. +const SELF_TEST_BATTERY_FLOOR = 17; + +// The key an assertion is filed under when no battery is open. It is not a +// declared battery, so it reds by the same set difference rather than silently +// inflating whichever battery happened to run last. +const UNATTRIBUTED_BATTERY = '(no battery open)'; + function selfTest() { + // The battery ledger this self-test's floor is evaluated against (#13489). + // `battery()` opens a battery; every assertion below is attributed to the one + // most recently opened, so a section that stops running stops registering and + // names ITSELF at the floor rather than going quiet. + const seen = new Map(); + let openBattery = null; + const battery = (name) => { + openBattery = name; + }; + const registerCase = () => { + const b = openBattery ?? UNATTRIBUTED_BATTERY; + seen.set(b, (seen.get(b) ?? 0) + 1); + }; const SELF = fileURLToPath(import.meta.url); let failed = 0; let cases = 0; const t = (label, ok, detail = '') => { + registerCase(); cases += 1; if (ok) { console.log(` ✓ ${label}`); @@ -722,6 +781,7 @@ function selfTest() { console.log('check-bash32-floor --self-test\n'); // --- the table itself ---------------------------------------------------- + battery('the table itself'); t('every construct has a unique id', new Set(CONSTRUCTS.map((c) => c.id)).size === CONSTRUCTS.length); t( 'every construct declares kind, version, breakage and a fix', @@ -741,6 +801,7 @@ function selfTest() { // So every row is driven in both directions against a real instance of the // construct it claims to describe, and against the 3.2 spelling that replaces // it — which must stay green, or the gate would refuse its own remedy. + battery('⭐ the pattern is not vacuous, and it is not greedy'); for (const c of CONSTRUCTS) { t(`${c.id}: the pattern matches a real \`${c.probe}\``, ids(c.probe).includes(c.id), `got ${JSON.stringify(ids(c.probe))}`); t( @@ -754,12 +815,14 @@ function selfTest() { // // `bash -n` parses without executing. A probe that does not parse would prove // only that the regex matches a typo. + battery('⭐ and the probes are real shell, not plausible-looking text'); for (const c of CONSTRUCTS) { const parse = spawnSync('bash', ['-n'], { input: `${c.probe}\n`, encoding: 'utf8' }); t(`${c.id}: the probe is shell this host can parse`, parse.status === 0, (parse.stderr || '').trim()); } // --- E1: full-line comments are prose, trailing comments are not --------- + battery('E1: full-line comments are prose, trailing comments are not'); t('E1 a full-line comment naming a construct is exempt', ids(' # no mapfile here, ever').length === 0); t('E1 a comment naming EPOCHSECONDS is exempt', ids('# EPOCHSECONDS is bash 5').length === 0); t( @@ -768,6 +831,7 @@ function selfTest() { ); // --- E2: variables are read through a sigil, and a guarded read is the fix + battery('E2: variables are read through a sigil, and a guarded read is the fix'); t('E2 `$EPOCHSECONDS` is an unguarded read → RED', ids('now=$EPOCHSECONDS').includes('epoch-vars')); t('E2 `${EPOCHSECONDS}` is an unguarded read → RED', ids('now=${EPOCHSECONDS}').includes('epoch-vars')); t('E2 `${EPOCHSECONDS:-}` is the repair → green', !ids('now="${EPOCHSECONDS:-}"').includes('epoch-vars')); @@ -784,6 +848,7 @@ function selfTest() { ); // --- E3: a builtin only executes in command position ---------------------- + battery('E3: a builtin only executes in command position'); t('E3 at the start of a line → RED', ids(' mapfile -t x < f').includes('mapfile')); t('E3 after a pipe → RED', ids('printf a | readarray -t x').includes('mapfile')); t('E3 after `&&` → RED', ids('cd "$d" && mapfile -t x < f').includes('mapfile')); @@ -806,6 +871,7 @@ function selfTest() { ); // --- the near-neighbours that are NOT bash 4, so must never redden -------- + battery('the near-neighbours that are NOT bash 4, so must never redden'); t('3.2-legal `&>` (non-append) is not flagged', ids('echo hi &> /dev/null').length === 0); t('3.2-legal `declare -a` / `-r` / `-i` are not flagged', ids('declare -ari x=1').length === 0); t('3.2-legal `${x//,/ }` is not flagged', ids('echo "${x//,/ }"').length === 0); @@ -830,6 +896,7 @@ function selfTest() { // on a row that already existed, so deleting the row is not the only way to // lose them: narrowing its pattern back to the `[[` spelling would too, and // that is a one-character edit no row count would notice. + battery('⭐ a COVERAGE FLOOR: deleting a row must redden this self-test'); for (const [label, line] of [ ['&>> (append-both)', 'exec "$@" &>> "$logfile"'], ['|& (pipe-both)', 'make build |& tee build.log'], @@ -849,6 +916,7 @@ function selfTest() { // repair. Pinned in BOTH directions: a one-sided pin passes with the // lookbehind deleted, because `;&` matching `;;&` is invisible from the // `;&`-only side. + battery('⭐ the two `case` terminators stay DISJOINT'); t( '`;;&` is the case-fallthrough row ALONE', ids('case x in x) echo a ;;& *) echo b ;; esac').join() === 'case-fallthrough', @@ -871,6 +939,7 @@ function selfTest() { // ⚠️ Every positive pins the ARRIVAL, not the departure. `length > 0` is // satisfied by a row that reports these lines under the WRONG id and prints // the wrong remedy, and "no longer the empty result" is not the claim here. + battery('⭐ the `-v` unary: three spellings, one release, one bracket trap'); for (const [label, line] of [ ['[[ -v name ]]', '[[ -v name ]] && echo yes'], ['[ -v name ]', '[ -v name ] && echo yes'], @@ -940,6 +1009,7 @@ function selfTest() { // The load-bearing half. A `|&` pattern that also matched `2>&1 |` would red // every correct pipeline in the repo — the gate refusing its own remedy — and // the failure text tells operators to write exactly that. + battery('⭐ the 3.2 replacements the new rows point at must stay GREEN'); t('`2>&1 |`, the replacement `|&` is a synonym FOR, is not flagged', ids('echo a 2>&1 | cat').length === 0); t('an ordinary pipe is not flagged', ids('grep -c . f | wc -l').length === 0); t('an ordinary background `&` is not flagged', ids('long_job &').length === 0); @@ -953,6 +1023,7 @@ function selfTest() { t('a `for` over an explicit list is not flagged', ids('for i in 0 10 20; do echo "$i"; done').length === 0); // --- E2 again, for the row the sweep added -------------------------------- + battery('E2 again, for the row the sweep added'); t('E2 `$BASHPID` is an unguarded read → RED', ids('p=$BASHPID').includes('bashpid')); t('E2 `${BASHPID}` is an unguarded read → RED', ids('p=${BASHPID}').includes('bashpid')); t('E2 `${BASHPID:-$$}` is the repair → green', !ids('p="${BASHPID:-$$}"').includes('bashpid')); @@ -961,6 +1032,7 @@ function selfTest() { t('and `$$` — the 3.2 spelling — is not flagged', ids('p=$$').length === 0); // --- population membership ----------------------------------------------- + battery('population membership'); t('a .sh name is shell', isShell('scripts/x.sh', 'echo hi').by === 'extension'); t( 'a shebang-only script is shell — the half a *.sh glob misses', @@ -977,6 +1049,7 @@ function selfTest() { // spelled as SOURCE LITERALS so the extractor can see them at all — an // assembled root is invisible to it, which is the same blind spot wearing a // template string. + battery('⭐ the declaration, and the two obligations it makes unreachable'); const ownSource = readFileSync(SELF, 'utf8'); t('every declared root is a subtree glob', POPULATION_ROOTS.every((r) => r.endsWith('/**'))); t('every declared root carries a separator, so none is a bare root', POPULATION_ROOTS.every((r) => r.includes('/'))); @@ -997,6 +1070,7 @@ function selfTest() { ); // --- ⭐ end to end, through the real discovery path ----------------------- + battery('⭐ end to end, through the real discovery path'); const bad = {}; for (const c of CONSTRUCTS) bad[`scripts/bad-${c.id}.sh`] = `#!/usr/bin/env bash\n${c.probe}\n`; const badRepo = fixtureRepo(bad); @@ -1040,6 +1114,7 @@ function selfTest() { // R7a's shape, and for R7a's reason: without this the leg below could pass by // proving nothing. `BASH_ENV` is sourced by every non-interactive bash, so the // child inherits the disabling — measured BOTH ways on a probe first. + battery('⭐ the instrument is real: the flagged construct really does break'); const simDir = mkdtempSync(join(tmpdir(), 'bash32-sim-')); const noBash4 = join(simDir, 'no-bash4-builtins.sh'); writeFileSync(noBash4, 'enable -n mapfile readarray 2> /dev/null\n'); @@ -1083,6 +1158,7 @@ function selfTest() { for (const d of [badRepo, cleanRepo, emptyRepo, simDir]) rmSync(d, { recursive: true, force: true }); // --- the real tree ------------------------------------------------------- + battery('the real tree'); const live = scanTree(REPO_ROOT); t( 'real-tree discovery finds shell to scan (a gate over nothing is not green)', @@ -1099,6 +1175,52 @@ function selfTest() { + `${live.byShebang} by shebang alone — ${live.findings.length} finding(s)`, ); + // ── The floor: every declared battery RAN, and ran its cases (#13489) ─── + // + // Evaluated after every battery has had its chance and BEFORE the verdict, so + // the success line below can only be printed by a run in which the set of + // batteries that registered assertions EQUALS the set declared. A set + // difference names WHICH battery stopped; a count says only that something did. + const floorFailure = (message) => { + failed += 1; + console.error(` FAIL ${message}`); + }; + const declaredBatteries = Object.keys(SELF_TEST_BATTERIES); + let floorBreached = false; + if (declaredBatteries.length < SELF_TEST_BATTERY_FLOOR) { + floorBreached = true; + floorFailure( + `SELF_TEST_BATTERIES declares ${declaredBatteries.length} batteries, below the pinned ` + + `${SELF_TEST_BATTERY_FLOOR} — a battery deleted from the roster takes its own floor with it.`, + ); + } + for (const [name, count] of seen) { + if (declaredBatteries.includes(name)) continue; + floorBreached = true; + floorFailure( + `self-test battery "${name}" registered ${count} case(s) but is not declared in ` + + 'SELF_TEST_BATTERIES — an assertion attributed to no declared battery is one nothing floors.', + ); + } + for (const name of declaredBatteries) { + const count = seen.get(name) ?? 0; + if (count >= SELF_TEST_BATTERIES[name]) continue; + floorBreached = true; + floorFailure( + count === 0 + ? `self-test battery "${name}" DID NOT RUN — 0 cases registered, ${SELF_TEST_BATTERIES[name]} pinned. ` + + 'The verdict below would have claimed those cases hold.' + : `self-test battery "${name}" registered ${count} case(s), below its pinned floor of ` + + `${SELF_TEST_BATTERIES[name]} — cases that used to run no longer do.`, + ); + } + if (floorBreached) { + floorFailure( + 'A battery at or below its floor means cases STOPPED RUNNING — the battery is the bug, not the ' + + 'number. Find what stopped registering (an early return, a deleted block, a guard that now ' + + 'skips) and restore it.', + ); + } if (failed > 0) { console.error(`\n✗ check-bash32-floor self-test failed (${failed} of ${cases} case(s)).`); process.exit(1); diff --git a/scripts/check-changeset-no-major.mjs b/scripts/check-changeset-no-major.mjs index f57e8087ad..00836bd0be 100644 --- a/scripts/check-changeset-no-major.mjs +++ b/scripts/check-changeset-no-major.mjs @@ -799,10 +799,65 @@ function main(argv) { // as one that passed (#13798). const SELF_TEST_VERDICT = 'check-changeset-no-major self-test reached its verdict'; +// ── The self-test's own battery roster and floor (#13489) ────────────────── +// +// `failures.length === 0` used to be this self-test's ONLY success condition, so +// "every case held" and "the cases never ran" printed the same line. Closed the +// way PR #13487 validated on check-doc-authoring: what is pinned is the +// registered NAMES, not a number. Every section opens with `battery('')`, +// every assertion is attributed to the battery most recently opened, and the +// floor requires the OPENED set to equal the DECLARED set with each battery at +// or above its own count. +// +// ⛔ A pinned TOTAL is not the repair: a battery dropping from 9 cases to 3 +// keeps a total "right" the moment a sibling grows. +// +// The counts are a FLOOR, not an equality — adding cases is ordinary work and +// must not red. A battery BELOW its floor means cases stopped running; the +// remedy is to find what stopped registering. +const SELF_TEST_BATTERIES = Object.freeze({ + 'The three quoting dialects the header names': 8, + 'THE FIX (#6923): a leading blank line': 3, + 'What must NOT be caught, each paired with its control': 9, + 'THE FIX (#7004): the shapes the old `([A-Za-z]+)\\s*$` anchor hid': 13, + 'The exemption switch, in BOTH directions': 17, + 'Order of operations is contract': 3, + 'Missing input is a failure, never a pass (#4690 / #7006)': 5, + 'The readers': 12, + 'The diff scoping, on real temp git repositories': 18, + '#7107: an `R` row whose BASE side is README.md subtracts NOTHING': 4, + '#6129 proper: main drift must not move the verdict': 5, + 'Missing input is a failure, never a pass (#4690)': 4, + 'The wiring: these fixtures must actually run on every PR': 15, +}); + +// DELETING an entry silences that battery's floor exactly as effectively as +// zeroing it, so the roster's own size is pinned too. +const SELF_TEST_BATTERY_FLOOR = 13; + +// The key an assertion is filed under when no battery is open. It is not a +// declared battery, so it reds by the same set difference rather than silently +// inflating whichever battery happened to run last. +const UNATTRIBUTED_BATTERY = '(no battery open)'; + function selfTest() { + // The battery ledger this self-test's floor is evaluated against (#13489). + // `battery()` opens a battery; every assertion below is attributed to the one + // most recently opened, so a section that stops running stops registering and + // names ITSELF at the floor rather than going quiet. + const seen = new Map(); + let openBattery = null; + const battery = (name) => { + openBattery = name; + }; + const registerCase = () => { + const b = openBattery ?? UNATTRIBUTED_BATTERY; + seen.set(b, (seen.get(b) ?? 0) + 1); + }; const failures = []; let checked = 0; const assert = (condition, description) => { + registerCase(); checked += 1; if (!condition) failures.push(description); }; @@ -830,6 +885,7 @@ function selfTest() { }; // ── The three quoting dialects the header names ─────────────────────────── + battery('The three quoting dialects the header names'); caught('a double-quoted name', MAJOR, ['@objectstack/spec']); caught('a single-quoted name', "---\n'@objectstack/spec': major\n---\n\nbody\n", ['@objectstack/spec']); caught('an unquoted name', '---\ndocs: major\n---\n\nbody\n', ['docs']); @@ -854,11 +910,13 @@ function selfTest() { // `lines[0]?.trim() !== '---'` turns exactly these two red. Measured with // @changesets/parse@0.4.3: both of these DO release a major, so a miss here is // a whole-stack major promoted past a guard that printed a tick. + battery('THE FIX (#6923): a leading blank line'); caught('a leading blank line before the fence', '\n' + MAJOR, ['@objectstack/spec']); caught('two leading blank lines', '\n\n' + MAJOR, ['@objectstack/spec']); caught('a leading blank line, single-quoted', "\n---\n'@objectstack/spec': major\n---\n\nbody\n", ['@objectstack/spec']); // ── What must NOT be caught, each paired with its control ───────────────── + battery('What must NOT be caught, each paired with its control'); assert(majorPackagesIn(MINOR).length === 0, 'parser: a `minor` bump is not a major'); assert(majorPackagesIn('---\n"@objectstack/spec": patch\n---\n\nbody\n').length === 0, 'parser: a `patch` bump is not a major'); // Control for both: the SAME text with `major` in the bump slot is caught, so @@ -894,6 +952,7 @@ function selfTest() { // (`([A-Za-z]+)\s*$`) turns exactly these red. Measured with // @changesets/parse@0.4.3: every one of them DOES release a major, so a miss // here is a whole-stack major promoted past a guard that printed a tick. + battery('THE FIX (#7004): the shapes the old `([A-Za-z]+)\\s*$` anchor hid'); caught('a trailing YAML comment', '---\n"@objectstack/spec": major # keep\n---\n\nbody\n', ['@objectstack/spec']); caught('a trailing comment after a tab', '---\n"@objectstack/spec": major\t# keep\n---\n\nbody\n', ['@objectstack/spec']); caught('a trailing comment containing a colon', '---\n"@objectstack/spec": major # note: keep\n---\n\nbody\n', ['@objectstack/spec']); @@ -934,6 +993,7 @@ function selfTest() { // ── The exemption switch, in BOTH directions ────────────────────────────── // This is the half that no CI run has ever executed. Everything below drives // it directly. `introduced` is now scan()'s output shape, never the stock. + battery('The exemption switch, in BOTH directions'); const pending = [{ file: '.changeset/a.md', majors: ['@objectstack/spec'] }]; const exempt = judge({ introduced: pending, pre: { mode: 'pre', tag: 'rc' } }); @@ -984,6 +1044,7 @@ function selfTest() { } // ── Order of operations is contract ─────────────────────────────────────── + battery('Order of operations is contract'); const cleanInPre = judge({ introduced: [], pre: { mode: 'pre', tag: 'rc' } }); assert(cleanInPre.verdict === 'clean', 'a diff introducing no major, in pre-mode ⇒ the ordinary tick, not the RC notice'); assert( @@ -1012,6 +1073,7 @@ function selfTest() { // contract. // ── Missing input is a failure, never a pass (#4690 / #7006) ────────────── + battery('Missing input is a failure, never a pass (#4690 / #7006)'); const unreadable = judge({ introduced: null, pre: { mode: 'exit' } }); assert(unreadable.verdict === 'unreadable-diff', 'a diff that could not be computed is its OWN verdict, not `clean`'); assert( @@ -1025,6 +1087,7 @@ function selfTest() { // ── The readers ────────────────────────────────────────────────────────── // `readChangesets` no longer feeds the verdict; it feeds `--list`. These pins // stay because `--list` is now the only stock view a curator has. + battery('The readers'); { const real = readChangesets(REPO_ROOT); // Reachability only, never SIZE (#8654): `.changeset/` itself is tracked @@ -1093,6 +1156,7 @@ function selfTest() { // fixture that is not two real commits would be testing an imitation of the // code path that ships. + battery('The diff scoping, on real temp git repositories'); const repos = []; const initRepo = (prefix) => { const dir = mkdtempSync(join(tmpdir(), prefix)); @@ -1284,6 +1348,7 @@ function selfTest() { // is subtracted today. The fixture therefore has to COMMIT a major-shaped // README to reach the path at all — without that, the case would pass with // or without the guard and would certify the hole instead of closing it. + battery('#7107: an `R` row whose BASE side is README.md subtracts NOTHING'); { const long = '\n\nbody long enough for git to score this as a rename rather than an add plus a delete\n'; const majorReadme = '---\n"@objectstack/spec": major\n---' + long; @@ -1319,6 +1384,7 @@ function selfTest() { // offenders); judged from the moved main tip it would be 0 too — but judged // as this gate USED to judge, reading the stock at HEAD, it is 1. All three // are asserted, so the fixture cannot be green because nothing is produced. + battery('#6129 proper: main drift must not move the verdict'); { const dir = initRepo('changeset-no-major-mergeref-'); writeInto(dir, { '.changeset/stock.md': MINOR }); @@ -1380,6 +1446,7 @@ function selfTest() { } // ── Missing input is a failure, never a pass (#4690) ───────────────────── + battery('Missing input is a failure, never a pass (#4690)'); { const { dir } = makeRepo({}, { 'a.txt': 'x\n' }); assert(resolveCommit('definitely-not-a-ref', dir) === null, 'an unresolvable base resolves to null (⇒ exit 1)'); @@ -1425,6 +1492,7 @@ function selfTest() { // pins, so a PR deleting both the step and this script is not caught here. // That is a deletion plainly visible in a `.github/**` diff rather than a // silent no-op. + battery('The wiring: these fixtures must actually run on every PR'); { const uncommented = (text) => text.split('\n').filter((l) => !/^\s*#/.test(l)).join('\n'); @@ -1507,6 +1575,51 @@ function selfTest() { ); } + // ── The floor: every declared battery RAN, and ran its cases (#13489) ─── + // + // Evaluated after every battery has had its chance and BEFORE the verdict, so + // the success line below can only be printed by a run in which the set of + // batteries that registered assertions EQUALS the set declared. A set + // difference names WHICH battery stopped; a count says only that something did. + const floorFailure = (message) => { + failures.push(message); + }; + const declaredBatteries = Object.keys(SELF_TEST_BATTERIES); + let floorBreached = false; + if (declaredBatteries.length < SELF_TEST_BATTERY_FLOOR) { + floorBreached = true; + floorFailure( + `SELF_TEST_BATTERIES declares ${declaredBatteries.length} batteries, below the pinned ` + + `${SELF_TEST_BATTERY_FLOOR} — a battery deleted from the roster takes its own floor with it.`, + ); + } + for (const [name, count] of seen) { + if (declaredBatteries.includes(name)) continue; + floorBreached = true; + floorFailure( + `self-test battery "${name}" registered ${count} case(s) but is not declared in ` + + 'SELF_TEST_BATTERIES — an assertion attributed to no declared battery is one nothing floors.', + ); + } + for (const name of declaredBatteries) { + const count = seen.get(name) ?? 0; + if (count >= SELF_TEST_BATTERIES[name]) continue; + floorBreached = true; + floorFailure( + count === 0 + ? `self-test battery "${name}" DID NOT RUN — 0 cases registered, ${SELF_TEST_BATTERIES[name]} pinned. ` + + 'The verdict below would have claimed those cases hold.' + : `self-test battery "${name}" registered ${count} case(s), below its pinned floor of ` + + `${SELF_TEST_BATTERIES[name]} — cases that used to run no longer do.`, + ); + } + if (floorBreached) { + floorFailure( + 'A battery at or below its floor means cases STOPPED RUNNING — the battery is the bug, not the ' + + 'number. Find what stopped registering (an early return, a deleted block, a guard that now ' + + 'skips) and restore it.', + ); + } if (failures.length > 0) { console.error(`✗ check-changeset-no-major --self-test — ${failures.length} failure(s)\n`); for (const failure of failures) console.error(` • ${failure}`); diff --git a/scripts/check-corpus-claim-drift.mjs b/scripts/check-corpus-claim-drift.mjs index f80d1a5c01..2cc137d71f 100644 --- a/scripts/check-corpus-claim-drift.mjs +++ b/scripts/check-corpus-claim-drift.mjs @@ -553,9 +553,67 @@ function ratchetRemedyCarriesAuthority(message) { // handshake is a flag rather than a returned sentinel. let selfTestReachedVerdict = false; +// ── The self-test's own battery roster and floor (#13489) ────────────────── +// +// `failures.length === 0` used to be this self-test's ONLY success condition, so +// "every case held" and "the cases never ran" printed the same line. Closed the +// way PR #13487 validated on check-doc-authoring: what is pinned is the +// registered NAMES, not a number. Every section opens with `battery('')`, +// every assertion is attributed to the battery most recently opened, and the +// floor requires the OPENED set to equal the DECLARED set with each battery at +// or above its own count. +// +// ⛔ A pinned TOTAL is not the repair: a battery dropping from 9 cases to 3 +// keeps a total "right" the moment a sibling grows. +// +// The counts are a FLOOR, not an equality — adding cases is ordinary work and +// must not red. A battery BELOW its floor means cases stopped running; the +// remedy is to find what stopped registering. +const SELF_TEST_BATTERIES = Object.freeze({ + 'The table is a table, and it ships exactly one row (#13582 ruling)': 3, + 'Table hygiene: a claim that matches the empty string': 2, + 'THE genericity proof, without shipping a second word': 3, + 'The REAL defect, quoted verbatim from the pre-repair tree': 8, + '#13745 row 2: the `(NoSQL)` PORTABILITY gloss': 14, + '#13745 row 3: the retired `$regex` spelling': 6, + '#13745 row 4: #13532\'s section `visibleWhen` binding claims': 12, + 'The LEGITIMATE usages, measured on the corpus this gate walks': 9, + 'Window behaviour, at the boundary': 1, + 'Overlapping claim members are ONE site': 1, + 'A row whose subject is unreachable is REFUSED, not silently green': 5, + 'The green body reports what was READ, not what the ledger holds': 6, + 'The #8435 authority convention, PLACEMENT': 6, + 'A missing ROOT is REFUSED, per root (#9932)': 4, + 'The dispatch-gates declaration (#9964\'s pattern)': 4, + 'At the PROGRAM level': 10, +}); + +// DELETING an entry silences that battery's floor exactly as effectively as +// zeroing it, so the roster's own size is pinned too. +const SELF_TEST_BATTERY_FLOOR = 16; + +// The key an assertion is filed under when no battery is open. It is not a +// declared battery, so it reds by the same set difference rather than silently +// inflating whichever battery happened to run last. +const UNATTRIBUTED_BATTERY = '(no battery open)'; + function selfTest() { + // The battery ledger this self-test's floor is evaluated against (#13489). + // `battery()` opens a battery; every assertion below is attributed to the one + // most recently opened, so a section that stops running stops registering and + // names ITSELF at the floor rather than going quiet. + const seen = new Map(); + let openBattery = null; + const battery = (name) => { + openBattery = name; + }; + const registerCase = () => { + const b = openBattery ?? UNATTRIBUTED_BATTERY; + seen.set(b, (seen.get(b) ?? 0) + 1); + }; const failures = []; const expect = (label, cond) => { + registerCase(); if (!cond) failures.push(label); }; const RULE = VOCABULARY[0]; @@ -572,6 +630,7 @@ function selfTest() { // FILL IT (so each word pays its own baseline on its own card). A gate that // honoured only the first would grow words by drive-by; one that honoured only // the second is a hardcoded regex again. + battery('The table is a table, and it ships exactly one row (#13582 ruling)'); const SHIPPED_ROW_IDS = [ 'exists-key-presence', 'exists-portability', @@ -602,6 +661,7 @@ function selfTest() { && Number.isInteger(r.window) && r.window >= 0 && r.truth && r.refs)); // ── Table hygiene: a claim that matches the empty string ─────────────────── + battery('Table hygiene: a claim that matches the empty string'); expect('a shipped row cannot match the EMPTY STRING', zeroWidthClaimRows(VOCABULARY).length === 0); expect('and the predicate DISCRIMINATES — a row whose claims CAN match empty is caught (without ' + 'this, the assertion above passes on a predicate that approves everything)', @@ -614,6 +674,7 @@ function selfTest() { // ever reaches is a single regex wearing a table's clothes, and nothing about // the shipped row can distinguish the two. So a SYNTHETIC row is driven through // the real engine — the same `analyzeFile` the production path calls. + battery('THE genericity proof, without shipping a second word'); const SYNTHETIC = { id: 'synthetic-probe', subject: '`$probeop`', @@ -651,6 +712,7 @@ function selfTest() { // git at `75b3bdc86^` and `e51c78f0c^`. They are the gate's reason to exist, so // they are pinned as fixtures rather than described: a rule that stopped // catching them would still pass every abstract assertion above. + battery('The REAL defect, quoted verbatim from the pre-repair tree'); const WAS_TABLE_ROW = '| `$exists` | Field exists (NoSQL) | `{ metadata: { $exists: true } }` |'; const WAS_OSCHECK = [ '{/* os:check */}', @@ -708,6 +770,7 @@ function selfTest() { // table has it in a column header three lines away. A bare /NoSQL/ claim // reddens both. So the claim is the RESTRICTION (a parenthesised gloss, or an // unnegated "NoSQL-only" / "MongoDB-only"), never the word. + battery('#13745 row 2: the `(NoSQL)` PORTABILITY gloss'); const PORTABILITY = VOCABULARY.find((r) => r.id === 'exists-portability'); expect('#13745 — the pre-repair table row carries the portability claim as well, and it is a ' + 'SEPARATE debt from the key-presence one on the same line: two rows, two ledger keys, so ' @@ -775,6 +838,7 @@ function selfTest() { // a regression pin — and the whole difficulty is that the pages teaching the // retirement say the word on every line. The claim is therefore the // LIVE-OPERATOR phrasing, never the mention. + battery('#13745 row 3: the retired `$regex` spelling'); const REGEX_ROW = VOCABULARY.find((r) => r.id === 'regex-retired'); expect('#13745 — `$regex` inside a live operator list is a site', count('**Supported operators:** `$eq`, `$ne`, `$contains`, `$regex`, `$null`', @@ -817,6 +881,7 @@ function selfTest() { // Read verbatim out of git at `b2dea862c^`, the tree #13532 corrected, at the // real line spacing — which is the whole reason this row's window is 9: the // binding-root table row sits nine lines below the nearest `visibleWhen`. + battery('#13745 row 4: #13532\'s section `visibleWhen` binding claims'); const VW = VOCABULARY.find((r) => r.id === 'section-visiblewhen-unbound'); const WAS_VW_OSCHECK = [ '{/* os:check */}', @@ -915,6 +980,7 @@ function selfTest() { // found by sweeping the claim phrases across both roots. Every one is green // STRUCTURALLY — none of these files mentions `$exists` — which is a stronger // outcome than baselining them: a baselined file carries a budget forever. + battery('The LEGITIMATE usages, measured on the corpus this gate walks'); const LEGITIMATE = [ ['objectstack-formula `has()` — a genuine key-existence check (the one the dispatch named)', '`has(record.x)` is **true whenever the key exists**, even when its value is null.'], @@ -945,12 +1011,14 @@ function selfTest() { count(`${LEGITIMATE[0][1]}\nSee also \`$exists\`.`) === 1); // ── Window behaviour, at the boundary ────────────────────────────────────── + battery('Window behaviour, at the boundary'); const at = (gap) => ['`$exists`', ...Array(gap).fill(''), 'the field exists'].join('\n'); expect(`a claim ${RULE.window} lines away is a site, and one ${RULE.window + 1} lines away is ` + 'not (the boundary is inclusive and it is the shipped row\'s own number)', count(at(RULE.window - 1)) === 1 && count(at(RULE.window)) === 0); // ── Overlapping claim members are ONE site ──────────────────────────────── + battery('Overlapping claim members are ONE site'); expect('"Field existence check" beside the spelling is ONE site: `existence`, the ' + '`field exists` shape and the phrase overlap, and counting each would write an inflated ' + 'number into a ledger that is only allowed to shrink', @@ -962,6 +1030,7 @@ function selfTest() { // refusal is not the outcome any of them is asserting. Derived from the table // so a new row cannot leave these fixtures behind silently, with a positive // control proving the derivation really does carry each spelling. + battery('A row whose subject is unreachable is REFUSED, not silently green'); const REACHABLE_LINE = `Reachability fixture: ${VOCABULARY.map((r) => r.subject).join(' ')}`; expect('#13745 — the reachability fixture carries EVERY shipped spelling. Derived from each ' + 'row\'s `subject`, so a row whose subject does not spell its own operator is caught HERE ' @@ -986,6 +1055,7 @@ function selfTest() { // // Synthetic fixtures, closed over by every assertion, so they stay correct // however the real corpus moves. ⛔ Do not "refresh" them against a live scan. + battery('The green body reports what was READ, not what the ledger holds'); const SCANNED = [{ root: 'content/docs', files: 189 }, { root: 'skills', files: 36 }]; const DEAD_SCAN = [{ root: 'content/docs', files: 0 }, { root: 'skills', files: 0 }]; const PAID_OFF = {}; @@ -1015,6 +1085,7 @@ function selfTest() { !== successSummary(SCANNED, LEDGER_ONE, VOCABULARY)); // ── The #8435 authority convention, PLACEMENT ───────────────────────────── + battery('The #8435 authority convention, PLACEMENT'); const newClaim = newClaimMessage('content/docs/example.mdx', RULE, 2); const grew = grewMessage('content/docs/example.mdx', RULE, 4, 5); const improved = improvedMessage('content/docs/example.mdx', RULE, 4, 2); @@ -1041,6 +1112,7 @@ function selfTest() { [newClaim, grew].every((m) => m.includes('HAS A VALUE') && m.includes(RULE.refs))); // ── A missing ROOT is REFUSED, per root (#9932) ─────────────────────────── + battery('A missing ROOT is REFUSED, per root (#9932)'); const PRESENT_ROOT = 'alpha/one'; const ABSENT_ROOT = 'bravo-two'; expect('#9932 — with NO root present, every one of them is reported', @@ -1061,6 +1133,7 @@ function selfTest() { // dispatched on a skills card with this gate missing from the brief. Derived // from ROOTS on both sides, so renaming a root cannot leave the declaration // describing the old population. + battery('The dispatch-gates declaration (#9964\'s pattern)'); const separatorless = ROOTS.filter((r) => !r.includes('/')); expect('every ROOT the hint extractor cannot see (no path separator) declares the subtree ' + 'spelling', separatorless.every((r) => ROOT_DIR_WATCH_HINTS.includes(`${r}/**`))); @@ -1077,6 +1150,7 @@ function selfTest() { // Everything above drives predicates. A predicate the program never consults // would satisfy all of it, so these build real trees and read a child // process's real exit status — never a pipe's. + battery('At the PROGRAM level'); const SELF = fileURLToPath(import.meta.url); const runIn = (cwd, args = []) => { const r = spawnSync(process.execPath, [SELF, ...args], { cwd, encoding: 'utf8' }); @@ -1178,6 +1252,51 @@ function selfTest() { rmSync(sandbox, { recursive: true, force: true }); } + // ── The floor: every declared battery RAN, and ran its cases (#13489) ─── + // + // Evaluated after every battery has had its chance and BEFORE the verdict, so + // the success line below can only be printed by a run in which the set of + // batteries that registered assertions EQUALS the set declared. A set + // difference names WHICH battery stopped; a count says only that something did. + const floorFailure = (message) => { + failures.push(message); + }; + const declaredBatteries = Object.keys(SELF_TEST_BATTERIES); + let floorBreached = false; + if (declaredBatteries.length < SELF_TEST_BATTERY_FLOOR) { + floorBreached = true; + floorFailure( + `SELF_TEST_BATTERIES declares ${declaredBatteries.length} batteries, below the pinned ` + + `${SELF_TEST_BATTERY_FLOOR} — a battery deleted from the roster takes its own floor with it.`, + ); + } + for (const [name, count] of seen) { + if (declaredBatteries.includes(name)) continue; + floorBreached = true; + floorFailure( + `self-test battery "${name}" registered ${count} case(s) but is not declared in ` + + 'SELF_TEST_BATTERIES — an assertion attributed to no declared battery is one nothing floors.', + ); + } + for (const name of declaredBatteries) { + const count = seen.get(name) ?? 0; + if (count >= SELF_TEST_BATTERIES[name]) continue; + floorBreached = true; + floorFailure( + count === 0 + ? `self-test battery "${name}" DID NOT RUN — 0 cases registered, ${SELF_TEST_BATTERIES[name]} pinned. ` + + 'The verdict below would have claimed those cases hold.' + : `self-test battery "${name}" registered ${count} case(s), below its pinned floor of ` + + `${SELF_TEST_BATTERIES[name]} — cases that used to run no longer do.`, + ); + } + if (floorBreached) { + floorFailure( + 'A battery at or below its floor means cases STOPPED RUNNING — the battery is the bug, not the ' + + 'number. Find what stopped registering (an early return, a deleted block, a guard that now ' + + 'skips) and restore it.', + ); + } if (failures.length) { for (const f of failures) console.error(` x self-test: ${f}`); console.error(`\ncheck-corpus-claim-drift --self-test: ${failures.length} failure(s).\n`); diff --git a/scripts/check-declared-population-live.mjs b/scripts/check-declared-population-live.mjs index 9b9f8c57cd..0fae61adfa 100644 --- a/scripts/check-declared-population-live.mjs +++ b/scripts/check-declared-population-live.mjs @@ -201,10 +201,54 @@ function main(argv) { // handshake is a flag rather than a returned sentinel. let selfTestReachedVerdict = false; +// ── The self-test's own battery roster and floor (#13489) ────────────────── +// +// `failures.length === 0` used to be this self-test's ONLY success condition, so +// "every case held" and "the cases never ran" printed the same line. Closed the +// way PR #13487 validated on check-doc-authoring: what is pinned is the +// registered NAMES, not a number. Every section opens with `battery('')`, +// every assertion is attributed to the battery most recently opened, and the +// floor requires the OPENED set to equal the DECLARED set with each battery at +// or above its own count. +// +// ⛔ A pinned TOTAL is not the repair: a battery dropping from 9 cases to 3 +// keeps a total "right" the moment a sibling grows. +// +// The counts are a FLOOR, not an equality — adding cases is ordinary work and +// must not red. A battery BELOW its floor means cases stopped running; the +// remedy is to find what stopped registering. +const SELF_TEST_BATTERIES = Object.freeze({ + 'The rule, on fixtures. Every case names the VERDICT, never "it returned': 10, + 'The live half: the fleet, and the two non-vacuity claims a count cannot': 6, +}); + +// DELETING an entry silences that battery's floor exactly as effectively as +// zeroing it, so the roster's own size is pinned too. +const SELF_TEST_BATTERY_FLOOR = 2; + +// The key an assertion is filed under when no battery is open. It is not a +// declared battery, so it reds by the same set difference rather than silently +// inflating whichever battery happened to run last. +const UNATTRIBUTED_BATTERY = '(no battery open)'; + function selfTest() { + // The battery ledger this self-test's floor is evaluated against (#13489). + // `battery()` opens a battery; every assertion below is attributed to the one + // most recently opened, so a section that stops running stops registering and + // names ITSELF at the floor rather than going quiet. + const seen = new Map(); + let openBattery = null; + const battery = (name) => { + openBattery = name; + }; + const registerCase = () => { + const b = openBattery ?? UNATTRIBUTED_BATTERY; + seen.set(b, (seen.get(b) ?? 0) + 1); + }; const failures = []; let checked = 0; const t = (name, ok, detail) => { + registerCase(); checked += 1; if (!ok) failures.push(detail ? `${name} -- ${detail}` : name); }; @@ -213,6 +257,7 @@ function selfTest() { // something": the defect this gate exists for produces a coherent, // plausible answer, and a case that only checked for an answer is green // against it. + battery('The rule, on fixtures. Every case names the VERDICT, never "it returned'); const live = new Set(['a/b.mjs', 'c/d']); const reaches = (h) => live.has(h); t('a family that declares nothing is not a finding', declarationVerdict([], reaches) === 'no-declaration'); @@ -242,6 +287,7 @@ function selfTest() { // ── The live half: the fleet, and the two non-vacuity claims a count cannot // make on its own. + battery('The live half: the fleet, and the two non-vacuity claims a count cannot'); const { rows, families, corpus } = sweep(); t(`the sweep discovers families to judge (${families})`, families > 0); t(`and a corpus to judge them against (${corpus} tracked files)`, corpus > 0); @@ -271,6 +317,51 @@ function selfTest() { files.some((f) => hintCovers('scripts/pm/dispatch-gates.mjs', f)), ); + // ── The floor: every declared battery RAN, and ran its cases (#13489) ─── + // + // Evaluated after every battery has had its chance and BEFORE the verdict, so + // the success line below can only be printed by a run in which the set of + // batteries that registered assertions EQUALS the set declared. A set + // difference names WHICH battery stopped; a count says only that something did. + const floorFailure = (message) => { + failures.push(message); + }; + const declaredBatteries = Object.keys(SELF_TEST_BATTERIES); + let floorBreached = false; + if (declaredBatteries.length < SELF_TEST_BATTERY_FLOOR) { + floorBreached = true; + floorFailure( + `SELF_TEST_BATTERIES declares ${declaredBatteries.length} batteries, below the pinned ` + + `${SELF_TEST_BATTERY_FLOOR} — a battery deleted from the roster takes its own floor with it.`, + ); + } + for (const [name, count] of seen) { + if (declaredBatteries.includes(name)) continue; + floorBreached = true; + floorFailure( + `self-test battery "${name}" registered ${count} case(s) but is not declared in ` + + 'SELF_TEST_BATTERIES — an assertion attributed to no declared battery is one nothing floors.', + ); + } + for (const name of declaredBatteries) { + const count = seen.get(name) ?? 0; + if (count >= SELF_TEST_BATTERIES[name]) continue; + floorBreached = true; + floorFailure( + count === 0 + ? `self-test battery "${name}" DID NOT RUN — 0 cases registered, ${SELF_TEST_BATTERIES[name]} pinned. ` + + 'The verdict below would have claimed those cases hold.' + : `self-test battery "${name}" registered ${count} case(s), below its pinned floor of ` + + `${SELF_TEST_BATTERIES[name]} — cases that used to run no longer do.`, + ); + } + if (floorBreached) { + floorFailure( + 'A battery at or below its floor means cases STOPPED RUNNING — the battery is the bug, not the ' + + 'number. Find what stopped registering (an early return, a deleted block, a guard that now ' + + 'skips) and restore it.', + ); + } if (failures.length) { console.error(`check-declared-population-live --self-test: ${failures.length} of ${checked} assertion(s) FAILED\n`); for (const f of failures) console.error(` - ${f}`); diff --git a/scripts/check-doc-route-spelling.mjs b/scripts/check-doc-route-spelling.mjs index f8db478137..46949b475e 100644 --- a/scripts/check-doc-route-spelling.mjs +++ b/scripts/check-doc-route-spelling.mjs @@ -625,13 +625,67 @@ function exitCodeFor(flagCount, advisory) { // handshake is a flag rather than a returned sentinel. let selfTestReachedVerdict = false; +// ── The self-test's own battery roster and floor (#13489) ────────────────── +// +// `failures.length === 0` used to be this self-test's ONLY success condition, so +// "every case held" and "the cases never ran" printed the same line. Closed the +// way PR #13487 validated on check-doc-authoring: what is pinned is the +// registered NAMES, not a number. Every section opens with `battery('')`, +// every assertion is attributed to the battery most recently opened, and the +// floor requires the OPENED set to equal the DECLARED set with each battery at +// or above its own count. +// +// ⛔ A pinned TOTAL is not the repair: a battery dropping from 9 cases to 3 +// keeps a total "right" the moment a sibling grows. +// +// The counts are a FLOOR, not an equality — adding cases is ordinary work and +// must not red. A battery BELOW its floor means cases stopped running; the +// remedy is to find what stopped registering. +const SELF_TEST_BATTERIES = Object.freeze({ + 'Unit: extraction tidy-up': 6, + 'Unit: the variant relation is exactly plural + lexicon': 5, + 'Unit: exit semantics': 3, + 'Ledger parsing reached the real files': 2, + 'Walk wiring': 3, + 'The teeth: measured drift class flags, by name': 10, + 'Precision pins: what must NOT flag': 11, + 'Red/green: dead root (#4916)': 2, + 'Red/green: empty root (#4932)': 2, + 'Red/green: extractor evaporation (the occurrence floor)': 2, + 'Red/green: the authority itself (#4932 applied to the ledgers)': 4, +}); + +// DELETING an entry silences that battery's floor exactly as effectively as +// zeroing it, so the roster's own size is pinned too. +const SELF_TEST_BATTERY_FLOOR = 11; + +// The key an assertion is filed under when no battery is open. It is not a +// declared battery, so it reds by the same set difference rather than silently +// inflating whichever battery happened to run last. +const UNATTRIBUTED_BATTERY = '(no battery open)'; + function selfTest() { + // The battery ledger this self-test's floor is evaluated against (#13489). + // `battery()` opens a battery; every assertion below is attributed to the one + // most recently opened, so a section that stops running stops registering and + // names ITSELF at the floor rather than going quiet. + const seen = new Map(); + let openBattery = null; + const battery = (name) => { + openBattery = name; + }; + const registerCase = () => { + const b = openBattery ?? UNATTRIBUTED_BATTERY; + seen.set(b, (seen.get(b) ?? 0) + 1); + }; const failures = []; const expect = (label, got, want) => { + registerCase(); if (got !== want) failures.push(` ✗ self-test "${label}": expected ${JSON.stringify(want)}, got ${JSON.stringify(got)}`); }; // ── Unit: extraction tidy-up ────────────────────────────────────────────── + battery('Unit: extraction tidy-up'); expect('query string is cut', tidyLiteral('/api/v1/data/task?limit=5'), '/api/v1/data/task'); expect('sentence punctuation is trimmed', tidyLiteral('/api/v1/search).'), '/api/v1/search'); expect('a closer that balances an opener survives', tidyLiteral('/api/v1/data/{object}'), '/api/v1/data/{object}'); @@ -640,6 +694,7 @@ function selfTest() { expect('bare base with slash collapses to the base', tidyLiteral('/api/v1/'), '/api/v1'); // ── Unit: the variant relation is exactly plural + lexicon ──────────────── + battery('Unit: the variant relation is exactly plural + lexicon'); expect('plural pair flags', isVariantPair('object', 'objects'), true); expect('es-plural pair flags', isVariantPair('view', 'viewes'), true); expect('lexicon pair flags (the measured third spelling)', isVariantPair('meta', 'metadata'), true); @@ -647,6 +702,7 @@ function selfTest() { expect('unrelated words are NOT variants', isVariantPair('keys', 'leads'), false); // ── Unit: exit semantics ────────────────────────────────────────────────── + battery('Unit: exit semantics'); expect('enforce mode reds on findings', exitCodeFor(1, false), 1); expect('advisory mode does not red on findings', exitCodeFor(1, true), 0); expect('clean is green in both modes', exitCodeFor(0, false) + exitCodeFor(0, true), 0); @@ -751,11 +807,13 @@ function selfTest() { const r = runScan(cfg); // ── Ledger parsing reached the real files ──────────────────────────── + battery('Ledger parsing reached the real files'); expect('ledger rows parsed (13 rest + 6 runtime; servedBy and note prose are not rows)', r.ledgerCounts.routes, 19); expect('wildcard families parsed', r.ledgerCounts.families, 2); // ── Walk wiring ────────────────────────────────────────────────────── + battery('Walk wiring'); expect('corpus files walked (releases/ and node_modules/ excluded)', r.fileCount, 3); expect('no verdict came from the releases tree', r.flags.some((f) => f.file.includes('releases')), false); @@ -763,6 +821,7 @@ function selfTest() { r.flags.some((f) => f.file.includes('node_modules')), false); // ── The teeth: measured drift class flags, by name ─────────────────── + battery('The teeth: measured drift class flags, by name'); expect('flag count', r.flags.length, 6); const flagged = r.flags.map((f) => f.literal).sort(); expect('the plural flags', flagged.includes('/api/v1/meta/objects/lead/state/status'), true); @@ -783,6 +842,7 @@ function selfTest() { stateFlags.some((f) => f.misses.some((m) => m.prose === 'objects' && m.row === 'object')), true); // ── Precision pins: what must NOT flag ─────────────────────────────── + battery('Precision pins: what must NOT flag'); expect('exact literals pass', r.stats.exact >= 10, true); expect('a wrong VALUE at a parameter position is not this gate’s business (meta/viewes)', r.flags.some((f) => f.literal.includes('viewes')), false); @@ -803,6 +863,7 @@ function selfTest() { && !r.flags.some((f) => f.literal === '/api/v1/reports/:id/'), true); // ── Red/green: dead root (#4916) ───────────────────────────────────── + battery('Red/green: dead root (#4916)'); renameSync(join(dir, 'skills'), join(dir, 'skills-renamed')); let deadErr = null; try { runScan(cfg); } catch (err) { deadErr = err; } @@ -811,6 +872,7 @@ function selfTest() { expect('the failure names the dead root only', deadErr?.roots?.join(','), 'skills'); // ── Red/green: empty root (#4932) ──────────────────────────────────── + battery('Red/green: empty root (#4932)'); const skillPath = join(dir, 'skills', 'demo', 'SKILL.md'); rmSync(skillPath); let emptyErr = null; @@ -820,6 +882,7 @@ function selfTest() { expect('the failure names the empty root only', emptyErr?.roots?.join(','), 'skills'); // ── Red/green: extractor evaporation (the occurrence floor) ────────── + battery('Red/green: extractor evaporation (the occurrence floor)'); writeFileSync(skillPath, 'No wire paths taught here any more.'); let floorErr = null; try { runScan(cfg); } catch (err) { floorErr = err; } @@ -828,6 +891,7 @@ function selfTest() { expect('the floor failure names the root', floorErr?.root, 'skills'); // ── Red/green: the authority itself (#4932 applied to the ledgers) ─── + battery('Red/green: the authority itself (#4932 applied to the ledgers)'); const restPath = join(dir, 'packages', 'rest', 'src', 'rest-route-ledger.ts'); renameSync(restPath, `${restPath}.moved`); let ledgerGone = null; @@ -851,6 +915,51 @@ function selfTest() { rmSync(dir, { recursive: true, force: true }); } + // ── The floor: every declared battery RAN, and ran its cases (#13489) ─── + // + // Evaluated after every battery has had its chance and BEFORE the verdict, so + // the success line below can only be printed by a run in which the set of + // batteries that registered assertions EQUALS the set declared. A set + // difference names WHICH battery stopped; a count says only that something did. + const floorFailure = (message) => { + failures.push(message); + }; + const declaredBatteries = Object.keys(SELF_TEST_BATTERIES); + let floorBreached = false; + if (declaredBatteries.length < SELF_TEST_BATTERY_FLOOR) { + floorBreached = true; + floorFailure( + `SELF_TEST_BATTERIES declares ${declaredBatteries.length} batteries, below the pinned ` + + `${SELF_TEST_BATTERY_FLOOR} — a battery deleted from the roster takes its own floor with it.`, + ); + } + for (const [name, count] of seen) { + if (declaredBatteries.includes(name)) continue; + floorBreached = true; + floorFailure( + `self-test battery "${name}" registered ${count} case(s) but is not declared in ` + + 'SELF_TEST_BATTERIES — an assertion attributed to no declared battery is one nothing floors.', + ); + } + for (const name of declaredBatteries) { + const count = seen.get(name) ?? 0; + if (count >= SELF_TEST_BATTERIES[name]) continue; + floorBreached = true; + floorFailure( + count === 0 + ? `self-test battery "${name}" DID NOT RUN — 0 cases registered, ${SELF_TEST_BATTERIES[name]} pinned. ` + + 'The verdict below would have claimed those cases hold.' + : `self-test battery "${name}" registered ${count} case(s), below its pinned floor of ` + + `${SELF_TEST_BATTERIES[name]} — cases that used to run no longer do.`, + ); + } + if (floorBreached) { + floorFailure( + 'A battery at or below its floor means cases STOPPED RUNNING — the battery is the bug, not the ' + + 'number. Find what stopped registering (an early return, a deleted block, a guard that now ' + + 'skips) and restore it.', + ); + } if (failures.length) { console.error(`\n✗ check-doc-route-spelling self-test failed:\n${failures.join('\n')}\n`); process.exit(1); diff --git a/scripts/check-docs-image-tag.mjs b/scripts/check-docs-image-tag.mjs index 1cab0ed5df..c3c90a3c8e 100644 --- a/scripts/check-docs-image-tag.mjs +++ b/scripts/check-docs-image-tag.mjs @@ -996,10 +996,63 @@ function report(findings, stats, expected, proseStats, driverStats) { // as one that passed (#13798). const SELF_TEST_VERDICT = 'check-docs-image-tag self-test reached its verdict'; +// ── The self-test's own battery roster and floor (#13489) ────────────────── +// +// `failures.length === 0` used to be this self-test's ONLY success condition, so +// "every case held" and "the cases never ran" printed the same line. Closed the +// way PR #13487 validated on check-doc-authoring: what is pinned is the +// registered NAMES, not a number. Every section opens with `battery('')`, +// every assertion is attributed to the battery most recently opened, and the +// floor requires the OPENED set to equal the DECLARED set with each battery at +// or above its own count. +// +// ⛔ A pinned TOTAL is not the repair: a battery dropping from 9 cases to 3 +// keeps a total "right" the moment a sibling grows. +// +// The counts are a FLOOR, not an equality — adding cases is ordinary work and +// must not red. A battery BELOW its floor means cases stopped running; the +// remedy is to find what stopped registering. +const SELF_TEST_BATTERIES = Object.freeze({ + 'Clean fixture: every shape that MUST stay silent': 9, + 'Dirty fixture: one control per limb, each a distinct failure': 12, + 'The classifier, asserted directly': 9, + 'The extractor\'s anchoring and position reporting': 6, + 'The expectation refuses to be unusable': 4, + 'The LIVE false-positive control (#9018\'s named risk, in situ)': 3, + 'The PROSE limb (#10229)': 22, + 'The LIVE prose control (#10229, in situ)': 2, + 'The driver-promise limb (#14510)': 20, + 'The LIVE driver control (#14510, in situ)': 2, + 'The green states its own scope': 5, +}); + +// DELETING an entry silences that battery's floor exactly as effectively as +// zeroing it, so the roster's own size is pinned too. +const SELF_TEST_BATTERY_FLOOR = 11; + +// The key an assertion is filed under when no battery is open. It is not a +// declared battery, so it reds by the same set difference rather than silently +// inflating whichever battery happened to run last. +const UNATTRIBUTED_BATTERY = '(no battery open)'; + async function selfTest() { + // The battery ledger this self-test's floor is evaluated against (#13489). + // `battery()` opens a battery; every assertion below is attributed to the one + // most recently opened, so a section that stops running stops registering and + // names ITSELF at the floor rather than going quiet. + const seen = new Map(); + let openBattery = null; + const battery = (name) => { + openBattery = name; + }; + const registerCase = () => { + const b = openBattery ?? UNATTRIBUTED_BATTERY; + seen.set(b, (seen.get(b) ?? 0) + 1); + }; const failures = []; let checked = 0; const assert = (condition, message) => { + registerCase(); checked++; if (!condition) failures.push(message); }; @@ -1019,6 +1072,7 @@ async function selfTest() { // The tag-table rows are copied VERBATIM from docker/README.md, because the // point of this control is the exact text living in the corpus today, not a // paraphrase of it that might differ in the one character that matters. + battery('Clean fixture: every shape that MUST stay silent'); write( 'clean/docs.md', [ @@ -1113,6 +1167,7 @@ async function selfTest() { ); // ── Dirty fixture: one control per limb, each a distinct failure ──────── + battery('Dirty fixture: one control per limb, each a distinct failure'); write( 'dirty/stale.md', [ @@ -1207,6 +1262,7 @@ async function selfTest() { ); // ── The classifier, asserted directly ─────────────────────────────────── + battery('The classifier, asserted directly'); assert(isConcreteVersion('17.0.0'), 'a plain X.Y.Z is concrete'); assert(isConcreteVersion('17.0.0-beta.1'), 'a prerelease is concrete (and compared as the WHOLE token)'); assert(isConcreteVersion('17.0.0.0'), 'a four-group typo is concrete, so it is compared rather than skipped'); @@ -1218,6 +1274,7 @@ async function selfTest() { assert(!isConcreteVersion('17.0.x'), "'17.0.x' is not concrete"); // ── The extractor's anchoring and position reporting ──────────────────── + battery('The extractor\'s anchoring and position reporting'); const positions = extractOccurrences('a\nb ghcr.io/objectstack-ai/objectstack:1.2.3 c\n'); assert(positions.length === 1, `an anchored tag mid-line is found once -- got ${positions.length}`); assert(positions[0].line === 2 && positions[0].column === 3, `line/column are 1-based -- got ${positions[0].line}:${positions[0].column}`); @@ -1237,6 +1294,7 @@ async function selfTest() { ); // ── The expectation refuses to be unusable ────────────────────────────── + battery('The expectation refuses to be unusable'); const rejects = (relative, contents, why) => { write(relative, contents); let threw = false; @@ -1258,6 +1316,7 @@ async function selfTest() { // The hermetic fixture above proves the classifier excludes 'X.Y.Z'. This // proves the metavariable is still IN the corpus and still excluded there -- // the assertion that would actually catch the gate going wrong on real data. + battery('The LIVE false-positive control (#9018\'s named risk, in situ)'); const root = scriptRepoRoot(); const controlPath = join(root, LIVE_CONTROL.file); if (existsSync(controlPath)) { @@ -1292,6 +1351,7 @@ async function selfTest() { // The DIRTY fixture: the corpus as `origin/main` actually stood at 17.1.0, // copied from the real files rather than paraphrased. This is the state #10229 // measured, so the control is a reproduction, not a hypothetical. + battery('The PROSE limb (#10229)'); write( 'prose/stale.mdx', [ @@ -1462,6 +1522,7 @@ async function selfTest() { // The fixtures prove the limb can go red. This proves it is pointed at real // claims that still exist -- the assertion that catches the enumeration // rotting after a docs rewrite, which is how all five claims got past 17.1.0. + battery('The LIVE prose control (#10229, in situ)'); const liveProse = checkProseClaims({ claims: PROSE_CLAIMS, root: scriptRepoRoot() }); assert( liveProse.findings.length === 0, @@ -1486,6 +1547,7 @@ async function selfTest() { // table naming drivers the image does NOT carry. Each of those has been a // plausible way to mis-parse this pair, so each is asserted rather than // reasoned about. + battery('The driver-promise limb (#14510)'); const driverDockerfile = (installLines) => [ '# comment mentioning pg and mysql2, which must not be parsed', 'FROM node:22-slim', @@ -1696,6 +1758,7 @@ async function selfTest() { // real pair, which still carries a real list -- the assertion that catches // both files being rewritten past their anchors at once, which no hermetic // fixture can see. + battery('The LIVE driver control (#14510, in situ)'); const liveDrivers = checkDriverPromise({ root: scriptRepoRoot() }); assert( liveDrivers.findings.length === 0, @@ -1708,6 +1771,7 @@ async function selfTest() { ); // ── The green states its own scope ────────────────────────────────────── + battery('The green states its own scope'); const line = summarise(clean.stats, expected, proseClean.stats, driverClean.stats); assert( line.includes('3 concrete pin(s) compared') && line.includes('4 rolling/floating tag(s) skipped'), @@ -1739,6 +1803,51 @@ async function selfTest() { rmSync(dir, { recursive: true, force: true }); } + // ── The floor: every declared battery RAN, and ran its cases (#13489) ─── + // + // Evaluated after every battery has had its chance and BEFORE the verdict, so + // the success line below can only be printed by a run in which the set of + // batteries that registered assertions EQUALS the set declared. A set + // difference names WHICH battery stopped; a count says only that something did. + const floorFailure = (message) => { + failures.push(message); + }; + const declaredBatteries = Object.keys(SELF_TEST_BATTERIES); + let floorBreached = false; + if (declaredBatteries.length < SELF_TEST_BATTERY_FLOOR) { + floorBreached = true; + floorFailure( + `SELF_TEST_BATTERIES declares ${declaredBatteries.length} batteries, below the pinned ` + + `${SELF_TEST_BATTERY_FLOOR} — a battery deleted from the roster takes its own floor with it.`, + ); + } + for (const [name, count] of seen) { + if (declaredBatteries.includes(name)) continue; + floorBreached = true; + floorFailure( + `self-test battery "${name}" registered ${count} case(s) but is not declared in ` + + 'SELF_TEST_BATTERIES — an assertion attributed to no declared battery is one nothing floors.', + ); + } + for (const name of declaredBatteries) { + const count = seen.get(name) ?? 0; + if (count >= SELF_TEST_BATTERIES[name]) continue; + floorBreached = true; + floorFailure( + count === 0 + ? `self-test battery "${name}" DID NOT RUN — 0 cases registered, ${SELF_TEST_BATTERIES[name]} pinned. ` + + 'The verdict below would have claimed those cases hold.' + : `self-test battery "${name}" registered ${count} case(s), below its pinned floor of ` + + `${SELF_TEST_BATTERIES[name]} — cases that used to run no longer do.`, + ); + } + if (floorBreached) { + floorFailure( + 'A battery at or below its floor means cases STOPPED RUNNING — the battery is the bug, not the ' + + 'number. Find what stopped registering (an early return, a deleted block, a guard that now ' + + 'skips) and restore it.', + ); + } if (failures.length) { console.error(`✗ check-docs-image-tag --self-test -- ${failures.length} failure(s)\n`); for (const failure of failures) console.error(` • ${failure}`); diff --git a/scripts/check-docs-nav-label.mjs b/scripts/check-docs-nav-label.mjs index 228d86a6a7..17b91df677 100644 --- a/scripts/check-docs-nav-label.mjs +++ b/scripts/check-docs-nav-label.mjs @@ -517,13 +517,65 @@ export async function main(argv = []) { // handshake is a flag rather than a returned sentinel. let selfTestReachedVerdict = false; +// ── The self-test's own battery roster and floor (#13489) ────────────────── +// +// `failures.length === 0` used to be this self-test's ONLY success condition, so +// "every case held" and "the cases never ran" printed the same line. Closed the +// way PR #13487 validated on check-doc-authoring: what is pinned is the +// registered NAMES, not a number. Every section opens with `battery('')`, +// every assertion is attributed to the battery most recently opened, and the +// floor requires the OPENED set to equal the DECLARED set with each battery at +// or above its own count. +// +// ⛔ A pinned TOTAL is not the repair: a battery dropping from 9 cases to 3 +// keeps a total "right" the moment a sibling grows. +// +// The counts are a FLOOR, not an equality — adding cases is ordinary work and +// must not red. A battery BELOW its floor means cases stopped running; the +// remedy is to find what stopped registering. +const SELF_TEST_BATTERIES = Object.freeze({ + 'the control file map and its per-source legs': 1, + 'leg A': 5, + 'leg C': 2, + 'leg D': 2, + 'leg E': 2, + 'leg B, over REAL modules': 5, + 'leg F': 3, + 'the real tree, and the population positive control': 3, + 'wiring: this gate really runs in CI': 2, +}); + +// DELETING an entry silences that battery's floor exactly as effectively as +// zeroing it, so the roster's own size is pinned too. +const SELF_TEST_BATTERY_FLOOR = 9; + +// The key an assertion is filed under when no battery is open. It is not a +// declared battery, so it reds by the same set difference rather than silently +// inflating whichever battery happened to run last. +const UNATTRIBUTED_BATTERY = '(no battery open)'; + export async function selfTest() { + // The battery ledger this self-test's floor is evaluated against (#13489). + // `battery()` opens a battery; every assertion below is attributed to the one + // most recently opened, so a section that stops running stops registering and + // names ITSELF at the floor rather than going quiet. + const seen = new Map(); + let openBattery = null; + const battery = (name) => { + openBattery = name; + }; + const registerCase = () => { + const b = openBattery ?? UNATTRIBUTED_BATTERY; + seen.set(b, (seen.get(b) ?? 0) + 1); + }; const failures = []; let checked = 0; const assert = (ok, what) => { + registerCase(); checked++; if (!ok) failures.push(what); }; + battery('the control file map and its per-source legs'); const legs = (violations) => [...new Set(violations.map((v) => v.leg))].sort(); // A file map that PASSES every source leg, as the control the mutations move. @@ -545,6 +597,7 @@ export async function selfTest() { assert(judgeSources(GOOD()).length === 0, `the control file map passes every source leg — got ${JSON.stringify(judgeSources(GOOD()))}`); // ── leg A ──────────────────────────────────────────────────────────────── + battery('leg A'); const leaked = GOOD(); leaked.set('apps/docs/app/og/docs/[...slug]/route.tsx', 'title={page.data.navTitle ?? page.data.title}\n'); assert(legs(judgeSources(leaked)).includes('A'), 'leg A fires when a consumer reads the key in code'); @@ -566,6 +619,7 @@ export async function selfTest() { assert(legs(judgeSources(undeclared)).includes('A'), 'leg A fires when the schema stops declaring the key'); // ── leg C ──────────────────────────────────────────────────────────────── + battery('leg C'); const unwired = GOOD(); unwired.set('apps/docs/lib/source.ts', 'export const source = loader({ plugins: [lucideIconsPlugin()] });\nexport const getLLMText = (page) => `# ${page.data.title}`;\n'); assert(legs(judgeSources(unwired)).includes('C'), 'leg C fires when the plugin is dropped from the loader'); @@ -575,6 +629,7 @@ export async function selfTest() { assert(legs(judgeSources(commentedOut)).includes('C'), 'leg C is not satisfied by the call COMMENTED OUT'); // ── leg D ──────────────────────────────────────────────────────────────── + battery('leg D'); const treeLeaf = GOOD(); treeLeaf.set(JSONLD, 'getBreadcrumbItems(page.url, tree, { includePage: true });\nconst t = page.data.title;\n'); assert(legs(judgeSources(treeLeaf)).includes('D'), 'leg D fires when the breadcrumb leaf is taken from the page tree again'); @@ -584,6 +639,7 @@ export async function selfTest() { assert(legs(judgeSources(noOption)).includes('D'), 'leg D fires when the option is absent — fumadocs defaults it to false, but silence is not a decision on the record'); // ── leg E ──────────────────────────────────────────────────────────────── + battery('leg E'); const rewired = GOOD(); rewired.set('apps/docs/app/llms.txt/route.ts', 'lines.push(treeNode.name);\n'); assert(legs(judgeSources(rewired)).includes('E'), 'leg E fires when a title consumer stops reading `page.data.title`'); @@ -593,6 +649,7 @@ export async function selfTest() { assert(legs(judgeSources(missing)).includes('E'), 'leg E fires when a pinned consumer is gone rather than reporting a clean zero'); // ── leg B, over REAL modules ───────────────────────────────────────────── + battery('leg B, over REAL modules'); const { mkdtempSync, rmSync, writeFileSync } = await import('node:fs'); const { tmpdir } = await import('node:os'); const tmp = mkdtempSync(join(tmpdir(), 'docs-nav-label-')); @@ -657,17 +714,20 @@ export async function selfTest() { } // ── leg F ──────────────────────────────────────────────────────────────── + battery('leg F'); assert(judgeFumadocs(['title', 'description', 'icon', 'full', '_openapi'], ['title', 'pages']).length === 0, 'leg F is green on fumadocs 16.14.4\'s real key sets'); assert(judgeFumadocs(['title', 'sidebarTitle'], ['title']).some((v) => v.leg === 'F'), 'leg F fires when fumadocs ships a first-class nav label'); assert(judgeFumadocs(['title'], ['title', 'navLabel']).some((v) => v.leg === 'F'), 'leg F watches `metaSchema` too — a per-page label override would land there'); // ── the real tree, and the population positive control ─────────────────── + battery('the real tree, and the population positive control'); const real = readCodeFiles(join(REPO_ROOT, 'apps/docs')); assert(real.size > 0, 'the real apps/docs scan reads a non-empty population — a clean zero over nothing is the failure this control exists for'); assert(real.has(RESOLVER) && real.has(LOADER) && real.has(JSONLD), 'the real scan reaches all three mechanism files'); assert(!real.has('apps/docs/node_modules/x.ts'), 'the walk skips installed and generated directories'); // ── wiring: this gate really runs in CI ────────────────────────────────── + battery('wiring: this gate really runs in CI'); const SELF = 'scripts/check-docs-nav-label.mjs'; let lint = null; try { @@ -680,6 +740,51 @@ export async function selfTest() { assert(lint.includes(`node ${SELF} --self-test`), 'wiring: lint.yml runs the --self-test leg too'); } + // ── The floor: every declared battery RAN, and ran its cases (#13489) ─── + // + // Evaluated after every battery has had its chance and BEFORE the verdict, so + // the success line below can only be printed by a run in which the set of + // batteries that registered assertions EQUALS the set declared. A set + // difference names WHICH battery stopped; a count says only that something did. + const floorFailure = (message) => { + failures.push(message); + }; + const declaredBatteries = Object.keys(SELF_TEST_BATTERIES); + let floorBreached = false; + if (declaredBatteries.length < SELF_TEST_BATTERY_FLOOR) { + floorBreached = true; + floorFailure( + `SELF_TEST_BATTERIES declares ${declaredBatteries.length} batteries, below the pinned ` + + `${SELF_TEST_BATTERY_FLOOR} — a battery deleted from the roster takes its own floor with it.`, + ); + } + for (const [name, count] of seen) { + if (declaredBatteries.includes(name)) continue; + floorBreached = true; + floorFailure( + `self-test battery "${name}" registered ${count} case(s) but is not declared in ` + + 'SELF_TEST_BATTERIES — an assertion attributed to no declared battery is one nothing floors.', + ); + } + for (const name of declaredBatteries) { + const count = seen.get(name) ?? 0; + if (count >= SELF_TEST_BATTERIES[name]) continue; + floorBreached = true; + floorFailure( + count === 0 + ? `self-test battery "${name}" DID NOT RUN — 0 cases registered, ${SELF_TEST_BATTERIES[name]} pinned. ` + + 'The verdict below would have claimed those cases hold.' + : `self-test battery "${name}" registered ${count} case(s), below its pinned floor of ` + + `${SELF_TEST_BATTERIES[name]} — cases that used to run no longer do.`, + ); + } + if (floorBreached) { + floorFailure( + 'A battery at or below its floor means cases STOPPED RUNNING — the battery is the bug, not the ' + + 'number. Find what stopped registering (an early return, a deleted block, a guard that now ' + + 'skips) and restore it.', + ); + } if (failures.length > 0) { console.error(`✗ check-docs-nav-label --self-test — ${failures.length} of ${checked} assertion(s) failed\n`); for (const f of failures) console.error(` • ${f}`); diff --git a/scripts/check-docs-redirects.mjs b/scripts/check-docs-redirects.mjs index 3ebf586d64..67cf038ad5 100644 --- a/scripts/check-docs-redirects.mjs +++ b/scripts/check-docs-redirects.mjs @@ -455,10 +455,57 @@ function report(findings, stats, label) { // as one that passed (#13798). const SELF_TEST_VERDICT = 'check-docs-redirects self-test reached its verdict'; +// ── The self-test's own battery roster and floor (#13489) ────────────────── +// +// `failures.length === 0` used to be this self-test's ONLY success condition, so +// "every case held" and "the cases never ran" printed the same line. Closed the +// way PR #13487 validated on check-doc-authoring: what is pinned is the +// registered NAMES, not a number. Every section opens with `battery('')`, +// every assertion is attributed to the battery most recently opened, and the +// floor requires the OPENED set to equal the DECLARED set with each battery at +// or above its own count. +// +// ⛔ A pinned TOTAL is not the repair: a battery dropping from 9 cases to 3 +// keeps a total "right" the moment a sibling grows. +// +// The counts are a FLOOR, not an equality — adding cases is ordinary work and +// must not red. A battery BELOW its floor means cases stopped running; the +// remedy is to find what stopped registering. +const SELF_TEST_BATTERIES = Object.freeze({ + 'Table 1: clean. Every resolution shape, and no chains.': 9, + 'Table 2: dirty. One control per limb, each a distinct failure.': 15, + 'The matcher\'s Next semantics, asserted directly': 20, + 'The loader refuses to report OK over nothing (#4690)': 3, + 'The green states its own scope': 1, +}); + +// DELETING an entry silences that battery's floor exactly as effectively as +// zeroing it, so the roster's own size is pinned too. +const SELF_TEST_BATTERY_FLOOR = 5; + +// The key an assertion is filed under when no battery is open. It is not a +// declared battery, so it reds by the same set difference rather than silently +// inflating whichever battery happened to run last. +const UNATTRIBUTED_BATTERY = '(no battery open)'; + async function selfTest() { + // The battery ledger this self-test's floor is evaluated against (#13489). + // `battery()` opens a battery; every assertion below is attributed to the one + // most recently opened, so a section that stops running stops registering and + // names ITSELF at the floor rather than going quiet. + const seen = new Map(); + let openBattery = null; + const battery = (name) => { + openBattery = name; + }; + const registerCase = () => { + const b = openBattery ?? UNATTRIBUTED_BATTERY; + seen.set(b, (seen.get(b) ?? 0) + 1); + }; const failures = []; let checked = 0; const assert = (condition, message) => { + registerCase(); checked++; if (!condition) failures.push(message); }; @@ -488,6 +535,7 @@ async function selfTest() { write('content/docs/not-a-dir.mdx', '# Not a dir\n'); // ── Table 1: clean. Every resolution shape, and no chains. ────────────── + battery('Table 1: clean. Every resolution shape, and no chains.'); const cleanTable = [ ['/old/plain', '/docs/plain'], ['/old/legacy', '/docs/legacy'], @@ -532,6 +580,7 @@ async function selfTest() { // // Ordered so that the CHAIN controls sit after the sources they collide // with: first match wins, so where a probe lands is an ordering fact. + battery('Table 2: dirty. One control per limb, each a distinct failure.'); const dirtyTable = [ /* 1 */ ['/old/gone', '/docs/no-such-page'], // DEAD -- nothing there at all /* 2 */ ['/old/section-ish', '/docs/dead-section'], // DEAD -- dir exists, no index page @@ -624,6 +673,7 @@ async function selfTest() { ); // ── The matcher's Next semantics, asserted directly ───────────────────── + battery('The matcher\'s Next semantics, asserted directly'); const matches = (source, url) => compileSource(source).match?.(url) === true; assert(matches('/a/:path*', '/a'), "'/a/:path*' matches the bare '/a' (zero segments)"); assert(matches('/a/:path*', '/a/deep/path'), "'/a/:path*' matches '/a/deep/path'"); @@ -649,6 +699,7 @@ async function selfTest() { assert(chainProbes('/docs/a').join() === '/docs/a', 'an exact destination is probed once, as itself'); // ── The loader refuses to report OK over nothing (#4690) ──────────────── + battery('The loader refuses to report OK over nothing (#4690)'); const rejects = async (relative, contents, why) => { write(relative, contents); let threw = false; @@ -664,6 +715,7 @@ async function selfTest() { await rejects('shape-redirects.mjs', 'export const docsRedirects = [["/a"]];\n', 'a malformed entry is rejected'); // ── The green states its own scope ────────────────────────────────────── + battery('The green states its own scope'); const line = summarise(clean.stats); assert( line.includes('7 page destination(s)') && line.includes('1 wildcard destination(s)') && line.includes('10 chain probe(s)'), @@ -673,6 +725,51 @@ async function selfTest() { rmSync(dir, { recursive: true, force: true }); } + // ── The floor: every declared battery RAN, and ran its cases (#13489) ─── + // + // Evaluated after every battery has had its chance and BEFORE the verdict, so + // the success line below can only be printed by a run in which the set of + // batteries that registered assertions EQUALS the set declared. A set + // difference names WHICH battery stopped; a count says only that something did. + const floorFailure = (message) => { + failures.push(message); + }; + const declaredBatteries = Object.keys(SELF_TEST_BATTERIES); + let floorBreached = false; + if (declaredBatteries.length < SELF_TEST_BATTERY_FLOOR) { + floorBreached = true; + floorFailure( + `SELF_TEST_BATTERIES declares ${declaredBatteries.length} batteries, below the pinned ` + + `${SELF_TEST_BATTERY_FLOOR} — a battery deleted from the roster takes its own floor with it.`, + ); + } + for (const [name, count] of seen) { + if (declaredBatteries.includes(name)) continue; + floorBreached = true; + floorFailure( + `self-test battery "${name}" registered ${count} case(s) but is not declared in ` + + 'SELF_TEST_BATTERIES — an assertion attributed to no declared battery is one nothing floors.', + ); + } + for (const name of declaredBatteries) { + const count = seen.get(name) ?? 0; + if (count >= SELF_TEST_BATTERIES[name]) continue; + floorBreached = true; + floorFailure( + count === 0 + ? `self-test battery "${name}" DID NOT RUN — 0 cases registered, ${SELF_TEST_BATTERIES[name]} pinned. ` + + 'The verdict below would have claimed those cases hold.' + : `self-test battery "${name}" registered ${count} case(s), below its pinned floor of ` + + `${SELF_TEST_BATTERIES[name]} — cases that used to run no longer do.`, + ); + } + if (floorBreached) { + floorFailure( + 'A battery at or below its floor means cases STOPPED RUNNING — the battery is the bug, not the ' + + 'number. Find what stopped registering (an early return, a deleted block, a guard that now ' + + 'skips) and restore it.', + ); + } if (failures.length) { console.error(`✗ check-docs-redirects --self-test -- ${failures.length} failure(s)\n`); for (const failure of failures) console.error(` • ${failure}`); diff --git a/scripts/check-driver-conformance.mjs b/scripts/check-driver-conformance.mjs index ad73041e47..3519847a55 100644 --- a/scripts/check-driver-conformance.mjs +++ b/scripts/check-driver-conformance.mjs @@ -1581,9 +1581,62 @@ function report() { // as one that passed (#13798). const SELF_TEST_VERDICT = 'check-driver-conformance self-test reached its verdict'; +// ── The self-test's own battery roster and floor (#13489) ────────────────── +// +// `failures.length === 0` used to be this self-test's ONLY success condition, so +// "every case held" and "the cases never ran" printed the same line. Closed the +// way PR #13487 validated on check-doc-authoring: what is pinned is the +// registered NAMES, not a number. Every section opens with `battery('')`, +// every assertion is attributed to the battery most recently opened, and the +// floor requires the OPENED set to equal the DECLARED set with each battery at +// or above its own count. +// +// ⛔ A pinned TOTAL is not the repair: a battery dropping from 9 cases to 3 +// keeps a total "right" the moment a sibling grows. +// +// The counts are a FLOOR, not an equality — adding cases is ordinary work and +// must not red. A battery BELOW its floor means cases stopped running; the +// remedy is to find what stopped registering. +const SELF_TEST_BATTERIES = Object.freeze({ + 'CONSUMED — what counts as coverage': 33, + 'Reverse proof for the dead-root hard error (#4930), made permanent.': 10, + 'The ratchet-remedy authority convention (#8435)': 3, + 'The dispatch-gates declaration (#10840)': 6, + 'The comment mask, as THIS gate uses it.': 2, + 'dialectStance: what a suite SAYS it runs on.': 6, + 'discoverDialectTestkit + dialectAudit, over a synthetic tree.': 14, + 'MATRIXED, both directions, over the same synthetic tree.': 13, + 'coveringFiles returns ALL of them, which is what the axis needs.': 2, + 'The #8435 authority convention, for the dialect ledger\'s own offer.': 3, + 'The real tree: the axis is WIRED IN, not merely defined.': 6, +}); + +// DELETING an entry silences that battery's floor exactly as effectively as +// zeroing it, so the roster's own size is pinned too. +const SELF_TEST_BATTERY_FLOOR = 11; + +// The key an assertion is filed under when no battery is open. It is not a +// declared battery, so it reds by the same set difference rather than silently +// inflating whichever battery happened to run last. +const UNATTRIBUTED_BATTERY = '(no battery open)'; + function selfTest() { + // The battery ledger this self-test's floor is evaluated against (#13489). + // `battery()` opens a battery; every assertion below is attributed to the one + // most recently opened, so a section that stops running stops registering and + // names ITSELF at the floor rather than going quiet. + const seen = new Map(); + let openBattery = null; + const battery = (name) => { + openBattery = name; + }; + const registerCase = () => { + const b = openBattery ?? UNATTRIBUTED_BATTERY; + seen.set(b, (seen.get(b) ?? 0) + 1); + }; const failures = []; const expect = (label, cond) => { + registerCase(); if (!cond) failures.push(label); }; @@ -1600,6 +1653,7 @@ function selfTest() { * superseded rule as text is what lets both pins stay falsifiable against the * thing they are actually about. */ + battery('CONSUMED — what counts as coverage'); const roamingRule = (symbol) => new RegExp(`import[\\s\\S]*?\\b${symbol}\\b[\\s\\S]*?from\\s+['"]@objectstack/spec/data['"]`); @@ -1885,6 +1939,7 @@ function selfTest() { // gate whose failure mode is discovering an empty axis. So break a root the way // a rename breaks it, require red naming that root and not the survivor, then // restore it and require green again. Red-then-green, in the same run, every run. + battery('Reverse proof for the dead-root hard error (#4930), made permanent.'); const tmpRoots = join(ROOT, 'node_modules', '.check-driver-conformance-selftest-roots'); try { mkdirSync(join(tmpRoots, 'live'), { recursive: true }); @@ -1948,6 +2003,7 @@ function selfTest() { // // (3) is what makes (2) worth having: without it, a predicate that approves // everything would keep this block green while the convention is gone. + battery('The ratchet-remedy authority convention (#8435)'); const consumed = consumedMessage('driver-example', { marker: 'PAGINATION_CASES', what: 'a sorted paged read is a partition', @@ -1989,6 +2045,7 @@ function selfTest() { // the brief. Both halves are DERIVED from DRIVERS_DIR rather than re-spelled, // so moving the driver tree cannot leave the declaration describing the old // location -- the failure mode a hand-kept copy has. + battery('The dispatch-gates declaration (#10840)'); const driversRel = DRIVERS_DIR.slice(ROOT.length + 1); expect('the declaration names the driver subtree this gate actually walks, derived from ' + 'DRIVERS_DIR rather than re-spelled beside it', @@ -2030,12 +2087,14 @@ function selfTest() { // What IS asserted here is that this gate routes through it at all: the two // cases below are the ones a private `stripComments` got wrong, so they name // the failure family rather than re-deriving the fix. + battery('The comment mask, as THIS gate uses it.'); expect('a comment cannot declare anything', !stripComments('const a = 1; // DIALECT_CELLS\n').includes('DIALECT_CELLS')); expect('and a quote inside a regex character class does not swallow the code after it (the ' + 'phantom-string family, which is why this gate uses the shared mask rather than its own)', stripComments('function f(s) { return /[\'"]/.test(s); }\nconst KEEP = 1;\n').includes('KEEP')); // -- dialectStance: what a suite SAYS it runs on. -- + battery('dialectStance: what a suite SAYS it runs on.'); const KIT_SPEC = './kit.testkit.js'; expect('iterating the cell list is the matrix stance', dialectStance("import { DIALECT_CELLS } from './kit.testkit.js';\nfor (const c of DIALECT_CELLS) {}\n", KIT_SPEC) === 'matrix'); @@ -2075,6 +2134,7 @@ function selfTest() { } // -- discoverDialectTestkit + dialectAudit, over a synthetic tree. -- + battery('discoverDialectTestkit + dialectAudit, over a synthetic tree.'); const tmpDialect = join(ROOT, 'node_modules', '.check-driver-conformance-selftest-dialect'); try { const dsrc = (d) => join(tmpDialect, d, 'src'); @@ -2155,6 +2215,7 @@ function selfTest() { // The direction that matters: a suite can satisfy DIALECTED completely and // still leave D-A3 enforced nowhere. This is the tree #12136 promotes the // invariant against. + battery('MATRIXED, both directions, over the same synthetic tree.'); const honestlyNarrow = drive([[cellFile, ['PAGINATION_CASES']]], []); expect('#12136 — a named-cell suite states a stance, so DIALECTED is satisfied', !honestlyNarrow.errs.some((e) => e.startsWith('DIALECTED:'))); @@ -2206,6 +2267,7 @@ function selfTest() { } // -- coveringFiles returns ALL of them, which is what the axis needs. -- + battery('coveringFiles returns ALL of them, which is what the axis needs.'); const tmpMulti = join(ROOT, 'node_modules', '.check-driver-conformance-selftest-covering'); try { mkdirSync(join(tmpMulti, 'src'), { recursive: true }); @@ -2222,6 +2284,7 @@ function selfTest() { } // -- The #8435 authority convention, for the dialect ledger's own offer. -- + battery('The #8435 authority convention, for the dialect ledger\'s own offer.'); const dialected = dialectedMessage( 'driver-example', 'packages/drivers/driver-example/src/a.test.ts', ['PAGINATION_CASES'], { specifier: './kit.testkit.js', cellIds: ['sqlite', 'pg'] }, @@ -2248,6 +2311,7 @@ function selfTest() { } // -- The real tree: the axis is WIRED IN, not merely defined. -- + battery('The real tree: the axis is WIRED IN, not merely defined.'); const liveKit = discoverDialectTestkit(join(DRIVERS_DIR, 'driver-sql')); expect('driver-sql is discovered as dialect-capable from disk', liveKit !== null); expect('and D-A3\'s two minimum dialects are both cells of it ("SQLite, Postgres at minimum")', @@ -2281,6 +2345,51 @@ function selfTest() { !live.errors.some((e) => e.startsWith('MATRIXED:')) && errs.length === 0); } + // ── The floor: every declared battery RAN, and ran its cases (#13489) ─── + // + // Evaluated after every battery has had its chance and BEFORE the verdict, so + // the success line below can only be printed by a run in which the set of + // batteries that registered assertions EQUALS the set declared. A set + // difference names WHICH battery stopped; a count says only that something did. + const floorFailure = (message) => { + failures.push(message); + }; + const declaredBatteries = Object.keys(SELF_TEST_BATTERIES); + let floorBreached = false; + if (declaredBatteries.length < SELF_TEST_BATTERY_FLOOR) { + floorBreached = true; + floorFailure( + `SELF_TEST_BATTERIES declares ${declaredBatteries.length} batteries, below the pinned ` + + `${SELF_TEST_BATTERY_FLOOR} — a battery deleted from the roster takes its own floor with it.`, + ); + } + for (const [name, count] of seen) { + if (declaredBatteries.includes(name)) continue; + floorBreached = true; + floorFailure( + `self-test battery "${name}" registered ${count} case(s) but is not declared in ` + + 'SELF_TEST_BATTERIES — an assertion attributed to no declared battery is one nothing floors.', + ); + } + for (const name of declaredBatteries) { + const count = seen.get(name) ?? 0; + if (count >= SELF_TEST_BATTERIES[name]) continue; + floorBreached = true; + floorFailure( + count === 0 + ? `self-test battery "${name}" DID NOT RUN — 0 cases registered, ${SELF_TEST_BATTERIES[name]} pinned. ` + + 'The verdict below would have claimed those cases hold.' + : `self-test battery "${name}" registered ${count} case(s), below its pinned floor of ` + + `${SELF_TEST_BATTERIES[name]} — cases that used to run no longer do.`, + ); + } + if (floorBreached) { + floorFailure( + 'A battery at or below its floor means cases STOPPED RUNNING — the battery is the bug, not the ' + + 'number. Find what stopped registering (an early return, a deleted block, a guard that now ' + + 'skips) and restore it.', + ); + } if (failures.length) { for (const f of failures) console.error(` x self-test: ${f}`); console.error(`\ncheck-driver-conformance --self-test: ${failures.length} failure(s).\n`); diff --git a/scripts/check-driver-memory-census.mjs b/scripts/check-driver-memory-census.mjs index f1981309d2..7ef9ca23d3 100644 --- a/scripts/check-driver-memory-census.mjs +++ b/scripts/check-driver-memory-census.mjs @@ -527,12 +527,60 @@ function report({ list = false } = {}) { // as one that passed (#13798). const SELF_TEST_VERDICT = 'check-driver-memory-census self-test reached its verdict'; +// ── The self-test's own battery roster and floor (#13489) ────────────────── +// +// `failures.length === 0` used to be this self-test's ONLY success condition, so +// "every case held" and "the cases never ran" printed the same line. Closed the +// way PR #13487 validated on check-doc-authoring: what is pinned is the +// registered NAMES, not a number. Every section opens with `battery('')`, +// every assertion is attributed to the battery most recently opened, and the +// floor requires the OPENED set to equal the DECLARED set with each battery at +// or above its own count. +// +// ⛔ A pinned TOTAL is not the repair: a battery dropping from 9 cases to 3 +// keeps a total "right" the moment a sibling grows. +// +// The counts are a FLOOR, not an equality — adding cases is ordinary work and +// must not red. A battery BELOW its floor means cases stopped running; the +// remedy is to find what stopped registering. +const SELF_TEST_BATTERIES = Object.freeze({ + 'Binding positions, one per form.': 8, + 'MENTIONS: the distinction the whole census rests on. Each of these really': 6, + 'Two bindings in one file are reported separately (dev-plugin.ts really': 1, + 'Manifest scanning: dependency fields yes, `name` no.': 5, + 'Reconciliation, driven on synthetic scans so a real violation never': 14, + 'Wiring: discovery must reach the real tree and find the ruled files. NOT': 7, +}); + +// DELETING an entry silences that battery's floor exactly as effectively as +// zeroing it, so the roster's own size is pinned too. +const SELF_TEST_BATTERY_FLOOR = 6; + +// The key an assertion is filed under when no battery is open. It is not a +// declared battery, so it reds by the same set difference rather than silently +// inflating whichever battery happened to run last. +const UNATTRIBUTED_BATTERY = '(no battery open)'; + function selfTest() { + // The battery ledger this self-test's floor is evaluated against (#13489). + // `battery()` opens a battery; every assertion below is attributed to the one + // most recently opened, so a section that stops running stops registering and + // names ITSELF at the floor rather than going quiet. + const seen = new Map(); + let openBattery = null; + const battery = (name) => { + openBattery = name; + }; + const registerCase = () => { + const b = openBattery ?? UNATTRIBUTED_BATTERY; + seen.set(b, (seen.get(b) ?? 0) + 1); + }; const failures = []; - const expect = (label, cond) => { if (!cond) failures.push(label); }; + const expect = (label, cond) => { registerCase(); if (!cond) failures.push(label); }; const kinds = (src) => scanSource('a.ts', src).map((h) => h.kind); // ── Binding positions, one per form. + battery('Binding positions, one per form.'); expect('static import is a binding', kinds(`import { InMemoryDriver } from '${SPECIFIER}';`).join() === 'import'); expect('export-from is a binding', @@ -552,6 +600,7 @@ function selfTest() { // ── MENTIONS: the distinction the whole census rests on. Each of these really // occurs in the tree today (tsup externals, comment prose, log strings). + battery('MENTIONS: the distinction the whole census rests on. Each of these really'); expect('a bundler externals array entry is a mention, not a binding', kinds(`export default { external: ['${SPECIFIER}'] };`).join() === 'string-literal'); expect('a bare constant holding the name is a mention, not a binding', @@ -566,10 +615,12 @@ function selfTest() { // ── Two bindings in one file are reported separately (dev-plugin.ts really // carries a dynamic import next to prose; a file could carry two forms). + battery('Two bindings in one file are reported separately (dev-plugin.ts really'); const two = kinds(`vi.mock('${SPECIFIER}', () => ({}));\nconst m = await import('${SPECIFIER}');`); expect('sibling bindings in one file are judged independently', two.join() === 'mock,dynamic-import'); // ── Manifest scanning: dependency fields yes, `name` no. + battery('Manifest scanning: dependency fields yes, `name` no.'); expect('dependencies is a declaration', scanManifest({ dependencies: { [SPECIFIER]: 'workspace:*' } }).map((d) => d.field).join() === 'dependencies'); expect('devDependencies is a declaration', @@ -582,6 +633,7 @@ function selfTest() { // ── Reconciliation, driven on synthetic scans so a real violation never // surfaces as a self-test failure (the least legible message available). + battery('Reconciliation, driven on synthetic scans so a real violation never'); const ruledFile = 'p/ruled.test.ts'; const okLedger = () => ({ ruledConsumers: [{ @@ -674,6 +726,7 @@ function selfTest() { // ── Wiring: discovery must reach the real tree and find the ruled files. NOT // asserted here: that the tree is clean — that is the gated run's job. + battery('Wiring: discovery must reach the real tree and find the ruled files. NOT'); if (existsSync(LEDGER_PATH)) { const realScan = scanRepo(); const realLedger = loadLedger(); @@ -725,6 +778,51 @@ function selfTest() { ruled.length <= RULED_CEILING); } + // ── The floor: every declared battery RAN, and ran its cases (#13489) ─── + // + // Evaluated after every battery has had its chance and BEFORE the verdict, so + // the success line below can only be printed by a run in which the set of + // batteries that registered assertions EQUALS the set declared. A set + // difference names WHICH battery stopped; a count says only that something did. + const floorFailure = (message) => { + failures.push(message); + }; + const declaredBatteries = Object.keys(SELF_TEST_BATTERIES); + let floorBreached = false; + if (declaredBatteries.length < SELF_TEST_BATTERY_FLOOR) { + floorBreached = true; + floorFailure( + `SELF_TEST_BATTERIES declares ${declaredBatteries.length} batteries, below the pinned ` + + `${SELF_TEST_BATTERY_FLOOR} — a battery deleted from the roster takes its own floor with it.`, + ); + } + for (const [name, count] of seen) { + if (declaredBatteries.includes(name)) continue; + floorBreached = true; + floorFailure( + `self-test battery "${name}" registered ${count} case(s) but is not declared in ` + + 'SELF_TEST_BATTERIES — an assertion attributed to no declared battery is one nothing floors.', + ); + } + for (const name of declaredBatteries) { + const count = seen.get(name) ?? 0; + if (count >= SELF_TEST_BATTERIES[name]) continue; + floorBreached = true; + floorFailure( + count === 0 + ? `self-test battery "${name}" DID NOT RUN — 0 cases registered, ${SELF_TEST_BATTERIES[name]} pinned. ` + + 'The verdict below would have claimed those cases hold.' + : `self-test battery "${name}" registered ${count} case(s), below its pinned floor of ` + + `${SELF_TEST_BATTERIES[name]} — cases that used to run no longer do.`, + ); + } + if (floorBreached) { + floorFailure( + 'A battery at or below its floor means cases STOPPED RUNNING — the battery is the bug, not the ' + + 'number. Find what stopped registering (an early return, a deleted block, a guard that now ' + + 'skips) and restore it.', + ); + } if (failures.length) { for (const f of failures) console.error(` x self-test: ${f}`); console.error(`\ncheck-driver-memory-census --self-test: ${failures.length} failure(s).\n`); diff --git a/scripts/check-empty-changeset.mjs b/scripts/check-empty-changeset.mjs index cf431bb41d..66ffb939dc 100644 --- a/scripts/check-empty-changeset.mjs +++ b/scripts/check-empty-changeset.mjs @@ -412,10 +412,73 @@ function list() { // as one that passed (#13798). const SELF_TEST_VERDICT = 'check-empty-changeset self-test reached its verdict'; +// ── The self-test's own battery roster and floor (#13489) ────────────────── +// +// `failures.length === 0` used to be this self-test's ONLY success condition, so +// "every case held" and "the cases never ran" printed the same line. Closed the +// way PR #13487 validated on check-doc-authoring: what is pinned is the +// registered NAMES, not a number. Every section opens with `battery('')`, +// every assertion is attributed to the battery most recently opened, and the +// floor requires the OPENED set to equal the DECLARED set with each battery at +// or above its own count. +// +// ⛔ A pinned TOTAL is not the repair: a battery dropping from 9 cases to 3 +// keeps a total "right" the moment a sibling grows. +// +// The counts are a FLOOR, not an equality — adding cases is ordinary work and +// must not red. A battery BELOW its floor means cases stopped running; the +// remedy is to find what stopped registering. +const SELF_TEST_BATTERIES = Object.freeze({ + 'RED 1: a PR that ADDS an empty-frontmatter changeset': 3, + 'GREEN 1: the stock. Empty changesets on base, untouched by the PR': 2, + 'GREEN 2: a PR that adds a NON-empty changeset': 1, + 'GREEN 3: a skills/**-only PR carrying NO changeset (route 2)': 2, + 'RED 2: a stock NON-empty changeset EMPTIED in place': 2, + 'GREEN 4: a stock EMPTY changeset whose prose is edited': 2, + 'RED 3: a new changeset with no frontmatter fence at all': 1, + 'GREEN 5: .changeset/README.md is not a changeset': 1, + 'RED 4: a stock non-empty changeset RENAMED AND EMPTIED in one commit': 5, + 'GREEN 6: a PURE rename of a stock EMPTY changeset stays exempt': 3, + 'GREEN 7: a pure rename of a stock DECLARING changeset is simply ok': 3, + 'RED 5: an `R` row whose BASE side is README.md is not "inherited"': 3, + '#6129: main drift must not move the verdict, in EITHER direction': 6, + '#6129, the other half: a base branch that DELETES': 2, + '#4690, one step later: no merge base at all is a failure': 1, + 'The consumer: this gate\'s own CI step (#6129)': 23, + 'The second consumer: where THIS SELF-TEST runs (#6509)': 12, + 'Parser unit rows': 8, + 'THE FIX (#7004): comments and quoted bump values': 12, + 'The family reads one block one way (#7004)': 25, + 'Missing input is a failure, never a pass (#4690)': 1, +}); + +// DELETING an entry silences that battery's floor exactly as effectively as +// zeroing it, so the roster's own size is pinned too. +const SELF_TEST_BATTERY_FLOOR = 21; + +// The key an assertion is filed under when no battery is open. It is not a +// declared battery, so it reds by the same set difference rather than silently +// inflating whichever battery happened to run last. +const UNATTRIBUTED_BATTERY = '(no battery open)'; + function selfTest() { + // The battery ledger this self-test's floor is evaluated against (#13489). + // `battery()` opens a battery; every assertion below is attributed to the one + // most recently opened, so a section that stops running stops registering and + // names ITSELF at the floor rather than going quiet. + const seen = new Map(); + let openBattery = null; + const battery = (name) => { + openBattery = name; + }; + const registerCase = () => { + const b = openBattery ?? UNATTRIBUTED_BATTERY; + seen.set(b, (seen.get(b) ?? 0) + 1); + }; const failures = []; let checked = 0; const assert = (cond, msg) => { + registerCase(); checked++; if (!cond) failures.push(msg); }; @@ -514,6 +577,7 @@ function selfTest() { // ── RED 1: a PR that ADDS an empty-frontmatter changeset ───────────────── // The #5799 shape verbatim: a skills/** change declaring nothing, via a new // empty changeset. This is the case the gate exists for. + battery('RED 1: a PR that ADDS an empty-frontmatter changeset'); { const { dir, base } = makeRepo( { '.changeset/README.md': '# Changesets\n', 'skills/demo/SKILL.md': 'v1\n' }, @@ -531,6 +595,7 @@ function selfTest() { // ── GREEN 1: the stock. Empty changesets on base, untouched by the PR ──── // The ruling's exemption, and the reason this gate reads a diff rather than // the directory: 182 such files sit on main and none of them may go red. + battery('GREEN 1: the stock. Empty changesets on base, untouched by the PR'); { const { dir, base } = makeRepo( { @@ -547,6 +612,7 @@ function selfTest() { } // ── GREEN 2: a PR that adds a NON-empty changeset ──────────────────────── + battery('GREEN 2: a PR that adds a NON-empty changeset'); { const { dir, base } = makeRepo( { 'packages/spec/src/index.ts': 'export const v = 1;\n' }, @@ -559,6 +625,7 @@ function selfTest() { // ── GREEN 3: a skills/**-only PR carrying NO changeset (route 2) ───────── // The #5947 destination. Such a PR takes the `skip-changeset` label; this // gate must have nothing to say about it, label or no label. + battery('GREEN 3: a skills/**-only PR carrying NO changeset (route 2)'); { const { dir, base } = makeRepo( { 'skills/objectstack-pm-dispatch/SKILL.md': 'two axes\n', '.changeset/stock-empty.md': EMPTY }, @@ -572,6 +639,7 @@ function selfTest() { // ── RED 2: a stock NON-empty changeset EMPTIED in place ────────────────── // The bypass `--diff-filter=A` alone cannot see. A new empty declaration is // a new empty declaration however it was spelled. + battery('RED 2: a stock NON-empty changeset EMPTIED in place'); { const { dir, base } = makeRepo( { '.changeset/was-declaring.md': DECLARING }, @@ -585,6 +653,7 @@ function selfTest() { // ── GREEN 4: a stock EMPTY changeset whose prose is edited ─────────────── // Still empty at base, so this PR created no new empty declaration. This is // the row that keeps the exemption honest under `--diff-filter=AMR`. + battery('GREEN 4: a stock EMPTY changeset whose prose is edited'); { const { dir, base } = makeRepo( { '.changeset/stock-empty.md': EMPTY }, @@ -596,6 +665,7 @@ function selfTest() { } // ── RED 3: a new changeset with no frontmatter fence at all ────────────── + battery('RED 3: a new changeset with no frontmatter fence at all'); { const { dir, base } = makeRepo({}, { '.changeset/no-fence.md': 'just a body, no fence\n' }); const r = scan({ cwd: dir, base }); @@ -603,6 +673,7 @@ function selfTest() { } // ── GREEN 5: .changeset/README.md is not a changeset ───────────────────── + battery('GREEN 5: .changeset/README.md is not a changeset'); { const { dir, base } = makeRepo({}, { '.changeset/README.md': '# Changesets\n\nhow to write one\n' }); const r = scan({ cwd: dir, base }); @@ -633,6 +704,7 @@ function selfTest() { // ── RED 4: a stock non-empty changeset RENAMED AND EMPTIED in one commit ── // Exactly RED 2 with a `git mv` bolted on -- the same brand-new empty // declaration, spelled so that `AM` could not see it at all. + battery('RED 4: a stock non-empty changeset RENAMED AND EMPTIED in one commit'); { const { dir, base } = makeRepo( { '.changeset/was-declaring.md': RENAMEABLE_DECLARING }, @@ -653,6 +725,7 @@ function selfTest() { // tidying a filename into a red. Note this case can ONLY be green through the // `R` path -- were the rename to degrade to add-plus-delete, the new path // would arrive as `A` + empty, which is RED 1. + battery('GREEN 6: a PURE rename of a stock EMPTY changeset stays exempt'); { const { dir, base } = makeRepo( { '.changeset/stock-empty.md': RENAMEABLE_EMPTY }, @@ -668,6 +741,7 @@ function selfTest() { } // ── GREEN 7: a pure rename of a stock DECLARING changeset is simply ok ──── + battery('GREEN 7: a pure rename of a stock DECLARING changeset is simply ok'); { const { dir, base } = makeRepo( { '.changeset/stock-declaring.md': RENAMEABLE_DECLARING }, @@ -693,6 +767,7 @@ function selfTest() { // exemption out for free on a head file that is a brand-new empty changeset. // This is the case the `isChangesetFile(basePath)` guard in the scan exists // for; delete the guard and this row goes green as `exempt`. + battery('RED 5: an `R` row whose BASE side is README.md is not "inherited"'); { const README = `# Changesets\n\n${RENAMEABLE}`; const { dir, base } = makeRepo( @@ -718,6 +793,7 @@ function selfTest() { // deliberately identical except for which side of the fork the offending // changeset is on, because a drift assertion on its own is satisfied by a // gate that has simply stopped looking. + battery('#6129: main drift must not move the verdict, in EITHER direction'); { const OFFENDER = '.changeset/an-empty-one.md'; @@ -785,6 +861,7 @@ function selfTest() { // changeset from main, and a two-dot diff then reads each one still sitting on // an un-rebased branch as newly added AND empty. This is the fixture that goes // red if the merge base is taken back out of scan() itself. + battery('#6129, the other half: a base branch that DELETES'); { const dir = mkdtempSync(join(tmpdir(), 'check-empty-changeset-deleted-')); repos.push(dir); @@ -829,6 +906,7 @@ function selfTest() { // ── #4690, one step later: no merge base at all is a failure ───────────── // Falling back to the raw base here would restore exactly the bug above, so // the scan throws and the CLI turns that into exit 1. + battery('#4690, one step later: no merge base at all is a failure'); { const { dir } = makeRepo({}, { 'a.txt': 'x\n' }); const other = mkdtempSync(join(tmpdir(), 'check-empty-changeset-unrelated-')); @@ -857,6 +935,7 @@ function selfTest() { // for it has to read that file. Without this block the workflow could be // reverted to the frozen `base.sha` tomorrow with every assertion above still // green, which is precisely the "returns in a different shape" #6129 rules out. + battery('The consumer: this gate\'s own CI step (#6129)'); { const workflow = join(REPO_ROOT, '.github/workflows/pr-automation.yml'); const present = existsSync(workflow); @@ -1132,6 +1211,7 @@ function selfTest() { // family asserting this family's wiring, which is a coupling with its own // cost. Stated so the next reader inherits the fact and not a false sense of // closure. + battery('The second consumer: where THIS SELF-TEST runs (#6509)'); { const lintPath = join(REPO_ROOT, '.github/workflows/lint.yml'); const lintPresent = existsSync(lintPath); @@ -1228,6 +1308,7 @@ function selfTest() { } // ── Parser unit rows ───────────────────────────────────────────────────── + battery('Parser unit rows'); assert(isEmptyDeclaration('---\n---\n\nbody\n'), 'parser: the canonical empty shape is empty'); assert(isEmptyDeclaration('\n---\n\n---\n\nbody\n'), 'parser: blank lines around/inside the fence stay empty'); assert(!isEmptyDeclaration(DECLARING), 'parser: a declaring changeset is not empty'); @@ -1261,6 +1342,7 @@ function selfTest() { // Predicted direction on reverse verification: restoring the old anchor // (`([A-Za-z]+)\s*$`) turns exactly these red — `isEmptyDeclaration` goes // true and the gate rejects a valid file. + battery('THE FIX (#7004): comments and quoted bump values'); const declares = (label, text, expected) => { const { packages } = declaredBumpsIn(text); assert( @@ -1320,6 +1402,7 @@ function selfTest() { // reached all four carriers at once, and the fourth // (`objectui-changeset-digest.mjs`) was not even named in the report, // because nothing mechanical connected it to the other three. + battery('The family reads one block one way (#7004)'); { const FAMILY = [ 'scripts/check-changeset-no-major.mjs', @@ -1425,6 +1508,7 @@ function selfTest() { } // ── Missing input is a failure, never a pass (#4690) ───────────────────── + battery('Missing input is a failure, never a pass (#4690)'); { const { dir } = makeRepo({}, { 'a.txt': 'x\n' }); assert(resolveCommit('definitely-not-a-ref', dir) === null, 'unresolvable base must resolve to null (-> exit 1)'); @@ -1433,6 +1517,51 @@ function selfTest() { for (const dir of repos) rmSync(dir, { recursive: true, force: true }); } + // ── The floor: every declared battery RAN, and ran its cases (#13489) ─── + // + // Evaluated after every battery has had its chance and BEFORE the verdict, so + // the success line below can only be printed by a run in which the set of + // batteries that registered assertions EQUALS the set declared. A set + // difference names WHICH battery stopped; a count says only that something did. + const floorFailure = (message) => { + failures.push(message); + }; + const declaredBatteries = Object.keys(SELF_TEST_BATTERIES); + let floorBreached = false; + if (declaredBatteries.length < SELF_TEST_BATTERY_FLOOR) { + floorBreached = true; + floorFailure( + `SELF_TEST_BATTERIES declares ${declaredBatteries.length} batteries, below the pinned ` + + `${SELF_TEST_BATTERY_FLOOR} — a battery deleted from the roster takes its own floor with it.`, + ); + } + for (const [name, count] of seen) { + if (declaredBatteries.includes(name)) continue; + floorBreached = true; + floorFailure( + `self-test battery "${name}" registered ${count} case(s) but is not declared in ` + + 'SELF_TEST_BATTERIES — an assertion attributed to no declared battery is one nothing floors.', + ); + } + for (const name of declaredBatteries) { + const count = seen.get(name) ?? 0; + if (count >= SELF_TEST_BATTERIES[name]) continue; + floorBreached = true; + floorFailure( + count === 0 + ? `self-test battery "${name}" DID NOT RUN — 0 cases registered, ${SELF_TEST_BATTERIES[name]} pinned. ` + + 'The verdict below would have claimed those cases hold.' + : `self-test battery "${name}" registered ${count} case(s), below its pinned floor of ` + + `${SELF_TEST_BATTERIES[name]} — cases that used to run no longer do.`, + ); + } + if (floorBreached) { + floorFailure( + 'A battery at or below its floor means cases STOPPED RUNNING — the battery is the bug, not the ' + + 'number. Find what stopped registering (an early return, a deleted block, a guard that now ' + + 'skips) and restore it.', + ); + } if (failures.length) { console.error(`✗ check-empty-changeset --self-test -- ${failures.length} failure(s)\n`); for (const f of failures) console.error(` - ${f}`); diff --git a/scripts/check-engine-double-contract.mjs b/scripts/check-engine-double-contract.mjs index 8e2198e515..e1ea8c1dc2 100644 --- a/scripts/check-engine-double-contract.mjs +++ b/scripts/check-engine-double-contract.mjs @@ -3343,9 +3343,81 @@ function report() { // as one that passed (#13798). const SELF_TEST_VERDICT = 'check-engine-double-contract self-test reached its verdict'; +// ── The self-test's own battery roster and floor (#13489) ────────────────── +// +// `failures.length === 0` used to be this self-test's ONLY success condition, so +// "every case held" and "the cases never ran" printed the same line. Closed the +// way PR #13487 validated on check-doc-authoring: what is pinned is the +// registered NAMES, not a number. Every section opens with `battery('')`, +// every assertion is attributed to the battery most recently opened, and the +// floor requires the OPENED set to equal the DECLARED set with each battery at +// or above its own count. +// +// ⛔ A pinned TOTAL is not the repair: a battery dropping from 9 cases to 3 +// keeps a total "right" the moment a sibling grows. +// +// The counts are a FLOOR, not an equality — adding cases is ordinary work and +// must not red. A battery BELOW its floor means cases stopped running; the +// remedy is to find what stopped registering. +const SELF_TEST_BATTERIES = Object.freeze({ + 'Detection: an unpinned engine fake is found, a pinned one is not flagged.': 5, + 'Scope: the DRIVER\'s delete(object, id, options) is a different contract': 3, + 'Shape coverage: the fake shapes this repo actually writes.': 2, + 'The MOCK CONSTRUCTOR spelling (#8639).': 6, + 'The DEFAULTED spelling (#9877), driven arm by arm.': 9, + 'Arity: a fake omits the parameters it ignores (#5629).': 9, + 'The `update` slice (#5480).': 15, + 'The SCOPED REPOSITORY, the third shape (#6327, from #5945).': 11, + 'The CONSUMER SEAMS (#8194)': 20, + 'The ratchet-remedy authority convention (#8435)': 2, + 'The INHERITANCE clause (#8553)': 5, + 'RETAINED (#9680): the pinned population is enumerated, not counted': 3, + 'The four loss worlds, each separated from its neighbours.': 4, + 'The clean direction: a ledger that matches the census reports nothing.': 1, + 'Each loss world reaches its OWN message, and never a neighbour\'s.': 7, + 'The growth direction. Both spellings, because a new FILE and a new double': 3, + 'Bootstrap: a missing ledger is ONE error, not one per row. The failure': 1, + 'DECLARED\'s twin for this ledger: an entry naming a verb no slice scans': 2, + 'The reason this invariant exists, stated as an assertion: the pinned': 2, + 'SEAMS_RETAINED (#9708): the seam POPULATION is enumerated, not counted': 4, + 'The four loss worlds, each separated from its neighbours. (2)': 4, + 'The clean direction, first: without it every assertion below could pass': 1, + 'Each loss world reaches its OWN message, and never a neighbour\'s. (2)': 5, + 'The growth direction — the one measured to fire most often (2 arrivals,': 3, + 'A MOVE — the shape this population has actually taken twice — must be': 1, + 'Bootstrap: one error for a missing artifact, not one per seam.': 1, + 'An entry naming a verb the seam scan never reads can never lose its': 2, + 'The declaration walker, which is what separates `function-removed` from': 11, + 'The UNRECOGNISED census (#9747)': 23, + 'The RECOGNIZER CENSUS (#9943)': 7, + '#11626: the DECLARED single-verb double': 14, +}); + +// DELETING an entry silences that battery's floor exactly as effectively as +// zeroing it, so the roster's own size is pinned too. +const SELF_TEST_BATTERY_FLOOR = 31; + +// The key an assertion is filed under when no battery is open. It is not a +// declared battery, so it reds by the same set difference rather than silently +// inflating whichever battery happened to run last. +const UNATTRIBUTED_BATTERY = '(no battery open)'; + function selfTest() { + // The battery ledger this self-test's floor is evaluated against (#13489). + // `battery()` opens a battery; every assertion below is attributed to the one + // most recently opened, so a section that stops running stops registering and + // names ITSELF at the floor rather than going quiet. + const seen = new Map(); + let openBattery = null; + const battery = (name) => { + openBattery = name; + }; + const registerCase = () => { + const b = openBattery ?? UNATTRIBUTED_BATTERY; + seen.set(b, (seen.get(b) ?? 0) + 1); + }; const failures = []; - const expect = (label, cond) => { if (!cond) failures.push(label); }; + const expect = (label, cond) => { registerCase(); if (!cond) failures.push(label); }; const IMPORT = "import { assertEngineDeleteDispatch } from '@objectstack/objectql';\n"; const engineFake = (deleteBody, header = '') => `${header} @@ -3360,6 +3432,7 @@ function makeEngine() { `; // ── Detection: an unpinned engine fake is found, a pinned one is not flagged. + battery('Detection: an unpinned engine fake is found, a pinned one is not flagged.'); let d = scanSource('a.test.ts', engineFake('return { ok: true };')); expect('finds an unpinned engine double', d.length === 1 && d[0].pinned === false); @@ -3384,6 +3457,7 @@ function makeEngine() { // ── Scope: the DRIVER's delete(object, id, options) is a different contract // and must not be swept in, or the gate drowns in false positives. + battery('Scope: the DRIVER\'s delete(object, id, options) is a different contract'); const driverFake = ` const driver = { async find(o: string) { return []; }, @@ -3409,6 +3483,7 @@ const driver = { expect('an object with no engine siblings is out of scope', scanSource('c.test.ts', bare).length === 0); // ── Shape coverage: the fake shapes this repo actually writes. + battery('Shape coverage: the fake shapes this repo actually writes.'); const classFake = `${IMPORT} class FakeEngine { async find(o: string, q?: any) { return []; } @@ -3438,6 +3513,7 @@ const engine = { // drives ONE arm of `unwrapCallImpl`, because this file has already measured // what an unfixtured arm is worth: "with only the `new Error` fixture above, // neutering the call-expression arm left the self-test GREEN". + battery('The MOCK CONSTRUCTOR spelling (#8639).'); const viFake = (init) => ` const engine = { find: vi.fn(async (o: string) => []), @@ -3476,6 +3552,7 @@ const engine = { // unguarded engine delete behind it. Same discipline as the #8639 block // above: one fixture per arm, because an unfixtured arm has already been // measured in this file to be worth nothing. + battery('The DEFAULTED spelling (#9877), driven arm by arm.'); d = scanSource('v.test.ts', viFake('overrides.delete ?? vi.fn(async (o: string, opts?: any) => ({ ok: true }))')); expect('#9877 — a `??`-defaulted engine delete is in scope', d.length === 1 && d[0].pinned === false); @@ -3548,6 +3625,7 @@ const driver = { // cases drive both halves of the sibling evidence that admits them now, // because the obvious cheap fix (admit every short-arity delete) is WRONG: // fake drivers drop their unused parameters exactly like fake engines do. + battery('Arity: a fake omits the parameters it ignores (#5629).'); const zeroArityEngine = ` const engine = { async find(o: string) { return []; }, @@ -3643,6 +3721,7 @@ const store = { // siblings (so it must not count itself), and the driver's `update` carries // its primary key in the same second position `delete`'s does but with a // payload behind it. + battery('The `update` slice (#5480).'); const U = SLICES.find((s) => s.verb === 'update'); const UIMPORT = "import { assertEngineUpdateDispatch } from '@objectstack/objectql';\n"; const engineFakeU = (updateBody, header = '') => `${header} @@ -3784,6 +3863,7 @@ const driver = { // the control does not merely accompany the claim — it is the same object // with the evidence removed, which is the only version that can distinguish // "the veto fired" from "discovery died". + battery('The SCOPED REPOSITORY, the third shape (#6327, from #5945).'); const repoBody = ` find: async (query?: any) => [], findOne: async (query?: any) => null, @@ -3904,6 +3984,7 @@ const engine = { // is the SAME source with one thing changed, and the control asserts the // seam IS found without it. + battery('The CONSUMER SEAMS (#8194)'); const ENV = "import { recordNotFoundError } from '@objectstack/core';\n"; const seamSrc = (body, header = '') => `${header} class Ingress { @@ -4105,6 +4186,7 @@ class Svc { // everything would keep (2) green with the convention gone. Its fixture is // SYNTHETIC rather than the real message with the marker stripped: derived, // it also fired on a rewording and misdescribed the cause. + battery('The ratchet-remedy authority convention (#8435)'); const pinned = pinnedMessage( { verb: 'update', symbols: new Set(['assertEngineUpdateDispatch']), producer: 'ObjectQL.update', pinCall: 'assertEngineUpdateDispatch(data, options)' }, @@ -4125,6 +4207,7 @@ class Svc { // on BOTH ratchet remedies (an author meets whichever one their file's // baseline state produces, so a clause on only one of them is a coin flip), // and the detector discriminates. + battery('The INHERITANCE clause (#8553)'); expect('#8553 — the PINNED remedy names the inheritance rule, so an author who reaches for ' + '`Object.assign` can tell whether they satisfied the rule or side-stepped it', remedyNamesInheritance(pinned)); @@ -4150,6 +4233,7 @@ class Svc { // Driven through the two pure functions the invariant is built from, so all // four loss worlds are exercised without creating and deleting real files. // `onDisk` and the two censuses are injected for exactly that reason. + battery('RETAINED (#9680): the pinned population is enumerated, not counted'); const LEDGER_OK = true; const noDisk = () => false; const onDisk = () => true; @@ -4172,6 +4256,7 @@ class Svc { .length === 0); // ── The four loss worlds, each separated from its neighbours. + battery('The four loss worlds, each separated from its neighbours.'); expect('a file gone from disk classifies as file-removed', classifyPinLoss({ onDisk: false, declared: 0, wasPinned: 1 }) === 'file-removed'); expect('a file on disk declaring no double classifies as double-removed', @@ -4184,6 +4269,7 @@ class Svc { // ── The clean direction: a ledger that matches the census reports nothing. // Without this every assertion below could pass on a function that always // errors, which is the guard-that-cannot-pass twin of #4118. + battery('The clean direction: a ledger that matches the census reports nothing.'); const cleanLedger = { entries: [{ file: 'a.test.ts', verb: 'delete', pinned: 1 }] }; const cleanCensus = [{ file: 'a.test.ts', verb: 'delete', pinned: 1 }]; expect('a ledger matching the census is silent', @@ -4191,6 +4277,7 @@ class Svc { dcount([['a.test.ts', 'delete', 1]]), onDisk).length === 0); // ── Each loss world reaches its OWN message, and never a neighbour's. + battery('Each loss world reaches its OWN message, and never a neighbour\'s.'); const lostFile = retainedErrors([], cleanLedger, LEDGER_OK, dcount([]), noDisk); expect('a deleted test file is reported as a legitimate decrease', lostFile.length === 1 && anyOf(lostFile, 'gone from disk') @@ -4221,6 +4308,7 @@ class Svc { // ── The growth direction. Both spellings, because a new FILE and a new double // in a known file arrive by different routes and only one was in the first draft. + battery('The growth direction. Both spellings, because a new FILE and a new double'); const grewNew = retainedErrors([{ file: 'new.test.ts', verb: 'delete', pinned: 1 }], { entries: [] }, LEDGER_OK, dcount([['new.test.ts', 'delete', 1]]), onDisk); expect('a newly pinned file the ledger does not record is reported', @@ -4236,6 +4324,7 @@ class Svc { // ── Bootstrap: a missing ledger is ONE error, not one per row. The failure // this guards is a fresh checkout reporting one problem per census row for a // single missing file. + battery('Bootstrap: a missing ledger is ONE error, not one per row. The failure'); const missing = retainedErrors( [{ file: 'a.test.ts', verb: 'delete', pinned: 1 }, { file: 'b.test.ts', verb: 'update', pinned: 1 }], { entries: [] }, false, dcount([]), onDisk); @@ -4244,6 +4333,7 @@ class Svc { // ── DECLARED's twin for this ledger: an entry naming a verb no slice scans // can never lose its pin, so it would record coverage nothing checks. + battery('DECLARED\'s twin for this ledger: an entry naming a verb no slice scans'); const badVerb = retainedErrors([], { entries: [{ file: 'a.test.ts', verb: 'destroy', pinned: 1 }] }, LEDGER_OK, dcount([]), onDisk); expect('a pinned-ledger entry naming an unscanned verb is rejected', @@ -4255,6 +4345,7 @@ class Svc { // population must be enumerated. A ledger holding only a COUNT cannot express // the swap that motivated #9680 -- one file loses a pin, another gains one -- // so the census rows carry identity, and this fails if they ever stop. + battery('The reason this invariant exists, stated as an assertion: the pinned'); expect('census rows carry file identity, not just a total', censusPinned(mixedSlices)[0].file === 'a.test.ts' && typeof censusPinned(mixedSlices)[0].verb === 'string'); @@ -4272,6 +4363,7 @@ class Svc { // every loss world is driven without creating and deleting real files, and // each assertion has a control that fails if the predicate under it started // approving everything. + battery('SEAMS_RETAINED (#9708): the seam POPULATION is enumerated, not counted'); const seamLedgerOf = (rows) => ({ entries: rows.map(([file, fn, verb, seams]) => ({ file, fn, verb, seams: seams ?? 1 })) }); const seamCensusOf = (rows) => rows.map(([file, fn, verb, seams]) => ({ file, fn, verb, seams: seams ?? 1 })); const declared = () => true; @@ -4295,6 +4387,7 @@ class Svc { .includes('line') === false); // ── The four loss worlds, each separated from its neighbours. + battery('The four loss worlds, each separated from its neighbours. (2)'); expect('a seam file gone from disk classifies as file-removed', classifySeamLoss({ onDisk: false, fnDeclared: false, discovered: 0 }) === 'file-removed'); expect('a live file no longer declaring the function classifies as function-removed', @@ -4306,12 +4399,14 @@ class Svc { // ── The clean direction, first: without it every assertion below could pass // on a function that always errors (#4118's twin). + battery('The clean direction, first: without it every assertion below could pass'); const cleanSeamLedger = seamLedgerOf([['a.ts', 'f', 'update']]); const cleanSeamCensus = seamCensusOf([['a.ts', 'f', 'update']]); expect('a seam ledger matching the census is silent', seamsRetainedErrors(cleanSeamCensus, cleanSeamLedger, LEDGER_OK, onDisk, declared).length === 0); // ── Each loss world reaches its OWN message, and never a neighbour's. + battery('Each loss world reaches its OWN message, and never a neighbour\'s. (2)'); const seamFileGone = seamsRetainedErrors([], cleanSeamLedger, LEDGER_OK, noDisk, notDeclared); expect('a deleted source file is reported as a legitimate decrease', seamFileGone.length === 1 && anyOf(seamFileGone, 'gone from disk') @@ -4340,6 +4435,7 @@ class Svc { // ── The growth direction — the one measured to fire most often (2 arrivals, // 0 departures in the 58 days to 2026-08-21). + battery('The growth direction — the one measured to fire most often (2 arrivals,'); const seamNew = seamsRetainedErrors(cleanSeamCensus, seamLedgerOf([]), LEDGER_OK, onDisk, declared); expect('a seam the ledger does not record is reported', seamNew.length === 1 && anyOf(seamNew, 'does not record it')); @@ -4353,12 +4449,14 @@ class Svc { // ── A MOVE — the shape this population has actually taken twice — must be // reported from both ends, or a rename reads as a silent swap. + battery('A MOVE — the shape this population has actually taken twice — must be'); const seamMoved = seamsRetainedErrors(seamCensusOf([['b.ts', 'f', 'update']]), cleanSeamLedger, LEDGER_OK, onDisk, notDeclared); expect('a seam that MOVED file is reported as both a loss and an arrival', seamMoved.length === 2 && anyOf(seamMoved, 'a.ts') && anyOf(seamMoved, 'b.ts')); // ── Bootstrap: one error for a missing artifact, not one per seam. + battery('Bootstrap: one error for a missing artifact, not one per seam.'); const seamMissing = seamsRetainedErrors( seamCensusOf([['a.ts', 'f', 'update'], ['b.ts', 'g', 'delete']]), seamLedgerOf([]), false, onDisk, declared); @@ -4367,6 +4465,7 @@ class Svc { // ── An entry naming a verb the seam scan never reads can never lose its // seam, so it would record a population nothing checks. + battery('An entry naming a verb the seam scan never reads can never lose its'); const seamBadVerb = seamsRetainedErrors([], seamLedgerOf([['a.ts', 'f', 'destroy']]), LEDGER_OK, onDisk, declared); expect('a seam-ledger entry naming an unread verb is rejected', @@ -4378,6 +4477,7 @@ class Svc { // `unrecognised`. Both directions on every shape a LIVE seam takes, because // a walker blind to one of them would classify that seam's loss as the // quieter story — and the classifier would still look healthy. + battery('The declaration walker, which is what separates `function-removed` from'); const namesOf = (src) => declaredFunctionNames( parseSourceFile('d.ts', src, ts.ScriptKind.TS)); expect('declaredFunctionNames reads a top-level function declaration (`callData`s shape)', @@ -4416,6 +4516,7 @@ class Svc { // a construct that is CORRECTLY out of scope must count as SCOPED OUT, never // as unrecognised. #8662 is why -- a correct OUT_OF_SCOPE verdict that reads // as noise discredits the whole direction on day one. + battery('The UNRECOGNISED census (#9747)'); const D = SLICES.find((s) => s.verb === 'delete'); const censusFake = (deleteMember, header = '') => `${header} function makeEngine() { @@ -4624,6 +4725,7 @@ const driver: any = { create: async (o: string, d: any) => d, find: async (o: st // proved on both sides -- a census whose kinds silently collapsed into one // another would print a confident table and hide the same blind spot the // constants did. + battery('The RECOGNIZER CENSUS (#9943)'); const kindsOf = (src) => { const sf = parseSourceFile('k.test.ts', src, ts.ScriptKind.TSX); const out = []; @@ -4670,6 +4772,7 @@ const driver: any = { create: async (o: string, d: any) => d, find: async (o: st // ENGINE_CONTRACT_NAME. The population fixtures deliberately carry ZERO // engine siblings, which is the whole point: before this route the only way // in was to pad the double with verbs its test never calls. + battery('#11626: the DECLARED single-verb double'); const CIMPORT = "import type { IDataEngine } from '@objectstack/spec/contracts';\n"; const UD = SLICES.find((sl) => sl.verb === 'update'); const DD = SLICES.find((sl) => sl.verb === 'delete'); @@ -4803,6 +4906,51 @@ const driver: any = { create: async (o: string, d: any) => d, find: async (o: st expect('#11626 — an UNDECLARED single-verb construct is in neither walk', cc2.unrecognised.length === 0 && cc2.scopedOut.length === 0); + // ── The floor: every declared battery RAN, and ran its cases (#13489) ─── + // + // Evaluated after every battery has had its chance and BEFORE the verdict, so + // the success line below can only be printed by a run in which the set of + // batteries that registered assertions EQUALS the set declared. A set + // difference names WHICH battery stopped; a count says only that something did. + const floorFailure = (message) => { + failures.push(message); + }; + const declaredBatteries = Object.keys(SELF_TEST_BATTERIES); + let floorBreached = false; + if (declaredBatteries.length < SELF_TEST_BATTERY_FLOOR) { + floorBreached = true; + floorFailure( + `SELF_TEST_BATTERIES declares ${declaredBatteries.length} batteries, below the pinned ` + + `${SELF_TEST_BATTERY_FLOOR} — a battery deleted from the roster takes its own floor with it.`, + ); + } + for (const [name, count] of seen) { + if (declaredBatteries.includes(name)) continue; + floorBreached = true; + floorFailure( + `self-test battery "${name}" registered ${count} case(s) but is not declared in ` + + 'SELF_TEST_BATTERIES — an assertion attributed to no declared battery is one nothing floors.', + ); + } + for (const name of declaredBatteries) { + const count = seen.get(name) ?? 0; + if (count >= SELF_TEST_BATTERIES[name]) continue; + floorBreached = true; + floorFailure( + count === 0 + ? `self-test battery "${name}" DID NOT RUN — 0 cases registered, ${SELF_TEST_BATTERIES[name]} pinned. ` + + 'The verdict below would have claimed those cases hold.' + : `self-test battery "${name}" registered ${count} case(s), below its pinned floor of ` + + `${SELF_TEST_BATTERIES[name]} — cases that used to run no longer do.`, + ); + } + if (floorBreached) { + floorFailure( + 'A battery at or below its floor means cases STOPPED RUNNING — the battery is the bug, not the ' + + 'number. Find what stopped registering (an early return, a deleted block, a guard that now ' + + 'skips) and restore it.', + ); + } if (failures.length) { for (const f of failures) console.error(` x self-test: ${f}`); console.error(`\ncheck-engine-double-contract --self-test: ${failures.length} failure(s).\n`); diff --git a/scripts/check-nul-bytes.mjs b/scripts/check-nul-bytes.mjs index e8b13d9569..84bd298aac 100644 --- a/scripts/check-nul-bytes.mjs +++ b/scripts/check-nul-bytes.mjs @@ -710,13 +710,61 @@ function checkCharClassReferences(root, assert) { // as one that passed (#13798). const SELF_TEST_VERDICT = 'check-nul-bytes self-test reached its verdict'; +// ── The self-test's own battery roster and floor (#13489) ────────────────── +// +// `failures.length === 0` used to be this self-test's ONLY success condition, so +// "every case held" and "the cases never ran" printed the same line. Closed the +// way PR #13487 validated on check-doc-authoring: what is pinned is the +// registered NAMES, not a number. Every section opens with `battery('')`, +// every assertion is attributed to the battery most recently opened, and the +// floor requires the OPENED set to equal the DECLARED set with each battery at +// or above its own count. +// +// ⛔ A pinned TOTAL is not the repair: a battery dropping from 9 cases to 3 +// keeps a total "right" the moment a sibling grows. +// +// The counts are a FLOOR, not an equality — adding cases is ordinary work and +// must not red. A battery BELOW its floor means cases stopped running; the +// remedy is to find what stopped registering. +const SELF_TEST_BATTERIES = Object.freeze({ + '#6984: everything BELOW this line is deliberately left unstaged': 3, + '#5157: the widening, proved in both directions': 10, + '#5460: DEL added to the set, proved in both directions': 10, + '#6984: the untracked half of the scan set, proved in both directions': 41, + '#6984, the CI direction: on a fully tracked tree the widening is a no-op': 4, + '#5646: the class is transcribed nowhere': 7, +}); + +// DELETING an entry silences that battery's floor exactly as effectively as +// zeroing it, so the roster's own size is pinned too. +const SELF_TEST_BATTERY_FLOOR = 6; + +// The key an assertion is filed under when no battery is open. It is not a +// declared battery, so it reds by the same set difference rather than silently +// inflating whichever battery happened to run last. +const UNATTRIBUTED_BATTERY = '(no battery open)'; + function selfTest() { + // The battery ledger this self-test's floor is evaluated against (#13489). + // `battery()` opens a battery; every assertion below is attributed to the one + // most recently opened, so a section that stops running stops registering and + // names ITSELF at the floor rather than going quiet. + const seen = new Map(); + let openBattery = null; + const battery = (name) => { + openBattery = name; + }; + const registerCase = () => { + const b = openBattery ?? UNATTRIBUTED_BATTERY; + seen.set(b, (seen.get(b) ?? 0) + 1); + }; const failures = []; // Counted rather than written down: some assertions run inside a loop, and a // hand-kept total in the success line is exactly the kind of number that // drifts silently once someone adds a case. let checked = 0; const assert = (cond, msg) => { + registerCase(); checked++; if (!cond) failures.push(msg); }; @@ -855,6 +903,7 @@ function selfTest() { // shape of the accident source -- an escape materialised into its byte while // an author was writing ABOUT control characters -- landing on the one kind // of file the index-only enumeration could not reach. + battery('#6984: everything BELOW this line is deliberately left unstaged'); write('packages/cli/test/new-case.test.ts', Buffer.concat([Buffer.from("const esc = '"), ESC, Buffer.from("';\n")])); // The cure, unstaged: the same literal written as escape TEXT stays green, // so the prescription is testable on this half of the set too and an author @@ -884,6 +933,7 @@ function selfTest() { // ── #5157: the widening, proved in both directions ────────────────────── // // Forward -- each specimen is flagged now. + battery('#5157: the widening, proved in both directions'); assert(flagged.has('packages/x/src/key.ts'), '#5157: a 0x01 key separator must be flagged'); assert(flagged.has('packages/cli/src/login.ts'), '#5157: a 0x03 Ctrl-key literal must be flagged'); assert(flagged.has('docs/range.md'), '#5157: 0x0b / 0x0c / 0x1f must be flagged'); @@ -914,6 +964,7 @@ function selfTest() { // // Forward -- a raw DEL is flagged, and reported as 0x7f so the prescription // can name the right escape. + battery('#5460: DEL added to the set, proved in both directions'); assert(flagged.has('packages/cli/src/prompt.ts'), '#5460: a raw 0x7f Backspace literal must be flagged'); assert( flagged.get('packages/cli/src/prompt.ts')?.bytes.join() === String(0x7f), @@ -958,6 +1009,7 @@ function selfTest() { // Forward -- a file that has been written but not staged is flagged, and is // reported as untracked so the author is not left wondering why `git status` // and this gate disagree. + battery('#6984: the untracked half of the scan set, proved in both directions'); assert( flagged.has('packages/cli/test/new-case.test.ts'), '#6984: an untracked-but-not-ignored file carrying a raw 0x1b must be flagged', @@ -1129,6 +1181,7 @@ function selfTest() { // tracked, scanned and flagged -- a real behaviour, but not the one under // test here, and it would turn the offender-set comparison below red for a // reason that has nothing to do with the enumeration widening. + battery('#6984, the CI direction: on a fully tracked tree the widening is a no-op'); execFileSync('git', ['add', '-A'], { cwd: dir }); const staged = scan(dir); assert(staged.untracked === 0, `#6984: a fully tracked tree has an empty untracked half, got ${staged.untracked}`); @@ -1152,8 +1205,54 @@ function selfTest() { // // Not a temp-repo fixture: the subject is the checked-in text of this repo's // own instruction files, which is exactly what drifted in #5577. + battery('#5646: the class is transcribed nowhere'); checkCharClassReferences(scriptRepoRoot(), assert); + // ── The floor: every declared battery RAN, and ran its cases (#13489) ─── + // + // Evaluated after every battery has had its chance and BEFORE the verdict, so + // the success line below can only be printed by a run in which the set of + // batteries that registered assertions EQUALS the set declared. A set + // difference names WHICH battery stopped; a count says only that something did. + const floorFailure = (message) => { + failures.push(message); + }; + const declaredBatteries = Object.keys(SELF_TEST_BATTERIES); + let floorBreached = false; + if (declaredBatteries.length < SELF_TEST_BATTERY_FLOOR) { + floorBreached = true; + floorFailure( + `SELF_TEST_BATTERIES declares ${declaredBatteries.length} batteries, below the pinned ` + + `${SELF_TEST_BATTERY_FLOOR} — a battery deleted from the roster takes its own floor with it.`, + ); + } + for (const [name, count] of seen) { + if (declaredBatteries.includes(name)) continue; + floorBreached = true; + floorFailure( + `self-test battery "${name}" registered ${count} case(s) but is not declared in ` + + 'SELF_TEST_BATTERIES — an assertion attributed to no declared battery is one nothing floors.', + ); + } + for (const name of declaredBatteries) { + const count = seen.get(name) ?? 0; + if (count >= SELF_TEST_BATTERIES[name]) continue; + floorBreached = true; + floorFailure( + count === 0 + ? `self-test battery "${name}" DID NOT RUN — 0 cases registered, ${SELF_TEST_BATTERIES[name]} pinned. ` + + 'The verdict below would have claimed those cases hold.' + : `self-test battery "${name}" registered ${count} case(s), below its pinned floor of ` + + `${SELF_TEST_BATTERIES[name]} — cases that used to run no longer do.`, + ); + } + if (floorBreached) { + floorFailure( + 'A battery at or below its floor means cases STOPPED RUNNING — the battery is the bug, not the ' + + 'number. Find what stopped registering (an early return, a deleted block, a guard that now ' + + 'skips) and restore it.', + ); + } if (failures.length) { console.error(`✗ check-nul-bytes --self-test -- ${failures.length} failure(s)\n`); for (const f of failures) console.error(` • ${f}`); diff --git a/scripts/check-position-name-fold-loaders.mjs b/scripts/check-position-name-fold-loaders.mjs index e220dd283b..7c35bcbfd3 100644 --- a/scripts/check-position-name-fold-loaders.mjs +++ b/scripts/check-position-name-fold-loaders.mjs @@ -297,10 +297,56 @@ function report(loaders) { // handshake is a flag rather than a returned sentinel. let selfTestReachedVerdict = false; +// ── The self-test's own battery roster and floor (#13489) ────────────────── +// +// `failures.length === 0` used to be this self-test's ONLY success condition, so +// "every case held" and "the cases never ran" printed the same line. Closed the +// way PR #13487 validated on check-doc-authoring: what is pinned is the +// registered NAMES, not a number. Every section opens with `battery('')`, +// every assertion is attributed to the battery most recently opened, and the +// floor requires the OPENED set to equal the DECLARED set with each battery at +// or above its own count. +// +// ⛔ A pinned TOTAL is not the repair: a battery dropping from 9 cases to 3 +// keeps a total "right" the moment a sibling grows. +// +// The counts are a FLOOR, not an equality — adding cases is ordinary work and +// must not red. A battery BELOW its floor means cases stopped running; the +// remedy is to find what stopped registering. +const SELF_TEST_BATTERIES = Object.freeze({ + 'REVERSE CONTROL. A zero from a scan that cannot report anything is not a': 3, + 'The allowed dispositions, each for its own stated reason.': 5, + 'The text matcher, both directions. A matcher that never matches turns': 5, + 'The permission-set extractor, on the real artifact shape.': 2, +}); + +// DELETING an entry silences that battery's floor exactly as effectively as +// zeroing it, so the roster's own size is pinned too. +const SELF_TEST_BATTERY_FLOOR = 4; + +// The key an assertion is filed under when no battery is open. It is not a +// declared battery, so it reds by the same set difference rather than silently +// inflating whichever battery happened to run last. +const UNATTRIBUTED_BATTERY = '(no battery open)'; + function selfTest() { + // The battery ledger this self-test's floor is evaluated against (#13489). + // `battery()` opens a battery; every assertion below is attributed to the one + // most recently opened, so a section that stops running stops registering and + // names ITSELF at the floor rather than going quiet. + const seen = new Map(); + let openBattery = null; + const battery = (name) => { + openBattery = name; + }; + const registerCase = () => { + const b = openBattery ?? UNATTRIBUTED_BATTERY; + seen.set(b, (seen.get(b) ?? 0) + 1); + }; let failed = 0; let cases = 0; const check = (label, actual, expected) => { + registerCase(); cases++; const ok = JSON.stringify(actual) === JSON.stringify(expected); if (!ok) { @@ -315,6 +361,7 @@ function selfTest() { // ── REVERSE CONTROL. A zero from a scan that cannot report anything is not a // reading. These are the adversarial inputs a clean tree does not contain. + battery('REVERSE CONTROL. A zero from a scan that cannot report anything is not a'); check( 'REVERSE: a non-test loader is a finding', classifyReference('packages/runtime/src/seed-marketplace-apps.ts'), @@ -332,6 +379,7 @@ function selfTest() { ); // ── The allowed dispositions, each for its own stated reason. + battery('The allowed dispositions, each for its own stated reason.'); check('test file is allowed', classifyReference('packages/metadata/src/plugin-artifact-forward-conversion.test.ts'), 'test'); check('file under test/ is allowed', classifyReference('packages/qa/dogfood/test/shared-showcase.ts'), 'test'); check('declared instrument is allowed', classifyReference('scripts/measure-position-name-fold-census.mjs'), 'instrument'); @@ -341,6 +389,7 @@ function selfTest() { // ── The text matcher, both directions. A matcher that never matches turns // every loader into a silent pass, and it is the half a clean tree cannot // exercise from the negative side alone. + battery('The text matcher, both directions. A matcher that never matches turns'); check('matcher sees the basename', referencesArtifact(`join(HERE, '__fixtures__/${ARTIFACT_BASENAME}')`), true); check('matcher sees a fixtures-directory glob', referencesArtifact(`glob('${FIXTURES_DIR}/*.artifact.json')`), true); check('matcher ignores an unrelated hotcrm mention', referencesArtifact("// the HotCRM shape from hotcrm#788"), false); @@ -352,6 +401,7 @@ function selfTest() { ); // ── The permission-set extractor, on the real artifact shape. + battery('The permission-set extractor, on the real artifact shape.'); check( 'extractor finds both folded names in the shipped artifact', FOLDED_NAMES.every((n) => collectPermissionSetNames(JSON.parse(readFileSync(join(REPO_ROOT, ARTIFACT), 'utf8'))).includes(n)), @@ -363,6 +413,52 @@ function selfTest() { [], ); + // ── The floor: every declared battery RAN, and ran its cases (#13489) ─── + // + // Evaluated after every battery has had its chance and BEFORE the verdict, so + // the success line below can only be printed by a run in which the set of + // batteries that registered assertions EQUALS the set declared. A set + // difference names WHICH battery stopped; a count says only that something did. + const floorFailure = (message) => { + failed += 1; + console.error(` FAIL ${message}`); + }; + const declaredBatteries = Object.keys(SELF_TEST_BATTERIES); + let floorBreached = false; + if (declaredBatteries.length < SELF_TEST_BATTERY_FLOOR) { + floorBreached = true; + floorFailure( + `SELF_TEST_BATTERIES declares ${declaredBatteries.length} batteries, below the pinned ` + + `${SELF_TEST_BATTERY_FLOOR} — a battery deleted from the roster takes its own floor with it.`, + ); + } + for (const [name, count] of seen) { + if (declaredBatteries.includes(name)) continue; + floorBreached = true; + floorFailure( + `self-test battery "${name}" registered ${count} case(s) but is not declared in ` + + 'SELF_TEST_BATTERIES — an assertion attributed to no declared battery is one nothing floors.', + ); + } + for (const name of declaredBatteries) { + const count = seen.get(name) ?? 0; + if (count >= SELF_TEST_BATTERIES[name]) continue; + floorBreached = true; + floorFailure( + count === 0 + ? `self-test battery "${name}" DID NOT RUN — 0 cases registered, ${SELF_TEST_BATTERIES[name]} pinned. ` + + 'The verdict below would have claimed those cases hold.' + : `self-test battery "${name}" registered ${count} case(s), below its pinned floor of ` + + `${SELF_TEST_BATTERIES[name]} — cases that used to run no longer do.`, + ); + } + if (floorBreached) { + floorFailure( + 'A battery at or below its floor means cases STOPPED RUNNING — the battery is the bug, not the ' + + 'number. Find what stopped registering (an early return, a deleted block, a guard that now ' + + 'skips) and restore it.', + ); + } if (failed > 0) { console.error(`\n✗ check-position-name-fold-loaders self-test failed (${failed} of ${cases} case(s)).`); process.exit(1); diff --git a/scripts/check-prerelease-pin-watch.mjs b/scripts/check-prerelease-pin-watch.mjs index a360307dd8..1ed369ce7c 100644 --- a/scripts/check-prerelease-pin-watch.mjs +++ b/scripts/check-prerelease-pin-watch.mjs @@ -114,6 +114,42 @@ import { dirname, join, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; import { isEntrypoint } from './invoked-as.mjs'; +// ── The self-test's own battery roster and floor (#13489) ────────────────── +// +// `failures.length === 0` used to be this self-test's ONLY success condition, so +// "every case held" and "the cases never ran" printed the same line. Closed the +// way PR #13487 validated on check-doc-authoring: what is pinned is the +// registered NAMES, not a number. Every section opens with `battery('')`, +// every assertion is attributed to the battery most recently opened, and the +// floor requires the OPENED set to equal the DECLARED set with each battery at +// or above its own count. +// +// ⛔ A pinned TOTAL is not the repair: a battery dropping from 9 cases to 3 +// keeps a total "right" the moment a sibling grows. +// +// The counts are a FLOOR, not an equality — adding cases is ordinary work and +// must not red. A battery BELOW its floor means cases stopped running; the +// remedy is to find what stopped registering. +const SELF_TEST_BATTERIES = Object.freeze({ + '1. the watch list is derived from the pins': 6, + '1b. the ledger rule: a note points at the pin, it does not restate it': 5, + '2. the three registry states': 7, + '3. the criterion is semver, NOT the `latest` tag': 3, + '4. a stable release only in a LATER line is its own case': 3, + '5. the reports say what they must': 8, + '6. a hit alongside an unreadable pin stays a hit, and says it may be': 2, + '7. end-to-end through the real CLI, exit codes included': 16, +}); + +// DELETING an entry silences that battery's floor exactly as effectively as +// zeroing it, so the roster's own size is pinned too. +const SELF_TEST_BATTERY_FLOOR = 8; + +// The key an assertion is filed under when no battery is open. It is not a +// declared battery, so it reds by the same set difference rather than silently +// inflating whichever battery happened to run last. +const UNATTRIBUTED_BATTERY = '(no battery open)'; + // This gate's whole population is ONE repo-root file, and the derivation // already reaches it — through the trigger key, not through a hint. Read from // the source rather than assumed: `prerelease-pin-watch.yml` declares @@ -649,8 +685,22 @@ async function main(argv) { // --------------------------------------------------------------------------- function selfTest() { + // The battery ledger this self-test's floor is evaluated against (#13489). + // `battery()` opens a battery; every assertion below is attributed to the one + // most recently opened, so a section that stops running stops registering and + // names ITSELF at the floor rather than going quiet. + const seen = new Map(); + let openBattery = null; + const battery = (name) => { + openBattery = name; + }; + const registerCase = () => { + const b = openBattery ?? UNATTRIBUTED_BATTERY; + seen.set(b, (seen.get(b) ?? 0) + 1); + }; const failures = []; const check = (name, cond, detail = '') => { + registerCase(); if (cond) console.log(` ✓ ${name}`); else { failures.push(`${name}${detail ? ` — ${detail}` : ''}`); @@ -661,6 +711,7 @@ function selfTest() { console.log('check-prerelease-pin-watch --self-test'); // --- 1. the watch list is derived from the pins --------------------------- + battery('1. the watch list is derived from the pins'); const YAML = [ 'packages:', ' - packages/*', @@ -710,6 +761,7 @@ function selfTest() { // Two correction rounds found one copy each and left the other (#11372 fixed // the pin comment, #11761 the note). What forbids a third copy is this check, // not the memory of those rounds. + battery('1b. the ledger rule: a note points at the pin, it does not restate it'); const restated = FOLLOW_UPS.flatMap((f) => restatedModelCounts(f.note).map((m) => `${f.match.source} -> "${m.trim()}"`), ); @@ -746,6 +798,7 @@ function selfTest() { ); // --- 2. the three registry states ---------------------------------------- + battery('2. the three registry states'); const entry = watch.find((w) => w.name === 'better-auth'); const onlyPre = judge(entry, { @@ -780,6 +833,7 @@ function selfTest() { // --- 3. the criterion is semver, NOT the `latest` tag -------------------- // The live shape on 2026-08-05: `latest` sits on the OLD line while the new // line is published. A `latest`-watching probe sleeps through this. + battery('3. the criterion is semver, NOT the `latest` tag'); const latestStillOld = judge(entry, { versions: ['1.6.26', '1.7.0'], distTags: { latest: '1.6.26', rc: '1.7.0-rc.4' }, @@ -799,6 +853,7 @@ function selfTest() { ); // --- 4. a stable release only in a LATER line is its own case ----------- + battery('4. a stable release only in a LATER line is its own case'); const later = judge(entry, { versions: ['1.7.0-rc.4', '1.8.0', '2.0.0'], distTags: {} }); check('stable only above the pinned line → AVAILABLE', later.verdict === 'available'); check('…classified `later`, not `in-line`', later.trigger === 'later', String(later.trigger)); @@ -810,6 +865,7 @@ function selfTest() { ); // --- 5. the reports say what they must ---------------------------------- + battery('5. the reports say what they must'); const hitText = render(evaluate([judge(entry, { versions: ['1.7.0'], distTags: {} })])); check('the AVAILABLE report names its follow-up card', hitText.includes('#3653'), hitText); check( @@ -862,6 +918,7 @@ function selfTest() { // --- 6. a hit alongside an unreadable pin stays a hit, and says it may be // incomplete (the itemization lesson from check:objectui-pin-fresh) -- + battery('6. a hit alongside an unreadable pin stays a hit, and says it may be'); const mixed = evaluate([judge(entry, { versions: ['1.7.0'], distTags: {} }), unreadable]); check('a hit is not diluted by a sibling read failure', mixed.verdict === 'available'); check( @@ -871,6 +928,7 @@ function selfTest() { ); // --- 7. end-to-end through the real CLI, exit codes included ------------- + battery('7. end-to-end through the real CLI, exit codes included'); const tmp = mkdtempSync(join(tmpdir(), 'prerelease-pin-watch-selftest-')); try { const cli = fileURLToPath(import.meta.url); @@ -1003,6 +1061,51 @@ function selfTest() { rmSync(tmp, { recursive: true, force: true }); } + // ── The floor: every declared battery RAN, and ran its cases (#13489) ─── + // + // Evaluated after every battery has had its chance and BEFORE the verdict, so + // the success line below can only be printed by a run in which the set of + // batteries that registered assertions EQUALS the set declared. A set + // difference names WHICH battery stopped; a count says only that something did. + const floorFailure = (message) => { + failures.push(message); + }; + const declaredBatteries = Object.keys(SELF_TEST_BATTERIES); + let floorBreached = false; + if (declaredBatteries.length < SELF_TEST_BATTERY_FLOOR) { + floorBreached = true; + floorFailure( + `SELF_TEST_BATTERIES declares ${declaredBatteries.length} batteries, below the pinned ` + + `${SELF_TEST_BATTERY_FLOOR} — a battery deleted from the roster takes its own floor with it.`, + ); + } + for (const [name, count] of seen) { + if (declaredBatteries.includes(name)) continue; + floorBreached = true; + floorFailure( + `self-test battery "${name}" registered ${count} case(s) but is not declared in ` + + 'SELF_TEST_BATTERIES — an assertion attributed to no declared battery is one nothing floors.', + ); + } + for (const name of declaredBatteries) { + const count = seen.get(name) ?? 0; + if (count >= SELF_TEST_BATTERIES[name]) continue; + floorBreached = true; + floorFailure( + count === 0 + ? `self-test battery "${name}" DID NOT RUN — 0 cases registered, ${SELF_TEST_BATTERIES[name]} pinned. ` + + 'The verdict below would have claimed those cases hold.' + : `self-test battery "${name}" registered ${count} case(s), below its pinned floor of ` + + `${SELF_TEST_BATTERIES[name]} — cases that used to run no longer do.`, + ); + } + if (floorBreached) { + floorFailure( + 'A battery at or below its floor means cases STOPPED RUNNING — the battery is the bug, not the ' + + 'number. Find what stopped registering (an early return, a deleted block, a guard that now ' + + 'skips) and restore it.', + ); + } if (failures.length) { console.error(`\n⛔ check-prerelease-pin-watch --self-test: ${failures.length} failure(s)`); for (const f of failures) console.error(` - ${f}`); diff --git a/scripts/check-published-readme-exports.mjs b/scripts/check-published-readme-exports.mjs index 4fb77339c7..4026112f2b 100644 --- a/scripts/check-published-readme-exports.mjs +++ b/scripts/check-published-readme-exports.mjs @@ -2073,15 +2073,81 @@ function run({ unreadReport: wantsUnreadReport = false } = {}) { // as one that passed (#13798). const SELF_TEST_VERDICT = 'check-published-readme-exports self-test reached its verdict'; +// ── The self-test's own battery roster and floor (#13489) ────────────────── +// +// `failures.length === 0` used to be this self-test's ONLY success condition, so +// "every case held" and "the cases never ran" printed the same line. Closed the +// way PR #13487 validated on check-doc-authoring: what is pinned is the +// registered NAMES, not a number. Every section opens with `battery('')`, +// every assertion is attributed to the battery most recently opened, and the +// floor requires the OPENED set to equal the DECLARED set with each battery at +// or above its own count. +// +// ⛔ A pinned TOTAL is not the repair: a battery dropping from 9 cases to 3 +// keeps a total "right" the moment a sibling grows. +// +// The counts are a FLOOR, not an equality — adding cases is ordinary work and +// must not red. A battery BELOW its floor means cases stopped running; the +// remedy is to find what stopped registering. +const SELF_TEST_BATTERIES = Object.freeze({ + 'readFences: which blocks are code, and which lines survive': 1, + 'the three MEASURED prose false positives': 1, + 'bash fences: an install line is not an import': 1, + 'the `diff` migration fence, verbatim from packages/objectql': 1, + 'clause shapes': 7, + 'multi-line clauses, and two statements that must not merge': 2, + 'call sites: only import-bound receivers': 2, + 'the adversarial position (#9610), which the fixture above cannot reach': 2, + 'receivers the fence BUILDS from an import-bound name (#9870)': 5, + 'the blind spot the GREEN line now has to state': 4, + 'specifier splitting': 3, + 'exports-map resolution, both shapes this repo writes': 4, + '`files` matcher': 4, + 'END TO END, both directions, on the shape #9532 measured': 4, + 'the supply end, all three arms': 5, + 'the consume end, on the shape measured in the tree': 3, + 'the population, MEASURED off a fixture rather than typed': 2, + 'the header: the line where this half was invisible': 1, + 'END TO END on the #9870 shape: the receiver is never import-bound': 5, + 'THE NAMESPACE BRANCH (#10367)': 10, + '⭐ `--unread-report`: `NOT read:` DECOMPOSED, AND THE CHECKSUM (#10815)': 4, + 'THE POPULATION AXIS, and the third refusal on it (#9911)': 7, + 'THE FOURTH STATE ON THAT AXIS (#10417)': 4, +}); + +// DELETING an entry silences that battery's floor exactly as effectively as +// zeroing it, so the roster's own size is pinned too. +const SELF_TEST_BATTERY_FLOOR = 23; + +// The key an assertion is filed under when no battery is open. It is not a +// declared battery, so it reds by the same set difference rather than silently +// inflating whichever battery happened to run last. +const UNATTRIBUTED_BATTERY = '(no battery open)'; + function selfTest() { + // The battery ledger this self-test's floor is evaluated against (#13489). + // `battery()` opens a battery; every assertion below is attributed to the one + // most recently opened, so a section that stops running stops registering and + // names ITSELF at the floor rather than going quiet. + const seen = new Map(); + let openBattery = null; + const battery = (name) => { + openBattery = name; + }; + const registerCase = () => { + const b = openBattery ?? UNATTRIBUTED_BATTERY; + seen.set(b, (seen.get(b) ?? 0) + 1); + }; const failures = []; const eq = (label, actual, expected) => { + registerCase(); const a = JSON.stringify(actual); const e = JSON.stringify(expected); if (a !== e) failures.push(`${label}\n expected ${e}\n actual ${a}`); }; // -- readFences: which blocks are code, and which lines survive ------------- + battery('readFences: which blocks are code, and which lines survive'); const fenceDoc = [ 'prose before', '```bash', @@ -2104,6 +2170,7 @@ function selfTest() { ); // -- the three MEASURED prose false positives ------------------------------- + battery('the three MEASURED prose false positives'); const prose = [ '- ❌ You only need the schemas — import [`@objectstack/spec`](../spec) alone.', '- ❌ You only need a REST client — import [`@objectstack/client`](../client).', @@ -2112,6 +2179,7 @@ function selfTest() { eq('extractImports — prose mentioning `import` is never a claim', extractImports(prose), []); // -- bash fences: an install line is not an import -------------------------- + battery('bash fences: an install line is not an import'); eq( 'extractImports — a bash fence is not scanned', extractImports('```bash\nimport from @objectstack/spec\n```'), @@ -2119,6 +2187,7 @@ function selfTest() { ); // -- the `diff` migration fence, verbatim from packages/objectql ------------- + battery('the `diff` migration fence, verbatim from packages/objectql'); const diffFence = [ '```diff', "- import { ObjectQL, SchemaRegistry } from '@objectql/core';", @@ -2132,6 +2201,7 @@ function selfTest() { ); // -- clause shapes ----------------------------------------------------------- + battery('clause shapes'); eq('parseImportClause — named', parseImportClause('{ A, B }').named, [ { imported: 'A', local: 'A' }, { imported: 'B', local: 'B' }, @@ -2155,6 +2225,7 @@ function selfTest() { }); // -- multi-line clauses, and two statements that must not merge -------------- + battery('multi-line clauses, and two statements that must not merge'); const multi = ['```ts', 'import {', ' Alpha,', ' Beta,', "} from '@objectstack/spec';", '```'].join('\n'); eq( 'extractImports — multi-line clause', @@ -2174,6 +2245,7 @@ function selfTest() { ); // -- call sites: only import-bound receivers --------------------------------- + battery('call sites: only import-bound receivers'); const calls = [ '```typescript', "import { ServiceAnalytics } from '@objectstack/service-analytics';", @@ -2202,6 +2274,7 @@ function selfTest() { // receiver starts at the character immediately after a DISCARDED `X.y(` match, // with nothing separating them. It is also the house spelling of the six READMEs // this gate exists for, so it is the likeliest wrong rewrite of any of them. + battery('the adversarial position (#9610), which the fixture above cannot reach'); const nestedReceiver = [ '```typescript', "import { CacheServicePlugin } from '@objectstack/service-cache';", @@ -2238,6 +2311,7 @@ function selfTest() { // documents sat on receivers with no type the gate could reach, against EIGHT // it was checking. 109 of the 262 are built out of a name the fence DID // import, and those are the ones these bindings reach. + battery('receivers the fence BUILDS from an import-bound name (#9870)'); const derivedDoc = [ '```typescript', "import { ObjectKernel } from '@objectstack/core';", @@ -2292,6 +2366,7 @@ function selfTest() { ); // -- the blind spot the GREEN line now has to state --------------------------- + battery('the blind spot the GREEN line now has to state'); const unreadDoc = [ '```typescript', "import { ObjectKernel } from '@objectstack/core';", @@ -2348,6 +2423,7 @@ function selfTest() { ); // -- specifier splitting ------------------------------------------------------ + battery('specifier splitting'); eq('splitSpecifier — scoped root', splitSpecifier('@objectstack/spec'), { name: '@objectstack/spec', subpath: '.', @@ -2359,6 +2435,7 @@ function selfTest() { eq('splitSpecifier — relative is not a package', splitSpecifier('./local.js'), null); // -- exports-map resolution, both shapes this repo writes --------------------- + battery('exports-map resolution, both shapes this repo writes'); const flat = { exports: { '.': { types: './dist/index.d.ts', import: './dist/index.js' } } }; eq('resolveTypesEntry — flat `types` condition', resolveTypesEntry(flat, '.'), { entry: './dist/index.d.ts', @@ -2384,6 +2461,7 @@ function selfTest() { }); // -- `files` matcher ----------------------------------------------------------- + battery('`files` matcher'); eq('filesMatcher — a directory entry takes everything beneath it', filesMatcher('dist')('dist/index.js'), true); eq('filesMatcher — README.md', filesMatcher('README.md')('README.md'), true); eq('filesMatcher — near-miss', filesMatcher('README.md')('docs/README.md'), false); @@ -2396,6 +2474,7 @@ function selfTest() { // (`defineStack().validate`). Collapsing them would let an instance-method // fixture pass against the static surface — the exact confusion #9870's // widening exists to avoid. + battery('END TO END, both directions, on the shape #9532 measured'); const fakeSymbol = (members, derived = {}) => ({ __members: new Set(members), __instance: new Set(derived.instance ?? []), @@ -2511,6 +2590,7 @@ function selfTest() { // its least-tested end, and the fake resolver alone can only ever reach one. // -- the supply end, all three arms ------------------------------------------ + battery('the supply end, all three arms'); eq('preTypeTarget — a workspace member defers to type resolution', preTypeTarget('@objectstack/spec', true), undefined); eq('preTypeTarget — ours by scope, in no directory of this repo', preTypeTarget('@objectstack/plugin-org-scoping', false), { unresolvable: true, @@ -2533,6 +2613,7 @@ function selfTest() { // `@objectstack/trigger-schedule`. The worst instance of the class and the one // that makes the case for it: a package misnaming ITSELF, on the page npm // renders for it, green through every run this gate has ever made. + battery('the consume end, on the shape measured in the tree'); const resolveWithScope = (name) => { const hit = resolveFake(name); if (hit) return hit; @@ -2615,6 +2696,7 @@ function selfTest() { // Same discipline as the `reachedTargets` pins below: a hand-written // `ownScope: 3` would pin the refusal's arithmetic while proving nothing about // the scan that produces the number. + battery('the population, MEASURED off a fixture rather than typed'); const scopeMix = [ { pkg: '@fixture/alpha', @@ -2684,6 +2766,7 @@ function selfTest() { // specifiers it could not place. Pinned as a resolved/total PAIR, so a // recogniser that stops matching shows up as a denominator that fell rather // than as a defect count that stayed at zero. + battery('the header: the line where this half was invisible'); const head = headerLine({ documents: 60, members: 77, importStatements: 214, typeEntries: 49, ownScope: 200, unresolvable: 5 }); eq( 'headerLine — the scoped population is printed as resolved/total, beside the older counts', @@ -2705,6 +2788,7 @@ function selfTest() { // sibling READMEs spell the same step `await kernel.bootstrap()`. No import // claim is wrong there, so the import half is silent by construction; before // this widening the call site was one of the 262 nobody read. + battery('END TO END on the #9870 shape: the receiver is never import-bound'); const derivedInstance = { pkg: '@objectstack/kernel', file: 'packages/kernel/README.md', @@ -2808,6 +2892,7 @@ function selfTest() { // must be silent, `spec.defineNothing` is not and must be reported. A fixture // that only checked for the absence of findings would pass just as happily on // the dead branch, where nothing is bound and nothing is read. + battery('THE NAMESPACE BRANCH (#10367)'); const namespaceCalls = { pkg: '@objectstack/spec', file: 'packages/spec/README.md', @@ -3002,6 +3087,7 @@ function selfTest() { // It is pinned in both directions over a fixture for the same reason the // namespace branch is (#10367) — the coverage is real at a tree population // of 0. + battery('⭐ `--unread-report`: `NOT read:` DECOMPOSED, AND THE CHECKSUM (#10815)'); const unreadHeavy = { pkg: '@objectstack/spec', file: 'packages/spec/README.md', @@ -3153,6 +3239,7 @@ function selfTest() { // function `run()` calls. A hand-written `targets: 0` would pin the refusal's // arithmetic while proving nothing about the scan that produces the zero, // which is the very species of vacuity this card is about. + battery('THE POPULATION AXIS, and the third refusal on it (#9911)'); const nothingResolves = [ { pkg: '@fixture/alpha', @@ -3275,6 +3362,7 @@ function selfTest() { // `analyzeDocument` the run calls. A hand-written `symbolChecks: 0` would pin // the refusal's arithmetic and prove nothing about the scan that produces the // zero — the species of vacuity this whole axis exists to refuse. + battery('THE FOURTH STATE ON THAT AXIS (#10417)'); const bindsNothing = [ { pkg: '@objectstack/spec', @@ -3458,6 +3546,51 @@ function selfTest() { // its own (#11510); every gate that consolidated onto it folds in its checks. failures.push(...workspaceEnumeratorSelfTest({ root: ROOT })); + // ── The floor: every declared battery RAN, and ran its cases (#13489) ─── + // + // Evaluated after every battery has had its chance and BEFORE the verdict, so + // the success line below can only be printed by a run in which the set of + // batteries that registered assertions EQUALS the set declared. A set + // difference names WHICH battery stopped; a count says only that something did. + const floorFailure = (message) => { + failures.push(message); + }; + const declaredBatteries = Object.keys(SELF_TEST_BATTERIES); + let floorBreached = false; + if (declaredBatteries.length < SELF_TEST_BATTERY_FLOOR) { + floorBreached = true; + floorFailure( + `SELF_TEST_BATTERIES declares ${declaredBatteries.length} batteries, below the pinned ` + + `${SELF_TEST_BATTERY_FLOOR} — a battery deleted from the roster takes its own floor with it.`, + ); + } + for (const [name, count] of seen) { + if (declaredBatteries.includes(name)) continue; + floorBreached = true; + floorFailure( + `self-test battery "${name}" registered ${count} case(s) but is not declared in ` + + 'SELF_TEST_BATTERIES — an assertion attributed to no declared battery is one nothing floors.', + ); + } + for (const name of declaredBatteries) { + const count = seen.get(name) ?? 0; + if (count >= SELF_TEST_BATTERIES[name]) continue; + floorBreached = true; + floorFailure( + count === 0 + ? `self-test battery "${name}" DID NOT RUN — 0 cases registered, ${SELF_TEST_BATTERIES[name]} pinned. ` + + 'The verdict below would have claimed those cases hold.' + : `self-test battery "${name}" registered ${count} case(s), below its pinned floor of ` + + `${SELF_TEST_BATTERIES[name]} — cases that used to run no longer do.`, + ); + } + if (floorBreached) { + floorFailure( + 'A battery at or below its floor means cases STOPPED RUNNING — the battery is the bug, not the ' + + 'number. Find what stopped registering (an early return, a deleted block, a guard that now ' + + 'skips) and restore it.', + ); + } if (failures.length > 0) { console.error(`✗ check:published-readme-exports --self-test — ${failures.length} failure(s)\n`); for (const f of failures) console.error(` ${f}`); diff --git a/scripts/check-published-readme-links.mjs b/scripts/check-published-readme-links.mjs index b121dd57ec..c4cc6052ec 100644 --- a/scripts/check-published-readme-links.mjs +++ b/scripts/check-published-readme-links.mjs @@ -731,9 +731,61 @@ async function run() { // as one that passed (#13798). const SELF_TEST_VERDICT = 'check-published-readme-links self-test reached its verdict'; +// ── The self-test's own battery roster and floor (#13489) ────────────────── +// +// `failures.length === 0` used to be this self-test's ONLY success condition, so +// "every case held" and "the cases never ran" printed the same line. Closed the +// way PR #13487 validated on check-doc-authoring: what is pinned is the +// registered NAMES, not a number. Every section opens with `battery('')`, +// every assertion is attributed to the battery most recently opened, and the +// floor requires the OPENED set to equal the DECLARED set with each battery at +// or above its own count. +// +// ⛔ A pinned TOTAL is not the repair: a battery dropping from 9 cases to 3 +// keeps a total "right" the moment a sibling grows. +// +// The counts are a FLOOR, not an equality — adding cases is ordinary work and +// must not red. A battery BELOW its floor means cases stopped running; the +// remedy is to find what stopped registering. +const SELF_TEST_BATTERIES = Object.freeze({ + 'extractLinks: discrimination': 7, + 'extractLinks: the CommonMark autolink': 11, + 'classify: every bucket, both directions': 14, + 'absoluteRemedy: both dead spellings, and the refusal': 6, + 'canonicalDocsUrl: the host swap, both directions': 7, + 'resolveDocsPage: the pageCandidates subtlety': 7, + 'checkDocument, against a real temp tree': 29, + 'Assertion 5 -- observed FAILING, then observed SILENT': 23, + 'the autolink reaches the assertions, not just the extractor': 9, + 'the empty-scan guard is real, not decorative': 1, +}); + +// DELETING an entry silences that battery's floor exactly as effectively as +// zeroing it, so the roster's own size is pinned too. +const SELF_TEST_BATTERY_FLOOR = 10; + +// The key an assertion is filed under when no battery is open. It is not a +// declared battery, so it reds by the same set difference rather than silently +// inflating whichever battery happened to run last. +const UNATTRIBUTED_BATTERY = '(no battery open)'; + function selfTest() { + // The battery ledger this self-test's floor is evaluated against (#13489). + // `battery()` opens a battery; every assertion below is attributed to the one + // most recently opened, so a section that stops running stops registering and + // names ITSELF at the floor rather than going quiet. + const seen = new Map(); + let openBattery = null; + const battery = (name) => { + openBattery = name; + }; + const registerCase = () => { + const b = openBattery ?? UNATTRIBUTED_BATTERY; + seen.set(b, (seen.get(b) ?? 0) + 1); + }; const failures = []; const ok = (label, cond) => { + registerCase(); if (!cond) failures.push(label); }; @@ -756,6 +808,7 @@ function selfTest() { }; // ---- extractLinks: discrimination ------------------------------------- + battery('extractLinks: discrimination'); const doc = [ 'Prose [a](/content/docs/x.mdx) here.', '', @@ -798,6 +851,7 @@ function selfTest() { // ACCEPT pins below are the population and the REJECT pins are the price: // widening a recognizer is only safe if what it newly claims is exactly the // links and nothing else that wears angle brackets. + battery('extractLinks: the CommonMark autolink'); ok('extractLinks reads a bare autolink', dests.includes('https://objectstack.ai/docs/autolinked')); ok('extractLinks reads a mailto autolink', dests.includes('mailto:hi@objectstack.ai')); ok( @@ -835,6 +889,7 @@ function selfTest() { ok('extractLinks finds exactly the eight PROSE destinations', links.length === 8); // ---- classify: every bucket, both directions -------------------------- + battery('classify: every bucket, both directions'); ok('classify: root-relative', classify('/content/docs/a.mdx') === 'root-relative'); ok('classify: root-relative (site route)', classify('/docs/a') === 'root-relative'); ok('classify: relative is NOT root-relative', classify('../../spec/src/automation/') === 'relative'); @@ -859,6 +914,7 @@ function selfTest() { ); // ---- absoluteRemedy: both dead spellings, and the refusal ------------- + battery('absoluteRemedy: both dead spellings, and the refusal'); ok( 'remedy: /content/docs/*.mdx -> absolute route', absoluteRemedy('/content/docs/automation/flows.mdx') @@ -890,6 +946,7 @@ function selfTest() { ); // ---- canonicalDocsUrl: the host swap, both directions ----------------- + battery('canonicalDocsUrl: the host swap, both directions'); ok( 'canonical: an alias is rewritten, path and fragment carried', canonicalDocsUrl('https://docs.objectstack.ai/docs/a/b#c') @@ -912,6 +969,7 @@ function selfTest() { ok('canonical: SILENT on a relative destination', canonicalDocsUrl('../sibling/README.md') === null); // ---- resolveDocsPage: the pageCandidates subtlety --------------------- + battery('resolveDocsPage: the pageCandidates subtlety'); ok('resolve: a page file', fixtureResolve('/docs/automation/flows') === 'automation/flows.mdx'); ok('resolve: a directory WITH an index', fixtureResolve('/docs/automation') === 'automation/index.mdx'); ok('resolve: the .md extension too', fixtureResolve('/docs/legacy-md-page') === 'legacy-md-page.md'); @@ -924,6 +982,7 @@ function selfTest() { // The resolution limbs read the filesystem, so they are exercised against the // REAL content root with links known to exist / not exist there. Using the // real tree keeps the self-test honest about the resolver it actually ships. + battery('checkDocument, against a real temp tree'); const table = [['/docs/guides/:path*', '/docs']]; // The fixture sits where a published README actually sits. Assertion 5 // resolves against the DOCUMENT's directory, so depth is load-bearing: at @@ -1041,6 +1100,7 @@ function selfTest() { // Real paths in the real tree, for the same reason assertions 3 and 4 use the // real content root: it keeps the self-test honest about the resolver that // actually ships. + battery('Assertion 5 -- observed FAILING, then observed SILENT'); const a5 = runDoc('See [Guide](../../MINI_KERNEL_GUIDE.md).'); ok( 'A5 FAILS on a relative target that is not in the tree', @@ -1141,6 +1201,7 @@ function selfTest() { // text. Pinned in the direction that is actually true, so a later author does // not read the missing A1 pin as a hole and "fix" it by dropping the scheme // requirement -- which would start claiming every HTML tag in the tree. + battery('the autolink reaches the assertions, not just the extractor'); const auto1 = runDoc('Docs: '); ok( 'a root-relative path in angle brackets is NOT an autolink (no scheme)', @@ -1190,9 +1251,55 @@ function selfTest() { ok('bracket-wearing non-links yield no links at all', autoNone.stats.links === 0); // ---- the empty-scan guard is real, not decorative --------------------- + battery('the empty-scan guard is real, not decorative'); const empty = runDoc('No links here at all.\n\n```ts\nconst x = 1;\n```\n'); ok('a document with no links yields no findings and no links', empty.stats.links === 0 && empty.findings.length === 0); + // ── The floor: every declared battery RAN, and ran its cases (#13489) ─── + // + // Evaluated after every battery has had its chance and BEFORE the verdict, so + // the success line below can only be printed by a run in which the set of + // batteries that registered assertions EQUALS the set declared. A set + // difference names WHICH battery stopped; a count says only that something did. + const floorFailure = (message) => { + failures.push(message); + }; + const declaredBatteries = Object.keys(SELF_TEST_BATTERIES); + let floorBreached = false; + if (declaredBatteries.length < SELF_TEST_BATTERY_FLOOR) { + floorBreached = true; + floorFailure( + `SELF_TEST_BATTERIES declares ${declaredBatteries.length} batteries, below the pinned ` + + `${SELF_TEST_BATTERY_FLOOR} — a battery deleted from the roster takes its own floor with it.`, + ); + } + for (const [name, count] of seen) { + if (declaredBatteries.includes(name)) continue; + floorBreached = true; + floorFailure( + `self-test battery "${name}" registered ${count} case(s) but is not declared in ` + + 'SELF_TEST_BATTERIES — an assertion attributed to no declared battery is one nothing floors.', + ); + } + for (const name of declaredBatteries) { + const count = seen.get(name) ?? 0; + if (count >= SELF_TEST_BATTERIES[name]) continue; + floorBreached = true; + floorFailure( + count === 0 + ? `self-test battery "${name}" DID NOT RUN — 0 cases registered, ${SELF_TEST_BATTERIES[name]} pinned. ` + + 'The verdict below would have claimed those cases hold.' + : `self-test battery "${name}" registered ${count} case(s), below its pinned floor of ` + + `${SELF_TEST_BATTERIES[name]} — cases that used to run no longer do.`, + ); + } + if (floorBreached) { + floorFailure( + 'A battery at or below its floor means cases STOPPED RUNNING — the battery is the bug, not the ' + + 'number. Find what stopped registering (an early return, a deleted block, a guard that now ' + + 'skips) and restore it.', + ); + } if (failures.length > 0) { console.error(`✗ check:published-readme-links --self-test — ${failures.length} failure(s)\n`); for (const f of failures) console.error(` ${f}`); diff --git a/scripts/check-query-options-erasure-ratchet.mjs b/scripts/check-query-options-erasure-ratchet.mjs index 64ad4f2e54..562afbffae 100644 --- a/scripts/check-query-options-erasure-ratchet.mjs +++ b/scripts/check-query-options-erasure-ratchet.mjs @@ -500,9 +500,54 @@ const GUARD_CLOSURE_CASES = [ // as one that passed (#13798). const SELF_TEST_VERDICT = 'check-query-options-erasure-ratchet self-test reached its verdict'; +// ── The self-test's own battery roster and floor (#13489) ────────────────── +// +// `failures.length === 0` used to be this self-test's ONLY success condition, so +// "every case held" and "the cases never ran" printed the same line. Closed the +// way PR #13487 validated on check-doc-authoring: what is pinned is the +// registered NAMES, not a number. Every section opens with `battery('')`, +// every assertion is attributed to the battery most recently opened, and the +// floor requires the OPENED set to equal the DECLARED set with each battery at +// or above its own count. +// +// ⛔ A pinned TOTAL is not the repair: a battery dropping from 9 cases to 3 +// keeps a total "right" the moment a sibling grows. +// +// The counts are a FLOOR, not an equality — adding cases is ordinary work and +// must not red. A battery BELOW its floor means cases stopped running; the +// remedy is to find what stopped registering. +const SELF_TEST_BATTERIES = Object.freeze({ + '1. The rule, in both directions, over synthetic sources': 48, + '2. The ratchet comparison, in both directions.': 8, + '3. The fatal-parse guard, in both directions (#10123).': 38, + '5. Parser headroom: the population\'s deepest file must actually PARSE.': 8, +}); + +// DELETING an entry silences that battery's floor exactly as effectively as +// zeroing it, so the roster's own size is pinned too. +const SELF_TEST_BATTERY_FLOOR = 4; + +// The key an assertion is filed under when no battery is open. It is not a +// declared battery, so it reds by the same set difference rather than silently +// inflating whichever battery happened to run last. +const UNATTRIBUTED_BATTERY = '(no battery open)'; + async function selfTest() { + // The battery ledger this self-test's floor is evaluated against (#13489). + // `battery()` opens a battery; every assertion below is attributed to the one + // most recently opened, so a section that stops running stops registering and + // names ITSELF at the floor rather than going quiet. + const seen = new Map(); + let openBattery = null; + const battery = (name) => { + openBattery = name; + }; + const registerCase = () => { + const b = openBattery ?? UNATTRIBUTED_BATTERY; + seen.set(b, (seen.get(b) ?? 0) + 1); + }; const failures = []; - const assert = (cond, msg) => { if (!cond) failures.push(msg); }; + const assert = (cond, msg) => { registerCase(); if (!cond) failures.push(msg); }; // ── 1. The rule, in both directions, over synthetic sources. ────────────── // @@ -527,6 +572,7 @@ async function selfTest() { // // The fatal is now a self-test FAILURE naming the fixture, not a zero. const hits = async (code, filePath = 'packages/objectql/src/__selftest__.ts') => { + registerCase(); const [result] = await lintTextStrict(eslint, code, { filePath, warnIgnored: false, @@ -537,6 +583,7 @@ async function selfTest() { return (result?.messages ?? []).filter((m) => m.ruleId === QUERY_OPTIONS_RULE_ID).length; }; + battery('1. The rule, in both directions, over synthetic sources'); const reports = [ ['argument 1, object literal', "await e.find('o', { where: {}, orderBy: [] } as any);"], ['argument 1, identifier', 'await e.findOne(t, query as any);'], @@ -626,6 +673,7 @@ async function selfTest() { ); // ── 2. The ratchet comparison, in both directions. ──────────────────────── + battery('2. The ratchet comparison, in both directions.'); const base = { 'a.ts': 2, 'b.ts': 1 }; const cases = [ ['identical + ceiling met is clean', { baseline: base, current: { ...base }, testCeiling: 10, testSites: 10, addedBaselineKeys: [] }, 0], @@ -651,6 +699,7 @@ async function selfTest() { // on today's (parseable) corpus would prove nothing at all. Both directions // are therefore driven through real ESLint output, and the fixture is a file // that genuinely does not parse rather than a hand-built message object. + battery('3. The fatal-parse guard, in both directions (#10123).'); { const [broken] = await lintTextUnguarded(eslint, 'export const x = (', { filePath: 'packages/objectql/src/__selftest_unparseable__.ts', @@ -846,6 +895,7 @@ async function selfTest() { // default stack while the gates' own whole-population runs failed 2/14. The // narrow scope is the worst case, so this trips a full margin before the // population run starts reddening other people's PRs. + battery('5. Parser headroom: the population\'s deepest file must actually PARSE.'); { assert( stackRearmPlan({ execArgv: [], env: {}, flagSupported: true }).rearm === true, @@ -896,6 +946,51 @@ async function selfTest() { // A missing config block must ABORT, never report clean. assert(eslintConfig.some(carriesRule), 'the config must carry the query-options rule'); + // ── The floor: every declared battery RAN, and ran its cases (#13489) ─── + // + // Evaluated after every battery has had its chance and BEFORE the verdict, so + // the success line below can only be printed by a run in which the set of + // batteries that registered assertions EQUALS the set declared. A set + // difference names WHICH battery stopped; a count says only that something did. + const floorFailure = (message) => { + failures.push(message); + }; + const declaredBatteries = Object.keys(SELF_TEST_BATTERIES); + let floorBreached = false; + if (declaredBatteries.length < SELF_TEST_BATTERY_FLOOR) { + floorBreached = true; + floorFailure( + `SELF_TEST_BATTERIES declares ${declaredBatteries.length} batteries, below the pinned ` + + `${SELF_TEST_BATTERY_FLOOR} — a battery deleted from the roster takes its own floor with it.`, + ); + } + for (const [name, count] of seen) { + if (declaredBatteries.includes(name)) continue; + floorBreached = true; + floorFailure( + `self-test battery "${name}" registered ${count} case(s) but is not declared in ` + + 'SELF_TEST_BATTERIES — an assertion attributed to no declared battery is one nothing floors.', + ); + } + for (const name of declaredBatteries) { + const count = seen.get(name) ?? 0; + if (count >= SELF_TEST_BATTERIES[name]) continue; + floorBreached = true; + floorFailure( + count === 0 + ? `self-test battery "${name}" DID NOT RUN — 0 cases registered, ${SELF_TEST_BATTERIES[name]} pinned. ` + + 'The verdict below would have claimed those cases hold.' + : `self-test battery "${name}" registered ${count} case(s), below its pinned floor of ` + + `${SELF_TEST_BATTERIES[name]} — cases that used to run no longer do.`, + ); + } + if (floorBreached) { + floorFailure( + 'A battery at or below its floor means cases STOPPED RUNNING — the battery is the bug, not the ' + + 'number. Find what stopped registering (an early return, a deleted block, a guard that now ' + + 'skips) and restore it.', + ); + } if (failures.length > 0) { console.error(`✗ self-test (${failures.length} failure(s)):\n`); for (const f of failures) console.error(` • ${f}`); diff --git a/scripts/check-react-page-adapter-contract.mjs b/scripts/check-react-page-adapter-contract.mjs index 1021defd70..381502970c 100644 --- a/scripts/check-react-page-adapter-contract.mjs +++ b/scripts/check-react-page-adapter-contract.mjs @@ -715,15 +715,65 @@ function report({ population, findings, censusProblems }) { // handshake is a flag rather than a returned sentinel. let selfTestReachedVerdict = false; +// ── The self-test's own battery roster and floor (#13489) ────────────────── +// +// `failures.length === 0` used to be this self-test's ONLY success condition, so +// "every case held" and "the cases never ran" printed the same line. Closed the +// way PR #13487 validated on check-doc-authoring: what is pinned is the +// registered NAMES, not a number. Every section opens with `battery('')`, +// every assertion is attributed to the battery most recently opened, and the +// floor requires the OPENED set to equal the DECLARED set with each battery at +// or above its own count. +// +// ⛔ A pinned TOTAL is not the repair: a battery dropping from 9 cases to 3 +// keeps a total "right" the moment a sibling grows. +// +// The counts are a FLOOR, not an equality — adding cases is ordinary work and +// must not red. A battery BELOW its floor means cases stopped running; the +// remedy is to find what stopped registering. +const SELF_TEST_BATTERIES = Object.freeze({ + 'The positive control, MOVED from the #10288 test file': 4, + 'The narrowing: a `.data` read BESIDE it is not an exemption': 3, + 'the `records` on that line is a LOCAL, not a `.records` read -- and it': 16, + 'The contracts this sweep must NOT fabricate findings on': 4, + 'The selector': 6, + 'The fence parser': 5, + 'The census control refuses to report OK over nothing (ruling 4)': 5, + 'The sweep wires the detectors to both halves, and NAMES A LINE': 8, +}); + +// DELETING an entry silences that battery's floor exactly as effectively as +// zeroing it, so the roster's own size is pinned too. +const SELF_TEST_BATTERY_FLOOR = 8; + +// The key an assertion is filed under when no battery is open. It is not a +// declared battery, so it reds by the same set difference rather than silently +// inflating whichever battery happened to run last. +const UNATTRIBUTED_BATTERY = '(no battery open)'; + export function selfTest() { + // The battery ledger this self-test's floor is evaluated against (#13489). + // `battery()` opens a battery; every assertion below is attributed to the one + // most recently opened, so a section that stops running stops registering and + // names ITSELF at the floor rather than going quiet. + const seen = new Map(); + let openBattery = null; + const battery = (name) => { + openBattery = name; + }; + const registerCase = () => { + const b = openBattery ?? UNATTRIBUTED_BATTERY; + seen.set(b, (seen.get(b) ?? 0) + 1); + }; const failures = []; let checked = 0; - const assert = (ok, name) => { checked++; if (!ok) failures.push(name); }; + const assert = (ok, name) => { registerCase(); checked++; if (!ok) failures.push(name); }; // ── The positive control, MOVED from the #10288 test file ──────────────── // "the scanners fire on a known-bad source" — carried verbatim, because // extending a population is exactly when a control quietly stops covering // what it used to. + battery('The positive control, MOVED from the #10288 test file'); const bad = ` const a = await adapter.find('showcase_project', { $filter: ['account', '=', sel], top: 500 }); const b = await adapter.find('showcase_invoice', { limit: 200 }); @@ -752,6 +802,7 @@ export function selfTest() { // used to bless -- the app-showcase one and, worse, the docs sample a // customer copies from. Both rendered correctly while teaching a spelling // `ObjectStackAdapter.find()` cannot emit, which is what the carve-out cost. + battery('The narrowing: a `.data` read BESIDE it is not an exemption'); assert( recordsReads(`const rows = Array.isArray(all) ? all : (all && (all.data || all.records)) || [];`).length === 1, 'a `data || records` alias IS a finding — the carve-out that blessed it is what let BOTH surviving repairs land as tolerance', @@ -778,6 +829,7 @@ export function selfTest() { // reports exactly these three across the whole population and nothing else. // The class survived three rounds (#11585 -> #13705 -> #13969) because // nothing pinned it; this block is the pin. + battery('the `records` on that line is a LOCAL, not a `.records` read -- and it'); assert( arrayIsArrayLimbs(`const records = result?.data ?? (Array.isArray(result) ? result : []);`).length === 1, 'the DOCS line #13969 deleted IS a finding — the same string the assertion above pins to zero for `recordsReads`', @@ -857,6 +909,7 @@ export function selfTest() { // ── The contracts this sweep must NOT fabricate findings on ───────────── // Both are real lines from `content/docs`, and both are CORRECT where they sit. + battery('The contracts this sweep must NOT fabricate findings on'); assert( unprefixedQueryKeys(`const customers = await engine.find('customer', { where: { industry: 'tech' }, limit: 10 });`).length === 0, 'an ObjectQL `engine.find` is a different contract — its unprefixed keys are not findings', @@ -875,6 +928,7 @@ export function selfTest() { ); // ── The selector ──────────────────────────────────────────────────────── + battery('The selector'); assert( isReactPageSample({ lang: 'jsx', body: `const adapter = useAdapter();` }), 'a tagged fence holding the adapter is selected on the hook alone', @@ -901,6 +955,7 @@ export function selfTest() { ); // ── The fence parser ──────────────────────────────────────────────────── + battery('The fence parser'); const md = [ 'intro', '```jsx', 'const a = 1;', '```', 'mid', '````md', '```jsx', 'nested, not a block of its own', '```', '````', @@ -914,6 +969,7 @@ export function selfTest() { assert(fencedBlocks('```jsx\nunterminated').length === 1, 'an unterminated fence still yields its body rather than shrinking the population'); // ── The census control refuses to report OK over nothing (ruling 4) ────── + battery('The census control refuses to report OK over nothing (ruling 4)'); const full = { appShowcase: CENSUS_ANCHORS.appShowcase.map((path) => ({ path })), docs: CENSUS_ANCHORS.docs.map((file) => ({ path: `${file}:1`, file })), @@ -939,6 +995,7 @@ export function selfTest() { ); // ── The sweep wires the detectors to both halves, and NAMES A LINE ─────── + battery('The sweep wires the detectors to both halves, and NAMES A LINE'); const fromPage = sweep({ appShowcase: [{ file: 'p.ts', startLine: 1, source: bad }], docs: [] }); assert(fromPage.length === 3, 'the sweep reports every finding from the app-showcase half'); assert( @@ -972,6 +1029,51 @@ export function selfTest() { assert(lineOfIndex('a\nb\nc', 4) === 3 && lineOfIndex('a\nb', 0) === 1, 'lineOfIndex counts newlines before the offset'); assert(lineOfText('x\n hit\ny', 'hit') === 2 && lineOfText('x', 'nope') === 1, 'lineOfText matches on the TRIMMED line, and falls back to 1'); + // ── The floor: every declared battery RAN, and ran its cases (#13489) ─── + // + // Evaluated after every battery has had its chance and BEFORE the verdict, so + // the success line below can only be printed by a run in which the set of + // batteries that registered assertions EQUALS the set declared. A set + // difference names WHICH battery stopped; a count says only that something did. + const floorFailure = (message) => { + failures.push(message); + }; + const declaredBatteries = Object.keys(SELF_TEST_BATTERIES); + let floorBreached = false; + if (declaredBatteries.length < SELF_TEST_BATTERY_FLOOR) { + floorBreached = true; + floorFailure( + `SELF_TEST_BATTERIES declares ${declaredBatteries.length} batteries, below the pinned ` + + `${SELF_TEST_BATTERY_FLOOR} — a battery deleted from the roster takes its own floor with it.`, + ); + } + for (const [name, count] of seen) { + if (declaredBatteries.includes(name)) continue; + floorBreached = true; + floorFailure( + `self-test battery "${name}" registered ${count} case(s) but is not declared in ` + + 'SELF_TEST_BATTERIES — an assertion attributed to no declared battery is one nothing floors.', + ); + } + for (const name of declaredBatteries) { + const count = seen.get(name) ?? 0; + if (count >= SELF_TEST_BATTERIES[name]) continue; + floorBreached = true; + floorFailure( + count === 0 + ? `self-test battery "${name}" DID NOT RUN — 0 cases registered, ${SELF_TEST_BATTERIES[name]} pinned. ` + + 'The verdict below would have claimed those cases hold.' + : `self-test battery "${name}" registered ${count} case(s), below its pinned floor of ` + + `${SELF_TEST_BATTERIES[name]} — cases that used to run no longer do.`, + ); + } + if (floorBreached) { + floorFailure( + 'A battery at or below its floor means cases STOPPED RUNNING — the battery is the bug, not the ' + + 'number. Find what stopped registering (an early return, a deleted block, a guard that now ' + + 'skips) and restore it.', + ); + } if (failures.length) { console.error(`✗ check-react-page-adapter-contract --self-test — ${failures.length} failure(s)\n`); for (const f of failures) console.error(` • ${f}`); diff --git a/scripts/check-release-page-status.mjs b/scripts/check-release-page-status.mjs index d33f860199..b256c6d033 100644 --- a/scripts/check-release-page-status.mjs +++ b/scripts/check-release-page-status.mjs @@ -543,13 +543,63 @@ const CURRENT_INDEX_V17 = // handshake is a flag rather than a returned sentinel. let selfTestReachedVerdict = false; +// ── The self-test's own battery roster and floor (#13489) ────────────────── +// +// `failures.length === 0` used to be this self-test's ONLY success condition, so +// "every case held" and "the cases never ran" printed the same line. Closed the +// way PR #13487 validated on check-doc-authoring: what is pinned is the +// registered NAMES, not a number. Every section opens with `battery('')`, +// every assertion is attributed to the battery most recently opened, and the +// floor requires the OPENED set to equal the DECLARED set with each battery at +// or above its own count. +// +// ⛔ A pinned TOTAL is not the repair: a battery dropping from 9 cases to 3 +// keeps a total "right" the moment a sibling grows. +// +// The counts are a FLOOR, not an equality — adding cases is ordinary work and +// must not red. A battery BELOW its floor means cases stopped running; the +// remedy is to find what stopped registering. +const SELF_TEST_BATTERIES = Object.freeze({ + 'Scope: the cutoff is stated in the OUTPUT, and it has no holes': 6, + 'Hazard 1: the GA predicate excludes prereleases, WITHOUT ordering': 3, + 'Hazard 2: the instrument is guarded, and the unreliable source is wired': 8, + 'The page matcher, both directions, against the REAL wordings': 7, + 'Scanning discipline: the blockquote, not the page': 2, + 'The index matcher, both directions, against the REAL entries': 6, + 'The released-assertion form, spelled out where an author will read it': 3, +}); + +// DELETING an entry silences that battery's floor exactly as effectively as +// zeroing it, so the roster's own size is pinned too. +const SELF_TEST_BATTERY_FLOOR = 7; + +// The key an assertion is filed under when no battery is open. It is not a +// declared battery, so it reds by the same set difference rather than silently +// inflating whichever battery happened to run last. +const UNATTRIBUTED_BATTERY = '(no battery open)'; + function selfTest() { + // The battery ledger this self-test's floor is evaluated against (#13489). + // `battery()` opens a battery; every assertion below is attributed to the one + // most recently opened, so a section that stops running stops registering and + // names ITSELF at the floor rather than going quiet. + const seen = new Map(); + let openBattery = null; + const battery = (name) => { + openBattery = name; + }; + const registerCase = () => { + const b = openBattery ?? UNATTRIBUTED_BATTERY; + seen.set(b, (seen.get(b) ?? 0) + 1); + }; const failures = []; const expect = (label, cond) => { + registerCase(); if (!cond) failures.push(label); }; // ── Scope: the cutoff is stated in the OUTPUT, and it has no holes ───────── + battery('Scope: the cutoff is stated in the OUTPUT, and it has no holes'); expect( 'scope — SCOPE_NOTE names the v16 floor, so a reader of the output learns the cutoff without ' + 'opening this file', @@ -590,6 +640,7 @@ function selfTest() { } // ── Hazard 1: the GA predicate excludes prereleases, WITHOUT ordering ────── + battery('Hazard 1: the GA predicate excludes prereleases, WITHOUT ordering'); expect( 'hazard-1 — a prerelease heading alone yields NO GA major (reusing the unanchored ' + "`releasedMajors()` pattern would make v18's page claim GA the moment 18.0.0-rc.0 publishes)", @@ -618,6 +669,7 @@ function selfTest() { // ── Hazard 2: the instrument is guarded, and the unreliable source is wired // so that its unreliability cannot produce a verdict ──────────────────── + battery('Hazard 2: the instrument is guarded, and the unreliable source is wired'); expect( 'hazard-2 — an EMPTY parse is reported as a broken instrument, never accepted as "nothing ' + 'shipped"', @@ -668,6 +720,7 @@ function selfTest() { ); // ── The page matcher, both directions, against the REAL wordings ─────────── + battery('The page matcher, both directions, against the REAL wordings'); { const p = pageStatusProblems(17, 'v17.mdx', STALE_V17); expect( @@ -714,6 +767,7 @@ function selfTest() { ); // ── Scanning discipline: the blockquote, not the page ───────────────────── + battery('Scanning discipline: the blockquote, not the page'); { const page = [ '---', @@ -745,6 +799,7 @@ function selfTest() { } // ── The index matcher, both directions, against the REAL entries ────────── + battery('The index matcher, both directions, against the REAL entries'); expect( 'index/RED — the real stale v16 entry "(current series: 16.0.0-rc.0)" is caught', indexStatusProblems(16, STALE_INDEX_V16).some((x) => x.includes('prerelease-version')), @@ -775,6 +830,7 @@ function selfTest() { ); // ── The released-assertion form, spelled out where an author will read it ── + battery('The released-assertion form, spelled out where an author will read it'); expect( 'remedy — the missing-assertion message names the accepted forms, so an author reworded into a ' + 'red build is told what to write rather than left to guess', @@ -794,6 +850,51 @@ function selfTest() { !releasedAssertionRe(17).test('17.0.0-rc.6 is released'), ); + // ── The floor: every declared battery RAN, and ran its cases (#13489) ─── + // + // Evaluated after every battery has had its chance and BEFORE the verdict, so + // the success line below can only be printed by a run in which the set of + // batteries that registered assertions EQUALS the set declared. A set + // difference names WHICH battery stopped; a count says only that something did. + const floorFailure = (message) => { + failures.push(message); + }; + const declaredBatteries = Object.keys(SELF_TEST_BATTERIES); + let floorBreached = false; + if (declaredBatteries.length < SELF_TEST_BATTERY_FLOOR) { + floorBreached = true; + floorFailure( + `SELF_TEST_BATTERIES declares ${declaredBatteries.length} batteries, below the pinned ` + + `${SELF_TEST_BATTERY_FLOOR} — a battery deleted from the roster takes its own floor with it.`, + ); + } + for (const [name, count] of seen) { + if (declaredBatteries.includes(name)) continue; + floorBreached = true; + floorFailure( + `self-test battery "${name}" registered ${count} case(s) but is not declared in ` + + 'SELF_TEST_BATTERIES — an assertion attributed to no declared battery is one nothing floors.', + ); + } + for (const name of declaredBatteries) { + const count = seen.get(name) ?? 0; + if (count >= SELF_TEST_BATTERIES[name]) continue; + floorBreached = true; + floorFailure( + count === 0 + ? `self-test battery "${name}" DID NOT RUN — 0 cases registered, ${SELF_TEST_BATTERIES[name]} pinned. ` + + 'The verdict below would have claimed those cases hold.' + : `self-test battery "${name}" registered ${count} case(s), below its pinned floor of ` + + `${SELF_TEST_BATTERIES[name]} — cases that used to run no longer do.`, + ); + } + if (floorBreached) { + floorFailure( + 'A battery at or below its floor means cases STOPPED RUNNING — the battery is the bug, not the ' + + 'number. Find what stopped registering (an early return, a deleted block, a guard that now ' + + 'skips) and restore it.', + ); + } if (failures.length > 0) { for (const f of failures) console.error(` x self-test: ${f}`); console.error(`\ncheck-release-page-status --self-test: ${failures.length} failure(s).\n`); diff --git a/scripts/check-release-section-coverage.mjs b/scripts/check-release-section-coverage.mjs index 30d13ebec7..3844f87e6b 100644 --- a/scripts/check-release-section-coverage.mjs +++ b/scripts/check-release-section-coverage.mjs @@ -615,7 +615,57 @@ const NO_PARENTHETICAL_INDEX_V13 = // handshake is a flag rather than a returned sentinel. let selfTestReachedVerdict = false; +// ── The self-test's own battery roster and floor (#13489) ────────────────── +// +// `failures.length === 0` used to be this self-test's ONLY success condition, so +// "every case held" and "the cases never ran" printed the same line. Closed the +// way PR #13487 validated on check-doc-authoring: what is pinned is the +// registered NAMES, not a number. Every section opens with `battery('')`, +// every assertion is attributed to the battery most recently opened, and the +// floor requires the OPENED set to equal the DECLARED set with each battery at +// or above its own count. +// +// ⛔ A pinned TOTAL is not the repair: a battery dropping from 9 cases to 3 +// keeps a total "right" the moment a sibling grows. +// +// The counts are a FLOOR, not an equality — adding cases is ordinary work and +// must not red. A battery BELOW its floor means cases stopped running; the +// remedy is to find what stopped registering. +const SELF_TEST_BATTERIES = Object.freeze({ + 'Scope: the floor is INHERITED, and that inheritance is enforced': 1, + 'The READ itself: cwd-independent, and a failed read REFUSES': 12, + 'Severity: advisory is the DEFAULT, and it is explained in the output': 5, + 'Instrument guards': 3, + 'The GA parse: prereleases never enter the set': 3, + 'newest-of-major: the `sort -V` trap, structurally absent': 5, + 'Assertion 1, against the REAL pages, both directions': 10, + 'Assertion 2, against the REAL index entries, both directions': 8, + 'The report itself': 3, +}); + +// DELETING an entry silences that battery's floor exactly as effectively as +// zeroing it, so the roster's own size is pinned too. +const SELF_TEST_BATTERY_FLOOR = 9; + +// The key an assertion is filed under when no battery is open. It is not a +// declared battery, so it reds by the same set difference rather than silently +// inflating whichever battery happened to run last. +const UNATTRIBUTED_BATTERY = '(no battery open)'; + export function selfTest() { + // The battery ledger this self-test's floor is evaluated against (#13489). + // `battery()` opens a battery; every assertion below is attributed to the one + // most recently opened, so a section that stops running stops registering and + // names ITSELF at the floor rather than going quiet. + const seen = new Map(); + let openBattery = null; + const battery = (name) => { + openBattery = name; + }; + const registerCase = () => { + const b = openBattery ?? UNATTRIBUTED_BATTERY; + seen.set(b, (seen.get(b) ?? 0) + 1); + }; // ⛔ FIRST, and it RETURNS rather than recording a case. The floor lives in // another file; a read that did not happen measured nothing — least of all // the sibling's floor — so it refuses with a code of its own instead of @@ -633,11 +683,13 @@ export function selfTest() { const failures = []; let cases = 0; const expect = (label, cond) => { + registerCase(); cases += 1; if (!cond) failures.push(label); }; // ── Scope: the floor is INHERITED, and that inheritance is enforced ──────── + battery('Scope: the floor is INHERITED, and that inheritance is enforced'); expect( `scope — the floor is READ from ${SIBLING_GATE} and equals this gate's (sibling: ` + `${reading.floor}, here: ${SCOPE_FLOOR_MAJOR}). Two gates reading the same pages under ` @@ -647,6 +699,7 @@ export function selfTest() { ); // ── The READ itself: cwd-independent, and a failed read REFUSES ──────────── + battery('The READ itself: cwd-independent, and a failed read REFUSES'); expect( 'scope/read — the sibling path is ABSOLUTE, resolved from this file. An absolute path cannot ' + 'depend on the working directory, which is the whole of the cwd repair: the bare relative ' @@ -754,6 +807,7 @@ export function selfTest() { ); // ── Severity: advisory is the DEFAULT, and it is explained in the output ─── + battery('Severity: advisory is the DEFAULT, and it is explained in the output'); expect( 'severity — SEVERITY_NOTE carries the MEASUREMENT behind the advisory choice (the two real gaps ' + 'and the PR count a hard fail would have charged), not just the choice', @@ -779,6 +833,7 @@ export function selfTest() { ); // ── Instrument guards ───────────────────────────────────────────────────── + battery('Instrument guards'); expect( 'instrument — an EMPTY parse is a BROKEN INSTRUMENT, never "nothing shipped"', instrumentProblems({ versions: [], inScopeMajors: [] }) @@ -796,6 +851,7 @@ export function selfTest() { ); // ── The GA parse: prereleases never enter the set ───────────────────────── + battery('The GA parse: prereleases never enter the set'); expect( 'parse — a prerelease heading yields NO version; an unanchored match would put 17.0.0 into the ' + 'set out of `## 17.0.0-rc.0` and then assertion 2 would take a maximum over prereleases', @@ -814,6 +870,7 @@ export function selfTest() { ); // ── newest-of-major: the `sort -V` trap, structurally absent ────────────── + battery('newest-of-major: the `sort -V` trap, structurally absent'); { // To `sort -V | tail -1` the "latest" of these is 17.0.0-rc.6. const versions = gaVersions('## 17.0.0-rc.6\n## 17.1.0\n## 17.0.0\n## 16.1.0\n'); @@ -843,6 +900,7 @@ export function selfTest() { ); // ── Assertion 1, against the REAL pages, both directions ────────────────── + battery('Assertion 1, against the REAL pages, both directions'); expect( 'coverage/RED — THE DEFECT: the real pre-#10232 v17 headings do not cover 17.1, so 17.1.0 is ' + 'reported. check-release-page-status returns EXIT=0 on this exact tree', @@ -899,6 +957,7 @@ export function selfTest() { ); // ── Assertion 2, against the REAL index entries, both directions ────────── + battery('Assertion 2, against the REAL index entries, both directions'); expect( 'index/RED — THE DEFECT: "(current series: 17.0.0, released 2026-08-14)" while 17.1.0 is the ' + 'newest release. The sibling gate passes this — it only rejects a PRE-release here', @@ -945,6 +1004,7 @@ export function selfTest() { ); // ── The report itself ───────────────────────────────────────────────────── + battery('The report itself'); expect( 'report — the advisory report carries SCOPE_NOTE and SEVERITY_NOTE, so a reader of the OUTPUT ' + 'learns the blind spot and why the finding is not fatal without opening this file', @@ -962,6 +1022,51 @@ export function selfTest() { && !renderFindings(['x'], true).includes(SEVERITY_NOTE), ); + // ── The floor: every declared battery RAN, and ran its cases (#13489) ─── + // + // Evaluated after every battery has had its chance and BEFORE the verdict, so + // the success line below can only be printed by a run in which the set of + // batteries that registered assertions EQUALS the set declared. A set + // difference names WHICH battery stopped; a count says only that something did. + const floorFailure = (message) => { + failures.push(message); + }; + const declaredBatteries = Object.keys(SELF_TEST_BATTERIES); + let floorBreached = false; + if (declaredBatteries.length < SELF_TEST_BATTERY_FLOOR) { + floorBreached = true; + floorFailure( + `SELF_TEST_BATTERIES declares ${declaredBatteries.length} batteries, below the pinned ` + + `${SELF_TEST_BATTERY_FLOOR} — a battery deleted from the roster takes its own floor with it.`, + ); + } + for (const [name, count] of seen) { + if (declaredBatteries.includes(name)) continue; + floorBreached = true; + floorFailure( + `self-test battery "${name}" registered ${count} case(s) but is not declared in ` + + 'SELF_TEST_BATTERIES — an assertion attributed to no declared battery is one nothing floors.', + ); + } + for (const name of declaredBatteries) { + const count = seen.get(name) ?? 0; + if (count >= SELF_TEST_BATTERIES[name]) continue; + floorBreached = true; + floorFailure( + count === 0 + ? `self-test battery "${name}" DID NOT RUN — 0 cases registered, ${SELF_TEST_BATTERIES[name]} pinned. ` + + 'The verdict below would have claimed those cases hold.' + : `self-test battery "${name}" registered ${count} case(s), below its pinned floor of ` + + `${SELF_TEST_BATTERIES[name]} — cases that used to run no longer do.`, + ); + } + if (floorBreached) { + floorFailure( + 'A battery at or below its floor means cases STOPPED RUNNING — the battery is the bug, not the ' + + 'number. Find what stopped registering (an early return, a deleted block, a guard that now ' + + 'skips) and restore it.', + ); + } if (failures.length > 0) { for (const f of failures) console.error(` x self-test: ${f}`); console.error(`\ncheck-release-section-coverage --self-test: ${failures.length} failure(s).\n`); diff --git a/scripts/check-resume-authority-declared.mjs b/scripts/check-resume-authority-declared.mjs index aca54b54c4..2f7959babd 100644 --- a/scripts/check-resume-authority-declared.mjs +++ b/scripts/check-resume-authority-declared.mjs @@ -340,9 +340,57 @@ function report({ list = false, scanRoots = DEFAULT_SCAN_ROOTS } = {}) { // as one that passed (#13798). const SELF_TEST_VERDICT = 'check-resume-authority-declared self-test reached its verdict'; +// ── The self-test's own battery roster and floor (#13489) ────────────────── +// +// `failures.length === 0` used to be this self-test's ONLY success condition, so +// "every case held" and "the cases never ran" printed the same line. Closed the +// way PR #13487 validated on check-doc-authoring: what is pinned is the +// registered NAMES, not a number. Every section opens with `battery('')`, +// every assertion is attributed to the battery most recently opened, and the +// floor requires the OPENED set to equal the DECLARED set with each battery at +// or above its own count. +// +// ⛔ A pinned TOTAL is not the repair: a battery dropping from 9 cases to 3 +// keeps a total "right" the moment a sibling grows. +// +// The counts are a FLOOR, not an equality — adding cases is ordinary work and +// must not red. A battery BELOW its floor means cases stopped running; the +// remedy is to find what stopped registering. +const SELF_TEST_BATTERIES = Object.freeze({ + 'The finding itself, and its two silences.': 4, + 'Scope: a non-pausing descriptor is not asked the question.': 2, + 'Structure, not text: the property must be the descriptor\'s OWN, not a': 2, + 'Two descriptors in one file are judged separately (crud-nodes.ts ships': 1, + 'A dynamically assembled argument is recorded, never judged.': 2, + 'A same-named local factory is not the spec\'s. Deliberately still counted:': 2, + 'Wiring: discovery must reach the real tree, and specifically the four': 5, +}); + +// DELETING an entry silences that battery's floor exactly as effectively as +// zeroing it, so the roster's own size is pinned too. +const SELF_TEST_BATTERY_FLOOR = 7; + +// The key an assertion is filed under when no battery is open. It is not a +// declared battery, so it reds by the same set difference rather than silently +// inflating whichever battery happened to run last. +const UNATTRIBUTED_BATTERY = '(no battery open)'; + function selfTest() { + // The battery ledger this self-test's floor is evaluated against (#13489). + // `battery()` opens a battery; every assertion below is attributed to the one + // most recently opened, so a section that stops running stops registering and + // names ITSELF at the floor rather than going quiet. + const seen = new Map(); + let openBattery = null; + const battery = (name) => { + openBattery = name; + }; + const registerCase = () => { + const b = openBattery ?? UNATTRIBUTED_BATTERY; + seen.set(b, (seen.get(b) ?? 0) + 1); + }; const failures = []; - const expect = (label, cond) => { if (!cond) failures.push(label); }; + const expect = (label, cond) => { registerCase(); if (!cond) failures.push(label); }; const descriptor = (body) => `import { defineActionDescriptor } from '@objectstack/spec/automation'; engine.registerNodeExecutor({ @@ -355,6 +403,7 @@ ${body} `; // ── The finding itself, and its two silences. + battery('The finding itself, and its two silences.'); let d = scanSource('a.ts', descriptor(" type: 'x', version: '1.0.0', name: 'X', supportsPause: true,")); expect('finds a pausing descriptor that omits resumeAuthority', d.length === 1 && d[0].supportsPause === true && d[0].declaresResumeAuthority === false); @@ -367,6 +416,7 @@ ${body} expect("an explicit 'service' satisfies the gate", d.length === 1 && d[0].declaresResumeAuthority === true); // ── Scope: a non-pausing descriptor is not asked the question. + battery('Scope: a non-pausing descriptor is not asked the question.'); d = scanSource('a.ts', descriptor(" type: 'x', version: '1.0.0', name: 'X',")); expect('a descriptor with no supportsPause is out of scope', d.length === 1 && d[0].supportsPause === null); @@ -376,6 +426,7 @@ ${body} // ── Structure, not text: the property must be the descriptor's OWN, not a // key of the same name nested inside its configSchema JSON Schema. This is // the case a regex cannot decide, and `screen` really does nest a schema. + battery('Structure, not text: the property must be the descriptor\'s OWN, not a'); const nested = descriptor(` type: 'x', version: '1.0.0', name: 'X', configSchema: { type: 'object', @@ -400,6 +451,7 @@ ${body} // ── Two descriptors in one file are judged separately (crud-nodes.ts ships // four), and a violation next to a compliant one is still found. + battery('Two descriptors in one file are judged separately (crud-nodes.ts ships'); const two = `import { defineActionDescriptor } from '@objectstack/spec/automation'; const a = defineActionDescriptor({ type: 'a', version: '1.0.0', name: 'A', supportsPause: true, resumeAuthority: 'service' }); const b = defineActionDescriptor({ type: 'b', version: '1.0.0', name: 'B', supportsPause: true }); @@ -409,6 +461,7 @@ const b = defineActionDescriptor({ type: 'b', version: '1.0.0', name: 'B', suppo d.length === 2 && d[0].declaresResumeAuthority === true && d[1].declaresResumeAuthority === false); // ── A dynamically assembled argument is recorded, never judged. + battery('A dynamically assembled argument is recorded, never judged.'); d = scanSource('dyn.ts', "const d = defineActionDescriptor(base);\n"); expect('a non-literal argument is opaque, not a violation', d.length === 1 && d[0].opaque === true); @@ -420,6 +473,7 @@ const b = defineActionDescriptor({ type: 'b', version: '1.0.0', name: 'B', suppo // ── A same-named local factory is not the spec's. Deliberately still counted: // the criterion is the literal's shape, and a hand-rolled factory producing // a descriptor has the same obligation. Asserted so the choice is visible. + battery('A same-named local factory is not the spec\'s. Deliberately still counted:'); d = scanSource('local.ts', "function defineActionDescriptor(x: any) { return x; }\nconst d = defineActionDescriptor({ type: 'x', supportsPause: true });\n"); expect('a same-named local factory is judged too (documented choice)', @@ -432,6 +486,7 @@ const b = defineActionDescriptor({ type: 'b', version: '1.0.0', name: 'B', suppo // tree is clean. That is the gated run's job, and duplicating it would make // a genuine violation surface as a self-test failure — the least legible // message available. + battery('Wiring: discovery must reach the real tree, and specifically the four'); const { found } = audit(); expect('discovers descriptors in the real tree', found.length > 0); const pausingTypes = new Set(); @@ -442,6 +497,51 @@ const b = defineActionDescriptor({ type: 'b', version: '1.0.0', name: 'B', suppo expect(`discovery reaches the '${t}' pausing built-in`, pausingTypes.has(t)); } + // ── The floor: every declared battery RAN, and ran its cases (#13489) ─── + // + // Evaluated after every battery has had its chance and BEFORE the verdict, so + // the success line below can only be printed by a run in which the set of + // batteries that registered assertions EQUALS the set declared. A set + // difference names WHICH battery stopped; a count says only that something did. + const floorFailure = (message) => { + failures.push(message); + }; + const declaredBatteries = Object.keys(SELF_TEST_BATTERIES); + let floorBreached = false; + if (declaredBatteries.length < SELF_TEST_BATTERY_FLOOR) { + floorBreached = true; + floorFailure( + `SELF_TEST_BATTERIES declares ${declaredBatteries.length} batteries, below the pinned ` + + `${SELF_TEST_BATTERY_FLOOR} — a battery deleted from the roster takes its own floor with it.`, + ); + } + for (const [name, count] of seen) { + if (declaredBatteries.includes(name)) continue; + floorBreached = true; + floorFailure( + `self-test battery "${name}" registered ${count} case(s) but is not declared in ` + + 'SELF_TEST_BATTERIES — an assertion attributed to no declared battery is one nothing floors.', + ); + } + for (const name of declaredBatteries) { + const count = seen.get(name) ?? 0; + if (count >= SELF_TEST_BATTERIES[name]) continue; + floorBreached = true; + floorFailure( + count === 0 + ? `self-test battery "${name}" DID NOT RUN — 0 cases registered, ${SELF_TEST_BATTERIES[name]} pinned. ` + + 'The verdict below would have claimed those cases hold.' + : `self-test battery "${name}" registered ${count} case(s), below its pinned floor of ` + + `${SELF_TEST_BATTERIES[name]} — cases that used to run no longer do.`, + ); + } + if (floorBreached) { + floorFailure( + 'A battery at or below its floor means cases STOPPED RUNNING — the battery is the bug, not the ' + + 'number. Find what stopped registering (an early return, a deleted block, a guard that now ' + + 'skips) and restore it.', + ); + } if (failures.length) { for (const f of failures) console.error(` x self-test: ${f}`); console.error(`\ncheck-resume-authority-declared --self-test: ${failures.length} failure(s).\n`); diff --git a/scripts/check-role-word.mjs b/scripts/check-role-word.mjs index 3f281ea70a..4c9f31579e 100644 --- a/scripts/check-role-word.mjs +++ b/scripts/check-role-word.mjs @@ -1033,9 +1033,62 @@ function missingRootsMessage(missing) { // handshake is a flag rather than a returned sentinel. let selfTestReachedVerdict = false; +// ── The self-test's own battery roster and floor (#13489) ────────────────── +// +// `failures.length === 0` used to be this self-test's ONLY success condition, so +// "every case held" and "the cases never ran" printed the same line. Closed the +// way PR #13487 validated on check-doc-authoring: what is pinned is the +// registered NAMES, not a number. Every section opens with `battery('')`, +// every assertion is attributed to the battery most recently opened, and the +// floor requires the OPENED set to equal the DECLARED set with each battery at +// or above its own count. +// +// ⛔ A pinned TOTAL is not the repair: a battery dropping from 9 cases to 3 +// keeps a total "right" the moment a sibling grows. +// +// The counts are a FLOOR, not an equality — adding cases is ordinary work and +// must not red. A battery BELOW its floor means cases stopped running; the +// remedy is to find what stopped registering. +const SELF_TEST_BATTERIES = Object.freeze({ + 'The ratchet-remedy authority convention (#8435)': 4, + 'The green body reports what was READ (#9910)': 5, + 'A missing ROOT is REFUSED, per root (#9932)': 8, + 'The dispatch-gates declaration (#9964\'s pattern)': 4, + 'The vendor-wire fence exemption (#10533)': 6, + 'Every way the MARKING could stop bounding the exemption': 15, + 'The diagnostics name the path that now exists': 8, + 'The exemption at the PROGRAM level': 6, + 'The generated-region exemption (#13586)': 7, + 'Direction of error: every malformed pair fails CLOSED and LOUD': 18, + 'The exemption at the PROGRAM level (2)': 6, +}); + +// DELETING an entry silences that battery's floor exactly as effectively as +// zeroing it, so the roster's own size is pinned too. +const SELF_TEST_BATTERY_FLOOR = 11; + +// The key an assertion is filed under when no battery is open. It is not a +// declared battery, so it reds by the same set difference rather than silently +// inflating whichever battery happened to run last. +const UNATTRIBUTED_BATTERY = '(no battery open)'; + function selfTest() { + // The battery ledger this self-test's floor is evaluated against (#13489). + // `battery()` opens a battery; every assertion below is attributed to the one + // most recently opened, so a section that stops running stops registering and + // names ITSELF at the floor rather than going quiet. + const seen = new Map(); + let openBattery = null; + const battery = (name) => { + openBattery = name; + }; + const registerCase = () => { + const b = openBattery ?? UNATTRIBUTED_BATTERY; + seen.set(b, (seen.get(b) ?? 0) + 1); + }; const failures = []; const expect = (label, cond) => { + registerCase(); if (!cond) failures.push(label); }; @@ -1049,6 +1102,7 @@ function selfTest() { // keep (2) green with the convention gone. (4) is this gate's own hazard: // both directions of its ratchet are spelled `--update`, and marking the // improvement path maintainer-only would teach the opposite of the rule. + battery('The ratchet-remedy authority convention (#8435)'); const real = newUseMessage('content/docs/example.mdx', 2); expect('#8435 — the ratchet-offer DETECTOR still matches the NEW-use message (else the check ' + 'below is vacuous)', @@ -1090,6 +1144,7 @@ function selfTest() { // corpus moves. ⛔ Do not "refresh" them to match a live scan — that would // turn a closed fixture into a figure the tree can falsify, which is the // very defect the comment sites above were cleaned of. + battery('The green body reports what was READ (#9910)'); const SCANNED = [{ root: 'content/docs', files: 179 }, { root: 'skills', files: 36 }]; const DEAD_SCAN = [{ root: 'content/docs', files: 0 }, { root: 'skills', files: 0 }]; const PAID_OFF = {}; @@ -1151,6 +1206,7 @@ function selfTest() { // // Fixture roots, not real ones: these names appear nowhere else in this file, // so `includes()` below cannot pass on incidental prose. + battery('A missing ROOT is REFUSED, per root (#9932)'); const PRESENT_ROOT = 'alpha/one'; const ABSENT_ROOT = 'bravo-two'; expect('#9932 — with NO root present, every one of them is reported (the total-scan case, ' @@ -1258,6 +1314,7 @@ function selfTest() { // the brief. The coupling is derived from ROOTS on both sides rather than // re-spelled, so widening or renaming a root cannot leave the declaration // describing the old population. + battery('The dispatch-gates declaration (#9964\'s pattern)'); const separatorless = ROOTS.filter((r) => !r.includes('/')); expect('the declaration exists for every ROOT the hint extractor cannot see (a root with no ' + 'path separator is refused as too generic, so it needs the subtree spelling)', @@ -1284,6 +1341,7 @@ function selfTest() { // // Fixtures differ from each other in exactly ONE line wherever possible, so a // failure names the property and not the fixture. + battery('The vendor-wire fence exemption (#10533)'); const VW_MDX = markerFor('.mdx', 'better-auth'); const VW_MD = markerFor('.md', 'better-auth'); /** Counted occurrences — what the ratchet actually compares against. */ @@ -1351,6 +1409,7 @@ function selfTest() { // ignored. (b) is the half that matters: a marker that silently checks nothing // reads as intentional, and its author meets a "count grew" verdict naming // neither the marker nor the reason it did not take. + battery('Every way the MARKING could stop bounding the exemption'); const nearMisses = [ ['a blank line between marker and fence', FENCED.replace(`${VW_MDX}\n`, `${VW_MDX}\n\n`)], ['the .md spelling in an .mdx file', FENCED.replace(VW_MDX, VW_MD)], @@ -1419,6 +1478,7 @@ function selfTest() { // ratchet with a vendor payload had no discoverable remedy, and the only one // named was the maintainer's. Derived from the constants, so renaming the // token cannot leave a message pointing at a marker nothing recognises. + battery('The diagnostics name the path that now exists'); const grew = grewMessage('content/docs/example.mdx', 4, 5); const newUse = newUseMessage('content/docs/example.mdx', 2); expect('#10533 — BOTH refusal messages name the vendor-wire marker (the count-GREW one is how ' @@ -1462,6 +1522,7 @@ function selfTest() { // Both fixture files live in the SAME root on purpose: it is the extension, // not the root, that selects the marker syntax, and a tree that put each // spelling in its "own" root would pass just as well under a root-keyed table. + battery('The exemption at the PROGRAM level'); const vwSandbox = mkdtempSync(join(tmpdir(), 'check-role-word-vendorwire-')); try { const [root] = ROOTS; @@ -1540,6 +1601,7 @@ function selfTest() { // path SEPARATOR on purpose: a path-shaped literal here would feed the // dispatch-gates hint extractor described at the top of this file a population // this gate does not read. + battery('The generated-region exemption (#13586)'); const GEN_BEGIN = '{/* BEGIN GENERATED: census (gen-census.mjs) — DO NOT EDIT */}'; const GEN_END = '{/* END GENERATED: census */}'; const GEN_BEGIN_MD = ''; @@ -1639,6 +1701,7 @@ function selfTest() { // a ratchet into a suggestion, so each fixture must (a) exempt NOTHING and // (b) be REPORTED — never silently ignored, which reads as intentional while // checking nothing. + battery('Direction of error: every malformed pair fails CLOSED and LOUD'); const UNTERMINATED = REGION.replace(`${GEN_END}\n`, ''); const untermReport = analyzeGeneratedRegions(UNTERMINATED, '.mdx'); expect('#13586 (D) — a BEGIN with no matching END exempts NOTHING and is REPORTED (honouring ' @@ -1766,6 +1829,7 @@ function selfTest() { // Everything above drives predicates. A predicate the program never consults // would satisfy all of it, so these build real trees and read a child // process's real exit status, never a pipe's. + battery('The exemption at the PROGRAM level (2)'); const genSandbox = mkdtempSync(join(tmpdir(), 'check-role-word-generated-')); try { const [root] = ROOTS; @@ -1824,6 +1888,51 @@ function selfTest() { rmSync(genSandbox, { recursive: true, force: true }); } + // ── The floor: every declared battery RAN, and ran its cases (#13489) ─── + // + // Evaluated after every battery has had its chance and BEFORE the verdict, so + // the success line below can only be printed by a run in which the set of + // batteries that registered assertions EQUALS the set declared. A set + // difference names WHICH battery stopped; a count says only that something did. + const floorFailure = (message) => { + failures.push(message); + }; + const declaredBatteries = Object.keys(SELF_TEST_BATTERIES); + let floorBreached = false; + if (declaredBatteries.length < SELF_TEST_BATTERY_FLOOR) { + floorBreached = true; + floorFailure( + `SELF_TEST_BATTERIES declares ${declaredBatteries.length} batteries, below the pinned ` + + `${SELF_TEST_BATTERY_FLOOR} — a battery deleted from the roster takes its own floor with it.`, + ); + } + for (const [name, count] of seen) { + if (declaredBatteries.includes(name)) continue; + floorBreached = true; + floorFailure( + `self-test battery "${name}" registered ${count} case(s) but is not declared in ` + + 'SELF_TEST_BATTERIES — an assertion attributed to no declared battery is one nothing floors.', + ); + } + for (const name of declaredBatteries) { + const count = seen.get(name) ?? 0; + if (count >= SELF_TEST_BATTERIES[name]) continue; + floorBreached = true; + floorFailure( + count === 0 + ? `self-test battery "${name}" DID NOT RUN — 0 cases registered, ${SELF_TEST_BATTERIES[name]} pinned. ` + + 'The verdict below would have claimed those cases hold.' + : `self-test battery "${name}" registered ${count} case(s), below its pinned floor of ` + + `${SELF_TEST_BATTERIES[name]} — cases that used to run no longer do.`, + ); + } + if (floorBreached) { + floorFailure( + 'A battery at or below its floor means cases STOPPED RUNNING — the battery is the bug, not the ' + + 'number. Find what stopped registering (an early return, a deleted block, a guard that now ' + + 'skips) and restore it.', + ); + } if (failures.length) { for (const f of failures) console.error(` x self-test: ${f}`); console.error(`\ncheck-role-word --self-test: ${failures.length} failure(s).\n`); diff --git a/scripts/check-runtime-services-index.mjs b/scripts/check-runtime-services-index.mjs index 3744ed3d35..8845c0d3a7 100644 --- a/scripts/check-runtime-services-index.mjs +++ b/scripts/check-runtime-services-index.mjs @@ -703,10 +703,59 @@ function main() { // as one that passed (#13798). const SELF_TEST_VERDICT = 'check-runtime-services-index self-test reached its verdict'; +// ── The self-test's own battery roster and floor (#13489) ────────────────── +// +// `failures.length === 0` used to be this self-test's ONLY success condition, so +// "every case held" and "the cases never ran" printed the same line. Closed the +// way PR #13487 validated on check-doc-authoring: what is pinned is the +// registered NAMES, not a number. Every section opens with `battery('')`, +// every assertion is attributed to the battery most recently opened, and the +// floor requires the OPENED set to equal the DECLARED set with each battery at +// or above its own count. +// +// ⛔ A pinned TOTAL is not the repair: a battery dropping from 9 cases to 3 +// keeps a total "right" the moment a sibling grows. +// +// The counts are a FLOOR, not an equality — adding cases is ordinary work and +// must not red. A battery BELOW its floor means cases stopped running; the +// remedy is to find what stopped registering. +const SELF_TEST_BATTERIES = Object.freeze({ + 'The clean tree is silent': 2, + 'Each limb observed FAILING': 8, + 'Check 5: declared registry slot vs the real registry (#9630)': 6, + 'Check 6: the stability matrix vs the pages on disk (#9684)': 3, + 'Check 7: a WRONG row, not just a missing one (#9684)': 4, + 'Check 8: the Source-of-Truth canonical-source list (#9629)': 11, + 'Checks 9-10: the label VOCABULARY (#9751)': 15, + 'Refuses to report OK over nothing': 2, +}); + +// DELETING an entry silences that battery's floor exactly as effectively as +// zeroing it, so the roster's own size is pinned too. +const SELF_TEST_BATTERY_FLOOR = 8; + +// The key an assertion is filed under when no battery is open. It is not a +// declared battery, so it reds by the same set difference rather than silently +// inflating whichever battery happened to run last. +const UNATTRIBUTED_BATTERY = '(no battery open)'; + function selfTest() { + // The battery ledger this self-test's floor is evaluated against (#13489). + // `battery()` opens a battery; every assertion below is attributed to the one + // most recently opened, so a section that stops running stops registering and + // names ITSELF at the floor rather than going quiet. + const seen = new Map(); + let openBattery = null; + const battery = (name) => { + openBattery = name; + }; + const registerCase = () => { + const b = openBattery ?? UNATTRIBUTED_BATTERY; + seen.set(b, (seen.get(b) ?? 0) + 1); + }; const failures = []; let checked = 0; - const assert = (cond, why) => { checked++; if (!cond) failures.push(why); }; + const assert = (cond, why) => { registerCase(); checked++; if (!cond) failures.push(why); }; const dir = mkdtempSync(join(tmpdir(), 'rt-services-index-')); try { @@ -803,12 +852,14 @@ function selfTest() { const findingsFor = (opts) => { writeTree(opts); return run(dir).findings; }; // ── The clean tree is silent ──────────────────────────────────────────── + battery('The clean tree is silent'); const clean = findingsFor(); assert(clean.length === 0, `a consistent tree reports nothing -- got ${JSON.stringify(clean)}`); assert(summarise({ pages: readPages(chapter), chapterList: names, kernelTable: names.map((n) => ({ accessor: n })) }).includes('3 chapter page(s)'), 'the summary names the counts, so a green can be read for its scope'); // ── Each limb observed FAILING ────────────────────────────────────────── // This is the #9604 defect itself: page + meta entry, absent from both lists. + battery('Each limb observed FAILING'); const sms = findingsFor({ list: ['data', 'email'], table: ['data', 'email'] }); assert(sms.some((f) => f.where.endsWith('runtime-services/index.mdx') && f.msg.includes('omits `services.sms`')), `the chapter list omitting a real page is caught -- got ${JSON.stringify(sms)}`); assert(sms.some((f) => f.where === KERNEL_INDEX && f.msg.includes('no row for `services.sms`')), `the kernel table omitting a real page is caught -- got ${JSON.stringify(sms)}`); @@ -824,6 +875,7 @@ function selfTest() { // ── Check 5: declared registry slot vs the real registry (#9630) ──────── // The defect itself: a page documenting an accessor that resolves to nothing. + battery('Check 5: declared registry slot vs the real registry (#9630)'); const ghost = findingsFor({ slot: (n) => (n === 'sms' ? 'storage' : n) }); assert( ghost.some((f) => f.where.endsWith(`sms${PAGE_SUFFIX}`) && f.msg.includes('no production registerService() call')), @@ -856,6 +908,7 @@ function selfTest() { // ── Check 6: the stability matrix vs the pages on disk (#9684) ───────── // The defect itself: seven rows for eight pages. + battery('Check 6: the stability matrix vs the pages on disk (#9684)'); const shortMatrix = findingsFor({ matrix: ['data', 'email'] }); assert( shortMatrix.some((f) => f.where.endsWith(VERSIONING_FILE) && f.msg.includes('no row for `services.sms`')), @@ -873,6 +926,7 @@ function selfTest() { // ── Check 7: a WRONG row, not just a missing one (#9684) ─────────────── // Membership checking passes both of these: the row is present and names a // real page, it just contradicts the label that page declares. + battery('Check 7: a WRONG row, not just a missing one (#9684)'); const wrongMatrix = findingsFor({ matrixLabel: (n) => (n === 'sms' ? 'experimental' : 'stable') }); assert( wrongMatrix.some((f) => f.where.endsWith(VERSIONING_FILE) && f.msg.includes('`experimental`') && f.msg.includes('declares `stable`')), @@ -905,6 +959,7 @@ function selfTest() { // The defect this card was filed for: the list advertising a canonical // source for a service the chapter never introduces. + battery('Check 8: the Source-of-Truth canonical-source list (#9629)'); const pageless = findingsFor({ sourceRows: [...defaultSourceRows, { label: 'Security', path: REAL_PATH }] }); assert( pageless.some((f) => f.where.endsWith('runtime-services/index.mdx') && f.msg.includes('"Security" names no page')), @@ -970,6 +1025,7 @@ function selfTest() { // with both tables faithfully repeating it. Check 7 is silent by // construction here -- the tables agree with the page -- so this is the // shape that was green before this limb existed. + battery('Checks 9-10: the label VOCABULARY (#9751)'); const undefinedLabel = findingsFor({ stability: (n) => (n === 'sms' ? 'beta' : 'stable') }); assert( undefinedLabel.some((f) => f.where.endsWith(`sms${PAGE_SUFFIX}`) && f.msg.includes('`beta`')), @@ -1052,6 +1108,7 @@ function selfTest() { } // ── Refuses to report OK over nothing ─────────────────────────────────── + battery('Refuses to report OK over nothing'); let threwVersioning = false; try { findingsFor({ versioning: false }); } catch { threwVersioning = true; } assert(threwVersioning, 'a chapter with no versioning.mdx is rejected, never reported OK over a matrix that is not there'); @@ -1063,6 +1120,51 @@ function selfTest() { rmSync(dir, { recursive: true, force: true }); } + // ── The floor: every declared battery RAN, and ran its cases (#13489) ─── + // + // Evaluated after every battery has had its chance and BEFORE the verdict, so + // the success line below can only be printed by a run in which the set of + // batteries that registered assertions EQUALS the set declared. A set + // difference names WHICH battery stopped; a count says only that something did. + const floorFailure = (message) => { + failures.push(message); + }; + const declaredBatteries = Object.keys(SELF_TEST_BATTERIES); + let floorBreached = false; + if (declaredBatteries.length < SELF_TEST_BATTERY_FLOOR) { + floorBreached = true; + floorFailure( + `SELF_TEST_BATTERIES declares ${declaredBatteries.length} batteries, below the pinned ` + + `${SELF_TEST_BATTERY_FLOOR} — a battery deleted from the roster takes its own floor with it.`, + ); + } + for (const [name, count] of seen) { + if (declaredBatteries.includes(name)) continue; + floorBreached = true; + floorFailure( + `self-test battery "${name}" registered ${count} case(s) but is not declared in ` + + 'SELF_TEST_BATTERIES — an assertion attributed to no declared battery is one nothing floors.', + ); + } + for (const name of declaredBatteries) { + const count = seen.get(name) ?? 0; + if (count >= SELF_TEST_BATTERIES[name]) continue; + floorBreached = true; + floorFailure( + count === 0 + ? `self-test battery "${name}" DID NOT RUN — 0 cases registered, ${SELF_TEST_BATTERIES[name]} pinned. ` + + 'The verdict below would have claimed those cases hold.' + : `self-test battery "${name}" registered ${count} case(s), below its pinned floor of ` + + `${SELF_TEST_BATTERIES[name]} — cases that used to run no longer do.`, + ); + } + if (floorBreached) { + floorFailure( + 'A battery at or below its floor means cases STOPPED RUNNING — the battery is the bug, not the ' + + 'number. Find what stopped registering (an early return, a deleted block, a guard that now ' + + 'skips) and restore it.', + ); + } if (failures.length) { console.error(`✗ check-runtime-services-index --self-test -- ${failures.length} failure(s)\n`); for (const f of failures) console.error(` • ${f}`); diff --git a/scripts/check-sdui-lockstep.mjs b/scripts/check-sdui-lockstep.mjs index 7b5565aebe..1b47e3f994 100644 --- a/scripts/check-sdui-lockstep.mjs +++ b/scripts/check-sdui-lockstep.mjs @@ -513,9 +513,56 @@ export const stamp = { code: SOMEWHERE_ELSE, message: 'x' }; // as one that passed (#13798). const SELF_TEST_VERDICT = 'check-sdui-lockstep self-test reached its verdict'; +// ── The self-test's own battery roster and floor (#13489) ────────────────── +// +// `failures.length === 0` used to be this self-test's ONLY success condition, so +// "every case held" and "the cases never ran" printed the same line. Closed the +// way PR #13487 validated on check-doc-authoring: what is pinned is the +// registered NAMES, not a number. Every section opens with `battery('')`, +// every assertion is attributed to the battery most recently opened, and the +// floor requires the OPENED set to equal the DECLARED set with each battery at +// or above its own count. +// +// ⛔ A pinned TOTAL is not the repair: a battery dropping from 9 cases to 3 +// keeps a total "right" the moment a sibling grows. +// +// The counts are a FLOOR, not an equality — adding cases is ordinary work and +// must not red. A battery BELOW its floor means cases stopped running; the +// remedy is to find what stopped registering. +const SELF_TEST_BATTERIES = Object.freeze({ + 'The constant-vs-literal decomposition (the false 24-vs-23)': 7, + 'The region reader': 7, + 'The verdict': 4, + 'Absence is loud: every degraded input REFUSES rather than passing': 8, + 'The wiring this gate needs to be reachable at all': 9, +}); + +// DELETING an entry silences that battery's floor exactly as effectively as +// zeroing it, so the roster's own size is pinned too. +const SELF_TEST_BATTERY_FLOOR = 5; + +// The key an assertion is filed under when no battery is open. It is not a +// declared battery, so it reds by the same set difference rather than silently +// inflating whichever battery happened to run last. +const UNATTRIBUTED_BATTERY = '(no battery open)'; + export function selfTest() { + // The battery ledger this self-test's floor is evaluated against (#13489). + // `battery()` opens a battery; every assertion below is attributed to the one + // most recently opened, so a section that stops running stops registering and + // names ITSELF at the floor rather than going quiet. + const seen = new Map(); + let openBattery = null; + const battery = (name) => { + openBattery = name; + }; + const registerCase = () => { + const b = openBattery ?? UNATTRIBUTED_BATTERY; + seen.set(b, (seen.get(b) ?? 0) + 1); + }; const failures = []; const check = (label, ok, detail) => { + registerCase(); if (!ok) failures.push(detail === undefined ? label : `${label} — ${detail}`); }; const extract = (sources) => @@ -525,6 +572,7 @@ export function selfTest() { }); // ── The constant-vs-literal decomposition (the false 24-vs-23) ──────────── + battery('The constant-vs-literal decomposition (the false 24-vs-23)'); const literalSide = extract({ 'a.ts': FIXTURE_LITERAL_SIDE }); const constantSide = extract({ 'b.ts': FIXTURE_CONSTANT_SIDE }); check( @@ -570,6 +618,7 @@ export function selfTest() { ); // ── The region reader ──────────────────────────────────────────────────── + battery('The region reader'); check('git blob id for an empty file', gitBlobHash('') === 'e69de29bb2d1d6434b8b29ae775ad8c2e48c5391'); check('git blob id for "x\\n"', gitBlobHash('x\n') === '587be6b4c3f93f93c489c0111bba5596147a26cb'); check( @@ -590,6 +639,7 @@ export function selfTest() { ); // ── The verdict ────────────────────────────────────────────────────────── + battery('The verdict'); const goodRecord = { objectui: { rev: 'a'.repeat(40) }, recordedAgainstPin: 'b'.repeat(40), @@ -625,6 +675,7 @@ export function selfTest() { })) === 'code-drift', ); // ── Absence is loud: every degraded input REFUSES rather than passing ───── + battery('Absence is loud: every degraded input REFUSES rather than passing'); check( 'a missing record is a finding, not a pass', kinds(judge({ record: null, ours: goodOurs, livePin: 'b'.repeat(40) })) === 'record-unusable', @@ -661,6 +712,7 @@ export function selfTest() { ); // ── The wiring this gate needs to be reachable at all ──────────────────── + battery('The wiring this gate needs to be reachable at all'); const pkg = JSON.parse(readFileSync(join(ROOT, 'package.json'), 'utf8')); check( 'the package script runs the self-test before the gate', @@ -705,6 +757,51 @@ export function selfTest() { inCode.includes(`'${PIN_FILE}'`), ); + // ── The floor: every declared battery RAN, and ran its cases (#13489) ─── + // + // Evaluated after every battery has had its chance and BEFORE the verdict, so + // the success line below can only be printed by a run in which the set of + // batteries that registered assertions EQUALS the set declared. A set + // difference names WHICH battery stopped; a count says only that something did. + const floorFailure = (message) => { + failures.push(message); + }; + const declaredBatteries = Object.keys(SELF_TEST_BATTERIES); + let floorBreached = false; + if (declaredBatteries.length < SELF_TEST_BATTERY_FLOOR) { + floorBreached = true; + floorFailure( + `SELF_TEST_BATTERIES declares ${declaredBatteries.length} batteries, below the pinned ` + + `${SELF_TEST_BATTERY_FLOOR} — a battery deleted from the roster takes its own floor with it.`, + ); + } + for (const [name, count] of seen) { + if (declaredBatteries.includes(name)) continue; + floorBreached = true; + floorFailure( + `self-test battery "${name}" registered ${count} case(s) but is not declared in ` + + 'SELF_TEST_BATTERIES — an assertion attributed to no declared battery is one nothing floors.', + ); + } + for (const name of declaredBatteries) { + const count = seen.get(name) ?? 0; + if (count >= SELF_TEST_BATTERIES[name]) continue; + floorBreached = true; + floorFailure( + count === 0 + ? `self-test battery "${name}" DID NOT RUN — 0 cases registered, ${SELF_TEST_BATTERIES[name]} pinned. ` + + 'The verdict below would have claimed those cases hold.' + : `self-test battery "${name}" registered ${count} case(s), below its pinned floor of ` + + `${SELF_TEST_BATTERIES[name]} — cases that used to run no longer do.`, + ); + } + if (floorBreached) { + floorFailure( + 'A battery at or below its floor means cases STOPPED RUNNING — the battery is the bug, not the ' + + 'number. Find what stopped registering (an early return, a deleted block, a guard that now ' + + 'skips) and restore it.', + ); + } if (failures.length) { console.error('check:sdui-lockstep --self-test FAILED'); for (const failure of failures) console.error(` - ${failure}`); diff --git a/scripts/check-shard-attestation.mjs b/scripts/check-shard-attestation.mjs index 92f8a8313d..187b9cfa40 100644 --- a/scripts/check-shard-attestation.mjs +++ b/scripts/check-shard-attestation.mjs @@ -911,10 +911,67 @@ async function main() { // as one that passed (#13798). const SELF_TEST_VERDICT = 'check-shard-attestation self-test reached its verdict'; +// ── The self-test's own battery roster and floor (#13489) ────────────────── +// +// `failures.length === 0` used to be this self-test's ONLY success condition, so +// "every case held" and "the cases never ran" printed the same line. Closed the +// way PR #13487 validated on check-doc-authoring: what is pinned is the +// registered NAMES, not a number. Every section opens with `battery('')`, +// every assertion is attributed to the battery most recently opened, and the +// floor requires the OPENED set to equal the DECLARED set with each battery at +// or above its own count. +// +// ⛔ A pinned TOTAL is not the repair: a battery dropping from 9 cases to 3 +// keeps a total "right" the moment a sibling grows. +// +// The counts are a FLOOR, not an equality — adding cases is ordinary work and +// must not red. A battery BELOW its floor means cases stopped running; the +// remedy is to find what stopped registering. +const SELF_TEST_BATTERIES = Object.freeze({ + '(i) all N positives ⇒ green': 1, + '(ii) N-1 positives, one never scheduled ⇒ red': 7, + '(iii) N-1 positives + one explicit failure ⇒ red': 4, + '(iv) filter-skipped family ⇒ green with expected-N adjusted to 0': 1, + 'the #4928 guard itself must not regress': 8, + '#3668 lifecycle: cancelled passes without counting': 2, + 'dogfood-gate: a matrix leg and a single leg under one context': 5, + 'foreign / stale credentials': 3, + '#11998: attempt scoping, in both directions': 26, + 'missing input is a failure, never a pass (#4690)': 2, + 'the classifier: adjacency, not co-occurrence (#6589)': 14, + '…and the PROGRAM word must be unquoted (#10889)': 12, + 'readAttestations over a real directory': 4, + 'the static drift guard, over fixture workflows': 17, + '#6589: a shard job is not a gate because a step says `--verify`': 23, +}); + +// DELETING an entry silences that battery's floor exactly as effectively as +// zeroing it, so the roster's own size is pinned too. +const SELF_TEST_BATTERY_FLOOR = 15; + +// The key an assertion is filed under when no battery is open. It is not a +// declared battery, so it reds by the same set difference rather than silently +// inflating whichever battery happened to run last. +const UNATTRIBUTED_BATTERY = '(no battery open)'; + async function selfTest() { + // The battery ledger this self-test's floor is evaluated against (#13489). + // `battery()` opens a battery; every assertion below is attributed to the one + // most recently opened, so a section that stops running stops registering and + // names ITSELF at the floor rather than going quiet. + const seen = new Map(); + let openBattery = null; + const battery = (name) => { + openBattery = name; + }; + const registerCase = () => { + const b = openBattery ?? UNATTRIBUTED_BATTERY; + seen.set(b, (seen.get(b) ?? 0) + 1); + }; const failures = []; let checked = 0; const assert = (condition, description) => { + registerCase(); checked += 1; if (!condition) failures.push(description); }; @@ -932,6 +989,7 @@ async function selfTest() { }); // ── (i) all N positives ⇒ green ─────────────────────────────────────────── + battery('(i) all N positives ⇒ green'); assert(testGate('success', [attest('test', 1, 3), attest('test', 2, 3), attest('test', 3, 3)]).ok, 'all 3 declared shards attested ⇒ green'); // ── (ii) N-1 positives, one never scheduled ⇒ red ───────────────────────── @@ -940,6 +998,7 @@ async function selfTest() { // executed zero tests while the queue was still consuming this run's // verdicts. Whitelisting the word (option A) would have published `Test // Core: success` over that. Counting cannot: the credential is absent. + battery('(ii) N-1 positives, one never scheduled ⇒ red'); const abandonedNotMoot = testGate('abandoned', [attest('test', 1, 3), attest('test', 2, 3)]); assert(!abandonedNotMoot.ok, 'abandoned-not-moot: 2 of 3 attested + aggregate `abandoned` ⇒ red'); assert( @@ -953,6 +1012,7 @@ async function selfTest() { } // ── (iii) N-1 positives + one explicit failure ⇒ red ────────────────────── + battery('(iii) N-1 positives + one explicit failure ⇒ red'); assert(!testGate('failure', [attest('test', 1, 3), attest('test', 2, 3)]).ok, 'a genuinely failing shard ⇒ red'); // COUNTER-EXAMPLE PIN 2 — "a real failure swallowed by a sibling's lifecycle // value" (#6082 comment 5208599210, run 31114735713: shard `Test Core (3/3)` @@ -971,8 +1031,10 @@ async function selfTest() { assert(!testGate('failure', [attest('test', 1, 3), attest('test', 2, 3), attest('test', 3, 3)]).ok, 'a full roster never overrides a declared `failure`'); // ── (iv) filter-skipped family ⇒ green with expected-N adjusted to 0 ────── + battery('(iv) filter-skipped family ⇒ green with expected-N adjusted to 0'); assert(testGate('skipped', []).ok, '#4928: skipped legs + filter success ⇒ green, expected-N adjusts to 0'); // ── the #4928 guard itself must not regress ─────────────────────────────── + battery('the #4928 guard itself must not regress'); for (const bad of ['failure', 'skipped', '']) { const guarded = testGate('skipped', [], bad); assert(!guarded.ok, `#4928 guard: skipped legs while filter result is '${bad || '(empty)'}' ⇒ red`); @@ -982,10 +1044,12 @@ async function selfTest() { assert(!testGate('skipped', [attest('test', 1, 3)]).ok, 'a credential from a leg that was reported skipped is a contradiction ⇒ red'); // ── #3668 lifecycle: cancelled passes without counting ──────────────────── + battery('#3668 lifecycle: cancelled passes without counting'); assert(testGate('cancelled', []).ok, '#3668: a cancelled matrix passes with zero credentials (superseded SHA)'); assert(testGate('cancelled', [attest('test', 1, 3)]).ok, '#3668: a leg that attested before the cancellation is allowed, not required'); // ── dogfood-gate: a matrix leg and a single leg under one context ───────── + battery('dogfood-gate: a matrix leg and a single leg under one context'); const dogfood = (dogfoodResult, verifyResult, ids) => judge({ gate: 'Dogfood Regression Gate', @@ -1005,6 +1069,7 @@ async function selfTest() { assert(!dogfood('success', 'skipped', fullDogfood).ok, 'dogfood: a credential from the leg reported skipped contradicts the skip ⇒ red'); // ── foreign / stale credentials ─────────────────────────────────────────── + battery('foreign / stale credentials'); assert(!testGate('success', [attest('test', 1, 3), attest('test', 2, 3), attest('test', 3, 3), attest('test', 4, 4)]).ok, 'a credential no declared leg accounts for ⇒ red'); assert( !testGate('success', [attest('test', 1, 3), attest('test', 2, 3), attest('test', 3, 3, '1234')]).ok, @@ -1024,6 +1089,7 @@ async function selfTest() { // succeeded, judged docs-only, the legs stayed `skipped` — and the gates // refused attempt 1's own credentials one by one. The two error clusters // below are quoted from that run's job logs verbatim. + battery('#11998: attempt scoping, in both directions'); const rerun = (legs, ids, runAttempt = '2', filterResult = 'success') => judge({ gate: 'Test Core', legs, filterResult, present: new Map(ids), runId: '99', runAttempt }); const testLeg = (result) => [{ job: 'test', total: 6, result }]; @@ -1123,6 +1189,7 @@ async function selfTest() { ); // ── missing input is a failure, never a pass (#4690) ────────────────────── + battery('missing input is a failure, never a pass (#4690)'); assert(!judge({ gate: 'Test Core', legs: [], filterResult: 'success', present: new Map(), runId: '99' }).ok, 'a gate with no legs verifies nothing ⇒ red'); assert(!testGate('', []).ok, 'an aggregate result that did not interpolate ⇒ red'); @@ -1131,6 +1198,7 @@ async function selfTest() { // is the point. The old test asked whether the basename and the flag both // occurred; these ask whether the flag is an ARGUMENT of a command that runs // the script. + battery('the classifier: adjacency, not co-occurrence (#6589)'); const REAL_GATE_COMMAND = "node scripts/check-shard-attestation.mjs --verify \\\n --gate 'Test Core' \\\n --dir \"$OS_ATTEST_DIR\""; assert(invokesScript('node scripts/check-shard-attestation.mjs --verify --gate x', '--verify'), 'the flag as an argument on the same line ⇒ an invocation'); assert(invokesScript(REAL_GATE_COMMAND, '--verify'), "ci.yml's real gate command, continuations and all ⇒ an invocation"); @@ -1168,6 +1236,7 @@ async function selfTest() { // #6589's cases above stay exactly as they were: a recognizer tightened until // nothing satisfies it is a worse defect than the prose it excluded, because // what a permanently red pin gets is loosened. + battery('…and the PROGRAM word must be unquoted (#10889)'); assert( !invokesScript('echo "node scripts/check-shard-attestation.mjs --verify is how you would check"', '--verify'), '#10889: an `echo` QUOTING the invocation is prose — the quoted blob is ONE argument, not a program being run', @@ -1235,6 +1304,7 @@ async function selfTest() { ); // ── readAttestations over a real directory ─────────────────────────────── + battery('readAttestations over a real directory'); const dir = mkdtempSync(join(tmpdir(), 'shard-attest-')); try { assert(readAttestations(join(dir, 'never-created')).present.size === 0, 'a missing download directory is zero credentials, not a crash'); @@ -1252,6 +1322,7 @@ async function selfTest() { assert(readAttestations(join(dir, 'a')).problems.length === 1, 'an unreadable credential is a problem, not a silent skip'); // ── the static drift guard, over fixture workflows ────────────────────── + battery('the static drift guard, over fixture workflows'); const good = readFileSync(join(scriptRepoRoot(), '.github', 'workflows', 'ci.yml'), 'utf8'); const withWorkflow = async (source) => { const root = mkdtempSync(join(tmpdir(), 'shard-attest-wf-')); @@ -1310,6 +1381,7 @@ async function selfTest() { // reported neither the classifier nor the flag — it reported that job // `test` lacked two properties only an aggregate gate owes, and following // that reading means editing the shard job's `if:`, i.e. breaking #3622. + battery('#6589: a shard job is not a gate because a step says `--verify`'); assert( REQUIRED_GATE_JOBS.every((id) => baseline.gateIds.includes(id)) && baseline.gateIds.length === REQUIRED_GATE_JOBS.length, `the real gate invocations, and only those, are classified as gates (${REQUIRED_GATE_JOBS.join(', ')})`, @@ -1374,6 +1446,51 @@ async function selfTest() { rmSync(dir, { recursive: true, force: true }); } + // ── The floor: every declared battery RAN, and ran its cases (#13489) ─── + // + // Evaluated after every battery has had its chance and BEFORE the verdict, so + // the success line below can only be printed by a run in which the set of + // batteries that registered assertions EQUALS the set declared. A set + // difference names WHICH battery stopped; a count says only that something did. + const floorFailure = (message) => { + failures.push(message); + }; + const declaredBatteries = Object.keys(SELF_TEST_BATTERIES); + let floorBreached = false; + if (declaredBatteries.length < SELF_TEST_BATTERY_FLOOR) { + floorBreached = true; + floorFailure( + `SELF_TEST_BATTERIES declares ${declaredBatteries.length} batteries, below the pinned ` + + `${SELF_TEST_BATTERY_FLOOR} — a battery deleted from the roster takes its own floor with it.`, + ); + } + for (const [name, count] of seen) { + if (declaredBatteries.includes(name)) continue; + floorBreached = true; + floorFailure( + `self-test battery "${name}" registered ${count} case(s) but is not declared in ` + + 'SELF_TEST_BATTERIES — an assertion attributed to no declared battery is one nothing floors.', + ); + } + for (const name of declaredBatteries) { + const count = seen.get(name) ?? 0; + if (count >= SELF_TEST_BATTERIES[name]) continue; + floorBreached = true; + floorFailure( + count === 0 + ? `self-test battery "${name}" DID NOT RUN — 0 cases registered, ${SELF_TEST_BATTERIES[name]} pinned. ` + + 'The verdict below would have claimed those cases hold.' + : `self-test battery "${name}" registered ${count} case(s), below its pinned floor of ` + + `${SELF_TEST_BATTERIES[name]} — cases that used to run no longer do.`, + ); + } + if (floorBreached) { + floorFailure( + 'A battery at or below its floor means cases STOPPED RUNNING — the battery is the bug, not the ' + + 'number. Find what stopped registering (an early return, a deleted block, a guard that now ' + + 'skips) and restore it.', + ); + } if (failures.length > 0) { console.error(`✗ check-shard-attestation --self-test -- ${failures.length} failure(s)\n`); for (const failure of failures) console.error(` • ${failure}`); diff --git a/scripts/check-single-authz-resolver.mjs b/scripts/check-single-authz-resolver.mjs index 77b55ee5c4..80c81986f0 100644 --- a/scripts/check-single-authz-resolver.mjs +++ b/scripts/check-single-authz-resolver.mjs @@ -545,9 +545,66 @@ function mentionOnlyFixtureBody(tables = GRANT_TABLES) { // handshake is a flag rather than a returned sentinel. let selfTestReachedVerdict = false; +// ── The self-test's own battery roster and floor (#13489) ────────────────── +// +// `failures.length === 0` used to be this self-test's ONLY success condition, so +// "every case held" and "the cases never ran" printed the same line. Closed the +// way PR #13487 validated on check-doc-authoring: what is pinned is the +// registered NAMES, not a number. Every section opens with `battery('')`, +// every assertion is attributed to the battery most recently opened, and the +// floor requires the OPENED set to equal the DECLARED set with each battery at +// or above its own count. +// +// ⛔ A pinned TOTAL is not the repair: a battery dropping from 9 cases to 3 +// keeps a total "right" the moment a sibling grows. +// +// The counts are a FLOOR, not an equality — adding cases is ordinary work and +// must not red. A battery BELOW its floor means cases stopped running; the +// remedy is to find what stopped registering. +const SELF_TEST_BATTERIES = Object.freeze({ + // ⛔ NOT today's count. This battery runs one case per exemption row (2 today) + // plus the one structural case, and that list is meant to LOSE rows — an + // exemption that no longer exempts anything is dead weight, as the comment + // over its declaration says. A floor at 3 would redden a legitimate deletion + // and train the next author to edit the floor, which is the one habit these + // floors exist to prevent. Pinned instead is the part that does not move with + // the list: the structural case ran AND at least one row was actually audited. + 'Every exemption carries its reason.': 2, + 'POSITIVE CONTROL, on the REAL repo.': 5, + 'The two ALLOW steps are separable, which is what makes them assertable.': 1, + 'The criterion is query-shaped, not mention-shaped (#6286).': 4, + 'The read-call spellings the criterion must recognise (POSITIVE polarity).': 23, + 'Reverse proof for the positive control (#6286), made permanent.': 7, + 'Reverse proof for the dead-root hard error (#4930), made permanent.': 6, + 'Reverse proof for the empty-scan hard error (#5916), same discipline.': 11, +}); + +// DELETING an entry silences that battery's floor exactly as effectively as +// zeroing it, so the roster's own size is pinned too. +const SELF_TEST_BATTERY_FLOOR = 8; + +// The key an assertion is filed under when no battery is open. It is not a +// declared battery, so it reds by the same set difference rather than silently +// inflating whichever battery happened to run last. +const UNATTRIBUTED_BATTERY = '(no battery open)'; + function selfTest() { + // The battery ledger this self-test's floor is evaluated against (#13489). + // `battery()` opens a battery; every assertion below is attributed to the one + // most recently opened, so a section that stops running stops registering and + // names ITSELF at the floor rather than going quiet. + const seen = new Map(); + let openBattery = null; + const battery = (name) => { + openBattery = name; + }; + const registerCase = () => { + const b = openBattery ?? UNATTRIBUTED_BATTERY; + seen.set(b, (seen.get(b) ?? 0) + 1); + }; const failures = []; const expect = (label, got, want) => { + registerCase(); if (got !== want) failures.push(` ✗ ${label}: expected ${JSON.stringify(want)}, got ${JSON.stringify(got)}`); }; @@ -555,6 +612,7 @@ function selfTest() { // The old list rotted because nothing required an entry to justify itself; two paths // sat there exempting callers from a predicate that could not fire. A path with no // reason is indistinguishable from a path someone added to make a red go away. + battery('Every exemption carries its reason.'); for (const [path, reason] of ALLOW) { expect(`ALLOW entry ${path} carries a reason`, typeof reason === 'string' && reason.length > 20, true); } @@ -565,6 +623,7 @@ function selfTest() { // silent: everything else in this self-test runs on fixtures the predicate cannot // disagree with. This one runs against the actual canonical resolver in the checkout. // Positive polarity — it goes RED when GRANT_TABLES drifts off the real tables. + battery('POSITIVE CONTROL, on the REAL repo.'); let realCanonicalSrc = null; try { realCanonicalSrc = readFileSync(join(ROOT, CANONICAL), 'utf8'); @@ -606,6 +665,7 @@ function selfTest() { // if the criterion stops recognising resolver shape) and MUST NOT be reported // (ALLOW's job). Asserting only the second would pass just as well if the heuristic // matched nothing at all — exactly the failure being fixed. + battery('The two ALLOW steps are separable, which is what makes them assertable.'); expect('the allow-listed explain mirror IS matched by the heuristic', queriesAllGrantTables(readFileSync(join(dir, 'packages/plugins/plugin-security/src/explain-engine.ts'), 'utf8')), true); @@ -614,6 +674,7 @@ function selfTest() { // because a predicate that matches nothing also matches no mention-only file. It is // here to pin the 18-of-20 noise reduction, and it is load-bearing ONLY next to the // positive assertions above and below, which do go red. + battery('The criterion is query-shaped, not mention-shaped (#6286).'); expect('a mention-only file is NOT a duplicate resolver (unquoted keys, prose, name lists)', queriesAllGrantTables(mentionOnlyFixtureBody()), false); expect('...and it is not reported even though it is NOT allow-listed', @@ -629,6 +690,7 @@ function selfTest() { // recall side of the criterion, which no fixture above can reach because // `resolverFixtureBody` only ever emits `ql.find`. The canonical resolver uses the // `tryFind` HELPER spelling, so losing it would break the positive control itself. + battery('The read-call spellings the criterion must recognise (POSITIVE polarity).'); const t0 = GRANT_TABLES[0]; for (const [label, src] of [ ['member call', `ql.find('${t0}', { where: {} })`], @@ -719,6 +781,7 @@ function selfTest() { // // This is the assertion whose ABSENCE was the bug: without it the rename produced a // gate that could not fail, and every other assertion in this file stayed green. + battery('Reverse proof for the positive control (#6286), made permanent.'); write(CANONICAL, resolverFixtureBody(GRANT_TABLES.map((t) => `${t}_v2`))); let lostErr = null; try { audit(dir); } catch (err) { lostErr = err; } @@ -756,6 +819,7 @@ function selfTest() { // the root the way a rename breaks it in the real repo, require red, require // the red to name the root, then restore it and require green again. // Red-then-green, in the same run, every run. + battery('Reverse proof for the dead-root hard error (#4930), made permanent.'); const renamed = join(dir, 'packages-renamed-by-self-test'); renameSync(join(dir, 'packages'), renamed); let deadErr = null; @@ -795,6 +859,7 @@ function selfTest() { // nothing must be RED, and the red must name that root only. This is the case // #4930's assertion cannot reach — nothing is renamed, nothing is unreadable, // the directory is right there; the corpus is simply not in it any more. + battery('Reverse proof for the empty-scan hard error (#5916), same discipline.'); const baseline = collectScanFiles(dir).length; expect('the compliant tree yields a corpus to reason over', baseline > 0, true); @@ -846,6 +911,51 @@ function selfTest() { rmSync(dir, { recursive: true, force: true }); } + // ── The floor: every declared battery RAN, and ran its cases (#13489) ─── + // + // Evaluated after every battery has had its chance and BEFORE the verdict, so + // the success line below can only be printed by a run in which the set of + // batteries that registered assertions EQUALS the set declared. A set + // difference names WHICH battery stopped; a count says only that something did. + const floorFailure = (message) => { + failures.push(message); + }; + const declaredBatteries = Object.keys(SELF_TEST_BATTERIES); + let floorBreached = false; + if (declaredBatteries.length < SELF_TEST_BATTERY_FLOOR) { + floorBreached = true; + floorFailure( + `SELF_TEST_BATTERIES declares ${declaredBatteries.length} batteries, below the pinned ` + + `${SELF_TEST_BATTERY_FLOOR} — a battery deleted from the roster takes its own floor with it.`, + ); + } + for (const [name, count] of seen) { + if (declaredBatteries.includes(name)) continue; + floorBreached = true; + floorFailure( + `self-test battery "${name}" registered ${count} case(s) but is not declared in ` + + 'SELF_TEST_BATTERIES — an assertion attributed to no declared battery is one nothing floors.', + ); + } + for (const name of declaredBatteries) { + const count = seen.get(name) ?? 0; + if (count >= SELF_TEST_BATTERIES[name]) continue; + floorBreached = true; + floorFailure( + count === 0 + ? `self-test battery "${name}" DID NOT RUN — 0 cases registered, ${SELF_TEST_BATTERIES[name]} pinned. ` + + 'The verdict below would have claimed those cases hold.' + : `self-test battery "${name}" registered ${count} case(s), below its pinned floor of ` + + `${SELF_TEST_BATTERIES[name]} — cases that used to run no longer do.`, + ); + } + if (floorBreached) { + floorFailure( + 'A battery at or below its floor means cases STOPPED RUNNING — the battery is the bug, not the ' + + 'number. Find what stopped registering (an early return, a deleted block, a guard that now ' + + 'skips) and restore it.', + ); + } if (failures.length) { console.error(`\n✗ check-single-authz-resolver self-test failed:\n${failures.join('\n')}\n`); process.exit(1); diff --git a/scripts/check-slot-lookup-ratchet.mjs b/scripts/check-slot-lookup-ratchet.mjs index 338bdabe26..ce4ff50beb 100644 --- a/scripts/check-slot-lookup-ratchet.mjs +++ b/scripts/check-slot-lookup-ratchet.mjs @@ -422,12 +422,59 @@ const DIFF_CASES = (() => { // as one that passed (#13798). const SELF_TEST_VERDICT = 'check-slot-lookup-ratchet self-test reached its verdict'; +// ── The self-test's own battery roster and floor (#13489) ────────────────── +// +// `failures.length === 0` used to be this self-test's ONLY success condition, so +// "every case held" and "the cases never ran" printed the same line. Closed the +// way PR #13487 validated on check-doc-authoring: what is pinned is the +// registered NAMES, not a number. Every section opens with `battery('')`, +// every assertion is attributed to the battery most recently opened, and the +// floor requires the OPENED set to equal the DECLARED set with each battery at +// or above its own count. +// +// ⛔ A pinned TOTAL is not the repair: a battery dropping from 9 cases to 3 +// keeps a total "right" the moment a sibling grows. +// +// The counts are a FLOOR, not an equality — adding cases is ordinary work and +// must not red. A battery BELOW its floor means cases stopped running; the +// remedy is to find what stopped registering. +const SELF_TEST_BATTERIES = Object.freeze({ + '1. The rule, in both directions, over synthetic sources.': 26, + '2. The grandfathering channel, as a synthetic witness pair.': 10, + '3. The ratchet comparison, in both directions.': 6, + '4. Both refusals, in both directions.': 5, + '5. The counter and the rule cannot disagree.': 2, +}); + +// DELETING an entry silences that battery's floor exactly as effectively as +// zeroing it, so the roster's own size is pinned too. +const SELF_TEST_BATTERY_FLOOR = 5; + +// The key an assertion is filed under when no battery is open. It is not a +// declared battery, so it reds by the same set difference rather than silently +// inflating whichever battery happened to run last. +const UNATTRIBUTED_BATTERY = '(no battery open)'; + async function selfTest() { + // The battery ledger this self-test's floor is evaluated against (#13489). + // `battery()` opens a battery; every assertion below is attributed to the one + // most recently opened, so a section that stops running stops registering and + // names ITSELF at the floor rather than going quiet. + const seen = new Map(); + let openBattery = null; + const battery = (name) => { + openBattery = name; + }; + const registerCase = () => { + const b = openBattery ?? UNATTRIBUTED_BATTERY; + seen.set(b, (seen.get(b) ?? 0) + 1); + }; const failures = []; - const assert = (cond, msg) => { if (!cond) failures.push(msg); }; + const assert = (cond, msg) => { registerCase(); if (!cond) failures.push(msg); }; /** Lint one fixture through a chosen config and COUNT it the way the gate does. */ const hitsUnder = async (config, code, filePath = FIXTURE_FILE) => { + registerCase(); const eslint = new ESLint({ cwd: repoRoot, overrideConfigFile: true, @@ -453,6 +500,7 @@ async function selfTest() { // The config is the REAL one from eslint.config.mjs with the baseline's // grandfathering lifted — the same construction `measure()` runs — so these // cases move with the rule and cannot be satisfied by a re-implementation. + battery('1. The rule, in both directions, over synthetic sources.'); const baseline = JSON.parse(readFileSync(resolve(repoRoot, BASELINE_PATH), 'utf8')); const measuring = measuringConfig(new Set(Object.keys(baseline))); @@ -470,6 +518,7 @@ async function selfTest() { // whole gate is built around — an ignored file is ignored COMPLETELY — so it // is proved rather than assumed, and proved in a way that survives the // baseline shrinking to zero entries. + battery('2. The grandfathering channel, as a synthetic witness pair.'); { const code = "const s = ctx.getService('data');"; const withEntry = eslintConfig.map((entry) => @@ -509,6 +558,7 @@ async function selfTest() { } // ── 3. The ratchet comparison, in both directions. ──────────────────────── + battery('3. The ratchet comparison, in both directions.'); for (const [name, input, expected] of DIFF_CASES) { const got = diffRatchet(input).length; assert(got === expected, `diffRatchet: ${name} — expected ${expected} error(s), got ${got}`); @@ -519,6 +569,7 @@ async function selfTest() { // Neither is reachable from a tree where the rule is healthy, so a green // production run says nothing about either — they are the strictest case of // the argument in this file's header. + battery('4. Both refusals, in both directions.'); { assert(ruleBlockProblem(eslintConfig) === null, 'the live config must carry the slot-lookup rule'); const renamed = eslintConfig.map((entry) => @@ -554,6 +605,7 @@ async function selfTest() { // // `countRuleHits` matches on the exact message, which is what lets four // report shapes be counted by a gate that knows of none of them. + battery('5. The counter and the rule cannot disagree.'); assert( countRuleHits([{ message: SLOT_LOOKUP_ANY_MESSAGE }, { message: 'something else' }]) === 1, 'the counter must match the rule message exactly', @@ -565,6 +617,51 @@ async function selfTest() { 'message must still be told where a legitimate `any` is declared', ); + // ── The floor: every declared battery RAN, and ran its cases (#13489) ─── + // + // Evaluated after every battery has had its chance and BEFORE the verdict, so + // the success line below can only be printed by a run in which the set of + // batteries that registered assertions EQUALS the set declared. A set + // difference names WHICH battery stopped; a count says only that something did. + const floorFailure = (message) => { + failures.push(message); + }; + const declaredBatteries = Object.keys(SELF_TEST_BATTERIES); + let floorBreached = false; + if (declaredBatteries.length < SELF_TEST_BATTERY_FLOOR) { + floorBreached = true; + floorFailure( + `SELF_TEST_BATTERIES declares ${declaredBatteries.length} batteries, below the pinned ` + + `${SELF_TEST_BATTERY_FLOOR} — a battery deleted from the roster takes its own floor with it.`, + ); + } + for (const [name, count] of seen) { + if (declaredBatteries.includes(name)) continue; + floorBreached = true; + floorFailure( + `self-test battery "${name}" registered ${count} case(s) but is not declared in ` + + 'SELF_TEST_BATTERIES — an assertion attributed to no declared battery is one nothing floors.', + ); + } + for (const name of declaredBatteries) { + const count = seen.get(name) ?? 0; + if (count >= SELF_TEST_BATTERIES[name]) continue; + floorBreached = true; + floorFailure( + count === 0 + ? `self-test battery "${name}" DID NOT RUN — 0 cases registered, ${SELF_TEST_BATTERIES[name]} pinned. ` + + 'The verdict below would have claimed those cases hold.' + : `self-test battery "${name}" registered ${count} case(s), below its pinned floor of ` + + `${SELF_TEST_BATTERIES[name]} — cases that used to run no longer do.`, + ); + } + if (floorBreached) { + floorFailure( + 'A battery at or below its floor means cases STOPPED RUNNING — the battery is the bug, not the ' + + 'number. Find what stopped registering (an early return, a deleted block, a guard that now ' + + 'skips) and restore it.', + ); + } if (failures.length > 0) { console.error(`✗ check-slot-lookup-ratchet --self-test: ${failures.length} failure(s).\n`); for (const f of failures) console.error(` • ${f}`); diff --git a/scripts/check-test-source-alias.mjs b/scripts/check-test-source-alias.mjs index 6d7e480931..2bc52d27d7 100644 --- a/scripts/check-test-source-alias.mjs +++ b/scripts/check-test-source-alias.mjs @@ -2419,12 +2419,64 @@ function buildFixtureTree() { // as one that passed (#13798). const SELF_TEST_VERDICT = 'check-test-source-alias self-test reached its verdict'; +// ── The self-test's own battery roster and floor (#13489) ────────────────── +// +// `failures.length === 0` used to be this self-test's ONLY success condition, so +// "every case held" and "the cases never ran" printed the same line. Closed the +// way PR #13487 validated on check-doc-authoring: what is pinned is the +// registered NAMES, not a number. Every section opens with `battery('')`, +// every assertion is attributed to the battery most recently opened, and the +// floor requires the OPENED set to equal the DECLARED set with each battery at +// or above its own count. +// +// ⛔ A pinned TOTAL is not the repair: a battery dropping from 9 cases to 3 +// keeps a total "right" the moment a sibling grows. +// +// The counts are a FLOOR, not an equality — adding cases is ordinary work and +// must not red. A battery BELOW its floor means cases stopped running; the +// remedy is to find what stopped registering. +const SELF_TEST_BATTERIES = Object.freeze({ + 'baseline — every registry-uncovered fixture is reported': 6, + 'the remediation hint is MEASURED, not a template (#8256)': 9, + 'the canary (#8020)': 14, + 'the cross-boundary walk (#8351)': 7, + 'the latent half (#9674)': 8, + 'the clocked-window rule (#10126)': 10, + 'the import clause is bounded to ONE statement (#12555)': 11, + 'the declared population must stay READABLE by the dispatch deriver': 11, + 'the declaration must still BE the workspace (#11510)': 22, +}); + +// DELETING an entry silences that battery's floor exactly as effectively as +// zeroing it, so the roster's own size is pinned too. +const SELF_TEST_BATTERY_FLOOR = 9; + +// The key an assertion is filed under when no battery is open. It is not a +// declared battery, so it reds by the same set difference rather than silently +// inflating whichever battery happened to run last. +const UNATTRIBUTED_BATTERY = '(no battery open)'; + function selfTest() { + // The battery ledger this self-test's floor is evaluated against (#13489). + // `battery()` opens a battery; every assertion below is attributed to the one + // most recently opened, so a section that stops running stops registering and + // names ITSELF at the floor rather than going quiet. + const seen = new Map(); + let openBattery = null; + const battery = (name) => { + openBattery = name; + }; + const registerCase = () => { + const b = openBattery ?? UNATTRIBUTED_BATTERY; + seen.set(b, (seen.get(b) ?? 0) + 1); + }; const root = buildFixtureTree(); const problems = []; const expect = (condition, message) => { + registerCase(); if (!condition) problems.push(message); }; + battery('baseline — every registry-uncovered fixture is reported'); const has = (failures, needle) => failures.some((f) => f.includes(needle)); try { @@ -2444,6 +2496,7 @@ function selfTest() { // only ever writes subpaths, and the message then repeats unchanged. Each // assertion below pins one fact the hint must carry from the measurement // rather than from a shape guess. + battery('the remediation hint is MEASURED, not a template (#8256)'); const subpathOnly = bare.failures.find((f) => f.startsWith('packages/subpath-only')) ?? ''; expect( subpathOnly.includes('@fx/core/logger') && @@ -2500,6 +2553,7 @@ function selfTest() { // Escaped-slash regex `find` AND template-literal `replacement`, together, // exactly as the two real configs write them. Both spellings have already // broken a reader of these configs; neither may break this one again. + battery('the canary (#8020)'); expect( !has(bare.failures, 'packages/canary'), 'the canary config (escaped-slash regex find + template-literal replacement) was reported as unaliased', @@ -2589,6 +2643,7 @@ function selfTest() { // resolution domain. Each of these pins one half of that; the registry // fixture keeps the ledger-only entries quiet so the rule-5 half is read // against a clean list. + battery('the cross-boundary walk (#8351)'); const cross = check(root, { '@fx/violator': ['@fx/core', '@fx/other'], '@fx/no-cross-unaliased': ['@fx/relay'], @@ -2630,6 +2685,7 @@ function selfTest() { // ── the latent half (#9674) ─────────────────────────────────────────── // Judged without a reader: the population is the dependency's export map. + battery('the latent half (#9674)'); expect( cross.failures.some((f) => f.includes('packages/latent-prefix-trap') && f.includes('@fx/publisher/leaf')), 'a published subpath the bare prefix entry would mangle went unreported because no test reaches it', @@ -2679,6 +2735,7 @@ function selfTest() { // one of these fixtures ALSO carries an unregistered ledger entry, so a bare // package-name search matches for a reason that has nothing to do with this // rule. Each silent assertion is therefore scoped to the rule's own sentence. + battery('the clocked-window rule (#10126)'); const clockedIn = (needle) => cross.failures.filter((f) => f.includes(needle) && f.includes('a CLOCKED window')); @@ -2766,6 +2823,7 @@ function selfTest() { // the whole defect was that the verdict depended on import ORDER rather than // on what the file loads, and a detector that silently stops matching // reports a spotless repo. + battery('the import clause is bounded to ONE statement (#12555)'); const specsOf = (code) => [...new Set(extractRuntimeImports(code))].sort(); const rowA = specsOf("import 'pkg/kernel';\nconst x = 1;\n"); const rowB = specsOf("import 'pkg/kernel';\nimport { X } from 'other';\n"); @@ -2855,6 +2913,7 @@ function selfTest() { // here. That is what the bare spelling cost, measured in that constant's // docblock (#9955). Asserted here rather than left to review because the // regression is a tidy-up nobody would flag. + battery('the declared population must stay READABLE by the dispatch deriver'); for (const glob of WORKSPACE_PARENT_GLOBS) { expect( glob.replace(/^(?:\.\.?(?:\/|$))+/, '').includes('/'), @@ -2878,6 +2937,7 @@ function selfTest() { // #11190 measurement that made consolidation safe in the first place. So // the declaration stays and the live parse becomes its CHECK, in both // directions, the shape check-published-files.mjs already uses. + battery('the declaration must still BE the workspace (#11510)'); const declaredParents = WORKSPACE_PARENT_GLOBS.map((g) => g.replace(/\/\*+$/, '')); const liveParents = readWorkspaceGlobs(REPO_ROOT) .filter((g) => !isExclusionGlob(g)) @@ -2903,6 +2963,51 @@ function selfTest() { rmSync(root, { recursive: true, force: true }); } + // ── The floor: every declared battery RAN, and ran its cases (#13489) ─── + // + // Evaluated after every battery has had its chance and BEFORE the verdict, so + // the success line below can only be printed by a run in which the set of + // batteries that registered assertions EQUALS the set declared. A set + // difference names WHICH battery stopped; a count says only that something did. + const floorFailure = (message) => { + problems.push(message); + }; + const declaredBatteries = Object.keys(SELF_TEST_BATTERIES); + let floorBreached = false; + if (declaredBatteries.length < SELF_TEST_BATTERY_FLOOR) { + floorBreached = true; + floorFailure( + `SELF_TEST_BATTERIES declares ${declaredBatteries.length} batteries, below the pinned ` + + `${SELF_TEST_BATTERY_FLOOR} — a battery deleted from the roster takes its own floor with it.`, + ); + } + for (const [name, count] of seen) { + if (declaredBatteries.includes(name)) continue; + floorBreached = true; + floorFailure( + `self-test battery "${name}" registered ${count} case(s) but is not declared in ` + + 'SELF_TEST_BATTERIES — an assertion attributed to no declared battery is one nothing floors.', + ); + } + for (const name of declaredBatteries) { + const count = seen.get(name) ?? 0; + if (count >= SELF_TEST_BATTERIES[name]) continue; + floorBreached = true; + floorFailure( + count === 0 + ? `self-test battery "${name}" DID NOT RUN — 0 cases registered, ${SELF_TEST_BATTERIES[name]} pinned. ` + + 'The verdict below would have claimed those cases hold.' + : `self-test battery "${name}" registered ${count} case(s), below its pinned floor of ` + + `${SELF_TEST_BATTERIES[name]} — cases that used to run no longer do.`, + ); + } + if (floorBreached) { + floorFailure( + 'A battery at or below its floor means cases STOPPED RUNNING — the battery is the bug, not the ' + + 'number. Find what stopped registering (an early return, a deleted block, a guard that now ' + + 'skips) and restore it.', + ); + } if (problems.length > 0) { console.error('check-test-source-alias --self-test FAILED:'); for (const problem of problems) console.error(` - ${problem}`); diff --git a/scripts/check-test-typecheck.mts b/scripts/check-test-typecheck.mts index 6f380753a5..aed1886aa3 100644 --- a/scripts/check-test-typecheck.mts +++ b/scripts/check-test-typecheck.mts @@ -516,7 +516,53 @@ function runTsc(): string { // as one that passed (#13798). const SELF_TEST_VERDICT = 'check-test-typecheck self-test reached its verdict'; +// ── The self-test's own battery roster and floor (#13489) ────────────────── +// +// `failures.length === 0` used to be this self-test's ONLY success condition, so +// "every case held" and "the cases never ran" printed the same line. Closed the +// way PR #13487 validated on check-doc-authoring: what is pinned is the +// registered NAMES, not a number. Every section opens with `battery('')`, +// every assertion is attributed to the battery most recently opened, and the +// floor requires the OPENED set to equal the DECLARED set with each battery at +// or above its own count. +// +// ⛔ A pinned TOTAL is not the repair: a battery dropping from 9 cases to 3 +// keeps a total "right" the moment a sibling grows. +// +// The counts are a FLOOR, not an equality — adding cases is ordinary work and +// must not red. A battery BELOW its floor means cases stopped running; the +// remedy is to find what stopped registering. +const SELF_TEST_BATTERIES: Readonly> = Object.freeze({ + 'the cardinality-preserving control (#13470)': 1, + 'What a signature must and must not notice (#13470)': 4, + 'The ratchet-remedy authority convention (#8435)': 7, + 'The same authority rule, applied to the per-SIGNATURE offer (#13470)': 3, + 'The ledger\'s own prose (#12624)': 11, +}); + +// DELETING an entry silences that battery's floor exactly as effectively as +// zeroing it, so the roster's own size is pinned too. +const SELF_TEST_BATTERY_FLOOR = 5; + +// The key an assertion is filed under when no battery is open. It is not a +// declared battery, so it reds by the same set difference rather than silently +// inflating whichever battery happened to run last. +const UNATTRIBUTED_BATTERY = '(no battery open)'; + function selfTest(): string { + // The battery ledger this self-test's floor is evaluated against (#13489). + // `battery()` opens a battery; every assertion below is attributed to the one + // most recently opened, so a section that stops running stops registering and + // names ITSELF at the floor rather than going quiet. + const seen = new Map(); + let openBattery: string | null = null; + const battery = (name: string): void => { + openBattery = name; + }; + const registerCase = (): void => { + const b = openBattery ?? UNATTRIBUTED_BATTERY; + seen.set(b, (seen.get(b) ?? 0) + 1); + }; // The two REAL signatures from the ablation that produced #13470: `packages/ // rest`'s two call sites passed a bad request AND a bad response, tsc showed // only the request error, and PR #13466's repair uncovered the response one. @@ -632,6 +678,7 @@ function selfTest(): string { } const expect = (label: string, cond: boolean): void => { + registerCase(); if (!cond) failures.push(label); }; @@ -639,6 +686,7 @@ function selfTest(): string { // populations really are the same size, so the number the ledger used to hold // is UNCHANGED across the swap. Without this the case would only be proving // "a changed count is red", which the per-file count already did. + battery('the cardinality-preserving control (#13470)'); expect( '⭐ #13470 — the substitution fixture is cardinality-preserving (else the case above proves nothing ' + 'the old per-file count could not already see)', @@ -674,6 +722,7 @@ function selfTest(): string { // them must produce an IDENTICAL ledger, or every commit churns a generated // file and the gate gets weakened. (ii) IDENTITY-SHARP: a different named // type must produce a different key, or the pin is decoration. + battery('What a signature must and must not notice (#13470)'); { const atLine = (line: number, type: string): string => `src/rest.test.ts(${line},7): error TS2345: Argument of type '{ json: Mock; ` @@ -735,6 +784,7 @@ function selfTest(): string { // // (3) is what makes (2) worth having: without it, a predicate that approved // everything would keep this block green while the convention is gone. + battery('The ratchet-remedy authority convention (#8435)'); const unledgered = evaluate(observed([['pin.test.ts', [['TS2578: Unused directive.', 1]]]]), {})[0] ?? ''; expect( '#8435 — the ratchet-offer DETECTOR still matches the unledgered-file verdict (else every ' @@ -837,6 +887,7 @@ function selfTest(): string { // shrink-only ratchet exactly as adding a whole file does, and it is the more // tempting of the two — the file total need not have moved, so it reads like // bookkeeping. VANISHED is its opposite and must stay unmarked. + battery('The same authority rule, applied to the per-SIGNATURE offer (#13470)'); const arrived = evaluate(observed([['a.test.ts', [[RES, 2]]]]), { 'a.test.ts': { [REQ]: 2 } }); const arrivedMsg = arrived.find((m) => m.includes('ARRIVED')) ?? ''; const vanishedMsg = arrived.find((m) => m.includes('VANISHED')) ?? ''; @@ -868,6 +919,7 @@ function selfTest(): string { // that clause. A departure pin alone is decoration: it passes against an // empty `_comment`, which is exactly the regression it would be there to // catch. + battery('The ledger\'s own prose (#12624)'); const written = buildLedger(observed([['a.test.ts', [[REQ, 3]]]]))._comment; const omissions = ledgerCommentOmissions(written); @@ -960,6 +1012,51 @@ function selfTest(): string { ); } + // ── The floor: every declared battery RAN, and ran its cases (#13489) ─── + // + // Evaluated after every battery has had its chance and BEFORE the verdict, so + // the success line below can only be printed by a run in which the set of + // batteries that registered assertions EQUALS the set declared. A set + // difference names WHICH battery stopped; a count says only that something did. + const floorFailure = (message: string): void => { + failures.push(message); + }; + const declaredBatteries = Object.keys(SELF_TEST_BATTERIES); + let floorBreached = false; + if (declaredBatteries.length < SELF_TEST_BATTERY_FLOOR) { + floorBreached = true; + floorFailure( + `SELF_TEST_BATTERIES declares ${declaredBatteries.length} batteries, below the pinned ` + + `${SELF_TEST_BATTERY_FLOOR} — a battery deleted from the roster takes its own floor with it.`, + ); + } + for (const [name, count] of seen) { + if (declaredBatteries.includes(name)) continue; + floorBreached = true; + floorFailure( + `self-test battery "${name}" registered ${count} case(s) but is not declared in ` + + 'SELF_TEST_BATTERIES — an assertion attributed to no declared battery is one nothing floors.', + ); + } + for (const name of declaredBatteries) { + const count = seen.get(name) ?? 0; + if (count >= SELF_TEST_BATTERIES[name]) continue; + floorBreached = true; + floorFailure( + count === 0 + ? `self-test battery "${name}" DID NOT RUN — 0 cases registered, ${SELF_TEST_BATTERIES[name]} pinned. ` + + 'The verdict below would have claimed those cases hold.' + : `self-test battery "${name}" registered ${count} case(s), below its pinned floor of ` + + `${SELF_TEST_BATTERIES[name]} — cases that used to run no longer do.`, + ); + } + if (floorBreached) { + floorFailure( + 'A battery at or below its floor means cases STOPPED RUNNING — the battery is the bug, not the ' + + 'number. Find what stopped registering (an early return, a deleted block, a guard that now ' + + 'skips) and restore it.', + ); + } if (failures.length) { console.error(`✗ check:test-typecheck --self-test — ${failures.length} failure(s)\n`); for (const f of failures) console.error(' • ' + f); diff --git a/scripts/check-verify-stand-in-erasure.mjs b/scripts/check-verify-stand-in-erasure.mjs index 96ca534792..116b814354 100644 --- a/scripts/check-verify-stand-in-erasure.mjs +++ b/scripts/check-verify-stand-in-erasure.mjs @@ -394,13 +394,59 @@ function report() { // as one that passed (#13798). const SELF_TEST_VERDICT = 'check-verify-stand-in-erasure self-test reached its verdict'; +// ── The self-test's own battery roster and floor (#13489) ────────────────── +// +// `failures.length === 0` used to be this self-test's ONLY success condition, so +// "every case held" and "the cases never ran" printed the same line. Closed the +// way PR #13487 validated on check-doc-authoring: what is pinned is the +// registered NAMES, not a number. Every section opens with `battery('')`, +// every assertion is attributed to the battery most recently opened, and the +// floor requires the OPENED set to equal the DECLARED set with each battery at +// or above its own count. +// +// ⛔ A pinned TOTAL is not the repair: a battery dropping from 9 cases to 3 +// keeps a total "right" the moment a sibling grows. +// +// The counts are a FLOOR, not an equality — adding cases is ordinary work and +// must not red. A battery BELOW its floor means cases stopped running; the +// remedy is to find what stopped registering. +const SELF_TEST_BATTERIES = Object.freeze({ + 'CLEAN: the scan separates an asserted driver argument from a clean one.': 9, + 'DISCOVERED: the shape test, on synthetic sources.': 2, + 'Wiring: discovery must reach the REAL tree, and reach the two helpers': 13, +}); + +// DELETING an entry silences that battery's floor exactly as effectively as +// zeroing it, so the roster's own size is pinned too. +const SELF_TEST_BATTERY_FLOOR = 3; + +// The key an assertion is filed under when no battery is open. It is not a +// declared battery, so it reds by the same set difference rather than silently +// inflating whichever battery happened to run last. +const UNATTRIBUTED_BATTERY = '(no battery open)'; + function selfTest() { + // The battery ledger this self-test's floor is evaluated against (#13489). + // `battery()` opens a battery; every assertion below is attributed to the one + // most recently opened, so a section that stops running stops registering and + // names ITSELF at the floor rather than going quiet. + const seen = new Map(); + let openBattery = null; + const battery = (name) => { + openBattery = name; + }; + const registerCase = () => { + const b = openBattery ?? UNATTRIBUTED_BATTERY; + seen.set(b, (seen.get(b) ?? 0) + 1); + }; const failures = []; const expect = (label, ok) => { + registerCase(); if (!ok) failures.push(label); }; // --- CLEAN: the scan separates an asserted driver argument from a clean one. + battery('CLEAN: the scan separates an asserted driver argument from a clean one.'); const guarded = new Set(['checkDateBucketParity', 'checkReadCoercion']); const clean = ` @@ -458,6 +504,7 @@ function selfTest() { ); // --- DISCOVERED: the shape test, on synthetic sources. + battery('DISCOVERED: the shape test, on synthetic sources.'); const synthetic = [ { file: 'index.ts', @@ -517,6 +564,7 @@ function selfTest() { // classified set matches. Those are the job of the run this self-test gates — // duplicating them would surface a genuine violation as a self-test failure, // the least legible message available. + battery('Wiring: discovery must reach the REAL tree, and reach the two helpers'); const real = discoverStandInChecks(); expect('discovery reaches the real packages/verify tree', real.size > 0); for (const name of ['checkReadCoercion', 'checkDateBucketParity']) { @@ -547,6 +595,51 @@ function selfTest() { realSites.length >= 10, ); + // ── The floor: every declared battery RAN, and ran its cases (#13489) ─── + // + // Evaluated after every battery has had its chance and BEFORE the verdict, so + // the success line below can only be printed by a run in which the set of + // batteries that registered assertions EQUALS the set declared. A set + // difference names WHICH battery stopped; a count says only that something did. + const floorFailure = (message) => { + failures.push(message); + }; + const declaredBatteries = Object.keys(SELF_TEST_BATTERIES); + let floorBreached = false; + if (declaredBatteries.length < SELF_TEST_BATTERY_FLOOR) { + floorBreached = true; + floorFailure( + `SELF_TEST_BATTERIES declares ${declaredBatteries.length} batteries, below the pinned ` + + `${SELF_TEST_BATTERY_FLOOR} — a battery deleted from the roster takes its own floor with it.`, + ); + } + for (const [name, count] of seen) { + if (declaredBatteries.includes(name)) continue; + floorBreached = true; + floorFailure( + `self-test battery "${name}" registered ${count} case(s) but is not declared in ` + + 'SELF_TEST_BATTERIES — an assertion attributed to no declared battery is one nothing floors.', + ); + } + for (const name of declaredBatteries) { + const count = seen.get(name) ?? 0; + if (count >= SELF_TEST_BATTERIES[name]) continue; + floorBreached = true; + floorFailure( + count === 0 + ? `self-test battery "${name}" DID NOT RUN — 0 cases registered, ${SELF_TEST_BATTERIES[name]} pinned. ` + + 'The verdict below would have claimed those cases hold.' + : `self-test battery "${name}" registered ${count} case(s), below its pinned floor of ` + + `${SELF_TEST_BATTERIES[name]} — cases that used to run no longer do.`, + ); + } + if (floorBreached) { + floorFailure( + 'A battery at or below its floor means cases STOPPED RUNNING — the battery is the bug, not the ' + + 'number. Find what stopped registering (an early return, a deleted block, a guard that now ' + + 'skips) and restore it.', + ); + } if (failures.length) { for (const f of failures) console.error(` x self-test: ${f}`); console.error(`\ncheck-verify-stand-in-erasure --self-test: ${failures.length} failure(s).\n`); diff --git a/scripts/check-where-matcher-conformance.mjs b/scripts/check-where-matcher-conformance.mjs index cb87e3a5fb..5ab21b59e1 100644 --- a/scripts/check-where-matcher-conformance.mjs +++ b/scripts/check-where-matcher-conformance.mjs @@ -838,10 +838,55 @@ function judgeFixture(src) { // as one that passed (#13798). const SELF_TEST_VERDICT = 'check-where-matcher-conformance self-test reached its verdict'; +// ── The self-test's own battery roster and floor (#13489) ────────────────── +// +// `failures.length === 0` used to be this self-test's ONLY success condition, so +// "every case held" and "the cases never ran" printed the same line. Closed the +// way PR #13487 validated on check-doc-authoring: what is pinned is the +// registered NAMES, not a number. Every section opens with `battery('')`, +// every assertion is attributed to the battery most recently opened, and the +// floor requires the OPENED set to equal the DECLARED set with each battery at +// or above its own count. +// +// ⛔ A pinned TOTAL is not the repair: a battery dropping from 9 cases to 3 +// keeps a total "right" the moment a sibling grows. +// +// The counts are a FLOOR, not an equality — adding cases is ordinary work and +// must not red. A battery BELOW its floor means cases stopped running; the +// remedy is to find what stopped registering. +const SELF_TEST_BATTERIES = Object.freeze({ + 'the conjoining fixture — discovery and verdict': 16, + '#8615: the captured-filter arm': 17, + 'the dispatch-gates declaration (#13163\'s landing obligation)': 6, +}); + +// DELETING an entry silences that battery's floor exactly as effectively as +// zeroing it, so the roster's own size is pinned too. +const SELF_TEST_BATTERY_FLOOR = 3; + +// The key an assertion is filed under when no battery is open. It is not a +// declared battery, so it reds by the same set difference rather than silently +// inflating whichever battery happened to run last. +const UNATTRIBUTED_BATTERY = '(no battery open)'; + function selfTest() { + // The battery ledger this self-test's floor is evaluated against (#13489). + // `battery()` opens a battery; every assertion below is attributed to the one + // most recently opened, so a section that stops running stops registering and + // names ITSELF at the floor rather than going quiet. + const seen = new Map(); + let openBattery = null; + const battery = (name) => { + openBattery = name; + }; + const registerCase = () => { + const b = openBattery ?? UNATTRIBUTED_BATTERY; + seen.set(b, (seen.get(b) ?? 0) + 1); + }; const failures = []; - const expect = (label, cond) => { if (!cond) failures.push(label); }; + const expect = (label, cond) => { registerCase(); if (!cond) failures.push(label); }; + battery('the conjoining fixture — discovery and verdict'); const correct = judgeFixture(FIXTURE_CORRECT); expect('the conjoining fixture is discovered', correct.found.length === 1); expect('the conjoining fixture is CONFORMING', correct.results[0]?.verdict === 'CONFORMING'); @@ -886,6 +931,7 @@ function selfTest() { expect('a matcher using a same-file helper is lifted', closure.results[0]?.verdict === 'CONFORMING'); // --- #8615: the captured-filter arm ------------------------------------ + battery('#8615: the captured-filter arm'); const capBlind = judgeFixture(FIXTURE_CAPTURED_BLIND); expect('a single-param callback capturing a same-file `where` is discovered', capBlind.found.length === 1); expect('its captured path is recorded', capBlind.found[0]?.capturedPath?.join('.') === 'where'); @@ -936,6 +982,7 @@ function selfTest() { // declaration is a silent gate; a surplus one is a LYING gate, and the price the // derivation records for a fabricated lead is higher than for a missing one. Driven // through this gate's OWN corpus walk, never a copy of its regex. + battery('the dispatch-gates declaration (#13163\'s landing obligation)'); const DECLARED_TAIL = '.test.ts'; const walked = testFilesUnder(join(repoRoot, SCAN_ROOT)) .map((abs) => relative(repoRoot, abs).replace(/\\/g, '/')); @@ -979,6 +1026,51 @@ function selfTest() { Boolean(nonTestSibling) && !admitted.has(nonTestSibling), ); + // ── The floor: every declared battery RAN, and ran its cases (#13489) ─── + // + // Evaluated after every battery has had its chance and BEFORE the verdict, so + // the success line below can only be printed by a run in which the set of + // batteries that registered assertions EQUALS the set declared. A set + // difference names WHICH battery stopped; a count says only that something did. + const floorFailure = (message) => { + failures.push(message); + }; + const declaredBatteries = Object.keys(SELF_TEST_BATTERIES); + let floorBreached = false; + if (declaredBatteries.length < SELF_TEST_BATTERY_FLOOR) { + floorBreached = true; + floorFailure( + `SELF_TEST_BATTERIES declares ${declaredBatteries.length} batteries, below the pinned ` + + `${SELF_TEST_BATTERY_FLOOR} — a battery deleted from the roster takes its own floor with it.`, + ); + } + for (const [name, count] of seen) { + if (declaredBatteries.includes(name)) continue; + floorBreached = true; + floorFailure( + `self-test battery "${name}" registered ${count} case(s) but is not declared in ` + + 'SELF_TEST_BATTERIES — an assertion attributed to no declared battery is one nothing floors.', + ); + } + for (const name of declaredBatteries) { + const count = seen.get(name) ?? 0; + if (count >= SELF_TEST_BATTERIES[name]) continue; + floorBreached = true; + floorFailure( + count === 0 + ? `self-test battery "${name}" DID NOT RUN — 0 cases registered, ${SELF_TEST_BATTERIES[name]} pinned. ` + + 'The verdict below would have claimed those cases hold.' + : `self-test battery "${name}" registered ${count} case(s), below its pinned floor of ` + + `${SELF_TEST_BATTERIES[name]} — cases that used to run no longer do.`, + ); + } + if (floorBreached) { + floorFailure( + 'A battery at or below its floor means cases STOPPED RUNNING — the battery is the bug, not the ' + + 'number. Find what stopped registering (an early return, a deleted block, a guard that now ' + + 'skips) and restore it.', + ); + } if (failures.length > 0) { console.error(`✗ check-where-matcher-conformance --self-test (${failures.length} failure(s)):\n`); for (const f of failures) console.error(` • ${f}`); diff --git a/scripts/check-workflow-status-functions.mjs b/scripts/check-workflow-status-functions.mjs index e589fa8a2b..0fa4ced881 100644 --- a/scripts/check-workflow-status-functions.mjs +++ b/scripts/check-workflow-status-functions.mjs @@ -300,10 +300,60 @@ function list() { // as one that passed (#13798). const SELF_TEST_VERDICT = 'check-workflow-status-functions self-test reached its verdict'; +// ── The self-test's own battery roster and floor (#13489) ────────────────── +// +// `failures.length === 0` used to be this self-test's ONLY success condition, so +// "every case held" and "the cases never ran" printed the same line. Closed the +// way PR #13487 validated on check-doc-authoring: what is pinned is the +// registered NAMES, not a number. Every section opens with `battery('')`, +// every assertion is attributed to the battery most recently opened, and the +// floor requires the OPENED set to equal the DECLARED set with each battery at +// or above its own count. +// +// ⛔ A pinned TOTAL is not the repair: a battery dropping from 9 cases to 3 +// keeps a total "right" the moment a sibling grows. +// +// The counts are a FLOOR, not an equality — adding cases is ordinary work and +// must not red. A battery BELOW its floor means cases stopped running; the +// remedy is to find what stopped registering. +const SELF_TEST_BATTERIES = Object.freeze({ + '1. A violating sample must go red': 7, + '2. Compliant samples must stay green': 7, + '3. Missing input must go red, in all four shapes (#4690)': 6, + 'and the `needs.*` step case is the one a careless regex would sweep in.': 3, + '5. Scope: needs.*.result is a status read, not a data read': 1, + '6. A folded expression is read as one expression': 4, + '7. Spelling variants the expression language accepts': 3, + '8. The real repository is what this gate actually guards': 3, +}); + +// DELETING an entry silences that battery's floor exactly as effectively as +// zeroing it, so the roster's own size is pinned too. +const SELF_TEST_BATTERY_FLOOR = 8; + +// The key an assertion is filed under when no battery is open. It is not a +// declared battery, so it reds by the same set difference rather than silently +// inflating whichever battery happened to run last. +const UNATTRIBUTED_BATTERY = '(no battery open)'; + function selfTest() { + // The battery ledger this self-test's floor is evaluated against (#13489). + // `battery()` opens a battery; every assertion below is attributed to the one + // most recently opened, so a section that stops running stops registering and + // names ITSELF at the floor rather than going quiet. + const seen = new Map(); + let openBattery = null; + const battery = (name) => { + openBattery = name; + }; + const registerCase = () => { + const b = openBattery ?? UNATTRIBUTED_BATTERY; + seen.set(b, (seen.get(b) ?? 0) + 1); + }; const failures = []; let checked = 0; const assert = (cond, msg) => { + registerCase(); checked++; if (!cond) failures.push(msg); }; @@ -324,6 +374,7 @@ function selfTest() { // ── 1. A violating sample must go red ──────────────────────────────────── // // Verbatim shape of publish-smoke.yml:98 as #5343 found it. + battery('1. A violating sample must go red'); const violating = makeRoot({ '.github/workflows/publish-smoke.yml': `name: Publish Smoke on: @@ -368,6 +419,7 @@ jobs: // All four status functions, the #4928 shape, and the negated form -- a rule // that only recognised `success()` would pass this fixture for the wrong // reason, so each spelling is asserted separately below. + battery('2. Compliant samples must stay green'); const green = makeRoot({ '.github/workflows/ci.yml': `name: CI on: [push] @@ -417,6 +469,7 @@ jobs: } // ── 3. Missing input must go red, in all four shapes (#4690) ───────────── + battery('3. Missing input must go red, in all four shapes (#4690)'); const noDir = makeRoot({ 'README.md': '# no workflows here\n' }); const missing = scan(noDir); assert(missing.problems.length === 1, `a missing ${WORKFLOW_DIR}/ is an input problem, got ${missing.problems.length}`); @@ -445,6 +498,7 @@ jobs: // This is the boundary the rule was narrowed to on purpose (#5343). Pinning // it here means a later widening has to be deliberate rather than accidental // -- and the `needs.*` step case is the one a careless regex would sweep in. + battery('and the `needs.*` step case is the one a careless regex would sweep in.'); const steps = makeRoot({ '.github/workflows/steps.yml': `name: Steps on: [push] @@ -477,6 +531,7 @@ jobs: assert(stepScope.jobIfs === 1, `only the job-level if: is counted, got ${stepScope.jobIfs}`); // ── 5. Scope: needs.*.result is a status read, not a data read ─────────── + battery('5. Scope: needs.*.result is a status read, not a data read'); const resultRead = makeRoot({ '.github/workflows/guard.yml': `name: Guard on: [push] @@ -502,6 +557,7 @@ jobs: // violation below at all -- the `needs.` read and the (absent) status // function sit on different source lines. Two workflows in this repo // already write `if: >-`. + battery('6. A folded expression is read as one expression'); const folded = makeRoot({ '.github/workflows/folded.yml': `name: Folded on: [push] @@ -541,6 +597,7 @@ jobs: assert(scan(foldedOk).violations.length === 0, 'the same folded expression with !cancelled() is green'); // ── 7. Spelling variants the expression language accepts ───────────────── + battery('7. Spelling variants the expression language accepts'); const variants = makeRoot({ '.github/workflows/variants.yml': `name: Variants on: [push] @@ -589,6 +646,7 @@ jobs: // fixtures above: fixtures prove the detector can go red, this proves the // tree it ships with is green. It also fails loudly if the repo's workflow // directory ever moves out from under the gate. + battery('8. The real repository is what this gate actually guards'); const real = scan(repoRoot()); assert(real.problems.length === 0, `the repo's own workflows parse cleanly, got ${real.problems[0]}`); assert(real.files > 0 && real.jobs > 0, 'the repo scan actually reads workflows'); @@ -600,6 +658,51 @@ jobs: for (const dir of roots) rmSync(dir, { recursive: true, force: true }); } + // ── The floor: every declared battery RAN, and ran its cases (#13489) ─── + // + // Evaluated after every battery has had its chance and BEFORE the verdict, so + // the success line below can only be printed by a run in which the set of + // batteries that registered assertions EQUALS the set declared. A set + // difference names WHICH battery stopped; a count says only that something did. + const floorFailure = (message) => { + failures.push(message); + }; + const declaredBatteries = Object.keys(SELF_TEST_BATTERIES); + let floorBreached = false; + if (declaredBatteries.length < SELF_TEST_BATTERY_FLOOR) { + floorBreached = true; + floorFailure( + `SELF_TEST_BATTERIES declares ${declaredBatteries.length} batteries, below the pinned ` + + `${SELF_TEST_BATTERY_FLOOR} — a battery deleted from the roster takes its own floor with it.`, + ); + } + for (const [name, count] of seen) { + if (declaredBatteries.includes(name)) continue; + floorBreached = true; + floorFailure( + `self-test battery "${name}" registered ${count} case(s) but is not declared in ` + + 'SELF_TEST_BATTERIES — an assertion attributed to no declared battery is one nothing floors.', + ); + } + for (const name of declaredBatteries) { + const count = seen.get(name) ?? 0; + if (count >= SELF_TEST_BATTERIES[name]) continue; + floorBreached = true; + floorFailure( + count === 0 + ? `self-test battery "${name}" DID NOT RUN — 0 cases registered, ${SELF_TEST_BATTERIES[name]} pinned. ` + + 'The verdict below would have claimed those cases hold.' + : `self-test battery "${name}" registered ${count} case(s), below its pinned floor of ` + + `${SELF_TEST_BATTERIES[name]} — cases that used to run no longer do.`, + ); + } + if (floorBreached) { + floorFailure( + 'A battery at or below its floor means cases STOPPED RUNNING — the battery is the bug, not the ' + + 'number. Find what stopped registering (an early return, a deleted block, a guard that now ' + + 'skips) and restore it.', + ); + } if (failures.length) { console.error(`✗ check-workflow-status-functions --self-test -- ${failures.length} failure(s)\n`); for (const f of failures) console.error(` • ${f}`); diff --git a/scripts/objectui-changeset-digest.mjs b/scripts/objectui-changeset-digest.mjs index 36b62d7e12..bd4aaf9d2e 100644 --- a/scripts/objectui-changeset-digest.mjs +++ b/scripts/objectui-changeset-digest.mjs @@ -212,6 +212,50 @@ import { dirname, join, resolve } from 'node:path'; import { fileURLToPath, pathToFileURL } from 'node:url'; import { isEntrypoint } from './invoked-as.mjs'; +// ── The self-test's own battery roster and floor (#13489) ────────────────── +// +// `failures.length === 0` used to be this self-test's ONLY success condition, so +// "every case held" and "the cases never ran" printed the same line. Closed the +// way PR #13487 validated on check-doc-authoring: what is pinned is the +// registered NAMES, not a number. Every section opens with `battery('')`, +// every assertion is attributed to the battery most recently opened, and the +// floor requires the OPENED set to equal the DECLARED set with each battery at +// or above its own count. +// +// ⛔ A pinned TOTAL is not the repair: a battery dropping from 9 cases to 3 +// keeps a total "right" the moment a sibling grows. +// +// The counts are a FLOOR, not an equality — adding cases is ordinary work and +// must not red. A battery BELOW its floor means cases stopped running; the +// remedy is to find what stopped registering. +const SELF_TEST_BATTERIES = Object.freeze({ + 'and one in RC pre-mode': 15, + '#6099: the `minor` + prose-annotation family': 17, + 'end-to-end through the shell driver': 5, + 'an unwalkable range: REFUSE, loudly (#14178)': 1, + 'the INITIAL pin still degrades, and says so': 1, + '#6175: a range whose `to` endpoint is a RELEASE COMMIT': 8, + '#6174: the undeclared commits are NAMED, not only counted': 12, + '#6494: the ADR-0087 disposition scaffold': 5, + '#6494 THE ROUND TRIP, through the REAL gate': 4, + '#7004: the frontmatter shapes the old entry anchor hid': 7, + '#7044: WHERE the fence is allowed to start': 7, + '#9408: WALK COMPLETENESS, not object presence': 12, + '#9408 through the shell driver: REFUSE, and say WHICH failure': 6, + '#14178: an ABSENT endpoint is the same failure, one step earlier': 9, + '#10495: is the revision being PINNED actually on objectui main?': 16, + 'R7: the bash 3.2 floor': 5, +}); + +// DELETING an entry silences that battery's floor exactly as effectively as +// zeroing it, so the roster's own size is pinned too. +const SELF_TEST_BATTERY_FLOOR = 16; + +// The key an assertion is filed under when no battery is open. It is not a +// declared battery, so it reds by the same set difference rather than silently +// inflating whichever battery happened to run last. +const UNATTRIBUTED_BATTERY = '(no battery open)'; + const __dirname = dirname(fileURLToPath(import.meta.url)); const REPO_ROOT = resolve(__dirname, '..'); @@ -1269,8 +1313,22 @@ function main(argv) { // --------------------------------------------------------------------------- function selfTest() { + // The battery ledger this self-test's floor is evaluated against (#13489). + // `battery()` opens a battery; every assertion below is attributed to the one + // most recently opened, so a section that stops running stops registering and + // names ITSELF at the floor rather than going quiet. + const seen = new Map(); + let openBattery = null; + const battery = (name) => { + openBattery = name; + }; + const registerCase = () => { + const b = openBattery ?? UNATTRIBUTED_BATTERY; + seen.set(b, (seen.get(b) ?? 0) + 1); + }; const failures = []; const check = (name, cond, detail = '') => { + registerCase(); if (cond) { console.log(` ✓ ${name}`); } else { @@ -1343,6 +1401,7 @@ function selfTest() { const fwPlain = join(tmp, 'fw-plain'); mkdirSync(join(fwPlain, '.changeset'), { recursive: true }); // --- and one in RC pre-mode --- + battery('and one in RC pre-mode'); const fwPre = join(tmp, 'fw-pre'); mkdirSync(join(fwPre, '.changeset'), { recursive: true }); writeFileSync(join(fwPre, '.changeset', 'pre.json'), '{"mode":"pre","tag":"rc"}\n'); @@ -1448,6 +1507,7 @@ function selfTest() { // WHERE the author put the annotation — one leads with it, one buries it in // the last paragraph — and a criterion that reads only the first paragraph // (which is all `summary` is) catches the first and misses the second. + battery('#6099: the `minor` + prose-annotation family'); const ui2 = join(tmp, 'objectui-annotated'); mkdirSync(join(ui2, '.changeset'), { recursive: true }); const g2 = (...args) => git(ui2, args); @@ -1647,6 +1707,7 @@ function selfTest() { ); // --- end-to-end through the shell driver ------------------------------- + battery('end-to-end through the shell driver'); const fwRun = join(tmp, 'fw-run'); mkdirSync(join(fwRun, 'scripts'), { recursive: true }); mkdirSync(join(fwRun, '.changeset'), { recursive: true }); @@ -1694,6 +1755,7 @@ function selfTest() { // CHANGELOG from a level objectui declared. What the case still asserts is // the half that mattered — the failure is loud, named, and takes the // operator to a remedy — plus the #10797 invariant the refusal now owes. + battery('an unwalkable range: REFUSE, loudly (#14178)'); const fwDegraded = join(tmp, 'fw-unwalkable'); mkdirSync(join(fwDegraded, 'scripts'), { recursive: true }); mkdirSync(join(fwDegraded, '.changeset'), { recursive: true }); @@ -1722,6 +1784,7 @@ function selfTest() { // removed ones had: there is no previous SHA, hence no range, hence no // remedy to name and nothing being guessed about a walk. A refusal here // would make the first-ever pin impossible to write. + battery('the INITIAL pin still degrades, and says so'); const fwInitial = join(tmp, 'fw-initial-pin'); mkdirSync(join(fwInitial, 'scripts'), { recursive: true }); mkdirSync(join(fwInitial, '.changeset'), { recursive: true }); @@ -1750,6 +1813,7 @@ function selfTest() { // consumes the changesets it ships, so every changeset added in the range // is gone at `to` and every read falls back. Its own repo on purpose: the // #4731 / #6099 fixtures above pin exact counts and must not shift under it. + battery('#6175: a range whose `to` endpoint is a RELEASE COMMIT'); const ui3 = join(tmp, 'objectui-released'); mkdirSync(join(ui3, '.changeset'), { recursive: true }); const g3 = (...args) => git(ui3, args); @@ -1885,6 +1949,7 @@ function selfTest() { // The row count here is deliberately > 1 so "the list agrees with the count" // is a real claim, and the cap can be made to fire without touching `max` // anywhere else. + battery('#6174: the undeclared commits are NAMED, not only counted'); const SUBJECT_3518 = 'fix(form): bind `previous` for field rules and stop resubmitting read-only fields (#3518)'; @@ -2093,6 +2158,7 @@ function selfTest() { // importing it would RUN the gate. The copy is belt-and-braces — the REAL // gate binary judges a real artifact in the round trip below, and that, not // this regex, is the authority on what the marker means. + battery('#6494: the ADR-0087 disposition scaffold'); const markersIn = (text) => [...text.matchAll(//g)].map((m) => m[1].replace(/\s+/g, ' ').trim(), @@ -2171,6 +2237,7 @@ function selfTest() { // A throwaway repo carrying the gate's required inputs (#4690: it refuses to // report a verdict without them) plus a COPY of the gate, so its REPO_ROOT // resolves here — the same idiom the bump-objectui.sh run above uses. + battery('#6494 THE ROUND TRIP, through the REAL gate'); const gateRepo = join(tmp, 'fw-gate'); mkdirSync(gateRepo, { recursive: true }); const gg = (...args) => git(gateRepo, args); @@ -2304,6 +2371,7 @@ function selfTest() { // Predicted direction on reverse verification: restoring the old anchor // (`([A-Za-z]+)\s*$`) turns C1-C5 red (packages goes `{}`) and C6 red in the // other direction (a phantom package named `# note`). + battery('#7004: the frontmatter shapes the old entry anchor hid'); const pkgsOf = (block) => JSON.stringify(parseChangeset(`---\n${block}\n---\n\nsummary\n`).packages); check('#7004 C1 a trailing YAML comment still declares its package', pkgsOf('"@object-ui/layout": major # keep') === '{"@object-ui/layout":"major"}', pkgsOf('"@object-ui/layout": major # keep')); check('#7004 C2 a trailing comment after a tab', pkgsOf('"@object-ui/layout": minor\t# keep') === '{"@object-ui/layout":"minor"}', pkgsOf('"@object-ui/layout": minor\t# keep')); @@ -2345,6 +2413,7 @@ function selfTest() { // the summary text. D6/D7 are the controls and stay green in both worlds, // which is what makes D1-D5 statements about the preamble rather than about // a fixture that parses as nothing either way. + battery('#7044: WHERE the fence is allowed to start'); const CS_MAJOR = '---\n"@object-ui/layout": major\n---\n\nDrop PageNodeRenderer.\n'; const parsedPkgs = (text) => JSON.stringify(parseChangeset(text).packages); check('#7044 D1 one leading blank line before the fence', parsedPkgs('\n' + CS_MAJOR) === '{"@object-ui/layout":"major"}', parsedPkgs('\n' + CS_MAJOR)); @@ -2392,6 +2461,7 @@ function selfTest() { // boundary AT `from`, hence outside the range, must also walk completely // (C7). C7 is the false positive that `--is-shallow-repository` alone would // produce, and the reason the invariant is positional rather than a flag. + battery('#9408: WALK COMPLETENESS, not object presence'); const ui6 = join(tmp, 'objectui-truncated'); mkdirSync(join(ui6, '.changeset'), { recursive: true }); const g6 = (...args) => git(ui6, args); @@ -2581,6 +2651,7 @@ function selfTest() { // (`patch`) into published CHANGELOG text, and no record beats a wrong one. // What survives unchanged is everything that made the old artifact readable // — WHICH failure this was, and the remedy — now said in a refusal. + battery('#9408 through the shell driver: REFUSE, and say WHICH failure'); const fwTrunc = join(tmp, 'fw-truncated'); mkdirSync(join(fwTrunc, 'scripts'), { recursive: true }); mkdirSync(join(fwTrunc, '.changeset'), { recursive: true }); @@ -2686,6 +2757,7 @@ function selfTest() { // deepens and derives the COMPLETE record, asserted equal to the record the // same range yields in a full clone. A guard that refused everything would // pass every refusal case here and fail that one. + battery('#14178: an ABSENT endpoint is the same failure, one step earlier'); const uiUp = join(tmp, 'objectui-upstream'); mkdirSync(join(uiUp, '.changeset'), { recursive: true }); const gUp = (...args) => git(uiUp, args); @@ -2905,6 +2977,7 @@ function selfTest() { // every case below asserts what it SAYS, and that it still exits 0 and // still writes the pin. Three answers are pinned, never two — "cannot // answer" must not borrow the wording of either verdict. + battery('#10495: is the revision being PINNED actually on objectui main?'); const mkFramework = (name, pinSha) => { const dir = join(tmp, name); mkdirSync(join(dir, 'scripts'), { recursive: true }); @@ -3149,6 +3222,7 @@ function selfTest() { // Both halves are needed and they catch different things: the static scan // sees parse-level constructs that `enable -n` cannot simulate, and the // simulated run proves the real code path completes without the builtins. + battery('R7: the bash 3.2 floor'); const bash4Constructs = new RegExp( [ 'exec\\s+\\{[A-Za-z_]', // bash 4.1 fd auto-allocation @@ -3293,6 +3367,51 @@ function selfTest() { rmSync(tmp, { recursive: true, force: true }); } + // ── The floor: every declared battery RAN, and ran its cases (#13489) ─── + // + // Evaluated after every battery has had its chance and BEFORE the verdict, so + // the success line below can only be printed by a run in which the set of + // batteries that registered assertions EQUALS the set declared. A set + // difference names WHICH battery stopped; a count says only that something did. + const floorFailure = (message) => { + failures.push(message); + }; + const declaredBatteries = Object.keys(SELF_TEST_BATTERIES); + let floorBreached = false; + if (declaredBatteries.length < SELF_TEST_BATTERY_FLOOR) { + floorBreached = true; + floorFailure( + `SELF_TEST_BATTERIES declares ${declaredBatteries.length} batteries, below the pinned ` + + `${SELF_TEST_BATTERY_FLOOR} — a battery deleted from the roster takes its own floor with it.`, + ); + } + for (const [name, count] of seen) { + if (declaredBatteries.includes(name)) continue; + floorBreached = true; + floorFailure( + `self-test battery "${name}" registered ${count} case(s) but is not declared in ` + + 'SELF_TEST_BATTERIES — an assertion attributed to no declared battery is one nothing floors.', + ); + } + for (const name of declaredBatteries) { + const count = seen.get(name) ?? 0; + if (count >= SELF_TEST_BATTERIES[name]) continue; + floorBreached = true; + floorFailure( + count === 0 + ? `self-test battery "${name}" DID NOT RUN — 0 cases registered, ${SELF_TEST_BATTERIES[name]} pinned. ` + + 'The verdict below would have claimed those cases hold.' + : `self-test battery "${name}" registered ${count} case(s), below its pinned floor of ` + + `${SELF_TEST_BATTERIES[name]} — cases that used to run no longer do.`, + ); + } + if (floorBreached) { + floorFailure( + 'A battery at or below its floor means cases STOPPED RUNNING — the battery is the bug, not the ' + + 'number. Find what stopped registering (an early return, a deleted block, a guard that now ' + + 'skips) and restore it.', + ); + } if (failures.length) { console.error(`\n⛔ objectui-changeset-digest --self-test: ${failures.length} failure(s)`); for (const f of failures) console.error(` - ${f}`); diff --git a/scripts/objectui-range.mjs b/scripts/objectui-range.mjs index d017e4f52d..90f25874fc 100644 --- a/scripts/objectui-range.mjs +++ b/scripts/objectui-range.mjs @@ -355,9 +355,58 @@ function main() { // handshake is a flag rather than a returned sentinel. let selfTestReachedVerdict = false; +// ── The self-test's own battery roster and floor (#13489) ────────────────── +// +// `failures.length === 0` used to be this self-test's ONLY success condition, so +// "every case held" and "the cases never ran" printed the same line. Closed the +// way PR #13487 validated on check-doc-authoring: what is pinned is the +// registered NAMES, not a number. Every section opens with `battery('')`, +// every assertion is attributed to the battery most recently opened, and the +// floor requires the OPENED set to equal the DECLARED set with each battery at +// or above its own count. +// +// ⛔ A pinned TOTAL is not the repair: a battery dropping from 9 cases to 3 +// keeps a total "right" the moment a sibling grows. +// +// The counts are a FLOOR, not an equality — adding cases is ordinary work and +// must not red. A battery BELOW its floor means cases stopped running; the +// remedy is to find what stopped registering. +const SELF_TEST_BATTERIES = Object.freeze({ + 'the class that went missing: it must be IN the artifact, and lead': 4, + '#6294: the leading section is keyed on the SHARED breaking verdict': 6, + 'what ships nothing is excluded, and SAYS SO in the artifact': 5, + 'a range that excludes NOTHING still says so (zeros are load-bearing)': 1, + '`--all` names the excluded entries instead of only counting them': 1, + 'end-to-end through the real CLI': 4, + '#11952: `--help`\'s self-read is bound to the header, not every': 2, +}); + +// DELETING an entry silences that battery's floor exactly as effectively as +// zeroing it, so the roster's own size is pinned too. +const SELF_TEST_BATTERY_FLOOR = 7; + +// The key an assertion is filed under when no battery is open. It is not a +// declared battery, so it reds by the same set difference rather than silently +// inflating whichever battery happened to run last. +const UNATTRIBUTED_BATTERY = '(no battery open)'; + function selfTest() { + // The battery ledger this self-test's floor is evaluated against (#13489). + // `battery()` opens a battery; every assertion below is attributed to the one + // most recently opened, so a section that stops running stops registering and + // names ITSELF at the floor rather than going quiet. + const seen = new Map(); + let openBattery = null; + const battery = (name) => { + openBattery = name; + }; + const registerCase = () => { + const b = openBattery ?? UNATTRIBUTED_BATTERY; + seen.set(b, (seen.get(b) ?? 0) + 1); + }; const failures = []; const check = (name, cond, detail = '') => { + registerCase(); if (cond) { console.log(` ✓ ${name}`); } else { @@ -460,6 +509,7 @@ function selfTest() { const { markdown } = renderRange({ fromSha: base, toSha: head, classified }); // --- the class that went missing: it must be IN the artifact, and lead --- + battery('the class that went missing: it must be IN the artifact, and lead'); check( 'a BREAKING `refactor(...)!` commit IS in the pasteable list', markdown.includes('PageNodeRenderer') && markdown.includes(breakingSha.slice(0, 9)), @@ -491,6 +541,7 @@ function selfTest() { // objectui never declares `major` in a release window, so a level-keyed // table left `### Breaking changes` structurally unrenderable and filed the // author-annotated breaking entries under "Features". + battery('#6294: the leading section is keyed on the SHARED breaking verdict'); check( 'a `minor` entry the AUTHOR annotated breaking is under Breaking changes', (sectionOf(markdown, 'Breaking changes') ?? '').includes('not the raw value'), @@ -543,6 +594,7 @@ function selfTest() { ); // --- what ships nothing is excluded, and SAYS SO in the artifact --- + battery('what ships nothing is excluded, and SAYS SO in the artifact'); check( 'a `fix(ci)` with an EMPTY frontmatter changeset is NOT listed', !markdown.includes('cross-repo token'), @@ -570,6 +622,7 @@ function selfTest() { ); // --- a range that excludes NOTHING still says so (zeros are load-bearing) --- + battery('a range that excludes NOTHING still says so (zeros are load-bearing)'); const clean = renderRange({ fromSha: base, toSha: head, @@ -589,6 +642,7 @@ function selfTest() { ); // --- `--all` names the excluded entries instead of only counting them --- + battery('`--all` names the excluded entries instead of only counting them'); const all = renderRange({ fromSha: base, toSha: head, classified, showExcluded: true }); check( '`--all` itemizes the excluded commits by subject', @@ -599,6 +653,7 @@ function selfTest() { ); // --- end-to-end through the real CLI ------------------------------------ + battery('end-to-end through the real CLI'); const cli = fileURLToPath(import.meta.url); const run = (args) => execFileSync('node', [cli, ...args], { @@ -639,6 +694,7 @@ function selfTest() { // the header is still there, and none of the six measured stray lines — // an internal helper's rationale (pinAt, mid-`main`) and this very // self-test's own section banner — leak into the usage text. + battery('#11952: `--help`\'s self-read is bound to the header, not every'); const helpOut = run(['--help']); check( '--help still carries the real header (Usage/Env sections intact)', @@ -662,6 +718,51 @@ function selfTest() { rmSync(tmp, { recursive: true, force: true }); } + // ── The floor: every declared battery RAN, and ran its cases (#13489) ─── + // + // Evaluated after every battery has had its chance and BEFORE the verdict, so + // the success line below can only be printed by a run in which the set of + // batteries that registered assertions EQUALS the set declared. A set + // difference names WHICH battery stopped; a count says only that something did. + const floorFailure = (message) => { + failures.push(message); + }; + const declaredBatteries = Object.keys(SELF_TEST_BATTERIES); + let floorBreached = false; + if (declaredBatteries.length < SELF_TEST_BATTERY_FLOOR) { + floorBreached = true; + floorFailure( + `SELF_TEST_BATTERIES declares ${declaredBatteries.length} batteries, below the pinned ` + + `${SELF_TEST_BATTERY_FLOOR} — a battery deleted from the roster takes its own floor with it.`, + ); + } + for (const [name, count] of seen) { + if (declaredBatteries.includes(name)) continue; + floorBreached = true; + floorFailure( + `self-test battery "${name}" registered ${count} case(s) but is not declared in ` + + 'SELF_TEST_BATTERIES — an assertion attributed to no declared battery is one nothing floors.', + ); + } + for (const name of declaredBatteries) { + const count = seen.get(name) ?? 0; + if (count >= SELF_TEST_BATTERIES[name]) continue; + floorBreached = true; + floorFailure( + count === 0 + ? `self-test battery "${name}" DID NOT RUN — 0 cases registered, ${SELF_TEST_BATTERIES[name]} pinned. ` + + 'The verdict below would have claimed those cases hold.' + : `self-test battery "${name}" registered ${count} case(s), below its pinned floor of ` + + `${SELF_TEST_BATTERIES[name]} — cases that used to run no longer do.`, + ); + } + if (floorBreached) { + floorFailure( + 'A battery at or below its floor means cases STOPPED RUNNING — the battery is the bug, not the ' + + 'number. Find what stopped registering (an early return, a deleted block, a guard that now ' + + 'skips) and restore it.', + ); + } if (failures.length) { console.error(`\n⛔ objectui-range --self-test: ${failures.length} failure(s)`); for (const f of failures) console.error(` - ${f}`); diff --git a/scripts/pr-labels.mjs b/scripts/pr-labels.mjs index 151c9124b8..c6b9cab431 100644 --- a/scripts/pr-labels.mjs +++ b/scripts/pr-labels.mjs @@ -523,14 +523,63 @@ async function runPaths(dryRun) { // as one that passed (#13798). const SELF_TEST_VERDICT = 'pr-labels self-test reached its verdict'; +// ── The self-test's own battery roster and floor (#13489) ────────────────── +// +// `failures.length === 0` used to be this self-test's ONLY success condition, so +// "every case held" and "the cases never ran" printed the same line. Closed the +// way PR #13487 validated on check-doc-authoring: what is pinned is the +// registered NAMES, not a number. Every section opens with `battery('')`, +// every assertion is attributed to the battery most recently opened, and the +// floor requires the OPENED set to equal the DECLARED set with each battery at +// or above its own count. +// +// ⛔ A pinned TOTAL is not the repair: a battery dropping from 9 cases to 3 +// keeps a total "right" the moment a sibling grows. +// +// The counts are a FLOOR, not an equality — adding cases is ordinary work and +// must not red. A battery BELOW its floor means cases stopped running; the +// remedy is to find what stopped registering. +const SELF_TEST_BATTERIES = Object.freeze({ + 'glob matching, including the zero-segment `**` case': 18, + 'size bucketing: `<`, not `<=`': 10, + 'the write plans: POST and DELETE only': 11, + 'the #10698 interleaving, replayed': 3, + 'config parsing': 6, + 'the REAL config, so drift fails lint rather than a PR run': 2, +}); + +// DELETING an entry silences that battery's floor exactly as effectively as +// zeroing it, so the roster's own size is pinned too. +const SELF_TEST_BATTERY_FLOOR = 6; + +// The key an assertion is filed under when no battery is open. It is not a +// declared battery, so it reds by the same set difference rather than silently +// inflating whichever battery happened to run last. +const UNATTRIBUTED_BATTERY = '(no battery open)'; + function selfTest() { + // The battery ledger this self-test's floor is evaluated against (#13489). + // `battery()` opens a battery; every assertion below is attributed to the one + // most recently opened, so a section that stops running stops registering and + // names ITSELF at the floor rather than going quiet. + const seen = new Map(); + let openBattery = null; + const battery = (name) => { + openBattery = name; + }; + const registerCase = () => { + const b = openBattery ?? UNATTRIBUTED_BATTERY; + seen.set(b, (seen.get(b) ?? 0) + 1); + }; const failures = []; const check = (name, actual, expected) => { + registerCase(); const a = JSON.stringify(actual); const e = JSON.stringify(expected); if (a !== e) failures.push(`${name}\n expected: ${e}\n actual: ${a}`); }; const throws = (name, fn, needle) => { + registerCase(); try { fn(); failures.push(`${name}: expected a ConfigError, none thrown`); @@ -543,6 +592,7 @@ function selfTest() { }; // --- glob matching, including the zero-segment `**` case ----------------- + battery('glob matching, including the zero-segment `**` case'); check('** matches zero segments', matchGlob('content/**/*', 'content/a.md'), true); check('** matches many segments', matchGlob('content/**/*', 'content/a/b/c.md'), true); check('** does not match the bare prefix', matchGlob('content/**/*', 'content'), false); @@ -564,6 +614,7 @@ function selfTest() { throws('extglob is refused', () => matchGlob('src/+(a|b).ts', 'src/a.ts'), '+'); // --- size bucketing: `<`, not `<=` -------------------------------------- + battery('size bucketing: `<`, not `<=`'); const buckets = [ { max: 10, label: 'size/xs' }, { max: 100, label: 'size/s' }, @@ -594,6 +645,7 @@ function selfTest() { ); // --- the write plans: POST and DELETE only ------------------------------ + battery('the write plans: POST and DELETE only'); const sizePlan = planSizeWrites({ prNumber: 42, target: 'size/l', @@ -647,6 +699,7 @@ function selfTest() { // The defect as a TEST rather than a paragraph. `applyPlan` models what the // three verbs do to a label set server-side; the PUT branch exists only so // the retired behaviour can be replayed next to the new one. + battery('the #10698 interleaving, replayed'); const applyPlan = (labels, plan) => { let set = [...labels]; for (const step of plan) { @@ -696,6 +749,7 @@ function selfTest() { ); // --- config parsing ------------------------------------------------------ + battery('config parsing'); const parsed = parseLabelerConfig( ["# a comment", "'documentation':", ' - changed-files:', ' - any-glob-to-any-file: ', " - 'content/**/*'", " - '**/*.md'", '', "'tests':", ' - changed-files:', ' - any-glob-to-any-file:', " - '**/*.test.ts'"].join('\n'), 'fixture.yml' @@ -721,6 +775,7 @@ function selfTest() { throws('a label with no globs is refused', () => parseLabelerConfig("'x':\n"), 'declares no globs'); // --- the REAL config, so drift fails lint rather than a PR run ----------- + battery('the REAL config, so drift fails lint rather than a PR run'); try { const realConfig = parseLabelerConfig(readFileSync(DEFAULT_LABELER_CONFIG, 'utf8'), DEFAULT_LABELER_CONFIG); if (realConfig.size === 0) failures.push('the checked-in .github/labeler.yml parsed to zero labels'); @@ -740,6 +795,51 @@ function selfTest() { failures.push(`the checked-in .github/labeler.yml is outside the supported subset: ${error.message}`); } + // ── The floor: every declared battery RAN, and ran its cases (#13489) ─── + // + // Evaluated after every battery has had its chance and BEFORE the verdict, so + // the success line below can only be printed by a run in which the set of + // batteries that registered assertions EQUALS the set declared. A set + // difference names WHICH battery stopped; a count says only that something did. + const floorFailure = (message) => { + failures.push(message); + }; + const declaredBatteries = Object.keys(SELF_TEST_BATTERIES); + let floorBreached = false; + if (declaredBatteries.length < SELF_TEST_BATTERY_FLOOR) { + floorBreached = true; + floorFailure( + `SELF_TEST_BATTERIES declares ${declaredBatteries.length} batteries, below the pinned ` + + `${SELF_TEST_BATTERY_FLOOR} — a battery deleted from the roster takes its own floor with it.`, + ); + } + for (const [name, count] of seen) { + if (declaredBatteries.includes(name)) continue; + floorBreached = true; + floorFailure( + `self-test battery "${name}" registered ${count} case(s) but is not declared in ` + + 'SELF_TEST_BATTERIES — an assertion attributed to no declared battery is one nothing floors.', + ); + } + for (const name of declaredBatteries) { + const count = seen.get(name) ?? 0; + if (count >= SELF_TEST_BATTERIES[name]) continue; + floorBreached = true; + floorFailure( + count === 0 + ? `self-test battery "${name}" DID NOT RUN — 0 cases registered, ${SELF_TEST_BATTERIES[name]} pinned. ` + + 'The verdict below would have claimed those cases hold.' + : `self-test battery "${name}" registered ${count} case(s), below its pinned floor of ` + + `${SELF_TEST_BATTERIES[name]} — cases that used to run no longer do.`, + ); + } + if (floorBreached) { + floorFailure( + 'A battery at or below its floor means cases STOPPED RUNNING — the battery is the bug, not the ' + + 'number. Find what stopped registering (an early return, a deleted block, a guard that now ' + + 'skips) and restore it.', + ); + } if (failures.length > 0) { console.error(`pr-labels --self-test: ${failures.length} failure(s)\n`); for (const failure of failures) console.error(` FAIL ${failure}`); diff --git a/scripts/sync-docs-image-tags.mjs b/scripts/sync-docs-image-tags.mjs index 2b2e490499..9649967524 100644 --- a/scripts/sync-docs-image-tags.mjs +++ b/scripts/sync-docs-image-tags.mjs @@ -292,10 +292,60 @@ function main() { // as one that passed (#13798). const SELF_TEST_VERDICT = 'sync-docs-image-tags self-test reached its verdict'; +// ── The self-test's own battery roster and floor (#13489) ────────────────── +// +// `failures.length === 0` used to be this self-test's ONLY success condition, so +// "every case held" and "the cases never ran" printed the same line. Closed the +// way PR #13487 validated on check-doc-authoring: what is pinned is the +// registered NAMES, not a number. Every section opens with `battery('')`, +// every assertion is attributed to the battery most recently opened, and the +// floor requires the OPENED set to equal the DECLARED set with each battery at +// or above its own count. +// +// ⛔ A pinned TOTAL is not the repair: a battery dropping from 9 cases to 3 +// keeps a total "right" the moment a sibling grows. +// +// The counts are a FLOOR, not an equality — adding cases is ordinary work and +// must not red. A battery BELOW its floor means cases stopped running; the +// remedy is to find what stopped registering. +const SELF_TEST_BATTERIES = Object.freeze({ + 'Control A: a STALE corpus is brought to the target': 9, + 'Control B: a CLEAN corpus is left BYTE-IDENTICAL, unwritten': 6, + 'Control C: selectivity ON ONE LINE': 2, + 'Control D: two pins on ONE line, of DIFFERENT lengths': 2, + 'Control E: the gate\'s verdicts the rewrite CANNOT fix': 2, + 'Control F: the expectation refuses to be unusable': 1, + 'Control G: the suffix invariant on the SHARED pattern list': 10, + 'Control H: the shared surface list is the gate\'s, not a copy': 3, +}); + +// DELETING an entry silences that battery's floor exactly as effectively as +// zeroing it, so the roster's own size is pinned too. +const SELF_TEST_BATTERY_FLOOR = 8; + +// The key an assertion is filed under when no battery is open. It is not a +// declared battery, so it reds by the same set difference rather than silently +// inflating whichever battery happened to run last. +const UNATTRIBUTED_BATTERY = '(no battery open)'; + function selfTest() { + // The battery ledger this self-test's floor is evaluated against (#13489). + // `battery()` opens a battery; every assertion below is attributed to the one + // most recently opened, so a section that stops running stops registering and + // names ITSELF at the floor rather than going quiet. + const seen = new Map(); + let openBattery = null; + const battery = (name) => { + openBattery = name; + }; + const registerCase = () => { + const b = openBattery ?? UNATTRIBUTED_BATTERY; + seen.set(b, (seen.get(b) ?? 0) + 1); + }; const failures = []; let checked = 0; const assert = (condition, message) => { + registerCase(); checked++; if (!condition) failures.push(message); }; @@ -313,6 +363,7 @@ function selfTest() { // ── Control A: a STALE corpus is brought to the target ────────────────── // // Every pattern, plus the two mis-pins a prefix-only comparison would miss. + battery('Control A: a STALE corpus is brought to the target'); const staleBody = [ '# Deploy', '', @@ -394,6 +445,7 @@ function selfTest() { // The over-eagerness control. Every shape that must never move is here: the // documented tag SCHEME, rolling tags, the placeholder, an interpolation, and // version-shaped historical prose. + battery('Control B: a CLEAN corpus is left BYTE-IDENTICAL, unwritten'); const cleanBody = [ '# Tags', '', @@ -453,6 +505,7 @@ function selfTest() { // A stale pin sharing a line with a rolling tag and a historical fact: only the // concrete anchored pin moves. This is the assertion that would catch a rewriter // that fell back to a global string replace of the old version. + battery('Control C: selectivity ON ONE LINE'); write( 'mixed/one-line.md', 'Was ghcr.io/objectstack-ai/objectstack:16.0.0 (see :latest, :17.0) — spec 16.0.0 removed it.\n', @@ -478,6 +531,7 @@ function selfTest() { // rewrite whose replacement changes length corrupts every later pin on the line. // The target here is one character LONGER than the first tag, so a wrong order // does not merely misplace the text, it produces visibly broken output. + battery('Control D: two pins on ONE line, of DIFFERENT lengths'); write( 'mixed/two-pins.md', 'ghcr.io/objectstack-ai/objectstack:16.0.0 then ghcr.io/objectstack-ai/objectstack:9.9.9 end\n', @@ -503,6 +557,7 @@ function selfTest() { // A surface that stopped pinning anything, and one that is gone. Neither is // repairable by rewriting, and both must survive as findings rather than being // quietly passed over at the one moment CI is not watching. + battery('Control E: the gate\'s verdicts the rewrite CANNOT fix'); write('rot/rotted.md', 'FROM ghcr.io/objectstack-ai/objectstack:latest\n'); const rot = syncSurfaces({ surfaces: [{ file: 'rot/rotted.md', why: 'fixture' }, { file: 'rot/gone.md', why: 'fixture' }], @@ -521,6 +576,7 @@ function selfTest() { // The catastrophic over-eager case: rewriting every doc pin to `workspace:*` or to // `undefined`. loadExpectedVersion is imported from the gate precisely so this // refusal is the same refusal, not a second implementation of it. + battery('Control F: the expectation refuses to be unusable'); write('bad/pkg.json', '{"version":"workspace:*"}'); let threw = false; try { @@ -535,6 +591,7 @@ function selfTest() { // "Swap the tail" is only safe while every PATTERN captures its tag as a suffix. // That is a property of a list this file does not own, so it is asserted against // the real PATTERNS rather than trusted. + battery('Control G: the suffix invariant on the SHARED pattern list'); for (const pattern of PATTERNS) { const sample = { 'image-tag': 'ghcr.io/objectstack-ai/objectstack:16.0.0', 'build-arg': 'OS_CLI_VERSION=16.0.0', 'npm-pin': '@objectstack/cli@16.0.0' }[pattern.name]; assert( @@ -560,6 +617,7 @@ function selfTest() { assert(suffixThrew, 'replacementFor REFUSES a match whose capture is not a suffix rather than corrupting the line'); // ── Control H: the shared surface list is the gate's, not a copy ──────── + battery('Control H: the shared surface list is the gate\'s, not a copy'); assert( Array.isArray(SURFACES) && SURFACES.length > 0 && SURFACES.every((surface) => typeof surface.file === 'string'), 'SURFACES is imported from check-docs-image-tag.mjs and non-empty — the rewriter and the gate read ONE list', @@ -573,6 +631,51 @@ function selfTest() { rmSync(dir, { recursive: true, force: true }); } + // ── The floor: every declared battery RAN, and ran its cases (#13489) ─── + // + // Evaluated after every battery has had its chance and BEFORE the verdict, so + // the success line below can only be printed by a run in which the set of + // batteries that registered assertions EQUALS the set declared. A set + // difference names WHICH battery stopped; a count says only that something did. + const floorFailure = (message) => { + failures.push(message); + }; + const declaredBatteries = Object.keys(SELF_TEST_BATTERIES); + let floorBreached = false; + if (declaredBatteries.length < SELF_TEST_BATTERY_FLOOR) { + floorBreached = true; + floorFailure( + `SELF_TEST_BATTERIES declares ${declaredBatteries.length} batteries, below the pinned ` + + `${SELF_TEST_BATTERY_FLOOR} — a battery deleted from the roster takes its own floor with it.`, + ); + } + for (const [name, count] of seen) { + if (declaredBatteries.includes(name)) continue; + floorBreached = true; + floorFailure( + `self-test battery "${name}" registered ${count} case(s) but is not declared in ` + + 'SELF_TEST_BATTERIES — an assertion attributed to no declared battery is one nothing floors.', + ); + } + for (const name of declaredBatteries) { + const count = seen.get(name) ?? 0; + if (count >= SELF_TEST_BATTERIES[name]) continue; + floorBreached = true; + floorFailure( + count === 0 + ? `self-test battery "${name}" DID NOT RUN — 0 cases registered, ${SELF_TEST_BATTERIES[name]} pinned. ` + + 'The verdict below would have claimed those cases hold.' + : `self-test battery "${name}" registered ${count} case(s), below its pinned floor of ` + + `${SELF_TEST_BATTERIES[name]} — cases that used to run no longer do.`, + ); + } + if (floorBreached) { + floorFailure( + 'A battery at or below its floor means cases STOPPED RUNNING — the battery is the bug, not the ' + + 'number. Find what stopped registering (an early return, a deleted block, a guard that now ' + + 'skips) and restore it.', + ); + } if (failures.length) { console.error(`✗ sync-docs-image-tags --self-test — ${failures.length} failure(s)\n`); for (const failure of failures) console.error(` • ${failure}`);