From bde0062f0d924ecd12ff2cd3fbb6f6948d9f870f Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 3 Sep 2026 04:56:30 +0000 Subject: [PATCH 1/4] test(scripts): pin a battery roster floor on 40 self-tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `failures.length === 0` was the only success condition in these gates' self-tests, so "every case held" and "the cases never ran" printed the same line. Transplants the PR #13487 shape validated on check-doc-authoring and carried to check-self-test-wired / check-self-test-workflow-commands in PR #13797: every section opens with `battery('')`, every assertion is attributed to the battery most recently opened, and a floor evaluated before the verdict requires the OPENED set to equal the DECLARED set with each battery at or above its own case count. The roster's own size is pinned too, so deleting an entry cannot silence a floor. Registered NAMES are what is pinned, never a total: a set difference says WHICH battery stopped, a count says only that something did. No case was rewritten — the only edits inside each self-test are the roster wiring (a `battery()` opener per existing section banner, one `registerCase()` line in the assertion helper, and the floor block before the verdict). Floors are the per-battery counts measured on this tree. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_018Swfxm6gT9ESmjSS7pqfVw --- scripts/check-adr-anchors.mjs | 99 ++++++++++++ scripts/check-agent-test-spelling.mjs | 111 +++++++++++++ scripts/check-aggregator-roster.mjs | 105 ++++++++++++ scripts/check-bash32-floor.mjs | 122 ++++++++++++++ scripts/check-changeset-no-major.mjs | 113 +++++++++++++ scripts/check-corpus-claim-drift.mjs | 119 ++++++++++++++ scripts/check-declared-population-live.mjs | 91 +++++++++++ scripts/check-doc-route-spelling.mjs | 109 +++++++++++++ scripts/check-docs-image-tag.mjs | 109 +++++++++++++ scripts/check-docs-nav-label.mjs | 105 ++++++++++++ scripts/check-docs-redirects.mjs | 97 +++++++++++ scripts/check-driver-conformance.mjs | 109 +++++++++++++ scripts/check-driver-memory-census.mjs | 100 +++++++++++- scripts/check-empty-changeset.mjs | 129 +++++++++++++++ scripts/check-engine-double-contract.mjs | 150 +++++++++++++++++- scripts/check-nul-bytes.mjs | 99 ++++++++++++ scripts/check-position-name-fold-loaders.mjs | 96 +++++++++++ scripts/check-prerelease-pin-watch.mjs | 103 ++++++++++++ scripts/check-published-readme-exports.mjs | 133 ++++++++++++++++ scripts/check-published-readme-links.mjs | 107 +++++++++++++ .../check-query-options-erasure-ratchet.mjs | 97 ++++++++++- scripts/check-react-page-adapter-contract.mjs | 104 +++++++++++- scripts/check-release-page-status.mjs | 101 ++++++++++++ scripts/check-release-section-coverage.mjs | 105 ++++++++++++ scripts/check-resume-authority-declared.mjs | 102 +++++++++++- scripts/check-role-word.mjs | 109 +++++++++++++ scripts/check-runtime-services-index.mjs | 104 +++++++++++- scripts/check-sdui-lockstep.mjs | 97 +++++++++++ scripts/check-shard-attestation.mjs | 117 ++++++++++++++ scripts/check-single-authz-resolver.mjs | 103 ++++++++++++ scripts/check-slot-lookup-ratchet.mjs | 99 +++++++++++- scripts/check-test-source-alias.mjs | 105 ++++++++++++ scripts/check-test-typecheck.mts | 97 +++++++++++ scripts/check-verify-stand-in-erasure.mjs | 93 +++++++++++ scripts/check-where-matcher-conformance.mjs | 94 ++++++++++- scripts/check-workflow-status-functions.mjs | 103 ++++++++++++ scripts/objectui-changeset-digest.mjs | 119 ++++++++++++++ scripts/objectui-range.mjs | 101 ++++++++++++ scripts/pr-labels.mjs | 100 ++++++++++++ scripts/sync-docs-image-tags.mjs | 103 ++++++++++++ 40 files changed, 4251 insertions(+), 8 deletions(-) diff --git a/scripts/check-adr-anchors.mjs b/scripts/check-adr-anchors.mjs index 43d0d06ceb..361f8be91a 100644 --- a/scripts/check-adr-anchors.mjs +++ b/scripts/check-adr-anchors.mjs @@ -238,6 +238,40 @@ 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, + 'Live tree: green as shipped, red under ablation': 23, + '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 +1164,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 +1285,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 +1385,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 +1497,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 +1665,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 +1878,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 +1971,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..ed8a720862 100644 --- a/scripts/check-single-authz-resolver.mjs +++ b/scripts/check-single-authz-resolver.mjs @@ -545,9 +545,59 @@ 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({ + 'Every exemption carries its reason.': 3, + '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 +605,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 +616,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 +658,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 +667,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 +683,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 +774,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 +812,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 +852,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 +904,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..fcae5de7c3 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 = 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 = null; + const battery = (name) => { + openBattery = name; + }; + const registerCase = () => { + 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) => { + 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}`); From 49274e3fd3117a36c2424692bc66b80e4c018a4f Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 3 Sep 2026 04:58:07 +0000 Subject: [PATCH 2/4] test(scripts): pin the structural invariant, not today's count, on two ledger-driven batteries MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two batteries register one case per row of a list that is meant to shrink: `KNOWN_NUMBER_COLLISIONS` in check-adr-anchors (a resolved collision is deleted, and a stale entry already fails) and `ALLOW` in check-single-authz-resolver (an exemption that exempts nothing is dead weight). A floor at today's count would redden every legitimate removal and train the next author to edit the floor — the one habit these floors exist to prevent. Pinned instead is the part that does not move with the list: the structural cases ran AND at least one row was audited, said in place over each entry. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_018Swfxm6gT9ESmjSS7pqfVw --- scripts/check-adr-anchors.mjs | 9 ++++++++- scripts/check-single-authz-resolver.mjs | 7 ++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/scripts/check-adr-anchors.mjs b/scripts/check-adr-anchors.mjs index 361f8be91a..292689d6ad 100644 --- a/scripts/check-adr-anchors.mjs +++ b/scripts/check-adr-anchors.mjs @@ -259,7 +259,14 @@ const SELF_TEST_BATTERIES = Object.freeze({ 'A tombstone resolves citations but is not anchorable (#7329)': 15, 'Cited numbers resolve (#6634)': 15, 'Cited decision LETTERS resolve (#9592)': 26, - 'Live tree: green as shipped, red under ablation': 23, + // ⛔ NOT today's count. This battery runs one case per `KNOWN_NUMBER_COLLISIONS` + // row (3 today) on top of 20 structural cases, and that list is one entries + // LEAVE — 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: 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, }); diff --git a/scripts/check-single-authz-resolver.mjs b/scripts/check-single-authz-resolver.mjs index ed8a720862..29682986f5 100644 --- a/scripts/check-single-authz-resolver.mjs +++ b/scripts/check-single-authz-resolver.mjs @@ -562,7 +562,12 @@ let selfTestReachedVerdict = false; // 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({ - 'Every exemption carries its reason.': 3, + // ⛔ NOT today's count. One case per `ALLOW` entry (2 today) plus the one + // structural case, and `ALLOW` is a shrink-only exemption list — an exemption + // that stops exempting anything is meant to be deleted (see the comment over + // the declaration). Pinned instead: the structural case ran AND at least one + // exemption 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, From 91cb5d06ce25292297ee56290e75bb786851f854 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 3 Sep 2026 05:06:20 +0000 Subject: [PATCH 3/4] test(scripts): reword the two ledger notes so neither reads as a ratchet-remedy offer The first spelling put `ALLOW` and the words "shrink-only" inside `anchorFor`'s window in check-single-authz-resolver, which flipped that file from `excluded` to `unmarked` under check:ratchet-remedy-authority (and then MISCLASSIFIED against its CONTROL row). Measured, both findings; the gate is green again with the same fact stated without the token. The adr-anchors note is reflowed for readability only. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_018Swfxm6gT9ESmjSS7pqfVw --- scripts/check-adr-anchors.mjs | 12 ++++++------ scripts/check-single-authz-resolver.mjs | 12 +++++++----- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/scripts/check-adr-anchors.mjs b/scripts/check-adr-anchors.mjs index 292689d6ad..26dba8fc10 100644 --- a/scripts/check-adr-anchors.mjs +++ b/scripts/check-adr-anchors.mjs @@ -260,12 +260,12 @@ const SELF_TEST_BATTERIES = Object.freeze({ '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 that list is one entries - // LEAVE — 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: the part that does not move with the list — - // the 20 structural cases ran AND at least one row was actually audited. + // 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, }); diff --git a/scripts/check-single-authz-resolver.mjs b/scripts/check-single-authz-resolver.mjs index 29682986f5..80c81986f0 100644 --- a/scripts/check-single-authz-resolver.mjs +++ b/scripts/check-single-authz-resolver.mjs @@ -562,11 +562,13 @@ let selfTestReachedVerdict = false; // 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. One case per `ALLOW` entry (2 today) plus the one - // structural case, and `ALLOW` is a shrink-only exemption list — an exemption - // that stops exempting anything is meant to be deleted (see the comment over - // the declaration). Pinned instead: the structural case ran AND at least one - // exemption was actually audited. + // ⛔ 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, From 437fd47c59a58dc8ad260a9d883b7fe359e08f1d Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 3 Sep 2026 05:17:39 +0000 Subject: [PATCH 4/4] test(scripts): type the roster wiring in the .mts gate scripts/check-test-typecheck.mts is the one .mts file in the batch and it is reached by the root `tsc --noEmit`, so the untyped `battery()` / `registerCase()` / `floorFailure()` insertions added 7 raw errors and drifted the frozen DEBT entry for @objectstack/spec-monorepo upward (26 -> 33, measured). Typing the wiring and widening the roster to `Readonly>` puts the count back at its recorded 26 with check:type-check-debt green. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_018Swfxm6gT9ESmjSS7pqfVw --- scripts/check-test-typecheck.mts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/scripts/check-test-typecheck.mts b/scripts/check-test-typecheck.mts index fcae5de7c3..aed1886aa3 100644 --- a/scripts/check-test-typecheck.mts +++ b/scripts/check-test-typecheck.mts @@ -532,7 +532,7 @@ const SELF_TEST_VERDICT = 'check-test-typecheck self-test reached its verdict'; // 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({ +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, @@ -554,12 +554,12 @@ function selfTest(): string { // `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) => { + const seen = new Map(); + let openBattery: string | null = null; + const battery = (name: string): void => { openBattery = name; }; - const registerCase = () => { + const registerCase = (): void => { const b = openBattery ?? UNATTRIBUTED_BATTERY; seen.set(b, (seen.get(b) ?? 0) + 1); }; @@ -1018,7 +1018,7 @@ function selfTest(): string { // 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) => { + const floorFailure = (message: string): void => { failures.push(message); }; const declaredBatteries = Object.keys(SELF_TEST_BATTERIES);