diff --git a/scripts/check-filter-alias-parity.mjs b/scripts/check-filter-alias-parity.mjs index 7b44a9333e..f54a07e3f9 100644 --- a/scripts/check-filter-alias-parity.mjs +++ b/scripts/check-filter-alias-parity.mjs @@ -91,6 +91,41 @@ const ts = await requireDefaultExport('typescript', () => import('typescript'), import { parseSourceFile } from './ts-parse.mjs'; 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 tree as it stands: both sides name the same four spellings.': 2, + '2. A fifth spelling on the NORMALIZER side only — the direction #8002 is about, and the one nothing else in the repo refuses.': 3, + '3. The same fifth spelling on BOTH sides is green again — the gate judges parity, not the size of the set.': 1, + '4. A fifth spelling on the INGRESS side only.': 2, + '5. A `$` alias folding INTO a filter spelling is a filter spelling. It reaches `where` through two hops, which is exactly the shape a reader comparing only the slot tables would miss.': 2, + '6. Rot: an unreadable shape must fail LOUDLY. A reader that matches nothing would otherwise compare two empty sets and report a pass.': 5, + '7. The wiring this gate depends on to run at all.': 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)'; + const ROOT = join(fileURLToPath(new URL('.', import.meta.url)), '..'); /** The three files that declare a filter-slot spelling, by repo-relative path. */ @@ -491,14 +526,29 @@ export const FILTER_SLOT_QUERY_PARAMS: readonly string[] = (() => { const SELF_TEST_VERDICT = 'check-filter-alias-parity self-test reached its verdict'; 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 batterySeen = new Map(); + let openBattery = null; + const battery = (name) => { + openBattery = name; + }; + const registerCase = () => { + const b = openBattery ?? UNATTRIBUTED_BATTERY; + batterySeen.set(b, (batterySeen.get(b) ?? 0) + 1); + }; const failures = []; const check = (name, condition, detail) => { + registerCase(); if (!condition) failures.push(`${name}${detail ? ` — ${detail}` : ''}`); }; const run = (protocolText, restText, specText = FIXTURE_SPEC) => judge({ specText, protocolText, restText }); // 1. The tree as it stands: both sides name the same four spellings. + battery('1. The tree as it stands: both sides name the same four spellings.'); { const { problems, protocolSet, restSet } = run(fixtureProtocol(), fixtureRest()); check('agreeing sides are green', problems.length === 0, problems[0]); @@ -512,6 +562,7 @@ function selfTest() { // 2. A fifth spelling on the NORMALIZER side only — the direction #8002 is // about, and the one nothing else in the repo refuses. + battery('2. A fifth spelling on the NORMALIZER side only — the direction #8002 is about, and the one nothing else in the repo refuses.'); { const { problems } = run(fixtureProtocol(['filters', '$filter', 'where_clause']), fixtureRest()); check('a normalizer-only fifth spelling is red', problems.length === 1, `saw ${problems.length}`); @@ -529,6 +580,7 @@ function selfTest() { // 3. The same fifth spelling on BOTH sides is green again — the gate judges // parity, not the size of the set. + battery('3. The same fifth spelling on BOTH sides is green again — the gate judges parity, not the size of the set.'); { const { problems } = run( fixtureProtocol(['filters', '$filter', 'where_clause']), @@ -538,6 +590,7 @@ function selfTest() { } // 4. A fifth spelling on the INGRESS side only. + battery('4. A fifth spelling on the INGRESS side only.'); { const { problems } = run(fixtureProtocol(), fixtureRest(['filters', '$filter', 'where_clause'])); check('an ingress-only fifth spelling is red', problems.length === 1, `saw ${problems.length}`); @@ -551,6 +604,7 @@ function selfTest() { // 5. A `$` alias folding INTO a filter spelling is a filter spelling. It // reaches `where` through two hops, which is exactly the shape a reader // comparing only the slot tables would miss. + battery('5. A `$` alias folding INTO a filter spelling is a filter spelling. It reaches `where` through two hops, which is exactly the shape a reader comparing only the slot tables would miss.'); { const { problems } = run( fixtureProtocol(['filters', '$filter'], [['$top', 'top'], ['$filters', 'filters']]), @@ -562,6 +616,7 @@ function selfTest() { // 6. Rot: an unreadable shape must fail LOUDLY. A reader that matches // nothing would otherwise compare two empty sets and report a pass. + battery('6. Rot: an unreadable shape must fail LOUDLY. A reader that matches nothing would otherwise compare two empty sets and report a pass.'); { const { problems } = run(fixtureProtocol(), '\nexport const SOMETHING_ELSE = [];\n'); check('a missing ingress declaration is red', problems.length === 1, `saw ${problems.length}`); @@ -595,6 +650,7 @@ function selfTest() { } // 7. The wiring this gate depends on to run at all. + battery('7. The wiring this gate depends on to run at all.'); { const pkg = JSON.parse(readFileSync(join(ROOT, 'package.json'), 'utf8')); const entry = pkg.scripts?.['check:filter-alias-parity']; @@ -609,6 +665,50 @@ function selfTest() { check('lint.yml runs the gate exactly once', wired.length === 1, `found ${wired.length}`); } + // ── 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 batterySeen) { + 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 = batterySeen.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:filter-alias-parity --self-test FAILED'); for (const f of failures) console.error(` - ${f}`); diff --git a/scripts/check-i18n-bundles.mjs b/scripts/check-i18n-bundles.mjs index f520562d21..3bf3decb06 100644 --- a/scripts/check-i18n-bundles.mjs +++ b/scripts/check-i18n-bundles.mjs @@ -103,6 +103,38 @@ import { import { EXIT_FINDINGS, EXIT_PREREQUISITE_NOT_MET } from './import-prerequisite.mjs'; import { findExtractConfigs, flagsFromDocstring } from './i18n-bundle-surface.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({ + 'Third classifier (#5217): the build prerequisite. Same anti-#4690 duty as': 34, + 'Fourth classifier (#7681): the OTHER prerequisite — a workspace package this': 24, + 'Fifth classifier (#11647): the POPULATION — is there anything to grade, and': 11, + 'The other cause, and the reason `=== 0` alone is not the whole condition: a': 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)'; + /** * The module this gate's POPULATION is enumerated by, declared as a whole * literal so the derivation can see it (#9116). @@ -436,8 +468,23 @@ function populationVerdict(population, activeFilter) { const SELF_TEST_VERDICT = 'check-i18n-bundles self-test reached its verdict'; 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 batterySeen = new Map(); + let openBattery = null; + const battery = (name) => { + openBattery = name; + }; + const registerCase = () => { + const b = openBattery ?? UNATTRIBUTED_BATTERY; + batterySeen.set(b, (batterySeen.get(b) ?? 0) + 1); + }; + battery('Third classifier (#5217): the build prerequisite. Same anti-#4690 duty as'); const failures = []; const expect = (name, cond, detail) => { + registerCase(); if (!cond) failures.push(`${name} — ${detail}`); }; @@ -635,6 +682,7 @@ function selfTest() { // send the reader to a rebuild that changes nothing — the #5862 defect (a // confident diagnosis pointing somewhere innocent) rebuilt one layer down. // ------------------------------------------------------------------------- + battery('Fourth classifier (#7681): the OTHER prerequisite — a workspace package this'); // The failure #7681 reported, as node actually prints it: produced locally by // importing a name that a package's built ESM does not export — a fixture @@ -794,6 +842,7 @@ function selfTest() { // classifier in this file is proven red against a recorded string, but "did // this gate look at anything at all?" can only be proven by looking. // ------------------------------------------------------------------------- + battery('Fifth classifier (#11647): the POPULATION — is there anything to grade, and'); const popCwdBefore = process.cwd(); let offRootPopulation; @@ -896,6 +945,7 @@ function selfTest() { // The other cause, and the reason `=== 0` alone is not the whole condition: a // filter that matched nothing is a typo, not an environment fact. + battery('The other cause, and the reason `=== 0` alone is not the whole condition: a'); const filterVerdict = populationVerdict(onRootPopulation, 'no-such-package'); expect('#11647 an unmatched --filter is refused', !!filterVerdict, 'a filter matching nothing must not render as OK (0 package(s))'); expect( @@ -936,6 +986,50 @@ function selfTest() { const longWalkError = unreadablePopulationDetail(new Error('E'.repeat(400))).join('\n'); expect('#11647 long walk errors are truncated', longWalkError.includes(`${'E'.repeat(160)}…`), 'a 400-char message must not be pasted whole'); + // ── 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 batterySeen) { + 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 = batterySeen.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:i18n --self-test — ${failures.length} failure(s)\n`); for (const f of failures) console.error(` ${f}`); diff --git a/scripts/check-i18n-coverage.mjs b/scripts/check-i18n-coverage.mjs index 024475b615..09bead52b4 100644 --- a/scripts/check-i18n-coverage.mjs +++ b/scripts/check-i18n-coverage.mjs @@ -183,6 +183,38 @@ import { } from './cli-build-prerequisite.mjs'; import { EXIT_FINDINGS, EXIT_PREREQUISITE_NOT_MET } from './import-prerequisite.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 per-config failure classifier and the collecting round (#6033). Pinned': 29, + '#11395 — the same rule, pinned over EVERY failure branch instead of one.': 23, + 'Root anchoring and the population classifier (#10907). These are the only': 4, + 'The build-prerequisite CLOSURE (#12564). What makes the remedy worth naming': 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 = 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)'; + const HERE = dirname(fileURLToPath(import.meta.url)); /** This script lives in `scripts/`, so the repo root is one level up (#10907). */ const REPO_ROOT = resolve(HERE, '..'); @@ -661,8 +693,23 @@ function measureAllConfigs(configPaths, measure) { const SELF_TEST_VERDICT = 'check-i18n-coverage self-test reached its verdict'; 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 batterySeen = new Map(); + let openBattery = null; + const battery = (name) => { + openBattery = name; + }; + const registerCase = () => { + const b = openBattery ?? UNATTRIBUTED_BATTERY; + batterySeen.set(b, (batterySeen.get(b) ?? 0) + 1); + }; + battery('The per-config failure classifier and the collecting round (#6033). Pinned'); const failures = []; const expect = (name, cond, detail) => { + registerCase(); if (!cond) failures.push(`${name} — ${detail}`); }; @@ -834,6 +881,7 @@ function selfTest() { // Each branch is driven with THREE configs failing the SAME way, with per-config // detail in the CLI's words — the shape a real round produces. // ------------------------------------------------------------------------- + battery('#11395 — the same rule, pinned over EVERY failure branch instead of one.'); const SAME_CAUSE_CONFIGS = [ 'examples/app-crm/objectstack.config.ts', @@ -918,6 +966,7 @@ function selfTest() { // recorded string, but "did this gate look at anything at all?" can only be // proven by looking. // ------------------------------------------------------------------------- + battery('Root anchoring and the population classifier (#10907). These are the only'); // The derivation must land on THIS repo's root — one level off would still find // a `scripts/` directory, so pin files only the root has, this gate's own two @@ -967,6 +1016,7 @@ function selfTest() { // the population. A partial closure is the worse half of this card's defect: it // looks derived, it is specific, and it still does not converge. // ------------------------------------------------------------------------- + battery('The build-prerequisite CLOSURE (#12564). What makes the remedy worth naming'); const derivedClosure = closureBuildFix(onRoot); expect( '#12564 the live population yields a closure', @@ -1105,6 +1155,50 @@ function selfTest() { `the real tree resolved to ${onRoot.length} config(s) and must be judged, not refused`, ); + // ── 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 batterySeen) { + 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 = batterySeen.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:i18n-coverage --self-test — ${failures.length} failure(s)\n`); for (const f of failures) console.error(` ${f}`); diff --git a/scripts/check-page-declaration-shape.mjs b/scripts/check-page-declaration-shape.mjs index 2ff82b21c8..00b6c35000 100644 --- a/scripts/check-page-declaration-shape.mjs +++ b/scripts/check-page-declaration-shape.mjs @@ -96,6 +96,42 @@ import { join } from 'node:path'; import { isEntrypoint } from './invoked-as.mjs'; import { maskComments } from './js-comment-mask.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 red case the card asks for': 1, + 'The two discoverable doors': 2, + 'Comment masking, both directions': 3, + 'The noise classes, refused by construction': 3, + 'The computed-carrier recognizer, both directions': 2, + 'Primitives': 2, + 'The live tree, through the SAME pass production runs': 4, + 'The declared population, held mechanically': 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)'; + const ROOT = new URL('..', import.meta.url).pathname; /** @@ -373,8 +409,22 @@ function findingMessage({ file, line, name, decl }) { let selfTestReachedVerdict = false; 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 batterySeen = new Map(); + let openBattery = null; + const battery = (name) => { + openBattery = name; + }; + const registerCase = () => { + const b = openBattery ?? UNATTRIBUTED_BATTERY; + batterySeen.set(b, (batterySeen.get(b) ?? 0) + 1); + }; let failed = 0; const t = (label, ok) => { + registerCase(); console.log(`${ok ? 'ok ' : 'FAIL'} ${label}`); if (!ok) failed++; }; @@ -398,6 +448,7 @@ function selfTest() { }; // ── The red case the card asks for ──────────────────────────────────────── + battery('The red case the card asks for'); t('an un-annotated raw-literal page in a bundle `pages:` array is FOUND (the ' + '#11480 shape: `export const X = { ... }` reaching the kernel)', judge(` @@ -406,6 +457,7 @@ function selfTest() { `).join(',') === 'ConnectPage'); // ── The two discoverable doors ──────────────────────────────────────────── + battery('The two discoverable doors'); t('the `: Page` annotation clears it', judge(` export const P: Page = { name: 'p', regions: [] }; export const BUNDLE = { pages: [P] }; @@ -417,6 +469,7 @@ function selfTest() { `).length === 0); // ── Comment masking, both directions ────────────────────────────────────── + battery('Comment masking, both directions'); t('a `pages:` array MENTIONED in a docblock does not count as a carrier — the ' + 'sibling gates\' own headers quote one', judge(` /** Registered as \`pages: [GhostPage]\` by the plugin. */ @@ -435,6 +488,7 @@ function selfTest() { `).join(',') === 'P'); // ── The noise classes, refused by construction ──────────────────────────── + battery('The noise classes, refused by construction'); t('a BOOK\'s page-NAME list is ignored — a string is a reference, never a ' + 'declaration (`pages: [\'showcase_index\']`, book.zod.ts)', judge(`export const BOOK = { groups: [{ pages: ['showcase_index', 'tour'] }] };`).length === 0); @@ -447,6 +501,7 @@ function selfTest() { judge(`export const X = { subpages: [Ghost] }; const y = a.pages[0];`).length === 0); // ── The computed-carrier recognizer, both directions ───────────────────── + battery('The computed-carrier recognizer, both directions'); const computed = computedCarrierSites(); t('the computed-carrier recognizer finds the one real unenumerable carrier ' + '(`pages: Object.values(pages)`, examples/app-crm/objectstack.config.ts)', @@ -457,6 +512,7 @@ function selfTest() { !computed.some((c) => c.file.endsWith('.zod.ts') || c.file.endsWith('utils/format.ts'))); // ── Primitives ──────────────────────────────────────────────────────────── + battery('Primitives'); t('matchBracket is quote-aware — a `]` inside a string cannot close the array', (() => { const s = `[ 'a]b', C ]`; @@ -466,6 +522,7 @@ function selfTest() { splitEntries(`A, { pages: [X, Y] }, B`).length === 3); // ── The live tree, through the SAME pass production runs ────────────────── + battery('The live tree, through the SAME pass production runs'); const live = scan(); t(`the walk reaches a real population (${live.files} sources, ${live.carriers.length} ` + 'identifier entries) — a gate that silently reads nothing is green forever', @@ -480,6 +537,7 @@ function selfTest() { live.carriers.some((c) => live.declarations.get(c.name)?.door === 'definePage')); // ── The declared population, held mechanically ──────────────────────────── + battery('The declared population, held mechanically'); t('every declared glob names a root this gate really walks — a declaration ' + 'that can drift from the scan is worse than none', carrierRoots().every((r) => existsSync(join(ROOT, r)))); @@ -487,6 +545,50 @@ function selfTest() { + 'it and neither shrink-only bare-root ledger is owed a row', PAGE_CARRIER_GLOBS.every((g) => g.includes('/'))); + // ── 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) => { console.log(`FAIL ${message}`); failed++; }; + 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 batterySeen) { + 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 = batterySeen.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.', + ); + } + console.log(failed ? `\ncheck-page-declaration-shape --self-test: ${failed} FAILED` : '\ncheck-page-declaration-shape --self-test: all passed'); selfTestReachedVerdict = true; return failed === 0; diff --git a/scripts/check-skill-identifier-liveness.mjs b/scripts/check-skill-identifier-liveness.mjs index 4d56f2c7b5..3e9d519374 100644 --- a/scripts/check-skill-identifier-liveness.mjs +++ b/scripts/check-skill-identifier-liveness.mjs @@ -215,6 +215,44 @@ import { fileURLToPath } from 'node:url'; import process from 'node:process'; 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({ + 'extractor': 26, + 'LEG 1 end to end, incl. the positive control': 7, + 'LEG 2: symbols, sections, and the two structural safety rules': 23, + 'both legs in ONE walk': 1, + '#8435: whose remedy is the expanding one': 3, + 'the ledgers move in opposite directions, and the source says so': 2, + 'dispatch-gates coupling, both ways': 4, + 'the shipped table is well-formed': 5, + 'suggest is advisory and must never be able to fail anything': 2, + 'refusal before scanning': 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 = 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)'; + const HERE = dirname(fileURLToPath(import.meta.url)); const REPO_ROOT = join(HERE, '..'); @@ -888,12 +926,26 @@ const SELF_TEST_VERDICT = 'check-skill-identifier-liveness self-test reached its * clothes. */ 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 batterySeen = new Map(); + let openBattery = null; + const battery = (name) => { + openBattery = name; + }; + const registerCase = () => { + const b = openBattery ?? UNATTRIBUTED_BATTERY; + batterySeen.set(b, (batterySeen.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 eq = (label, a, b) => expect(`${label} (got ${JSON.stringify(a)}, want ${JSON.stringify(b)})`, JSON.stringify(a) === JSON.stringify(b)); // ── extractor ──────────────────────────────────────────────────────────── + battery('extractor'); eq('citationOf: bare backticked identifier', citationOf('`foo`'), 'foo'); eq('citationOf: trailing parenthetical is STRIPPED, not rejected', citationOf('`none` (default)'), 'none'); @@ -948,6 +1000,7 @@ function selfTest() { deadSegments('etl.schedule', new Set(['etl'])), ['schedule']); // ── LEG 1 end to end, incl. the positive control ───────────────────────── + battery('LEG 1 end to end, incl. the positive control'); const corpus1 = [{ file: 'skills/s/SKILL.md', text: ['| Key | Meaning |', '|:---|:---|', '| `liveKey` | ok |', '| `phantomKey` | bad |'].join('\n'), @@ -974,6 +1027,7 @@ function selfTest() { expect('the stale message asks for --update', staleExemptionMessage(r1s.stale).includes('--update')); // ── LEG 2: symbols, sections, and the two structural safety rules ──────── + battery('LEG 2: symbols, sections, and the two structural safety rules'); const specText = [ "export const Colour = z.enum(['red', 'green', 'blue']);", "const Legacy = z.enum(['a', // 'notAMember'", @@ -1074,6 +1128,7 @@ function selfTest() { expect('duplicate binding ids are refused', r2dupe.errors.some((e) => e.includes('duplicate binding id'))); // ── both legs in ONE walk ──────────────────────────────────────────────── + battery('both legs in ONE walk'); const both = run({ corpus: [{ file: 'skills/s/SKILL.md', text: `${doc}\n| K | M |\n|:--|:--|\n| \`ghost\` | x |` }], index: idxDoc, bindings: [synthBinding], sources: sources2, ledger: noLedger, @@ -1083,6 +1138,7 @@ function selfTest() { && both.errors.some((e) => e.startsWith('[leg2-missing-row]'))); // ── #8435: whose remedy is the expanding one ───────────────────────────── + battery('#8435: whose remedy is the expanding one'); expect('#8435 — the phantom message marks the ledger path ' + RATCHET_AUTHORITY_MARKER, phantomMessage({ file: 'f', line: 1, identifier: 'x' }, ['x']).includes(RATCHET_AUTHORITY_MARKER)); expect('#8435 — the missing-row message marks the ledger path ' + RATCHET_AUTHORITY_MARKER, @@ -1092,12 +1148,14 @@ function selfTest() { < phantomMessage({ file: 'f', line: 1, identifier: 'x' }, ['x']).indexOf(RATCHET_AUTHORITY_MARKER)); // ── the ledgers move in opposite directions, and the source says so ────── + battery('the ledgers move in opposite directions, and the source says so'); expect('--update is documented as PRUNE-ONLY for Leg 1 exemptions', /--update\s+NEVER adds one/.test(selfSource())); expect('the header states the asymmetry between the two ledgers', selfSource().includes('PRUNE-ONLY') && selfSource().includes('REWRITTEN FROM THE TREE')); // ── dispatch-gates coupling, both ways ─────────────────────────────────── + battery('dispatch-gates coupling, both ways'); eq('every separator-less ROOT is declared for dispatch-gates', ROOTS.filter((r) => !r.includes('/')).map((r) => `${r}/**`).sort(), [...ROOT_DIR_WATCH_HINTS].sort()); @@ -1117,6 +1175,7 @@ function selfTest() { hintDecl[0].includes("'skills/**'") && !hintDecl[0].includes('map(')); // ── the shipped table is well-formed ───────────────────────────────────── + battery('the shipped table is well-formed'); eq('shipped binding ids are unique', BINDINGS.length, new Set(BINDINGS.map((b) => b.id)).size); expect('every shipped binding carries a `why` a reviewer can act on', @@ -1129,6 +1188,7 @@ function selfTest() { BINDINGS.every((b) => b.source.startsWith('packages/'))); // ── --suggest is advisory and must never be able to fail anything ─────── + battery('suggest is advisory and must never be able to fail anything'); const sug = suggestions({ corpus: corpus2, sources: sources2, bindings: [] }); expect('--suggest returns candidates without raising findings', Array.isArray(sug)); expect('--suggest skips what is already registered', @@ -1136,11 +1196,56 @@ function selfTest() { .every((c) => c.symbol !== 'Colour')); // ── refusal before scanning ────────────────────────────────────────────── + battery('refusal before scanning'); eq('a configured root that does not resolve is refused, not skipped', missingRoots(['skills', 'nope'], REPO_ROOT, (p) => !String(p).endsWith('nope')), ['nope']); expect('the refusal explains why a partial population is not a verdict', missingRootsMessage(['nope']).includes('nobody configured')); + // ── 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 batterySeen) { + 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 = batterySeen.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(`\ncheck-skill-identifier-liveness --self-test: ${failures.length} failure(s).\n`); for (const f of failures) console.error(` ✗ ${f}`); diff --git a/scripts/check-type-source-resolution.mjs b/scripts/check-type-source-resolution.mjs index 4a81aa97ab..2f6ed1be98 100644 --- a/scripts/check-type-source-resolution.mjs +++ b/scripts/check-type-source-resolution.mjs @@ -161,6 +161,45 @@ import { configsNamedByTypecheck, selfTest as typecheckConfigsSelfTest } from '. import { tmpdir } from 'node:os'; import process from 'node:process'; +// ── 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 defect is caught': 4, + 'THE TRAP: `@fx/spec*`, star not after a separator': 2, + 'a rule that matches nothing is not coverage': 2, + 'the CORRECT spellings must stay quiet': 6, + 'false positives': 3, + 'fail-closed': 2, + '#11490: the population is per PROGRAM': 6, + 'the registry, audited in BOTH directions': 10, + 'census guard: sibling-config discovery going quiet is INVISIBLE': 14, + 'the import clause is bounded to ONE statement (#12555)': 8, + '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 = 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)'; + const HERE = dirname(fileURLToPath(import.meta.url)); const REPO_ROOT = resolve(HERE, '..'); @@ -1782,9 +1821,23 @@ function buildFixtureTree() { const SELF_TEST_VERDICT = 'check-type-source-resolution self-test reached its verdict'; 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 batterySeen = new Map(); + let openBattery = null; + const battery = (name) => { + openBattery = name; + }; + const registerCase = () => { + const b = openBattery ?? UNATTRIBUTED_BATTERY; + batterySeen.set(b, (batterySeen.get(b) ?? 0) + 1); + }; const root = buildFixtureTree(); const problems = []; const expect = (condition, message) => { + registerCase(); if (!condition) problems.push(message); }; const has = (failures, needle) => failures.some((f) => f.includes(needle)); @@ -1794,6 +1847,7 @@ function selfTest() { const bare = check(root, {}); // ── the defect is caught ────────────────────────────────────────────── + battery('the defect is caught'); expect(reported(bare, 'packages/violator'), 'a package with no `paths` at all was not reported'); expect( reported(bare, 'packages/type-only'), @@ -1805,6 +1859,7 @@ function selfTest() { // ── THE TRAP: `@fx/spec*`, star not after a separator ───────────────── // Every specifier in this fixture lands under `src/`, so "did it reach // source" says yes. The gate must still refuse the spelling. + battery('THE TRAP: `@fx/spec*`, star not after a separator'); expect(reported(bare, 'packages/star-trap'), 'the `@pkg*` trap (star not preceded by a separator) was NOT flagged'); expect( has(bare.failures, '@fx/spec-tools'), @@ -1812,10 +1867,12 @@ function selfTest() { ); // ── a rule that matches nothing is not coverage ─────────────────────── + battery('a rule that matches nothing is not coverage'); expect(reported(bare, 'packages/missing-target'), 'a `paths` target that does not exist was accepted as a rule'); expect(has(bare.failures, 'does not exist'), 'the missing-target diagnostic did not say the target is missing'); // ── the CORRECT spellings must stay quiet ───────────────────────────── + battery('the CORRECT spellings must stay quiet'); expect(!reported(bare, 'packages/compliant'), 'the two-rule compliant config was reported'); expect(!reported(bare, 'packages/jsonc'), 'a CORRECT config was reported because its comments were not stripped'); expect(!reported(bare, 'packages/inherits'), 'a correct `paths` block inherited through `extends` was not seen'); @@ -1830,6 +1887,7 @@ function selfTest() { ); // ── false positives ─────────────────────────────────────────────────── + battery('false positives'); expect(!reported(bare, 'packages/no-workspace-dep'), 'a package with no workspace dep at all was flagged'); expect(!reported(bare, 'packages/consumes-source'), 'a dep whose types already point at source was flagged'); expect( @@ -1838,6 +1896,7 @@ function selfTest() { ); // ── fail-closed ─────────────────────────────────────────────────────── + battery('fail-closed'); expect(reported(bare, 'packages/unparseable'), 'an unparseable tsconfig was read as resolving nothing'); expect(has(bare.failures, 'cannot be read'), 'an unparseable tsconfig did not fail as unreadable'); @@ -1848,6 +1907,7 @@ function selfTest() { // import were REPORTED through the build config and SILENT through the // prescribed sibling, so what has to hold is that the two spellings now // report the SAME THING. + battery('#11490: the population is per PROGRAM'); expect( reported(bare, 'packages/sibling-config'), 'an exposure reachable only through the sibling `tsconfig.test.json` this repo PRESCRIBES was not ' @@ -1879,6 +1939,7 @@ function selfTest() { ); // ── the registry, audited in BOTH directions ────────────────────────── + battery('the registry, audited in BOTH directions'); const measuredNames = { '@fx/violator': ['@fx/spec'], '@fx/type-only': ['@fx/spec'], @@ -1964,6 +2025,7 @@ function selfTest() { // line nobody has ever seen fire. Same shape as (17) minus the `typecheck` // script that names the sibling — which is also exactly what the whole // widening looks like after a regression. + battery('census guard: sibling-config discovery going quiet is INVISIBLE'); const singleProgram = join(tmpdir(), `os-type-source-resolution-single-${process.pid}`); rmSync(singleProgram, { recursive: true, force: true }); mkdirSync(join(singleProgram, 'packages'), { recursive: true }); @@ -2031,6 +2093,7 @@ function selfTest() { // a false GREEN on an axis whose whole job is fail-closed, so it is pinned // just as hard. A detector that silently stops matching reports a spotless // repo. + battery('the import clause is bounded to ONE statement (#12555)'); const typeSpecs = (code) => [...new Set(extractTypeImports(code))].sort(); const tA = typeSpecs("import 'pkg/kernel';\nconst x = 1;\n"); const tB = typeSpecs("import 'pkg/kernel';\nimport { X } from 'other';\n"); @@ -2088,6 +2151,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)) @@ -2113,6 +2177,50 @@ 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 batterySeen) { + 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 = batterySeen.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-type-source-resolution --self-test FAILED:'); for (const problem of problems) console.error(` - ${problem}`); diff --git a/scripts/docs-audit/check-audit-scope.mjs b/scripts/docs-audit/check-audit-scope.mjs index 53dd724ef4..60debd41a3 100644 --- a/scripts/docs-audit/check-audit-scope.mjs +++ b/scripts/docs-audit/check-audit-scope.mjs @@ -122,6 +122,36 @@ import { fileURLToPath } from 'node:url'; import { createContext, runInContext } from 'node:vm'; 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({ + 'release-owned pages: in scope, read-only (#4920)': 25, + 'the injection contract (#13591)': 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 = 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)'; + const HERE = dirname(fileURLToPath(import.meta.url)); const REPO_ROOT = execFileSync('git', ['rev-parse', '--show-toplevel'], { cwd: HERE }) .toString() @@ -815,9 +845,24 @@ async function checkInjection(source) { * discovered the next time a directory is renamed. */ 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 batterySeen = new Map(); + let openBattery = null; + const battery = (name) => { + openBattery = name; + }; + const registerCase = () => { + const b = openBattery ?? UNATTRIBUTED_BATTERY; + batterySeen.set(b, (batterySeen.get(b) ?? 0) + 1); + }; + battery('release-owned pages: in scope, read-only (#4920)'); let failed = 0; let total = 0; const check = (label, want, got) => { + registerCase(); total++; if (JSON.stringify(got) !== JSON.stringify(want)) { console.error(` ✗ ${label}: expected ${JSON.stringify(want)}, got ${JSON.stringify(got)}`); @@ -825,6 +870,7 @@ async function selfTest() { } }; const throws = (label, fn, needle) => { + registerCase(); total++; try { fn(); @@ -979,6 +1025,7 @@ async function selfTest() { // that can silently break is the BODY's consumption of it, not the list. Observed on // the real workflow, then broken four ways in memory — each way is a shape a // plausible future edit takes, and each must be seen to go red. + battery('the injection contract (#13591)'); check('the workflow consumes the scope it is handed', [], await checkScopeInjection(workflowSource)); const injectionMutants = [ @@ -1024,6 +1071,50 @@ async 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) => { console.error(` ✗ ${message}`); failed++; }; + 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 batterySeen) { + 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 = batterySeen.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) { console.error(`\n✗ check-audit-scope self-test failed (${failed} case(s)).`); process.exit(1); diff --git a/scripts/measure-stall-guard-headroom.mjs b/scripts/measure-stall-guard-headroom.mjs index 8980c0a7b4..6730ca416e 100644 --- a/scripts/measure-stall-guard-headroom.mjs +++ b/scripts/measure-stall-guard-headroom.mjs @@ -156,6 +156,49 @@ import { requireDependency } from './import-prerequisite.mjs'; import { isEntrypoint } from './invoked-as.mjs'; import { WORKFLOW_DIR, guardDefaults, scan } from './check-stall-guard-budget.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 declared population, held against the constant the sweep READS': 4, + '1. GREEN: a short healthy run clears both paths.': 2, + '2. RED, deferred only: p+s big enough to lose the cap path but not the window path. With W=10 C=20 T=30 that is any p+s in (10, 20).': 3, + '3. RED, both paths: a run longer than T - W.': 2, + '4. REFUSAL: a payload with no guarded step at all must not read as green.': 2, + '5. No input at all is a refusal, not a vacuous pass.': 1, + '6. The margin is NAMED, never folded in silently.': 1, + '7. A step whose timestamps are unusable is DROPPED, never counted as zero.': 1, + '8. The population claim this whole card rests on, CHECKED not assumed: the step name resolves a site here only because it is unique across the sweep. The day that stops being true, this assertion is the alarm.': 2, + '9. ASYMMETRIC OBSERVATIONS -- the sharpest direction, and the one that is not conservative. Only the LOOSE job ran, and it ran fast. Keyed on the name, the TIGHT site (T 30m) was handed that 2m reading and printed `COVERED, 8m00s to spare` while quoting `worst on \\`Loose Job\\`` -- a confident green for a step these runs never executed. It must now say it was not observed, and nothing else.': 3, + '10. Both observed, with wildly different readings. Keyed on the name the tight site inherited the loose job\'s 60m and was reported UNCOVERED; each site must now be judged against its OWN observation.': 3, + '11. REFUSAL: a payload that carries neither `workflow_name` nor a job name cannot separate the two candidates. Nothing here is resolvable, so nothing is picked -- not the worst, not the first, not the average.': 4, + '12. REFUSAL when the TRIPLE ITSELF collides -- two guarded steps sharing a name inside ONE job. A fully identified payload does not help: the two sites are indistinguishable on (file, job, step), so a fourth component would be needed and there is none. The refusal branch has to know that, which is why this case exists and does not assert a resolution.': 2, + '13. The matcher has to accept the shapes THIS tree really produces -- matrix job names expanded from a `${{ ... }}` template under the workflow\'s own `name:`. A join that only works on fixtures is not one, and this sweeps EVERY site rather than a hand-picked easy one.': 3, + '14. The other side of the same matcher: an observation whose runner identity CONTRADICTS the only site carrying that step name is excluded and said out loud, and the site reads NOT OBSERVED. That is the safe direction -- an excluded observation cannot invent headroom -- but the old join attributed it, which is this defect with the collision size fixed at one.': 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 = 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)'; + /** Refusal to measure, kept distinct from a finding -- see check-stall-guard-budget. */ export const EXIT_REFUSED = 2; @@ -719,9 +762,23 @@ export async function main(argv, io = {}) { let selfTestReachedVerdict = false; 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 batterySeen = new Map(); + let openBattery = null; + const battery = (name) => { + openBattery = name; + }; + const registerCase = () => { + const b = openBattery ?? UNATTRIBUTED_BATTERY; + batterySeen.set(b, (batterySeen.get(b) ?? 0) + 1); + }; const failures = []; let checked = 0; const assert = (name, ok, detail) => { + registerCase(); checked += 1; if (!ok) failures.push(detail ? `${name} -- ${detail}` : name); }; @@ -737,6 +794,7 @@ export async function selfTest() { // `WORKFLOW_DIR`, the constant `scan` joins every workflow path from: move the // read and this reds, in this file, rather than in a dispatch brief nobody // reads as evidence. + battery('The declared population, held against the constant the sweep READS'); assert( 'the declared watch-hint population is exactly the workflow directory this tool sweeps', ROOT_DIR_WATCH_HINTS.length === 1 && ROOT_DIR_WATCH_HINTS[0] === `${WORKFLOW_DIR}/**`, @@ -855,12 +913,14 @@ export async function selfTest() { const site = swept.sites.find((s) => s.step === "Run this shard's tests") ?? swept.sites[0]; // 1. GREEN: a short healthy run clears both paths. + battery('1. GREEN: a short healthy run clears both paths.'); const green = await drive(['--from', write(payload(site, 30, 60))]); assert('a short healthy run reports covered on both paths', green.code === 0, green.out); assert('...and the green is a MEASUREMENT: it names the observation it made', /worst observed: p /.test(green.out), green.out); // 2. RED, deferred only: p+s big enough to lose the cap path but not the window path. // With W=10 C=20 T=30 that is any p+s in (10, 20). + battery('2. RED, deferred only: p+s big enough to lose the cap path but not the window path. With W=10 C=20 T=30 that is any p+s in (10, 20).'); const midSec = Math.round((site.budget - site.cap + (site.budget - site.window)) / 2 * 60); const mid = await drive(['--from', write(payload(site, 30, midSec - 30))]); assert('a run that only loses the DEFERRED path is reported red', mid.code === 1, mid.out); @@ -868,24 +928,29 @@ export async function selfTest() { assert('...while still reporting the undeferred path as covered', /undeferred.*COVERED/.test(mid.out), mid.out); // 3. RED, both paths: a run longer than T - W. + battery('3. RED, both paths: a run longer than T - W.'); const both = await drive(['--from', write(payload(site, 30, (site.budget - site.window) * 60))]); assert('a run that loses BOTH paths is reported red', both.code === 1, both.out); assert('...and says the undeferred path is uncovered too', /undeferred.*UNCOVERED/.test(both.out), both.out); // 4. REFUSAL: a payload with no guarded step at all must not read as green. + battery('4. REFUSAL: a payload with no guarded step at all must not read as green.'); const empty = await drive(['--from', write(payload(site, 30, 60, 'Some Unrelated Step'))]); assert('a payload with no guarded step REFUSES rather than printing green', empty.code === EXIT_REFUSED, empty.out); assert('...and says nothing was measured', /REFUSING/.test(empty.out), empty.out); // 5. No input at all is a refusal, not a vacuous pass. + battery('5. No input at all is a refusal, not a vacuous pass.'); const none = await drive([]); assert('no --run and no --from refuses', none.code === EXIT_REFUSED, none.out); // 6. The margin is NAMED, never folded in silently. + battery('6. The margin is NAMED, never folded in silently.'); const withMargin = await drive(['--from', write(payload(site, 30, 60)), '--margin-minutes', '5']); assert('a supplied margin is disclosed in the output', /margin:\s+\+5m/.test(withMargin.out), withMargin.out); // 7. A step whose timestamps are unusable is DROPPED, never counted as zero. + battery('7. A step whose timestamps are unusable is DROPPED, never counted as zero.'); const broken = payload(site, 30, 60); broken.jobs[0].steps[1].completed_at = null; const dropped = await drive(['--from', write(broken)]); @@ -896,6 +961,7 @@ export async function selfTest() { // 8. The population claim this whole card rests on, CHECKED not assumed: the // step name resolves a site here only because it is unique across the // sweep. The day that stops being true, this assertion is the alarm. + battery('8. The population claim this whole card rests on, CHECKED not assumed: the step name resolves a site here only because it is unique across the sweep. The day that stops being true, this assertion is the alarm.'); const names = swept.sites.map((s) => s.step); assert( 'every guard-wrapped step in this tree carries a distinct name (the name-as-key premise)', @@ -923,6 +989,7 @@ export async function selfTest() { // `COVERED, 8m00s to spare` while quoting `worst on \`Loose Job\`` -- a // confident green for a step these runs never executed. It must now say // it was not observed, and nothing else. + battery('9. ASYMMETRIC OBSERVATIONS -- the sharpest direction, and the one that is not conservative. Only the LOOSE job ran, and it ran fast. Keyed on the name, the TIGHT site (T 30m) was handed that 2m reading and printed `COVERED, 8m00s to spare` while quoting `worst on \\`Loose Job\\`` -- a confident green for a step these runs never executed. It must now say it was not observed, and nothing else.'); const onlyLoose = await drive([ '--root', coNamed, '--from', write({ jobs: [runnerJob({ job: 'Loose Job', workflow: 'Loose', step: SHARED, prepSec: 60, runSec: 60 })] }), @@ -942,6 +1009,7 @@ export async function selfTest() { // 10. Both observed, with wildly different readings. Keyed on the name the // tight site inherited the loose job's 60m and was reported UNCOVERED; // each site must now be judged against its OWN observation. + battery('10. Both observed, with wildly different readings. Keyed on the name the tight site inherited the loose job\'s 60m and was reported UNCOVERED; each site must now be judged against its OWN observation.'); const bothSeen = await drive([ '--root', coNamed, '--from', write({ @@ -966,6 +1034,7 @@ export async function selfTest() { // 11. REFUSAL: a payload that carries neither `workflow_name` nor a job name // cannot separate the two candidates. Nothing here is resolvable, so // nothing is picked -- not the worst, not the first, not the average. + battery('11. REFUSAL: a payload that carries neither `workflow_name` nor a job name cannot separate the two candidates. Nothing here is resolvable, so nothing is picked -- not the worst, not the first, not the average.'); const blind = write({ jobs: [{ conclusion: 'success', @@ -985,6 +1054,7 @@ export async function selfTest() { // sites are indistinguishable on (file, job, step), so a fourth component // would be needed and there is none. The refusal branch has to know that, // which is why this case exists and does not assert a resolution. + battery('12. REFUSAL when the TRIPLE ITSELF collides -- two guarded steps sharing a name inside ONE job. A fully identified payload does not help: the two sites are indistinguishable on (file, job, step), so a fourth component would be needed and there is none. The refusal branch has to know that, which is why this case exists and does not assert a resolution.'); const sameJob = fixtureRoot({ 'c.yml': `name: Twin\non: push\njobs:\n twin:\n name: Twin Job\n timeout-minutes: 30\n runs-on: ubuntu-latest\n steps:\n` + @@ -1002,6 +1072,7 @@ export async function selfTest() { // matrix job names expanded from a `${{ ... }}` template under the // workflow's own `name:`. A join that only works on fixtures is not one, // and this sweeps EVERY site rather than a hand-picked easy one. + battery('13. The matcher has to accept the shapes THIS tree really produces -- matrix job names expanded from a `${{ ... }}` template under the workflow\'s own `name:`. A join that only works on fixtures is not one, and this sweeps EVERY site rather than a hand-picked easy one.'); const wholeTree = await drive([ '--from', write({ jobs: swept.sites.map((s) => runnerJob({ job: runnerNameFor(s), workflow: workflowNameFor(s), step: s.step, prepSec: 30, runSec: 60 })) }), ]); @@ -1019,6 +1090,7 @@ export async function selfTest() { // direction -- an excluded observation cannot invent headroom -- but the // old join attributed it, which is this defect with the collision size // fixed at one. + battery('14. The other side of the same matcher: an observation whose runner identity CONTRADICTS the only site carrying that step name is excluded and said out loud, and the site reads NOT OBSERVED. That is the safe direction -- an excluded observation cannot invent headroom -- but the old join attributed it, which is this defect with the collision size fixed at one.'); const foreign = await drive([ '--from', write({ jobs: [runnerJob({ job: 'Some Other Job', workflow: 'Some Other Workflow', step: site.step, prepSec: 30, runSec: 60 })] }), ]); @@ -1028,6 +1100,50 @@ export async function selfTest() { for (const dir of dirs) 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 batterySeen) { + 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 = batterySeen.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(`measure-stall-guard-headroom --self-test: ${failures.length} of ${checked} assertion(s) FAILED\n`); for (const f of failures) console.error(` - ${f}`); diff --git a/scripts/pm/check-governed-merges.mjs b/scripts/pm/check-governed-merges.mjs index 8f1afdf21a..0585ed5a1b 100644 --- a/scripts/pm/check-governed-merges.mjs +++ b/scripts/pm/check-governed-merges.mjs @@ -644,6 +644,56 @@ import { fileURLToPath } from 'node:url'; import { historyHorizon } from './git-history.mjs'; 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({ + 'the governed predicate: the 2026-08-18 unified list, exactly': 8, + 'the dispatch-gates declaration (#9979)': 8, + 'since parsing': 4, + 'the window: landing order, not committer dates (#12633)': 15, + 'since-ref is topological (#12633 route B)': 7, + '#13424: every ref resolves in ITS OWN repo, never only in self': 8, + 'the words an operator reads about the window': 4, + 'classification + replay fixtures': 11, + 'multi-repo scope (#9619)': 17, + 'remote reachability + mirror freshness (#13307)': 12, + 'the REAL prober, on real git fixtures (#13307)': 11, + '#13836, the shallow-clone path, both directions': 6, + 'sweep-code provenance (#13307 reopen)': 5, + 'the report words an operator reads': 6, + 'an unreachable remote never renders as a clean window (#13307)': 9, + 'a truncated history is UNAUDITED, not a clean sweep (#9902)': 11, + 'attribution: the channel chain and its named fallback (#9619)': 19, + 'the attribution column\'s THIRD case (#12645)': 13, + 'the --test pre-arm predicate (#9550)': 31, + 'the generator co-edit fence (#11084), pinned in BOTH directions': 10, + 'the #11705 generator-owned rows inside `skills/**`': 23, + '#11705 end to end, against the REAL generator': 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 = 22; + +// 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 scriptPath = fileURLToPath(import.meta.url); const scriptDir = dirname(scriptPath); @@ -2329,9 +2379,23 @@ const REGISTER_SAMPLES = { const SELF_TEST_VERDICT = 'check-governed-merges self-test reached its verdict'; 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 batterySeen = new Map(); + let openBattery = null; + const battery = (name) => { + openBattery = name; + }; + const registerCase = () => { + const b = openBattery ?? UNATTRIBUTED_BATTERY; + batterySeen.set(b, (batterySeen.get(b) ?? 0) + 1); + }; let checked = 0; const failures = []; const assert = (name, cond, detail) => { + registerCase(); checked++; if (!cond) failures.push(`${name}: ${detail ?? ''}`); }; @@ -2341,6 +2405,7 @@ async function selfTest() { const dateWindowFor = (iso) => resolveWindow({ sinceArg: iso }); // ── the governed predicate: the 2026-08-18 unified list, exactly ────────── + battery('the governed predicate: the 2026-08-18 unified list, exactly'); const ids = (paths) => governedPathsIn(paths).map((s) => s.id); assert('all-five-surfaces-declared-in-order', GOVERNED_SURFACES.map((s) => s.id).join(',') === 'adr,claude-tree,skills-catalog,agents-md,claude-md', GOVERNED_SURFACES.map((s) => s.id).join(',')); assert('adr-prefix', ids(['docs/adr/0001-x.md']).join() === 'adr'); @@ -2359,6 +2424,7 @@ async function selfTest() { // tool entirely, so a wrong or missing entry runs perfectly green here and // shows up only as a dev dispatched on a root-file card who is not told that // the card is GOVERNED. + battery('the dispatch-gates declaration (#9979)'); const rootExacts = GOVERNED_SURFACES.filter((s) => s.exact).map((s) => s.exact); assert('every-exact-root-row-declares-a-watch-hint', rootExacts.every((f) => ROOT_FILE_WATCH_HINTS.includes(`${f}/**`)), JSON.stringify(rootExacts)); assert('the-declaration-names-no-file-this-register-does-not-govern', ROOT_FILE_WATCH_HINTS.every((h) => rootExacts.includes(h.replace(/\/\*+$/, ''))), JSON.stringify(ROOT_FILE_WATCH_HINTS)); @@ -2376,6 +2442,7 @@ async function selfTest() { assert('no-pr-in-subject', pullNumberFromSubject('chore: direct push') === null); // ── --since parsing ─────────────────────────────────────────────────────── + battery('since parsing'); const now = new Date('2026-08-18T12:00:00Z'); assert('since-hours', parseSince('24h', now) === '2026-08-17T12:00:00.000Z'); assert('since-days', parseSince('7d', now) === '2026-08-11T12:00:00.000Z'); @@ -2391,6 +2458,7 @@ async function selfTest() { // The old semantics dropped it and printed a clean window; the assertions // below pin BOTH halves of the replacement — that it is listed, and that a // skew the budget cannot cover reads as INCOMPLETE rather than as absent. + battery('the window: landing order, not committer dates (#12633)'); const ROUND_TIP_DATE = '2026-08-14T05:55:02Z'; const qs7Governed = { sha: '01a7337fc0'.padEnd(40, '0'), date: '2026-08-14T05:40:28Z', subject: 'docs(adr): kernel object ownership (#8620)' }; // Newest first, as `git log --first-parent` prints it. `qs7Governed` sits @@ -2466,6 +2534,7 @@ async function selfTest() { rootStop.anchorAtEdge === false, JSON.stringify(rootStop.floorIso)); // ── --since-ref is topological (#12633 route B) ─────────────────────────── + battery('since-ref is topological (#12633 route B)'); const refDates = { 'v5.0.0-rc.3': '2026-08-14T05:55:02Z', deadbee: '2026-08-13T00:00:00Z' }; const topo = resolveWindow({ sinceRefArgs: ['v5.0.0-rc.3'], resolveRefDate: (r) => refDates[r] ?? null }); assert('a-bare---since-ref-is-a-TOPOLOGICAL-window', topo.mode === 'topological' && topo.bareRef === 'v5.0.0-rc.3', JSON.stringify(topo)); @@ -2488,6 +2557,7 @@ async function selfTest() { // the self checkout about a sibling's tip. The control below is the old // self-only resolver, verbatim in behaviour: it still errors, which is what // proves the fix moved the question and not the failure. + battery('#13424: every ref resolves in ITS OWN repo, never only in self'); const uiOnly = resolveWindow({ sinceRefArgs: ['objectui=uitip000000'], resolveRefDate: (r, repoId) => (repoId === 'objectui' && r === 'uitip000000' ? '2026-08-14T05:55:02Z' : null), @@ -2539,6 +2609,7 @@ async function selfTest() { fellBack.mode === 'date' && /does not resolve in this checkout/.test(fellBack.fellBack), JSON.stringify(fellBack.fellBack)); // ── the words an operator reads about the window ───────────────────────── + battery('the words an operator reads about the window'); const dateWords = describeWindow(budgeted); assert('the-date-window-line-states-the-budget-it-subtracted-and-both-boundaries', dateWords.includes('3600 s') && dateWords.includes(budgeted.requestedIso) && dateWords.includes('2026-08-14T04:55:02.000Z'), dateWords); @@ -2552,6 +2623,7 @@ async function selfTest() { edgeWords.includes('3600 s') && /COMPLIANCE/.test(edgeWords) && edgeWords.includes('--since-ref'), edgeWords); // ── classification + replay fixtures ───────────────────────────────────── + battery('classification + replay fixtures'); assert('ungoverned-commit-classifies-null', classifyCommit({ sha: 'a'.repeat(40), date: '2026-08-18T00:00:00Z', subject: 'fix: x (#1)' }, ['packages/spec/src/index.ts']) === null); for (const replay of REPLAYS) { const entry = classifyCommit({ sha: 'b'.repeat(40), date: '2026-08-18T00:00:00Z', subject: replay.subject }, replay.files); @@ -2569,6 +2641,7 @@ async function selfTest() { assert('exit-test-not-governed-is-0', EXIT_TEST_NOT_GOVERNED === 0); // ── multi-repo scope (#9619) ────────────────────────────────────────────── + battery('multi-repo scope (#9619)'); assert('five-governed-repos-declared', GOVERNED_REPOS.map((r) => r.id).join(',') === 'objectstack,objectui,cloud,objectos,hotcrm', GOVERNED_REPOS.map((r) => r.id).join(',')); // #14867: hotcrm pinned by SLUG as well as id. The defect this closes is not // a wrong row — it is NO row: an unconfigured repo prints nothing at all, so @@ -2647,6 +2720,7 @@ async function selfTest() { // The leg that answers "is this checkout a live mirror at all". Every branch // is pinned because the defect it replaces was a row every clause of which // was literally TRUE about the local snapshot. + battery('remote reachability + mirror freshness (#13307)'); assert('a-remote-tracking-ref-splits-into-remote-and-branch', JSON.stringify(remoteRefParts('origin/main')) === '{"remote":"origin","branch":"main"}', JSON.stringify(remoteRefParts('origin/main'))); assert('a-ref-that-names-no-remote-does-not-parse', remoteRefParts('main') === null && remoteRefParts('') === null && remoteRefParts(null) === null); @@ -2710,6 +2784,7 @@ async function selfTest() { // actually reads the world. These fixtures are local bare repos over the // FILE transport: real `git ls-remote`, no network, ~1 s, so `--self-test` // stays offline exactly as its usage line claims. + battery('the REAL prober, on real git fixtures (#13307)'); const fxRoot = mkdtempSync(join(tmpdir(), 'governed-merges-remote-')); try { const g = (cwd, ...rest) => @@ -2832,6 +2907,7 @@ async function selfTest() { // one (its row names the floor it was swept against); one whose floor sits // inside the window refuses with the precondition NAMED in the footer, so // a dropped repo needs no cross-run footer diffing to attribute. + battery('#13836, the shallow-clone path, both directions'); const coShallowCovered = join(fxRoot, 'co-shallow-covered'); const coShallowInside = join(fxRoot, 'co-shallow-inside'); g(fxRoot, 'clone', '-q', '--depth', '2', `file://${bareLive}`, coShallowCovered); @@ -2923,6 +2999,7 @@ async function selfTest() { // reads are asserted against THIS repo — direction-agnostically, because a // dev iterating on this very file legitimately runs it with uncommitted // edits, and that state must render as the loud mismatch, not as a red pin. + battery('sweep-code provenance (#13307 reopen)'); const blobA = 'a'.repeat(40); const blobB = 'b'.repeat(40); const matchLine = describeSweepCode({ head: 'abc1234', blob: blobA, headBlob: blobA, error: null }); @@ -2945,6 +3022,7 @@ async function selfTest() { assert('the-report-head-carries-the-sweep-code-line-when-a-reading-is-supplied', withCode.includes('sweep code: HEAD abc1234'), withCode); // ── the report words an operator reads ──────────────────────────────────── + battery('the report words an operator reads'); const allAudited = resolved.map((r) => ({ ...r, status: 'audited', reason: null, tip: { sha: 'c'.repeat(40), date: '2026-08-18T00:00:00Z' }, scanned: 3 })); const clean = renderReport({ window: dateWindowFor('2026-08-17T00:00:00Z'), repos: allAudited, scanned: 12, entries: [], lookups: 0 }); assert('clean-window-says-clean-and-costs-zero-lookups', clean.includes('clean window') && clean.includes('0 API lookup(s)'), clean); @@ -2964,6 +3042,7 @@ async function selfTest() { // that sameness IS the fix: no second mechanism was introduced, so this row // cannot drift away from the #4690 rule the others obey. Assert on the TICK // (the phrase "NOT a clean window" contains "clean window"). + battery('an unreachable remote never renders as a clean window (#13307)'); const deadMirror = renderReport({ window: dateWindowFor('2026-08-17T00:00:00Z'), repos: [ @@ -3038,6 +3117,7 @@ async function selfTest() { // `✅ clean window` over ~40 governed merges that GitHub lists for the same // window. The tick is what a maintainer reads, so the assertions below are // on the tick and on the direction the reason names, not on the phrase. + battery('a truncated history is UNAUDITED, not a clean sweep (#9902)'); const truncated = truncatedHorizonReason({ ref: 'origin/main', horizon: { @@ -3081,6 +3161,7 @@ async function selfTest() { assert('the-violation-contract-is-stated-on-every-sweep', clean.includes('violation signal') && loud.includes('violation signal')); // ── attribution: the channel chain and its named fallback (#9619) ───────── + battery('attribution: the channel chain and its named fallback (#9619)'); const anonOnly = attributionChannels({}); assert('with-no-token-anonymous-REST-is-still-tried', anonOnly.length === 1 && anonOnly[0].id === 'anonymous', JSON.stringify(anonOnly.map((c) => c.id))); const both = attributionChannels({ GITHUB_TOKEN: 'x' }); @@ -3129,6 +3210,7 @@ async function selfTest() { // the loudest line the sweep prints — is case 3, and it used to render // case 2's words with zero channels tried. All three are pinned as a set, // because the defect was a two-way split covering three facts. + battery('the attribution column\'s THIRD case (#12645)'); const notLookedUp = renderReport({ window: dateWindowFor('2026-08-13T00:00:00Z'), repos: allAudited, scanned: 3, entries: [noPr], lookups: 0 }); assert('a-pr-less-entry-is-NOT-LOOKED-UP-not-a-failed-lookup', notLookedUp.includes('merged_by NOT LOOKED UP') && !notLookedUp.includes('every channel failed'), notLookedUp); assert('and-it-says-WHY-nothing-was-queried', notLookedUp.includes('no PR number in the subject') && notLookedUp.includes('not a channel failure'), notLookedUp); @@ -3160,6 +3242,7 @@ async function selfTest() { === 'merged_by x @ y (via env-token)'); // ── the --test pre-arm predicate (#9550) ────────────────────────────────── + battery('the --test pre-arm predicate (#9550)'); const governedCase = testVerdict(['AGENTS.md']); assert('--test-on-the-#9527-file-list-answers-GOVERNED', governedCase.governed === true && governedCase.hitPaths.join() === 'AGENTS.md', JSON.stringify(governedCase)); assert('--test-governed-renders-the-no-flip-no-enqueue-no-arm-instruction', renderTestVerdict(governedCase).includes('GOVERNED') && renderTestVerdict(governedCase).includes('arms auto-merge'), renderTestVerdict(governedCase)); @@ -3270,6 +3353,7 @@ async function selfTest() { // The fence decides whether `--test` recomputes at all, so a bug in either // direction is load-bearing: too loose re-opens the self-certification, too // tight would break the ruled pure-regeneration lift. + battery('the generator co-edit fence (#11084), pinned in BOTH directions'); const fenceRow = GENERATED_SURFACE_EXCEPTIONS.find((e) => e.id === 'spec-skill-refs'); const fencePrefix = fenceRow.trustedGeneratorPrefixes[0]; // Direction A — co-edit: the generator is touched, so no recompute happens @@ -3323,6 +3407,7 @@ async function selfTest() { // reproducible from its generator, proven per file. Two halves, both the // generator's own answer — it declared the path among its outputs, and its // `--check` reported no drift — and every other input fails closed. + battery('the #11705 generator-owned rows inside `skills/**`'); const skillRefs = GENERATED_SURFACE_EXCEPTIONS.find((e) => e.id === 'spec-skill-refs'); const reactBlocks = GENERATED_SURFACE_EXCEPTIONS.find((e) => e.id === 'spec-react-blocks'); const genIndex = REGISTER_SAMPLES['spec-skill-refs']; @@ -3429,6 +3514,7 @@ async function selfTest() { // to a temp file. Both branches assert; a missing toolchain is the merge-group // guard job's real environment, where fail-closed is the correct answer, so // it is pinned rather than skipped. + battery('#11705 end to end, against the REAL generator'); const liveRun = runSinkGenerator(resolve(scriptDir, '..', '..'), skillRefs); let liveNote; if (Array.isArray(liveRun.outputs) && liveRun.outputs.length > 0) { @@ -3458,6 +3544,50 @@ async function selfTest() { rejectedRender.includes('GOVERNED') && rejectedRender.includes('did NOT lift') && rejectedRender.includes('differs (fixture)'), rejectedRender); assert('a-verdict-without-exceptions-renders-exactly-as-before', renderExceptionLines(testVerdict(['AGENTS.md'])) === '' && !renderTestVerdict(testVerdict(['AGENTS.md'])).includes('#9866')); + // ── 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 batterySeen) { + 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 = batterySeen.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-governed-merges --self-test — ${failures.length} failure(s)\n`); for (const failure of failures) console.error(` • ${failure}`); diff --git a/scripts/pm/check-governed-prose.mjs b/scripts/pm/check-governed-prose.mjs index 3365306bdf..5575ba2476 100644 --- a/scripts/pm/check-governed-prose.mjs +++ b/scripts/pm/check-governed-prose.mjs @@ -79,6 +79,40 @@ import { fileURLToPath } from 'node:url'; import process from 'node:process'; 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({ + 'sliceRegion': 5, + 'codeSpansIn': 3, + 'looksLikeGovernedGlob': 5, + 'verdict': 6, + 'the shipped register + regions, end to end': 5, + 'the dispatch-gates declaration (#9979)': 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 = 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)'; + // A dynamic import, because this file used to have to reach the sibling with // `--self-test` withheld from `process.argv`: `check-governed-merges.mjs` ran // its own self-test at module scope, so running OUR `--self-test` also ran the @@ -261,13 +295,28 @@ function runGate() { let selfTestReachedVerdict = false; 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 batterySeen = new Map(); + let openBattery = null; + const battery = (name) => { + openBattery = name; + }; + const registerCase = () => { + const b = openBattery ?? UNATTRIBUTED_BATTERY; + batterySeen.set(b, (batterySeen.get(b) ?? 0) + 1); + }; const cases = []; const assert = (name, actual, expected) => { + registerCase(); const ok = JSON.stringify(actual) === JSON.stringify(expected); cases.push({ name, ok, actual, expected }); }; // --- sliceRegion ------------------------------------------------------- + battery('sliceRegion'); const doc = ['intro', 'START here', '`a/**` and `b/**`', 'tail', 'END there', 'after'].join('\n'); assert('region is start-inclusive/end-exclusive', sliceRegion(doc, 'START', 'END').text, 'START here\n`a/**` and `b/**`\ntail'); assert('region reports 1-based bounds', [sliceRegion(doc, 'START', 'END').startLine, sliceRegion(doc, 'START', 'END').endLine], [2, 4]); @@ -277,11 +326,13 @@ function selfTest() { assert('end anchor is searched after start only', sliceRegion('END first\nSTART x\nz', 'START', 'END').ok, false); // --- codeSpansIn ------------------------------------------------------- + battery('codeSpansIn'); assert('code spans are extracted in order', codeSpansIn('see `x/**` then `y`'), ['x/**', 'y']); assert('a span never crosses a newline', codeSpansIn('`open\nclose`'), []); assert('bold markers around a span do not leak in', codeSpansIn('**`docs/adr/**`**'), ['docs/adr/**']); // --- looksLikeGovernedGlob -------------------------------------------- + battery('looksLikeGovernedGlob'); assert('a **-glob is glob-shaped', ['docs/adr/**', '.claude/**', 'skills/**'].every(looksLikeGovernedGlob), true); assert('a bare filename is not glob-shaped', looksLikeGovernedGlob('AGENTS.md'), false); assert('a trailing-slash prefix is not glob-shaped', looksLikeGovernedGlob('docs/adr/'), false); @@ -289,6 +340,7 @@ function selfTest() { assert('a span with spaces is not glob-shaped', looksLikeGovernedGlob('node -e "**"'), false); // --- verdict ----------------------------------------------------------- + battery('verdict'); const register = ['docs/adr/**', '.claude/**', 'skills/**', 'AGENTS.md', 'CLAUDE.md']; const good = 'governed: `docs/adr/**` + `.claude/**` + `skills/**` + `AGENTS.md` + `CLAUDE.md`. See `origin/main`.'; assert('complete prose passes both halves', verdict(good, register), { missing: [], unknown: [] }); @@ -317,6 +369,7 @@ function selfTest() { assert('unrelated spans are ignored', verdict(`${good} run \`pnpm check:adr-anchors\``, register), { missing: [], unknown: [] }); // --- the shipped register + regions, end to end ------------------------ + battery('the shipped register + regions, end to end'); const globs = registerGlobs(); assert('the register is non-empty', globs.length > 0, true); for (const surface of PROSE_SURFACES) { @@ -332,6 +385,7 @@ function selfTest() { // tool entirely, so a wrong or missing entry runs perfectly green here and // shows up only as a dev dispatched on an AGENTS.md card with this gate // absent from the brief. + battery('the dispatch-gates declaration (#9979)'); const rootSurfaces = PROSE_SURFACES.map((s) => s.path).filter((p) => !p.includes('/')); assert('every separator-less prose surface declares a root-file watch hint', rootSurfaces.every((p) => ROOT_FILE_WATCH_HINTS.includes(`${p}/**`)), true); assert('the declaration names no file this gate does not read', ROOT_FILE_WATCH_HINTS.every((h) => PROSE_SURFACES.some((s) => s.path === h.replace(/\/\*+$/, ''))), true); @@ -340,6 +394,50 @@ function selfTest() { // form appearing there would make this gate read a path that does not exist. assert('the declared form is NOT a PROSE_SURFACES path', PROSE_SURFACES.some((s) => ROOT_FILE_WATCH_HINTS.includes(s.path)), 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) => { cases.push({ name: message, ok: false, actual: 'the battery did not run', expected: 'the battery runs its pinned cases' }); }; + 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 batterySeen) { + 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 = batterySeen.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.', + ); + } + const failed = cases.filter((c) => !c.ok); for (const c of failed) { console.error(` ✗ ${c.name}\n expected ${JSON.stringify(c.expected)}\n actual ${JSON.stringify(c.actual)}`); diff --git a/scripts/pm/check-governed-queue-guard.mjs b/scripts/pm/check-governed-queue-guard.mjs index 805522da32..62b86b1cd3 100644 --- a/scripts/pm/check-governed-queue-guard.mjs +++ b/scripts/pm/check-governed-queue-guard.mjs @@ -236,6 +236,51 @@ import { } from './check-governed-merges.mjs'; 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({ + 'the register is READ, never restated (#9840)': 6, + 'the exit contract as a table': 4, + 'the merge-queue head ref': 6, + 'event payloads, including the malformed ones': 5, + 'the approval predicate': 3, + '⭐ The fail-open direction a naive `.some(r => r.state === \'APPROVED\')`': 5, + 'the 2026-08-27 pinned predicate (the queue leg\'s)': 12, + 'decomposition, and the multi-PR group trap': 6, + 'the verdict table, both events': 10, + 'the pull_request leg is an EARLY WARNING and never reddens': 3, + 'the replay fixtures: the three incidents this guard descends from': 9, + '⭐ the ordering guarantee, measured with a spy that THROWS': 7, + 'the words a reader acts on (requirement (e))': 26, + '⭐ #14063 END TO END: what the dependency install actually buys': 9, + 'the PR-head reader: throws, never defaults (exit 4 at the caller)': 3, + 'the WIRING pin: the workflow still spells this context name': 8, + '⭐ #14063: the environment the exemption needs, pinned to the YAML': 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 = 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)'; + const scriptDir = dirname(fileURLToPath(import.meta.url)); const repoRoot = resolve(scriptDir, '..', '..'); @@ -994,9 +1039,23 @@ const REPLAYS = [ let selfTestReachedVerdict = false; 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 batterySeen = new Map(); + let openBattery = null; + const battery = (name) => { + openBattery = name; + }; + const registerCase = () => { + const b = openBattery ?? UNATTRIBUTED_BATTERY; + batterySeen.set(b, (batterySeen.get(b) ?? 0) + 1); + }; let checked = 0; const failures = []; const assert = (name, cond, detail) => { + registerCase(); checked += 1; if (!cond) failures.push(`${name}${detail ? `: ${detail}` : ''}`); }; @@ -1018,6 +1077,7 @@ export async function selfTest() { // The one assertion that would catch this file growing its own copy of the // surface list: every surface the register declares must be answerable // through it here, including one added tomorrow. + battery('the register is READ, never restated (#9840)'); for (const surface of GOVERNED_SURFACES) { const sample = surface.prefix ? `${surface.prefix}sample.md` : surface.exact; const { governed } = decomposeGovernedWork([row(1, [sample])]); @@ -1026,6 +1086,7 @@ export async function selfTest() { assert('this-file-restates-no-surface-list', GOVERNED_SURFACES.length >= 5 && governedPathsIn(['docs/adrs/z.md', 'examples/AGENTS.md']).length === 0); // ── the exit contract as a table ────────────────────────────────────────── + battery('the exit contract as a table'); assert('exit-clear-is-0', EXIT_CLEAR === 0); assert('exit-cannot-run-is-1', EXIT_CANNOT_RUN === 1); assert( @@ -1036,6 +1097,7 @@ export async function selfTest() { assert('the-unapproved-refusal-shares-the-siblings-GOVERNED-code-3', EXIT_REFUSED_UNAPPROVED === 3); // ── the merge-queue head ref ────────────────────────────────────────────── + battery('the merge-queue head ref'); assert('queue-ref-yields-its-pr', pullNumberFromQueueRef('refs/heads/gh-readonly-queue/main/pr-11387-484ae0019cd') === 11387); assert('queue-ref-without-the-refs-prefix-too', pullNumberFromQueueRef('gh-readonly-queue/main/pr-42-abcdef1') === 42); assert('a-base-branch-with-a-slash-is-still-read', pullNumberFromQueueRef('refs/heads/gh-readonly-queue/release/v5/pr-7-abcdef1') === 7); @@ -1044,6 +1106,7 @@ export async function selfTest() { assert('nonsense-is-null-never-a-number', pullNumberFromQueueRef(undefined) === null && pullNumberFromQueueRef('') === null); // ── event payloads, including the malformed ones ────────────────────────── + battery('event payloads, including the malformed ones'); const mg = resolveEventContext({ eventName: 'merge_group', payload: { merge_group: { base_sha: 'b'.repeat(40), head_sha: 'h'.repeat(40), head_ref: 'refs/heads/gh-readonly-queue/main/pr-99-abcdef1', base_ref: 'refs/heads/main' } }, @@ -1060,11 +1123,13 @@ export async function selfTest() { assert('an-unsupported-event-CANNOT-RUN-and-names-both-events-it-does-read', !unsupported.ok && /merge_group/.test(unsupported.reason) && /pull_request/.test(unsupported.reason), unsupported.reason); // ── the approval predicate ──────────────────────────────────────────────── + battery('the approval predicate'); assert('an-approval-is-an-approval', approvalVerdict(approved('hotlong')).state === 'approved'); assert('no-reviews-at-all-is-unapproved', approvalVerdict([]).state === 'unapproved'); assert('a-COMMENTED-review-is-not-an-approval', approvalVerdict([{ state: 'COMMENTED', user: { login: 'a' } }]).state === 'unapproved'); // ⭐ The fail-open direction a naive `.some(r => r.state === 'APPROVED')` // gets wrong, and the only direction this file may not be wrong in. + battery('⭐ The fail-open direction a naive `.some(r => r.state === \'APPROVED\')`'); assert( 'an-approval-later-superseded-by-CHANGES_REQUESTED-is-NOT-an-approval', approvalVerdict([ @@ -1095,6 +1160,7 @@ export async function selfTest() { // membership pin iterates it and the membership assertion pins it to the // ruling: a silent edit to the set fails here, and nothing else in the repo // restates the names as data. + battery('the 2026-08-27 pinned predicate (the queue leg\'s)'); assert('the-authorized-set-is-exactly-the-ruled-two-accounts', GOVERNED_APPROVERS.join() === 'os-zhuang,hotlong'); for (const login of GOVERNED_APPROVERS) { assert(`an-authorized-approval-pinned-to-the-current-head-passes: ${login}`, pinnedApprovalVerdict([approvedAt(login, HEAD)], HEAD).state === 'approved'); @@ -1127,6 +1193,7 @@ export async function selfTest() { ); // ── decomposition, and the multi-PR group trap ──────────────────────────── + battery('decomposition, and the multi-PR group trap'); const clearRows = [row(1, ['packages/spec/src/index.ts', 'content/docs/x.mdx'])]; assert('a-clear-diff-decomposes-to-nothing', decomposeGovernedWork(clearRows).governed.length === 0 && decomposeGovernedWork(clearRows).unattributed.length === 0); const mixed = decomposeGovernedWork([row(5, ['AGENTS.md', 'packages/spec/src/index.ts'])]); @@ -1140,6 +1207,7 @@ export async function selfTest() { assert('an-UNGOVERNED-commit-naming-no-pr-is-simply-not-our-business', decomposeGovernedWork([{ sha: 'd'.repeat(40), subject: 'x', pr: null, paths: ['README.md'] }]).unattributed.length === 0); // ── the verdict table, both events ──────────────────────────────────────── + battery('the verdict table, both events'); const clearV = run('merge_group', clearRows); assert('a-clear-merge-group-is-CLEAR-and-exits-0', clearV.conclusion === 'clear' && clearV.exitCode === EXIT_CLEAR); assert('and-it-made-zero-review-lookups', clearV.apiCalls === 0); @@ -1165,6 +1233,7 @@ export async function selfTest() { assert('one-approved-pr-does-NOT-carry-an-unapproved-sibling-through-the-same-group', partial.exitCode === EXIT_REFUSED_UNAPPROVED); // ── the pull_request leg is an EARLY WARNING and never reddens ──────────── + battery('the pull_request leg is an EARLY WARNING and never reddens'); const warnedV = run('pull_request', [row(9527, ['AGENTS.md'])], new Map([[9527, approvalVerdict([])]])); assert('a-governed-unapproved-PULL-REQUEST-is-WARNED-not-refused', warnedV.conclusion === 'warned' && warnedV.exitCode === EXIT_CLEAR); assert( @@ -1176,6 +1245,7 @@ export async function selfTest() { assert('and-an-unattributed-governed-commit-does-not-redden-a-pr-run-either', run('pull_request', [{ sha: 'c'.repeat(40), subject: 'x', pr: null, paths: ['CLAUDE.md'] }]).exitCode === EXIT_CLEAR); // ── the replay fixtures: the three incidents this guard descends from ───── + battery('the replay fixtures: the three incidents this guard descends from'); for (const replay of REPLAYS) { const rows = [row(replay.pr, replay.files, 'e'.repeat(40), replay.subject)]; const queued = run('merge_group', rows, new Map([[replay.pr, pinnedApprovalVerdict([], HEAD)]])); @@ -1191,6 +1261,7 @@ export async function selfTest() { // "The path test runs first and a clear diff makes no API call" is a claim // about control flow, so it is tested by making the API impossible to touch. // A mock returning [] would have passed against a version that called it. + battery('⭐ the ordering guarantee, measured with a spy that THROWS'); let apiTouched = 0; const explode = () => { apiTouched += 1; @@ -1304,6 +1375,7 @@ export async function selfTest() { ); // ── the words a reader acts on (requirement (e)) ────────────────────────── + battery('the words a reader acts on (requirement (e))'); const refusalText = renderGuardVerdict(refusedV); assert('a-refusal-names-the-exact-paths-that-matched', refusalText.includes('AGENTS.md'), refusalText); assert('a-refusal-names-the-pull-request', refusalText.includes('#9527'), refusalText); @@ -1418,6 +1490,7 @@ export async function selfTest() { // answer produces — certified, un-run, drifted — and the workflow pins below // assert the environment that makes the certified answer reachable at all. // Neither half is worth much alone; together they are the claim. + battery('⭐ #14063 END TO END: what the dependency install actually buys'); const regenPaths = ['objectstack-ai', 'objectstack-api', 'objectstack-data', 'objectstack-query'].map( (skill) => `skills/${skill}/references/_index.md`, ); @@ -1499,6 +1572,7 @@ export async function selfTest() { assert('a-recompute-that-THROWS-never-lifts-it-propagates-into-CANNOT-RUN', liftThrew !== null && /EACCES/.test(liftThrew), String(liftThrew)); // ── the PR-head reader: throws, never defaults (exit 4 at the caller) ──── + battery('the PR-head reader: throws, never defaults (exit 4 at the caller)'); const fakeRes = (body, ok = true, status = 200) => async () => ({ ok, status, json: async () => body }); const readerArgs = { apiUrl: 'https://api.example', slug: 'o/r', token: null }; assert( @@ -1522,6 +1596,7 @@ export async function selfTest() { // #6865's whole defect — and the name declared here becomes a name nothing // publishes. Read from disk on purpose: a constant asserting against itself // proves nothing. + battery('the WIRING pin: the workflow still spells this context name'); try { const wf = readFileSync(join(repoRoot, '.github', 'workflows', CHECK_WORKFLOW), 'utf8'); assert('the-workflow-exists-and-declares-the-job-id-this-file-names', wf.includes(`\n ${CHECK_JOB_ID}:\n`), CHECK_JOB_ID); @@ -1541,6 +1616,7 @@ export async function selfTest() { // generator and every #11705 row fails closed, which is precisely the state // the 2026-09-01 ruling ended. A green self-test over a workflow that had // silently lost its install would be the loudest possible false negative. + battery('⭐ #14063: the environment the exemption needs, pinned to the YAML'); const audit = installStepAudit(wf); assert('the-job-installs-the-workspace-dependencies-the-recompute-runs-on', audit.installIndex !== -1 && /--frozen-lockfile/.test(audit.installCommand), audit.installCommand || '(no pnpm install step)'); assert('the-job-acquires-pnpm-through-the-shared-composite-not-a-second-spelling', audit.acquiresPnpm, JSON.stringify(audit.steps)); @@ -1575,6 +1651,50 @@ export async function selfTest() { assert('the-workflow-file-is-readable', false, String(error?.message ?? error).split('\n')[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 batterySeen) { + 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 = batterySeen.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.', + ); + } + for (const f of failures) console.error(` ✗ ${f}`); if (failures.length > 0) { console.error(`✗ check-governed-queue-guard self-test: ${failures.length} of ${checked} case(s) failed.`); diff --git a/scripts/pm/ci-failure.mjs b/scripts/pm/ci-failure.mjs index f474d6d9fd..bf01c47548 100644 --- a/scripts/pm/ci-failure.mjs +++ b/scripts/pm/ci-failure.mjs @@ -298,6 +298,48 @@ import { import { PROXY_FLAG, PROXY_REARM_GUARD, proxyRearmPlan } from './check-governed-merges.mjs'; 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({ + 'latestPerName: the superseded-run trap': 4, + 'classifyAnnotation: the four measured shapes': 9, + 'stepsIntegrity: the pre/post numbering gap is NOT truncation': 8, + 'assertionStatus: four answers, not two': 11, + 'the job log (#10141)': 34, + 'isRosterJob': 5, + 'runIdOf': 3, + 'stepBodies / resolveStep': 8, + 'proxyRearmFor: the transport trap': 7, + 'usageText: --help tracks the header instead of a line number': 4, + 'verdictOf: the exit table': 11, + 'midWalkVerdict: the mid-walk net (#10155)': 12, + 'probeTransport: the two stages, and the FOURTH container class': 11, + 'renderFixLines: one remedy is one `fix:` marker (#10362)': 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 = 14; + +// 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 HERE = dirname(fileURLToPath(import.meta.url)); const ROOT = resolve(HERE, '..', '..'); // Which board this file reads, resolved by the sweeper's own resolver rather @@ -1566,13 +1608,28 @@ function render(result, target) { const SELF_TEST_VERDICT = 'ci-failure self-test reached its verdict'; 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 batterySeen = new Map(); + let openBattery = null; + const battery = (name) => { + openBattery = name; + }; + const registerCase = () => { + const b = openBattery ?? UNATTRIBUTED_BATTERY; + batterySeen.set(b, (batterySeen.get(b) ?? 0) + 1); + }; const failures = []; const t = (label, actual, expected = true) => { + registerCase(); const ok = JSON.stringify(actual) === JSON.stringify(expected); if (!ok) failures.push(`${label}\n expected ${JSON.stringify(expected)}\n actual ${JSON.stringify(actual)}`); }; // -- latestPerName: the superseded-run trap ------------------------------- + battery('latestPerName: the superseded-run trap'); const dupes = [ { id: 1, name: 'CI', conclusion: 'cancelled', started_at: '2026-08-19T01:00:00Z' }, { id: 2, name: 'CI', conclusion: 'success', started_at: '2026-08-19T02:00:00Z' }, @@ -1591,6 +1648,7 @@ async function selfTest() { t('latestPerName on an empty list yields nothing, not a throw', latestPerName([]).kept.length, 0); // -- classifyAnnotation: the four measured shapes ------------------------- + battery('classifyAnnotation: the four measured shapes'); t( 'a file-anchored annotation is the assertion', classifyAnnotation({ @@ -1653,6 +1711,7 @@ async function selfTest() { t('a missing message does not throw', classifyAnnotation({}).kind, 'other'); // -- stepsIntegrity: the pre/post numbering gap is NOT truncation --------- + battery('stepsIntegrity: the pre/post numbering gap is NOT truncation'); const testCoreShard = [ ...Array.from({ length: 16 }, (_, i) => ({ number: i + 1, name: `step ${i + 1}`, conclusion: 'success' })), { number: 30, name: 'Post Setup pnpm cache', conclusion: 'skipped' }, @@ -1693,6 +1752,7 @@ async function selfTest() { t('a non-array is `unknown`, never a throw', stepsIntegrity(undefined).verdict, 'unknown'); // -- assertionStatus: four answers, not two ------------------------------- + battery('assertionStatus: four answers, not two'); t( 'a file-anchored annotation means the assertion was retrieved', assertionStatus([{ kind: 'exit-status' }, { kind: 'assertion', path: 'a.test.ts' }]).kind, @@ -1757,6 +1817,7 @@ async function selfTest() { // 32436578705, fetched 2026-08-21 (see the header). Runner colour escapes are // dropped: this file must stay free of raw control bytes, and no branch here // reads them. + battery('the job log (#10141)'); t( 'the runner timestamp is stripped so the assertion starts at column 1', stripLogTimestamp("2026-08-21T01:34:45.6366823Z ##[error]src/x.ts(42,1): error TS2578: Unused '@ts-expect-error' directive."), @@ -1923,6 +1984,7 @@ async function selfTest() { ); // -- isRosterJob ---------------------------------------------------------- + battery('isRosterJob'); t('the aggregate roster job is recognised', isRosterJob(['Verify test shard results']), true); t('...and the dogfood one', isRosterJob(['Verify dogfood shard results']), true); t('a shard job is not a roster job', isRosterJob(["Run this shard's tests"]), false); @@ -1930,6 +1992,7 @@ async function selfTest() { t('no failed steps is not a roster job', isRosterJob([]), false); // -- runIdOf -------------------------------------------------------------- + battery('runIdOf'); t( 'the run id rides along on details_url, so it costs no request', runIdOf({ details_url: 'https://github.com/o/r/actions/runs/32204206019/job/95924070749' }), @@ -1939,6 +2002,7 @@ async function selfTest() { t('a check-run with no urls yields null', runIdOf({}), null); // -- stepBodies / resolveStep -------------------------------------------- + battery('stepBodies / resolveStep'); const wf = [ 'jobs:', ' gates:', @@ -2000,6 +2064,7 @@ async function selfTest() { t('the real workflow tree still resolves a known gate step to a runnable command', live.length > 0 && live[0].kind === 'run', true); // -- proxyRearmFor: the transport trap ------------------------------------ + battery('proxyRearmFor: the transport trap'); const proxied = { HTTPS_PROXY: 'http://127.0.0.1:38113' }; t( 'a --self-test run never re-execs — it opens no socket', @@ -2041,6 +2106,7 @@ async function selfTest() { ); // -- usageText: --help tracks the header instead of a line number --------- + battery('usageText: --help tracks the header instead of a line number'); const fakeHeader = [ '#!/usr/bin/env node', '// Copyright', @@ -2075,6 +2141,7 @@ async function selfTest() { t('...and --help stops before the header sections', liveUsage.includes('## '), false); // -- verdictOf: the exit table -------------------------------------------- + battery('verdictOf: the exit table'); const withAssertion = { assertions: [{ kind: 'assertion' }] }; const without = { assertions: [] }; t('no checks at all is UNDETERMINED, never GREEN', verdictOf({ failing: [], pending: 0, total: 0 }).exit, EXIT_UNDETERMINED); @@ -2128,6 +2195,7 @@ async function selfTest() { // assertion text was retrieved for EVERY failing check". Measured against // main at 2e39181a with the probe green and the walk's first page refused: // stack trace, EXIT=1. Every pin below exists so that cannot come back. + battery('midWalkVerdict: the mid-walk net (#10155)'); const refusedTransport = { kind: 'repo-scope-refused', headline: 'repo-scoped reads are refused', detail: ['d'], fix: ['f'] }; const healthyTransport = { kind: 'reachable', headline: 'api.github.com is reachable', detail: [], fix: [] }; const boom = Object.assign(new Error('GET /repos/o/r/commits/abc/check-runs?per_page=100&page=1 -> HTTP 403'), { status: 403 }); @@ -2211,6 +2279,7 @@ async function selfTest() { // no other: `/rate_limit` -> 200 with a real quota and `server: github.com`, // `/user` -> 200 with the real login, `GET /repos/objectstack-ai/objectui` -> // 403 with no `server: github.com` and no `x-ratelimit-*` headers at all. + battery('probeTransport: the two stages, and the FOURTH container class'); const PLACEHOLDER = 'proxy00000abcd'; // the 14-char proxy placeholder, `unrecognized` shape const healthyRate = { status: 200, rateLimitRemaining: 14982 }; const refusedRepo = { status: 403, rateLimitRemaining: null }; @@ -2295,6 +2364,7 @@ async function selfTest() { // pinning is that this file renders what `classifyTransportProbe` actually // emits. A fixture would only restate the printer's own assumption about the // shape, which is the assumption that was wrong. + battery('renderFixLines: one remedy is one `fix:` marker (#10362)'); const refusedFix = classifyTransportProbe({ token: 'ghp_x', authed: { status: 200, rateLimitRemaining: 4999 }, @@ -2335,6 +2405,50 @@ async function selfTest() { t('an absent fix renders nothing at all', renderFixLines(undefined), []); t('a one-line remedy is just the marked line', renderFixLines(['do the thing']), [' fix: do the thing']); + // ── 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 batterySeen) { + 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 = batterySeen.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(`✗ ci-failure --self-test (${failures.length} failure(s)):\n`); for (const f of failures) console.error(` • ${f}`); diff --git a/scripts/pm/git-history.mjs b/scripts/pm/git-history.mjs index b405ad82fc..6c1b474231 100644 --- a/scripts/pm/git-history.mjs +++ b/scripts/pm/git-history.mjs @@ -80,6 +80,37 @@ import { tmpdir } from 'node:os'; import { join } from 'node:path'; 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({ + 'pure decisions': 10, + 'real repos': 15, + 'historyHorizon: the read-only reading the #9902 adopters call': 12, +}); + +// 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)'; + const DEFAULT_REF = 'origin/main'; /** Slack applied below `--since` when deepening, absorbing commit-date skew. */ const DEFAULT_MARGIN_DAYS = 7; @@ -435,14 +466,29 @@ function main(argv) { let selfTestReachedVerdict = false; 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 batterySeen = new Map(); + let openBattery = null; + const battery = (name) => { + openBattery = name; + }; + const registerCase = () => { + const b = openBattery ?? UNATTRIBUTED_BATTERY; + batterySeen.set(b, (batterySeen.get(b) ?? 0) + 1); + }; let failures = 0; const t = (name, ok, detail = '') => { + registerCase(); if (ok) { process.stdout.write(` ✓ ${name}\n`); return; } failures += 1; process.stdout.write(` ✗ ${name}${detail ? `\n ${detail}` : ''}\n`); }; // ── pure decisions ──────────────────────────────────────────────────────── + battery('pure decisions'); const day = 24 * 60 * 60 * 1000; const t0 = Date.parse('2026-08-01T00:00:00Z'); @@ -485,6 +531,7 @@ function selfTest() { // available — between it and the nearest commit stamp. `collect-release-notes.sh // --self-test`, which runs over an identical fixture in the same `lint.yml` // step, has always spelled its window this way. + battery('real repos'); const FIXTURE_EPOCH = '2026-06-01T12:00:00Z'; const FIXTURE_COMMITS = 40; const WINDOW_SINCE = '2026-06-20T00:00:00Z'; @@ -586,6 +633,7 @@ function selfTest() { + 'not the shallow flag', isShallow(shallowDeep) === true); // ── historyHorizon: the read-only reading the #9902 adopters call ─────── + battery('historyHorizon: the read-only reading the #9902 adopters call'); const hFull = historyHorizon({ cwd: full, ref: 'origin/main', sinceMs: Date.parse(WINDOW_SINCE) }); t('historyHorizon clears a complete clone and reports no floor', hFull.covered === true && hFull.shallow === false && hFull.floor === null && hFull.remedy === null, @@ -638,6 +686,50 @@ 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) => { failures += 1; process.stdout.write(` ✗ ${message}\n`); }; + 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 batterySeen) { + 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 = batterySeen.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.', + ); + } + process.stdout.write(failures === 0 ? '\ngit-history --self-test: all cases passed.\n' : `\ngit-history --self-test: ${failures} FAILED.\n`); selfTestReachedVerdict = true;