From f248bc95367860a6fe6549e2d55692c15222958c Mon Sep 17 00:00:00 2001 From: "claude[bot]" <209825114+claude[bot]@users.noreply.github.com> Date: Thu, 3 Sep 2026 07:55:56 +0000 Subject: [PATCH] test(scripts): give 30 more self-tests a battery roster and floor Batch 1c of #13799: PR #13487's roster-and-floor shape, exactly as batch 1 landed it, transplanted onto the `scripts/**` self-tests that already carry its shape -- a block-bodied assertion helper inside the self-test body, at least two named section banners, and a body-level verdict site. Per file: a module-level frozen `SELF_TEST_BATTERIES` roster of NAMES with a per-battery floor, `SELF_TEST_BATTERY_FLOOR` pinning the roster's own size, `UNATTRIBUTED_BATTERY`, a `battery('')` opener at each existing section banner, one `registerCase()` at the head of the existing assertion helper, and the floor evaluated immediately before the existing verdict so its messages enter the file's own failure sink. 432 batteries, 2591 cases. No case is rewritten, none reordered and no assertion changes meaning: every file's `--self-test` exits 0 with byte-identical stdout AND stderr before and after. The roster block goes after the import block in every file, which is TDZ-safe by construction. 15 of the 45 files the dispatch named do not meet that criterion when measured against the tree and are left for a later batch rather than improvised; the reason per file is in the PR body. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01YWBXxmS9sXHoqnozSNwHnE --- scripts/check-adr-0087-registration.mjs | 185 ++++++++++++++++++ scripts/check-auth-mount-ledger.mjs | 126 +++++++++++- scripts/check-ci-filter-parity.mjs | 103 ++++++++++ scripts/check-comment-mask-adoption.mjs | 102 ++++++++++ scripts/check-console-injection.mjs | 109 +++++++++++ scripts/check-cross-repo-closer-outcome.mjs | 99 ++++++++++ scripts/check-dev-prereqs.mjs | 121 ++++++++++++ scripts/check-dispatcher-error-vocabulary.mjs | 94 ++++++++- scripts/check-doc-frontmatter.mjs | 111 +++++++++++ scripts/check-docs-locale-catch-all.mjs | 119 +++++++++++ scripts/check-docs-section-name.mjs | 133 +++++++++++++ scripts/check-engine-split-ratio.mjs | 94 +++++++++ scripts/check-error-status-conformance.mjs | 136 ++++++++++++- scripts/check-i18n-stale-fill.mjs | 94 +++++++++ scripts/check-init-service-contract.mjs | 129 +++++++++++- scripts/check-keyed-text-bounds.mjs | 108 ++++++++++ scripts/check-merge-queue-triage-outcome.mjs | 104 +++++++++- scripts/check-overlay-whitelist-table.mjs | 127 ++++++++++++ scripts/check-quick-reference-counts.mjs | 133 +++++++++++++ scripts/check-ratchet-remedy-authority.mjs | 130 +++++++++++- scripts/check-required-contexts.mjs | 133 +++++++++++++ scripts/check-route-envelope.mjs | 145 +++++++++++++- scripts/check-section-landing-index.mjs | 113 +++++++++++ scripts/check-stall-guard-budget.mjs | 109 +++++++++++ scripts/check-system-context-census.mjs | 124 ++++++++++++ scripts/check-undeclared-dep-imports.mjs | 109 ++++++++++- scripts/check-vendor-version-stamps.mjs | 101 ++++++++++ scripts/docs-audit/affected-docs.mjs | 146 ++++++++++++++ scripts/release-github-releases.mjs | 109 +++++++++++ scripts/sync-template-versions.mjs | 105 ++++++++++ 30 files changed, 3543 insertions(+), 8 deletions(-) diff --git a/scripts/check-adr-0087-registration.mjs b/scripts/check-adr-0087-registration.mjs index bd098e5cd3..4e01682502 100644 --- a/scripts/check-adr-0087-registration.mjs +++ b/scripts/check-adr-0087-registration.mjs @@ -356,6 +356,82 @@ import { fileURLToPath } from 'node:url'; import { isEntrypoint } from './invoked-as.mjs'; import { blank, maskComments, scanSource } 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({ + 'G1: a non-breaking changeset is not this gate\'s business': 1, + 'R1: THE #6011 SHAPE -- declared breaking, no ledger entry, no marker': 5, + 'R2: the catch-all exemption cannot cover a FROM -> TO prescription': 4, + 'G2: THE RECONCILIATION STEP -- register it and the same input passes': 1, + 'R3: `registered` naming an id that does not exist': 3, + 'R4: `registered` claiming a registration this PR did not make': 3, + 'G3: already-registered, naming a genuinely pre-existing entry': 1, + 'R5: already-registered pointing at an id this diff just added': 3, + 'R6: `unpublished` claimed for a published package': 3, + 'G4: `unpublished` on a genuinely private package': 1, + 'R7: an unknown category is refused (the vocabulary is closed)': 3, + 'R8: an empty justification is refused': 2, + 'G5: the catch-all, on a changeset carrying no prescription': 1, + 'R9: two markers is ambiguous, not "the first one wins"': 2, + 'R10: THE #6419 SHAPE -- a REAL prescription, written in Chinese with -': 4, + 'R11: the same, framed by a HEADING instead of an inline label': 3, + 'G7: ordinary prose arrows under the catch-all stay GREEN': 1, + 'R12: THE #6497 SHAPE -- a rewrite TABLE with no arrow in it': 4, + 'R13: THE #6559 SHAPE -- the frame is the TABLE\'S OWN HEADER ROW': 4, + 'G8: a CAPABILITY table under the same framing stays GREEN': 1, + 'G9: THE #6967 SHAPE -- a changeset that POINTS AT prescriptions': 1, + 'R14: ...and the SAME sentence with the goods still refuses the catch-all': 4, + 'The #8299 category: `runtime-interface-only`': 12, + '#12881: a metadata surface that names the symbol only in PROSE': 30, + 'The #13080 category: `type-surface-only`': 26, + 'TSO-6048: THE REGRESSION PIN -- the founding case must never admit': 7, + 'TSO-N: the predicate set is pinned BY NAME, never by count': 3, + 'TSO-U: unit pins on predicate 4\'s readers': 19, + 'G6: a changeset that was ALREADY breaking at base is inherited': 1, + 'R15: a changeset RENAMED AND turned breaking in the same commit': 5, + 'G10: a PURE rename of an ALREADY-breaking stock changeset': 3, + 'R16: an `R` row whose BASE side is README.md inherits NOTHING': 4, + 'Input assertions (#4690)': 5, + 'I2c: the #4690 posture SURVIVES the #8658 repair -- a rev with no': 1, + 'I2d: the synthetic convention-rot controls run on a repo whose stock DOES': 4, + 'V3: the real repository is the case that matters -- this gate\'s own vocabulary': 10, + 'Unit pins on the two pattern-shaped judgements': 29, + 'the floors: what the new vocabulary must refuse': 9, + 'the floors: labels that are NOT mentions, and mentions that ARE evidenced': 9, + 'P51-P60: the HARD-WRAPPED mention, and the floors that keep the cure from': 11, + 'P62-P68: the framed region closes at the same or a SHALLOWER heading, not': 7, + 'U1-U12 (#8299): unit pins on the runtime-interface-only primitives': 13, + 'S1-S5: the `--audit-stock` classifier (#6350)': 7, + 'PRE1-PRE3: `.changeset/pre/` is CONSUMED stock, not pending stock': 4, + 'PRE3: `entries` stays UNFILTERED. A tree whose changesets have all moved': 4, + 'P9-P14 (#7004): the shapes the old entry anchor hid from signal (1)': 7, + 'I1 (#6566): a bare `import` of this module must NOT run the gate': 3, + 'I2 (#6566): the SAME file, run as the entry point, still dispatches': 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 = 48; + +// The key an assertion is filed under when no battery is open. It is not a +// declared battery, so it reds by the same set difference rather than silently +// inflating whichever battery happened to run last. +const UNATTRIBUTED_BATTERY = '(no battery open)'; + const __dirname = dirname(fileURLToPath(import.meta.url)); const REPO_ROOT = resolve(__dirname, '..'); @@ -3231,9 +3307,24 @@ function auditStock(cwd, head) { const SELF_TEST_VERDICT = 'check-adr-0087-registration 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 = []; let checked = 0; const assert = (cond, msg) => { + registerCase(); checked++; if (!cond) failures.push(msg); }; @@ -3325,6 +3416,7 @@ function selfTest() { }; // ---- G1: a non-breaking changeset is not this gate's business ------------- + battery('G1: a non-breaking changeset is not this gate\'s business'); green('G1 non-breaking changeset ignored', run(mk({ files: { '.changeset/nb.md': CS({ bumps: [['@objectstack/spec', 'patch']], body: 'a patch\n\nprose.\n' }) }, }))); @@ -3332,6 +3424,7 @@ function selfTest() { // ---- R1: THE #6011 SHAPE -- declared breaking, no ledger entry, no marker -- // #6048's changeset reconstructed: major + **BREAKING** + a FROM -> TO block, // and nothing said about the ledger. + battery('R1: THE #6011 SHAPE -- declared breaking, no ledger entry, no marker'); const SIX048 = CS({ bumps: [['@objectstack/runtime', 'major']], body: '**BREAKING**: `ctx.user.roles` removed\n\n### 迁移:FROM → TO\n\n```js\n// FROM\nctx.user.roles;\n// TO\nctx.user.positions;\n```\n', @@ -3342,6 +3435,7 @@ function selfTest() { })), [/tidy-donkeys-yawn/, /no `adr-0087:` disposition marker/, /#6148/, /adr-0087: registered/]); // ---- R2: the catch-all exemption cannot cover a FROM -> TO prescription ---- + battery('R2: the catch-all exemption cannot cover a FROM -> TO prescription'); red('R2 no-migration-prescription contradicted by the body', run(mk({ files: { '.changeset/tidy-donkeys-yawn.md': SIX048.replace( @@ -3353,6 +3447,7 @@ function selfTest() { })), [/contradicts the changeset's own body/, /Evidence \(from-to-label\)/, /#6048/]); // ---- G2: THE RECONCILIATION STEP -- register it and the same input passes -- + battery('G2: THE RECONCILIATION STEP -- register it and the same input passes'); green('G2 same input, registered by this diff', run(mk({ headIds: ['old-entry-one', 'old-entry-two', 'actor-user-roles-to-positions'], files: { @@ -3365,16 +3460,19 @@ function selfTest() { }))); // ---- R3: `registered` naming an id that does not exist -------------------- + battery('R3: `registered` naming an id that does not exist'); red('R3 registered names a nonexistent id', run(mk({ files: { '.changeset/x.md': CS({ body: '**BREAKING** x\n\n\n' }) }, })), [/do not exist in/, /no-such-entry/]); // ---- R4: `registered` claiming a registration this PR did not make -------- + battery('R4: `registered` claiming a registration this PR did not make'); red('R4 registered names only pre-existing ids', run(mk({ files: { '.changeset/x.md': CS({ body: '**BREAKING** x\n\n\n' }) }, })), [/none of those ids is NEW in this diff/, /already-registered/]); // ---- G3: already-registered, naming a genuinely pre-existing entry -------- + battery('G3: already-registered, naming a genuinely pre-existing entry'); green('G3 already-registered on a base entry', run(mk({ files: { '.changeset/x.md': CS({ body: '**BREAKING** x\n\n\n' }), @@ -3382,6 +3480,7 @@ function selfTest() { }))); // ---- R5: already-registered pointing at an id this diff just added -------- + battery('R5: already-registered pointing at an id this diff just added'); red('R5 already-registered names a freshly added id', run(mk({ headIds: ['old-entry-one', 'old-entry-two', 'brand-new-entry'], files: { @@ -3390,6 +3489,7 @@ function selfTest() { })), [/names only id\(s\) this very diff ADDS/, /adr-0087: registered brand-new-entry/]); // ---- R6: `unpublished` claimed for a published package -------------------- + battery('R6: `unpublished` claimed for a published package'); red('R6 unpublished is false', run(mk({ files: { '.changeset/x.md': CS({ body: '**BREAKING** x\n\n\n' }), @@ -3397,6 +3497,7 @@ function selfTest() { })), [/is PUBLISHED/, /@objectstack\/spec/]); // ---- G4: `unpublished` on a genuinely private package --------------------- + battery('G4: `unpublished` on a genuinely private package'); green('G4 unpublished on a private package', run(mk({ files: { '.changeset/x.md': CS({ @@ -3411,16 +3512,19 @@ function selfTest() { }))); // ---- R7: an unknown category is refused (the vocabulary is closed) -------- + battery('R7: an unknown category is refused (the vocabulary is closed)'); red('R7 unknown category', run(mk({ files: { '.changeset/x.md': CS({ body: '**BREAKING** x\n\n\n' }) }, })), [/unknown `not-required` category/, /vocabulary is closed/]); // ---- R8: an empty justification is refused -------------------------------- + battery('R8: an empty justification is refused'); red('R8 justification too short', run(mk({ files: { '.changeset/x.md': CS({ body: '**BREAKING** x\n\n\n' }) }, })), [/character justification/]); // ---- G5: the catch-all, on a changeset carrying no prescription ----------- + battery('G5: the catch-all, on a changeset carrying no prescription'); green('G5 no-migration-prescription with no FROM/TO in the body', run(mk({ files: { '.changeset/x.md': CS({ body: '**BREAKING** x\n\n\n' }), @@ -3428,6 +3532,7 @@ function selfTest() { }))); // ---- R9: two markers is ambiguous, not "the first one wins" --------------- + battery('R9: two markers is ambiguous, not "the first one wins"'); red('R9 two disposition markers', run(mk({ files: { '.changeset/x.md': CS({ body: '**BREAKING** x\n\n\n\n' }), @@ -3443,6 +3548,7 @@ function selfTest() { // author had written real identifiers instead of the placeholder words FROM/TO. // Reverse-verified: restore the old pattern and this case reports "expected RED, // got green" -- it is green for the empty reason, which is exactly the defect. + battery('R10: THE #6419 SHAPE -- a REAL prescription, written in Chinese with -'); red('R10 the #6419 shape (a real Chinese prescription under the catch-all)', run(mk({ files: { '.changeset/x.md': CS({ @@ -3457,6 +3563,7 @@ function selfTest() { // ---- R11: the same, framed by a HEADING instead of an inline label --------- // `## 迁移` + a list of rewrites is the other natural spelling; a line-only rule // would read the list as unframed prose and pass it. + battery('R11: the same, framed by a HEADING instead of an inline label'); red('R11 a prescription under a migration HEADING', run(mk({ files: { '.changeset/x.md': CS({ @@ -3473,6 +3580,7 @@ function selfTest() { // version step, a dependency direction, a request trace. None is a prescription, // and refusing them would leave an entitled author with no honest disposition -- // the #6419 shape pointed the other way. + battery('G7: ordinary prose arrows under the catch-all stay GREEN'); green('G7 prose arrows are not prescriptions', run(mk({ files: { '.changeset/x.md': CS({ @@ -3494,6 +3602,7 @@ function selfTest() { // on top. Both pre-#6497 branches required an arrow, so this was read as "no // prescription at all" and the exemption was granted. Reverse-verified: delete // the table arm and this case reports "expected RED, got green". + battery('R12: THE #6497 SHAPE -- a rewrite TABLE with no arrow in it'); const SIX497 = CS({ bumps: [['@objectstack/runtime', 'major']], body: @@ -3523,6 +3632,7 @@ function selfTest() { // "expected RED, got green". It is the ONLY integration case in this file that // can move that way, which is why the arm's other assertions (P33-P38) are // labelled floors rather than counted as coverage. + battery('R13: THE #6559 SHAPE -- the frame is the TABLE\'S OWN HEADER ROW'); const SIX559 = CS({ bumps: [['@objectstack/spec', 'major']], body: @@ -3550,6 +3660,7 @@ function selfTest() { // NOTE, honestly: this case is green with the table arm and green without it. // Deleting an arm can only make a detector match LESS, so no false-positive floor // can move under reverse verification. It pins the boundary, not the capability. + battery('G8: a CAPABILITY table under the same framing stays GREEN'); green('G8 a one-coded-cell capability table is not a prescription', run(mk({ files: { '.changeset/x.md': CS({ @@ -3576,6 +3687,7 @@ function selfTest() { // // This is the RED set under reverse verification: restore the branch-1 limb // (accept every placeholder occurrence) and this case goes red -- verified. + battery('G9: THE #6967 SHAPE -- a changeset that POINTS AT prescriptions'); green('G9 the #6967 shape (a POINTER to prescriptions is not a prescription)', run(mk({ files: { '.changeset/v17-anchor.md': CS({ @@ -3597,6 +3709,7 @@ function selfTest() { // FROM → TO mappings baked into it include:` over a list of real renames), so the // pair G9/R14 pins that the rule separates POINTERS from PRESCRIPTIONS and not // prose from markup. + battery('R14: ...and the SAME sentence with the goods still refuses the catch-all'); red('R14 a cited FROM → TO with the rewrites present still contradicts the catch-all', run(mk({ files: { '.changeset/x.md': CS({ @@ -3617,6 +3730,7 @@ function selfTest() { // Every accept/refuse pair below is driven off that collision on purpose: the // predicate is only worth having if the SAME NAME under two paths comes out two // different ways, which is what a bare-name grep cannot do. + battery('The #8299 category: `runtime-interface-only`'); const RIO_FILES = { 'packages/services/service-package/src/index.ts': 'export interface PackagePublishDriverFault { message: string }\n\n' + @@ -3703,6 +3817,7 @@ function selfTest() { // an ordinary runtime module, and a metadata surface that names it -- with only // WHERE the name sits changing between them. That is the whole content of the // rule, so a case that passed for any other reason would not discriminate. + battery('#12881: a metadata surface that names the symbol only in PROSE'); // RIO-G2: THE #12881 CASE. A `*.zod.ts` docblock explains what the runtime type // keys on. It is a sentence: nothing is declared, nothing is imported, and @@ -3876,6 +3991,7 @@ function selfTest() { // attributable to the predicate it is named for. The greens and reds share one // fixture family for the reason RIO's do: a case that passed for another reason // would not discriminate. + battery('The #13080 category: `type-surface-only`'); const TSO_BASE_CLIENT = 'export class ObjectStackClient {\n' + ' analytics = {\n' + @@ -4035,6 +4151,7 @@ function selfTest() { // pin -- never a silent skip (#4690). // (b) a SCAN-level pin driving the full shipping `scan()` over a two-commit // reconstruction of the #6048 diff, claiming this category. + battery('TSO-6048: THE REGRESSION PIN -- the founding case must never admit'); const REAL_ACTOR_USER = 'packages/runtime/src/security/actor-user.ts'; let realActorUserText = null; try { realActorUserText = readFileSync(join(REPO_ROOT, REAL_ACTOR_USER), 'utf8'); } catch { /* reported below */ } @@ -4084,6 +4201,7 @@ function selfTest() { // exactly that defect. Both halves are needed: the exported NAMES are the // contract, and the second assertion is what proves the exported list is the // list the shipping function actually evaluates rather than a decorative one. + battery('TSO-N: the predicate set is pinned BY NAME, never by count'); assert( JSON.stringify(TYPE_SURFACE_PREDICATES) === JSON.stringify(['published', 'no-spec-diff', 'no-metadata-surface-diff', 'narrowed-from-erased']), @@ -4111,6 +4229,7 @@ function selfTest() { } // ---- TSO-U: unit pins on predicate 4's readers ------------------------------ + battery('TSO-U: unit pins on predicate 4\'s readers'); assert(unwrapPromise('Promise') === 'AnalyticsResult', 'TSO-U1: a whole-string Promise unwraps'); assert(unwrapPromise('Promise< any >') === 'any', 'TSO-U2: spacing does not defeat the unwrap'); assert(unwrapPromise('Promise | undefined') === null, 'TSO-U3: a Promise that is only PART of the type does not unwrap -- a greedy match here reads `any> | undefined` and is wrong in the ADMITTING direction'); @@ -4135,6 +4254,7 @@ function selfTest() { assert(readDeclaredTypeSurface('export interface Other { a: 1 }\n', 'Missing') === null, 'TSO-U19: a symbol that is not there reads as null -- never as "it must have been erased"'); // ---- G6: a changeset that was ALREADY breaking at base is inherited ------- + battery('G6: a changeset that was ALREADY breaking at base is inherited'); { const r = mk({ files: {} }); // modify the stock breaking changeset -- it was breaking at base, so it is not @@ -4166,6 +4286,7 @@ function selfTest() { // The #7045 shape, and R1's own shape with a `git mv` bolted on: a declaration // this PR introduces, arriving at a path that did not exist at the branch point, // saying nothing about the ledger. + battery('R15: a changeset RENAMED AND turned breaking in the same commit'); { const r = mk({ baseFiles: { '.changeset/pending.md': CS({ bumps: [['@objectstack/spec', 'patch']], body: RENAMEABLE_BODY }) }, @@ -4191,6 +4312,7 @@ function selfTest() { // again. This case can only be green through the `R` path -- were the rename to // degrade to add-plus-delete the new path would arrive as `A`, and `A` never // reads the base side at all, so it would be RED. + battery('G10: a PURE rename of an ALREADY-breaking stock changeset'); { const r = mk({ files: {} }); git(['mv', '.changeset/stock-breaking.md', '.changeset/stock-breaking-moved.md'], r.dir); @@ -4213,6 +4335,7 @@ function selfTest() { // documentation and declares nothing, so reading "already breaking at base" off // it would exempt a genuinely new breaking changeset. Delete the // `isChangesetFile(basePath)` guard in the scan and this case goes green. + battery('R16: an `R` row whose BASE side is README.md inherits NOTHING'); { const BREAKING_README = `# Changesets\n\n**BREAKING** ${RENAMEABLE_BODY}`; const r = mk({ @@ -4233,6 +4356,7 @@ function selfTest() { } // ---- Input assertions (#4690) -------------------------------------------- + battery('Input assertions (#4690)'); { const r = mk({ files: {} }); assert(assertInputs({ cwd: r.dir, head: 'HEAD' }).length === 0, 'I0: a well-formed repo must produce no input problems'); @@ -4307,6 +4431,7 @@ function selfTest() { // I2c: the #4690 posture SURVIVES the #8658 repair -- a rev with no // `.changeset/` at all (not even README/config) is unreadable input, and // unreadable input is a refusal, never a pass. + battery('I2c: the #4690 posture SURVIVES the #8658 repair -- a rev with no'); const dir = mkdtempSync(join(tmpdir(), 'adr0087-nodir-')); cleanup.push(dir); const w = (rel, text) => { mkdirSync(dirname(join(dir, rel)), { recursive: true }); writeFileSync(join(dir, rel), text); }; @@ -4332,6 +4457,7 @@ function selfTest() { // controls refuse -- cannot be staged from here without mutating this // module; it is verified by ablation: see the reverse-verification record // on the PR that introduced the controls, #8658.) + battery('I2d: the synthetic convention-rot controls run on a repo whose stock DOES'); const r = mk({ files: {} }); const probs = assertInputs({ cwd: r.dir, head: 'HEAD' }); assert(probs.length === 0, `I2d: the synthetic breaking-detector controls add no problems on a healthy detector -- got: ${probs.join('|')}`); @@ -4373,6 +4499,7 @@ function selfTest() { // V3: the real repository is the case that matters -- this gate's own vocabulary // and the real ADR must agree at the tip being tested, not just in fixtures. + battery('V3: the real repository is the case that matters -- this gate\'s own vocabulary'); const realAdr = readFileSync(join(REPO_ROOT, ADR_0087), 'utf8'); const documented = documentedCategories(realAdr); for (const c of CATEGORIES) assert(documented.has(c), `V3: ${ADR_0087} must document the \`${c}\` category`); @@ -4385,6 +4512,7 @@ function selfTest() { // must keep matching, or the #6048 shape stops being caught. P4-P5 are the // original false-positive floor. P9-P17 are #6419: the branch that reads real // prescriptions, and the prose shapes it must still refuse. + battery('Unit pins on the two pattern-shaped judgements'); assert(hasMigrationPrescription('### 迁移:FROM → TO\n'), 'P1: the Chinese prescription heading must match'); assert(hasMigrationPrescription('**FROM → TO**\n'), 'P2: the inline FROM -> TO heading must match'); assert(hasMigrationPrescription('| FROM (legacy) | TO (primitives) |\n'), 'P3: a FROM/TO table header must match'); @@ -4462,6 +4590,7 @@ function selfTest() { 'P32: a THREE-column table still frames, as long as the new column follows the old one', ); // --- the floors: what the new vocabulary must refuse ----------------------- + battery('the floors: what the new vocabulary must refuse'); assert( !hasMigrationPrescription('| route | was | now |\n|---|---|---|\n| `POST /share-links` | `{ link }` | `{ success: true, data: link }` |\n'), 'P33: `| was | now |` is this repo\'s BEHAVIOUR-comparison header, not a rewrite -- `was` is deliberately not an OLD word', @@ -4506,6 +4635,7 @@ function selfTest() { 'P41: `their FROM → TO migration` -- a governing word beats the framing word that follows', ); // --- the floors: labels that are NOT mentions, and mentions that ARE evidenced -- + battery('the floors: labels that are NOT mentions, and mentions that ARE evidenced'); assert( findMigrationPrescription('**Removed keys and their prescriptions (FROM → TO):**\n\n- `App.version` → an app is versioned by its package\n')?.branch === 'from-to-label', 'P42: a parenthesised label is opened by `(`, not governed by `prescriptions` -- `app-dead-authoring-keys.md`', @@ -4554,6 +4684,7 @@ function selfTest() { // and every other way of opening a line keeps what it had. P52 is the positive // control the specimen assertions are worthless without -- if it ever goes red // with P51 green, the arm has stopped seeing rather than started discriminating. + battery('P51-P60: the HARD-WRAPPED mention, and the floors that keep the cure from'); assert( !hasMigrationPrescription( 'The AGENTS.md post-task checklist requires breaking changesets to carry their\nFROM → TO migration because "this text ships to consumers as `CHANGELOG.md`\ninside the npm package and is what an upgrading agent greps after the tombstone\nerror."\n', @@ -4610,6 +4741,7 @@ function selfTest() { // P65-P66 are false-positive floors -- green with the fix and green without it, // which is said out loud rather than counted as coverage, because they pin where // a framed region STOPS and a region that never stops frames the whole document. + battery('P62-P68: the framed region closes at the same or a SHALLOWER heading, not'); const NESTED_TABLE = '## Migration\n\n### Method namespace\n\n| before | after |\n| --- | --- |\n| `client.projects.list()` | `client.environments.list()` |\n'; assert( findMigrationPrescription(NESTED_TABLE)?.branch === 'framed-table', @@ -4645,6 +4777,7 @@ function selfTest() { // The path classifier is the half that decides what the reference scan even // reads, so a silent narrowing of it would make the exemption easier to hold // while every RIO case above stayed green. + battery('U1-U12 (#8299): unit pins on the runtime-interface-only primitives'); assert(metadataSurfaceKind('packages/spec/src/ai/agent.zod.ts') === 'a Zod schema', 'U1: a *.zod.ts is a metadata surface'); assert(metadataSurfaceKind('packages/spec/src/contracts/data-driver.ts') === 'a spec contracts/** entry', 'U2: a spec contract is a metadata surface'); assert(metadataSurfaceKind('examples/app-crm/src/objects/account.object.ts') === 'an object definition', 'U3: an object definition is a metadata surface'); @@ -4678,6 +4811,7 @@ function selfTest() { // the one precedence rule between them. They live in the self-test (which CI // runs) even though the audit itself is operator-invoked, because the classifier // reuses the gate's judging functions and would rot with them. + battery('S1-S5: the `--audit-stock` classifier (#6350)'); { const PKGS = new Map([ ['@objectstack/spec', { private: false, file: 'packages/spec/package.json' }], @@ -4730,6 +4864,7 @@ function selfTest() { // by a PR is judged like any other. Filter the verdict instead of the audit and // `.changeset/pre/` becomes the place to put a breaking change you do not want // read. + battery('PRE1-PRE3: `.changeset/pre/` is CONSUMED stock, not pending stock'); { const { dir } = mk({ files: { @@ -4754,6 +4889,7 @@ function selfTest() { // into `.changeset/pre/` is the ordinary state of main mid-window; narrowing // the readability probe as well would turn it into a #4690 refusal and undo // #8658 by a different route. Pending stock is legitimately zero there. + battery('PRE3: `entries` stays UNFILTERED. A tree whose changesets have all moved'); const { dir } = mk({ files: { '.changeset/stock-breaking.md': null, @@ -4786,6 +4922,7 @@ function selfTest() { // Predicted direction on reverse verification: restoring the old anchor // (`([A-Za-z]+)\s*$`) turns P9-P13 red (breaking goes false, signals loses // `major`) and P14 red in the other direction (a phantom `# note` bump). + battery('P9-P14 (#7004): the shapes the old entry anchor hid from signal (1)'); const PLAIN = 'a summary line\n\nsome prose that is quite long indeed and explains the change.\n'; const bumpsOf = (text) => parseChangeset(text).bumps.map((b) => `${b.pkg}=${b.bump}`); assert( @@ -4824,6 +4961,7 @@ function selfTest() { // fixture repo, prints its verdict, `process.exit(1)`s, and the importer's // own line never runs -- the behaviour measured in #6566, which forced // PR #6556 into a subprocess fixture instead of an import. + battery('I1 (#6566): a bare `import` of this module must NOT run the gate'); { const { dir, base } = mk({ files: { '.changeset/unanswered-breaking.md': CS({ body: '**BREAKING** x\n\nno marker here\n' }) }, @@ -4877,6 +5015,7 @@ function selfTest() { // `--list` exiting 0, stayed green under that second ablation -- a script // with no CLI at all also exits 0. An exit code cannot distinguish "the // branch ran and succeeded" from "nothing ran"; only the printed table can. + battery('I2 (#6566): the SAME file, run as the entry point, still dispatches'); const cli = (...args) => spawnSync(process.execPath, [join(dir, copy), ...args], { cwd: dir, encoding: 'utf8' }); const gate = cli(); @@ -4898,6 +5037,52 @@ function selfTest() { for (const d of cleanup) rmSync(d, { 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(`✗ check-adr-0087-registration --self-test -- ${failures.length} failure(s)\n`); for (const f of failures) console.error(` • ${f}\n`); diff --git a/scripts/check-auth-mount-ledger.mjs b/scripts/check-auth-mount-ledger.mjs index c14f7cd83e..cd7b24ab4a 100644 --- a/scripts/check-auth-mount-ledger.mjs +++ b/scripts/check-auth-mount-ledger.mjs @@ -120,6 +120,52 @@ import { join, resolve } from 'node:path'; import { maskComments } from './js-comment-mask.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 base path is DERIVED, and its absence is not an empty population.': 2, + 'LOAD-BEARING NEGATIVE: a mount with an exact row is clean.': 1, + 'LOAD-BEARING POSITIVE: a mount added with no row REDDENS, naming the route.': 2, + 'THE RIGHT BOUNDARY, both directions. This is the defect class #10534 fell into.': 4, + 'The method is part of the identity: same path, different verb, is a different route.': 1, + 'CONSTRAINT 3: the lanes are excluded, and adding one does not redden.': 2, + 'Mounts that are not under basePath are not this ledger\'s business.': 2, + 'A commented-out mount is not a mount.': 2, + 'A string-literal mount under basePath is still a mount (no `${basePath}` required).': 1, + 'CONSTRAINT 4: what cannot be read is reported, never skipped.': 3, + 'The vendor inventory accounts for a shadowing mount, and says so.': 2, + 'The rationale half: a pasted row does not satisfy this gate.': 5, + 'A row whose mount is gone fails (the direction the hand-written pin already had).': 1, + 'PENDING_DISPOSITION, reconciled in BOTH directions.': 5, + '#8435 remedy authority. PLACEMENT is pinned here, per-gate, because the': 4, + 'Parse anchors: a moved anchor is a REFUSAL input, never an empty population.': 2, + 'The escaped-quote shape the real notes use is measured, not truncated.': 1, + 'And the real inputs on disk are readable, so the anchors have not moved.': 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 = 18; + +// 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 = resolve(new URL('..', import.meta.url).pathname); /** The two inputs. Module-scope literals, so `dispatch-gates` derives this @@ -558,16 +604,32 @@ const REAL_NOTE = 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 fail = []; let cases = 0; - const ok = (cond, what) => { cases += 1; if (!cond) fail.push(what); }; + const ok = (cond, what) => { registerCase(); cases += 1; if (!cond) fail.push(what); }; const kinds = (r) => r.findings.map((f) => f.kind).sort(); // -- The base path is DERIVED, and its absence is not an empty population. + battery('The base path is DERIVED, and its absence is not an empty population.'); ok(deriveBasePath(FIXTURE_PREAMBLE) === FIXTURE_BASE, 'basePath was not derived from the plugin'); ok(deriveBasePath('const basePath = 42;') === null, 'a plugin with no derivable basePath did not refuse'); // -- LOAD-BEARING NEGATIVE: a mount with an exact row is clean. + battery('LOAD-BEARING NEGATIVE: a mount with an exact row is clean.'); ok( runFixture( 'rawApp.post(`${basePath}/admin/unlock-user`, h);', @@ -577,6 +639,7 @@ function selfTest() { ); // -- LOAD-BEARING POSITIVE: a mount added with no row REDDENS, naming the route. + battery('LOAD-BEARING POSITIVE: a mount added with no row REDDENS, naming the route.'); { const r = runFixture('rawApp.post(`${basePath}/admin/zzz-new`, h);', []); ok(kinds(r).includes('unaccounted-mount'), 'an unledgered mount did not redden the gate'); @@ -587,6 +650,7 @@ function selfTest() { } // -- THE RIGHT BOUNDARY, both directions. This is the defect class #10534 fell into. + battery('THE RIGHT BOUNDARY, both directions. This is the defect class #10534 fell into.'); { // The shorter route is mounted; only the LONGER sibling is ledgered. const r = runFixture( @@ -621,6 +685,7 @@ function selfTest() { } // -- The method is part of the identity: same path, different verb, is a different route. + battery('The method is part of the identity: same path, different verb, is a different route.'); ok( runFixture( 'rawApp.get(`${basePath}/config`, h);', @@ -630,6 +695,7 @@ function selfTest() { ); // -- CONSTRAINT 3: the lanes are excluded, and adding one does not redden. + battery('CONSTRAINT 3: the lanes are excluded, and adding one does not redden.'); { const r = runFixture( 'rawApp.all(`${basePath}/*`, h);\n' + @@ -642,6 +708,7 @@ function selfTest() { } // -- Mounts that are not under basePath are not this ledger's business. + battery('Mounts that are not under basePath are not this ledger\'s business.'); ok( runFixture("rawApp.get('/.well-known/openid-configuration', h);", []).findings.length === 0, 'a .well-known mount outside basePath was treated as an auth-ledger mount', @@ -652,6 +719,7 @@ function selfTest() { ); // -- A commented-out mount is not a mount. + battery('A commented-out mount is not a mount.'); ok( runFixture('// rawApp.post(`${basePath}/admin/ghost`, h);', []).findings.length === 0, 'a commented-out mount was counted -- comment masking is not reaching the scan', @@ -662,12 +730,14 @@ function selfTest() { ); // -- A string-literal mount under basePath is still a mount (no `${basePath}` required). + battery('A string-literal mount under basePath is still a mount (no `${basePath}` required).'); ok( runFixture("rawApp.post('/api/v1/auth/admin/literal', h);", []).findings.some((f) => f.text.includes('/admin/literal')), 'a mount written with a literal path instead of the template bypassed the census', ); // -- CONSTRAINT 4: what cannot be read is reported, never skipped. + battery('CONSTRAINT 4: what cannot be read is reported, never skipped.'); ok( kinds(runFixture("rawApp.on('POST', `${basePath}/x`, h);", [])).includes('unreadable-mount'), 'rawApp.on(...) was silently skipped instead of reported', @@ -682,6 +752,7 @@ function selfTest() { ); // -- The vendor inventory accounts for a shadowing mount, and says so. + battery('The vendor inventory accounts for a shadowing mount, and says so.'); { const r = runFixture( 'rawApp.post(`${basePath}/admin/ban-user`, h);', @@ -693,6 +764,7 @@ function selfTest() { } // -- The rationale half: a pasted row does not satisfy this gate. + battery('The rationale half: a pasted row does not satisfy this gate.'); ok( kinds(runFixture( 'rawApp.post(`${basePath}/admin/pasted`, h);', @@ -731,6 +803,7 @@ function selfTest() { ); // -- A row whose mount is gone fails (the direction the hand-written pin already had). + battery('A row whose mount is gone fails (the direction the hand-written pin already had).'); ok( kinds(runFixture( '', @@ -740,6 +813,7 @@ function selfTest() { ); // -- PENDING_DISPOSITION, reconciled in BOTH directions. + battery('PENDING_DISPOSITION, reconciled in BOTH directions.'); { const mount = 'rawApp.post(`${basePath}/set-initial-password`, h);'; const p = [{ route: 'POST /api/v1/auth/set-initial-password', issue: '#10975', why: 'x' }]; @@ -776,6 +850,7 @@ function selfTest() { // farm-wide sweep deliberately checks only PRESENCE (its header states the // split: "Presence here, placement there"). Both paths that expand // PENDING_DISPOSITION must name their owner IN THE MESSAGE THE AUTHOR READS. + battery('#8435 remedy authority. PLACEMENT is pinned here, per-gate, because the'); ok( RATCHET_AUTHORITY === '⛔ MAINTAINER-ONLY', 'the authority token is not the spelling scripts/check-ratchet-remedy-authority.mjs sweeps for', @@ -807,20 +882,69 @@ function selfTest() { ); // -- Parse anchors: a moved anchor is a REFUSAL input, never an empty population. + battery('Parse anchors: a moved anchor is a REFUSAL input, never an empty population.'); ok(parseLedgerRows('export const SOMETHING_ELSE = [];') === null, 'a missing AUTH_ROUTE_LEDGER anchor parsed as zero rows'); ok(parseVendorSurface('export const SOMETHING_ELSE = [];') === null, 'a missing surface anchor parsed as zero rows'); // -- The escaped-quote shape the real notes use is measured, not truncated. + battery('The escaped-quote shape the real notes use is measured, not truncated.'); ok( unescape("objectui app-shell\\'s wizard").length === 'objectui app-shell\'s wizard'.length, 'an escaped quote in a note was mis-measured', ); // -- And the real inputs on disk are readable, so the anchors have not moved. + battery('And the real inputs on disk are readable, so the anchors have not moved.'); for (const rel of [MOUNT_SOURCE, LEDGER_SOURCE]) { ok(existsSync(join(ROOT, rel)), `${rel} does not exist -- this gate's anchor moved`); } + // ── 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) => { + fail.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 (fail.length) { console.error('check-auth-mount-ledger --self-test FAILED:'); for (const f of fail) console.error(` - ${f}`); diff --git a/scripts/check-ci-filter-parity.mjs b/scripts/check-ci-filter-parity.mjs index c7bf14d2cd..00bf58fd26 100644 --- a/scripts/check-ci-filter-parity.mjs +++ b/scripts/check-ci-filter-parity.mjs @@ -135,6 +135,41 @@ const { parse } = await requireDependency('yaml', () => import('yaml'), import.m import { CROSS_PACKAGE_TEST_INPUTS } from './cross-package-test-inputs.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 coverage rule, both limbs and both directions': 7, + '(2) THE SAME-ROOT-DIFFERENT-FILE CASE': 3, + '(3) the three coverage outcomes, end to end through `judge`': 7, + '(4) the reverse direction: a `crosspkg` entry covering nothing': 3, + '(5) refusals: never a clean zero over a subject that was not read': 10, + '(6) the real tree': 10, + '(7) WIRING: the gate and its self-test really run in CI': 2, +}); + +// DELETING an entry silences that battery's floor exactly as effectively as +// zeroing it, so the roster's own size is pinned too. +const SELF_TEST_BATTERY_FLOOR = 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 HERE = resolve(fileURLToPath(import.meta.url), '..'); const REPO_ROOT = resolve(HERE, '..'); @@ -421,6 +456,20 @@ function list(root = REPO_ROOT, table = CROSS_PACKAGE_TEST_INPUTS) { 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); + }; + /** A ci.yml source carrying the two scheduling lists, in the real shape. */ const REAL_TEST_IF = "${{ !cancelled() && (needs.filter.outputs.core != 'false' || needs.filter.outputs.crosspkg != 'false') }}"; @@ -452,6 +501,7 @@ export async function selfTest() { const failures = []; let checked = 0; const assert = (cond, label) => { + registerCase(); checked += 1; if (!cond) failures.push(label); }; @@ -472,6 +522,7 @@ export async function selfTest() { }; // ── (1) the coverage rule, both limbs and both directions ─────────────── + battery('(1) the coverage rule, both limbs and both directions'); assert(coverageVerdict('scripts/**', ['scripts/**']).covered, 'a glob a list contains VERBATIM is covered'); assert( coverageVerdict('content/docs/api/error-catalog.mdx', ['content/**']).kind === 'subtree', @@ -502,6 +553,7 @@ export async function selfTest() { // `core` names `.github/workflows/ci.yml`. A rule that asked "is this glob's // ROOT mentioned anywhere?" would answer covered for a DIFFERENT file under // that root, and the ten declarations #10015 fixed included exactly this one. + battery('(2) THE SAME-ROOT-DIFFERENT-FILE CASE'); const sameRoot = judge( fixtureWorkflow({ core: ['packages/**', '.github/workflows/ci.yml'], crosspkg: ['scripts/**'] }), table(['.github/workflows/scaffold-e2e.yml']), @@ -521,6 +573,7 @@ export async function selfTest() { assert(uncoveredGlobs(sameRootFixed).length === 0, '-- and naming the file itself in `crosspkg` covers it'); // ── (3) the three coverage outcomes, end to end through `judge` ────────── + battery('(3) the three coverage outcomes, end to end through `judge`'); const viaCore = judge(fixtureWorkflow(), table(['packages/lint/src/**'])); assert(uncoveredGlobs(viaCore).length === 0, 'a glob covered by `core` PASSES'); assert(viaCore.covered[0].via === 'packages/**', '-- and the verdict names the entry that covered it'); @@ -545,6 +598,7 @@ export async function selfTest() { ); // ── (4) the reverse direction: a `crosspkg` entry covering nothing ─────── + battery('(4) the reverse direction: a `crosspkg` entry covering nothing'); const stale = judge(fixtureWorkflow({ crosspkg: ['scripts/**', 'tools/**'] }), table(['scripts/x.mjs'])); assert(stale.stale.join(',') === 'tools/**', 'a `crosspkg` entry that covers no declaration is reported stale'); assert(uncoveredGlobs(stale).length === 0, '-- while the declaration it does cover stays covered'); @@ -552,6 +606,7 @@ export async function selfTest() { assert(notStale.stale.length === 0, 'an entry covering a declaration through the SUBTREE limb is not stale'); // ── (5) refusals: never a clean zero over a subject that was not read ──── + battery('(5) refusals: never a clean zero over a subject that was not read'); const refusal = (source, tbl = table(['packages/a/**'])) => judge(source, tbl).refusal; assert( /could not be read as YAML/.test(refusal('jobs:\n filter:\n \tbad: [') ?? ''), @@ -593,6 +648,7 @@ export async function selfTest() { ); // ── (6) the real tree ─────────────────────────────────────────────────── + battery('(6) the real tree'); const real = judge(readFileSync(join(REPO_ROOT, CI_WORKFLOW), 'utf8'), CROSS_PACKAGE_TEST_INPUTS); assert(!real.refusal, `the checked-in ci.yml is readable by this gate -- ${real.refusal ?? ''}`); assert((real.declarations ?? []).length > 0, 'the checked-in table declares something to judge'); @@ -655,6 +711,7 @@ export async function selfTest() { ); // ── (7) WIRING: the gate and its self-test really run in CI ────────────── + battery('(7) WIRING: the gate and its self-test really run in CI'); const SELF = 'scripts/check-ci-filter-parity.mjs'; let lint = null; try { @@ -667,6 +724,52 @@ export async function selfTest() { assert(lint.includes(`node ${SELF} --self-test`), 'wiring: lint.yml runs the --self-test leg too'); } + // ── The floor: every declared battery RAN, and ran its cases (#13489) ─── + // + // Evaluated after every battery has had its chance and BEFORE the verdict, so + // the success line below can only be printed by a run in which the set of + // batteries that registered assertions EQUALS the set declared. A set + // difference names WHICH battery stopped; a count says only that something did. + const floorFailure = (message) => { + failures.push(message); + }; + const declaredBatteries = Object.keys(SELF_TEST_BATTERIES); + let floorBreached = false; + if (declaredBatteries.length < SELF_TEST_BATTERY_FLOOR) { + floorBreached = true; + floorFailure( + `SELF_TEST_BATTERIES declares ${declaredBatteries.length} batteries, below the pinned ` + + `${SELF_TEST_BATTERY_FLOOR} — a battery deleted from the roster takes its own floor with it.`, + ); + } + for (const [name, count] of 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-ci-filter-parity --self-test — ${failures.length} of ${checked} assertion(s) failed\n`); for (const f of failures) console.error(` • ${f}`); diff --git a/scripts/check-comment-mask-adoption.mjs b/scripts/check-comment-mask-adoption.mjs index 57915ce9db..87e182cb4d 100644 --- a/scripts/check-comment-mask-adoption.mjs +++ b/scripts/check-comment-mask-adoption.mjs @@ -194,6 +194,40 @@ import { fileURLToPath } from 'node:url'; 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({ + 'Each SHAPE fires on the spelling measured in the tree': 8, + 'The ADOPTER must stay silent: this is the whole point': 3, + 'Prose must stay silent: the 18% fabrication this gate would otherwise': 3, + 'Negatives that must not fabricate': 5, + 'Ledger invariants': 3, + 'The instrument itself: the known-good probe MUST be recognised': 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 = 6; + +// The key an assertion is filed under when no battery is open. It is not a +// declared battery, so it reds by the same set difference rather than silently +// inflating whichever battery happened to run last. +const UNATTRIBUTED_BATTERY = '(no battery open)'; + const HERE = resolve(fileURLToPath(import.meta.url), '..'); const REPO_ROOT = resolve(HERE, '..'); @@ -435,14 +469,30 @@ function list() { let selfTestReachedVerdict = false; export function selfTest() { + // The battery ledger this self-test's floor is evaluated against (#13489). + // `battery()` opens a battery; every assertion below is attributed to the one + // most recently opened, so a section that stops running stops registering and + // names ITSELF at the floor rather than going quiet. + const 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) => { + registerCase(); console.log(`${ok ? 'ok ' : 'FAIL'} ${name}`); if (!ok) failures++; }; const ids = (src) => shapesIn(src).join(','); // ── Each SHAPE fires on the spelling measured in the tree ──────────────── + battery('Each SHAPE fires on the spelling measured in the tree'); t('the naive block-comment regex is caught', ids(String.raw`const b = src.replace(/\/\*[\s\S]*?\*\//g, '');`) === 'regex-block'); t('...including the [^] spelling of the same body', @@ -462,6 +512,7 @@ export function selfTest() { ids('function maskComments(source) { return source; }') === 'scanner-decl'); // ── The ADOPTER must stay silent: this is the whole point ──────────────── + battery('The ADOPTER must stay silent: this is the whole point'); t('an IMPORT of the shared stripper is NOT a finding', ids("import { stripComments } from '../../scripts/js-comment-mask.mjs';") === ''); t('...and neither is CALLING it', @@ -471,6 +522,7 @@ export function selfTest() { // ── Prose must stay silent: the 18% fabrication this gate would otherwise // aim at exactly the files that already complied ──────────────────────── + battery('Prose must stay silent: the 18% fabrication this gate would otherwise'); t('the same regex QUOTED IN PROSE is not a finding', ids(String.raw`/* We used to write src.replace(/\/\*[\s\S]*?\*\//g, '') here. */ const m = maskComments(src);`) === ''); @@ -480,6 +532,7 @@ const m = maskComments(src);`) === ''); ids(String.raw`const RE = /\/\*[\s\S]*?\*\//g;`) === 'regex-block'); // ── Negatives that must not fabricate ──────────────────────────────────── + battery('Negatives that must not fabricate'); t('an ordinary URL is not a stripper', ids("const u = 'https://example.com/a';") === ''); // Measured false positive from the first run of this gate, kept as a pin: // the name alone is not the finding, the FUNCTION it binds is. @@ -491,6 +544,7 @@ const m = maskComments(src);`) === ''); t('an empty source finds nothing', ids('') === ''); // ── Ledger invariants ──────────────────────────────────────────────────── + battery('Ledger invariants'); t('every recorded row carries a known verdict and a real reason', [...LEDGER.values()].every((r) => VERDICTS.has(r.verdict) && Array.isArray(r.shapes) && r.shapes.length > 0 @@ -506,9 +560,57 @@ const m = maskComments(src);`) === ''); // firing on a stripper nobody disputes. `js-comment-mask.mjs` is that probe: // it is the tree's one sanctioned comment stripper, and it exports the very // name this gate recognises. + battery('The instrument itself: the known-good probe MUST be recognised'); t('POSITIVE CONTROL — the shared module itself reads as a stripper', shapesIn(readFileSync(join(REPO_ROOT, CANONICAL), 'utf8')).includes('scanner-decl')); + // ── 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}`); + failures++; + }; + 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(`\n${failures === 0 ? 'PASS' : 'FAIL'} check-comment-mask-adoption --self-test (${failures} failure(s))`); selfTestReachedVerdict = true; return failures === 0 ? 0 : 1; diff --git a/scripts/check-console-injection.mjs b/scripts/check-console-injection.mjs index 76fa9489d7..f4c1f4bb44 100644 --- a/scripts/check-console-injection.mjs +++ b/scripts/check-console-injection.mjs @@ -168,6 +168,44 @@ import { } from './console-spec-probes.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. Clean pass: fresh witness in the bundle, stale detector nowhere.': 1, + '2. THE DEFECT: the restored bundle carries the published spec.': 1, + '3. Partial restore: the stamp\'s own witness is missing from the assets.': 1, + '4. Probe expiry: this tree\'s spec now carries the stale detector too.': 3, + '7. No dist at all: nothing to verify, unless one was required.': 2, + '7b. THE SECOND VACUITY PATH (objectstack#10428): an unbuilt spec means the': 4, + '7c. THE THIRD VACUITY PATH (objectstack#10595): a well-formed stamp whose': 4, + '7d. The producer cannot emit that stamp in the first place. writeStamp is': 2, + '8. A build that found no skew records it, and this gate says so honestly.': 3, + '12. ROUND TRIP against the real assert script: whatever it stamps, this gate': 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 ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); /** @@ -515,11 +553,26 @@ function stampFor({ skew = true, freshWitness = FRESH, staleDetector = STALE } = const SELF_TEST_VERDICT = 'check-console-injection 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 = []; let checked = 0; const root = tmpdir('check-console-injection'); const expect = (label, actual, wanted) => { + registerCase(); checked += 1; if (actual !== wanted) failures.push(`${label}: expected ${wanted}, got ${actual}`); }; @@ -527,6 +580,7 @@ function selfTest() { const specDir = makeSpecPkg(path.join(root, 'spec-ahead'), [FRESH, 'A shared description both specs carry always']); // 1. Clean pass: fresh witness in the bundle, stale detector nowhere. + battery('1. Clean pass: fresh witness in the bundle, stale detector nowhere.'); { const dist = makeDist(path.join(root, 'ok'), `console(${JSON.stringify(FRESH)})`, stampFor()); expect('clean pass', evaluate({ distDir: dist, specDir }).code, 0); @@ -541,6 +595,7 @@ function selfTest() { // was found to be vacuous. Both strings present is also the real shape: the // console bundle holds a second, transitive copy of this tree's spec via the // injected client, which is exactly why the check is two-sided. + battery('2. THE DEFECT: the restored bundle carries the published spec.'); { const dist = makeDist( path.join(root, 'stale'), @@ -563,6 +618,7 @@ function selfTest() { } // 3. Partial restore: the stamp's own witness is missing from the assets. + battery('3. Partial restore: the stamp\'s own witness is missing from the assets.'); { const dist = makeDist(path.join(root, 'partial'), 'console("unrelated bundle text")', stampFor()); const r = evaluate({ distDir: dist, specDir }); @@ -574,6 +630,7 @@ function selfTest() { } // 4. Probe expiry: this tree's spec now carries the stale detector too. + battery('4. Probe expiry: this tree\'s spec now carries the stale detector too.'); { const caught = makeSpecPkg(path.join(root, 'spec-caught-up'), [FRESH, STALE]); const dist = makeDist(path.join(root, 'expired'), `console(${JSON.stringify(FRESH)})`, stampFor()); @@ -591,6 +648,7 @@ function selfTest() { } // 7. No dist at all: nothing to verify, unless one was required. + battery('7. No dist at all: nothing to verify, unless one was required.'); { const dist = path.join(root, 'absent'); expect('no dist passes', evaluate({ distDir: dist, specDir }).code, 0); @@ -605,6 +663,7 @@ function selfTest() { // before the fix this fixture exited 0 with an `ℹ`, which is precisely a // green that asserts nothing, so only a red and its branch-unique wording // can tell the fixed script from the broken one. + battery('7b. THE SECOND VACUITY PATH (objectstack#10428): an unbuilt spec means the'); { const unbuilt = makeUnbuiltSpecPkg(path.join(root, 'spec-unbuilt')); const dist = makeDist(path.join(root, 'unbuilt-tree'), `console(${JSON.stringify(FRESH)})`, stampFor()); @@ -652,6 +711,7 @@ function selfTest() { // and every substantive verdict is derived per entry, so the gate asserts // nothing at all — strictly more vacuous than 7b, which still ran the two // bundle assertions. + battery('7c. THE THIRD VACUITY PATH (objectstack#10595): a well-formed stamp whose'); { const empty = { stampVersion: 1, generatedBy: 'scripts/assert-console-spec-injection.mjs', packages: [] }; const dist = makeDist(path.join(root, 'empty-stamp'), `console(${JSON.stringify(FRESH)})`, empty); @@ -708,6 +768,7 @@ function selfTest() { // the one call site every producer passes, and the entries array is meant // to GROW (objectstack#9659) — the day it is derived rather than literal, // an empty result becomes producible. Refused at the write. + battery('7d. The producer cannot emit that stamp in the first place. writeStamp is'); { const dir = fs.mkdtempSync(path.join(root, 'writestamp-')); checked += 1; @@ -728,6 +789,7 @@ function selfTest() { } // 8. A build that found no skew records it, and this gate says so honestly. + battery('8. A build that found no skew records it, and this gate says so honestly.'); { const dist = makeDist( path.join(root, 'noskew'), @@ -767,6 +829,7 @@ function selfTest() { // 12. ROUND TRIP against the real assert script: whatever it stamps, this gate // must accept. This is the drift the shared module exists to prevent, and // the only assertion here that proves the two halves still agree. + battery('12. ROUND TRIP against the real assert script: whatever it stamps, this gate'); { const injected = makeSpecPkg(path.join(root, 'rt-injected'), [FRESH, 'Shared text in both specs for the round trip']); const vendored = makeSpecPkg(path.join(root, 'rt-vendored'), [STALE, 'Shared text in both specs for the round trip']); @@ -788,6 +851,52 @@ function selfTest() { fs.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.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-console-injection --self-test -- ${failures.length} failure(s)\n`); for (const f of failures) console.error(` ${f}`); diff --git a/scripts/check-cross-repo-closer-outcome.mjs b/scripts/check-cross-repo-closer-outcome.mjs index 9765330775..4487ffc2c9 100644 --- a/scripts/check-cross-repo-closer-outcome.mjs +++ b/scripts/check-cross-repo-closer-outcome.mjs @@ -124,6 +124,39 @@ import { requireDependency } from './import-prerequisite.mjs'; const { isMap, isSeq, parseDocument } = await requireDependency('yaml', () => import('yaml'), import.meta.url); import { isEntrypoint } from './invoked-as.mjs'; +// ── The self-test's own battery roster and floor (#13489) ────────────────── +// +// `failures.length === 0` used to be this self-test's ONLY success condition, so +// "every case held" and "the cases never ran" printed the same line. Closed the +// way PR #13487 validated on check-doc-authoring: what is pinned is the +// registered NAMES, not a number. Every section opens with `battery('')`, +// every assertion is attributed to the battery most recently opened, and the +// floor requires the OPENED set to equal the DECLARED set with each battery at +// or above its own count. +// +// ⛔ A pinned TOTAL is not the repair: a battery dropping from 9 cases to 3 +// keeps a total "right" the moment a sibling grows. +// +// The counts are a FLOOR, not an equality — adding cases is ordinary work and +// must not red. A battery BELOW its floor means cases stopped running; the +// remedy is to find what stopped registering. +const SELF_TEST_BATTERIES = Object.freeze({ + '1. The unmutated shipped script must be green -- otherwise every red below': 1, + '2. Every mutation must be REACHED and must turn the battery red, in the': 75, + '3. A script that does not compile is caught before any scenario runs -- the': 1, + '4. Missing input is a failure, never a pass (#4690).': 1, + '5. Wiring. A check nobody runs is the #4449 shape this repo keeps paying': 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 = 5; + +// The key an assertion is filed under when no battery is open. It is not a +// declared battery, so it reds by the same set difference rather than silently +// inflating whichever battery happened to run last. +const UNATTRIBUTED_BATTERY = '(no battery open)'; + const WORKFLOW = '.github/workflows/cross-repo-issue-closer.yml'; const JOB = 'close-foreign-issues'; const SELF = 'scripts/check-cross-repo-closer-outcome.mjs'; @@ -1152,6 +1185,20 @@ const MUTATIONS = [ const SELF_TEST_VERDICT = 'check-cross-repo-closer-outcome 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 root = repoRoot(); const { source, problems } = extractScript(root); if (problems.length > 0) { @@ -1162,12 +1209,14 @@ async function selfTest() { const failures = []; let checked = 0; const assert = (cond, msg) => { + registerCase(); checked++; if (!cond) failures.push(msg); }; // 1. The unmutated shipped script must be green -- otherwise every red below // proves nothing about the mutation. + battery('1. The unmutated shipped script must be green -- otherwise every red below'); const clean = await judge(source); assert( clean.failures.length === 0, @@ -1176,6 +1225,7 @@ async function selfTest() { // 2. Every mutation must be REACHED and must turn the battery red, in the // scenarios it names. + battery('2. Every mutation must be REACHED and must turn the battery red, in the'); for (const m of MUTATIONS) { assert(source.includes(m.from), `${m.id}: its anchor is present in the shipped script (a no-op mutation proves nothing)`); if (!source.includes(m.from)) continue; @@ -1193,15 +1243,18 @@ async function selfTest() { // 3. A script that does not compile is caught before any scenario runs -- the // 2026-08-02 failure class, which no outcome assertion could ever see. + battery('3. A script that does not compile is caught before any scenario runs -- the'); const broken = await judge(`${source}\nconst github = 1;`); assert(broken.failures.length === 1 && broken.failures[0].id === 'C0', 'a non-compiling script is reported as C0, once'); // 4. Missing input is a failure, never a pass (#4690). + battery('4. Missing input is a failure, never a pass (#4690).'); const gone = extractScript(join(root, 'scripts')); assert(gone.source === null && gone.problems.length === 1, 'a missing workflow file is an input problem, not a pass'); // 5. Wiring. A check nobody runs is the #4449 shape this repo keeps paying // for, so the step that invokes it is pinned here. + battery('5. Wiring. A check nobody runs is the #4449 shape this repo keeps paying'); const lint = join(root, '.github', 'workflows', 'lint.yml'); assert(existsSync(lint), 'wiring: .github/workflows/lint.yml exists -- it is where this check runs'); if (existsSync(lint)) { @@ -1210,6 +1263,52 @@ async function selfTest() { assert(body.includes(`${SELF} --self-test`), 'wiring: lint.yml runs the --self-test half too'); } + // ── The floor: every declared battery RAN, and ran its cases (#13489) ─── + // + // Evaluated after every battery has had its chance and BEFORE the verdict, so + // the success line below can only be printed by a run in which the set of + // batteries that registered assertions EQUALS the set declared. A set + // difference names WHICH battery stopped; a count says only that something did. + const floorFailure = (message) => { + failures.push(message); + }; + const declaredBatteries = Object.keys(SELF_TEST_BATTERIES); + let floorBreached = false; + if (declaredBatteries.length < SELF_TEST_BATTERY_FLOOR) { + floorBreached = true; + floorFailure( + `SELF_TEST_BATTERIES declares ${declaredBatteries.length} batteries, below the pinned ` + + `${SELF_TEST_BATTERY_FLOOR} — a battery deleted from the roster takes its own floor with it.`, + ); + } + for (const [name, count] of 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-cross-repo-closer-outcome --self-test -- ${failures.length} failure(s)\n`); for (const f of failures) console.error(` • ${f}`); diff --git a/scripts/check-dev-prereqs.mjs b/scripts/check-dev-prereqs.mjs index 12e99ce2d1..a6cd2a43c3 100644 --- a/scripts/check-dev-prereqs.mjs +++ b/scripts/check-dev-prereqs.mjs @@ -243,6 +243,50 @@ import { workspaceMemberDirs, } from './workspace-enumerator.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. Built workspace → green, and the count reflects what was inspected.': 3, + '2. Unbuilt package → red, named, with the plain build fix.': 10, + '3. Dependencies absent → the one-line fix has to install first.': 1, + '4. Exclusions: a package is only judged on an entry point under dist/.': 2, + '5. Nested and literal member patterns both expand.': 2, + '6. A member pattern this gate cannot expand must fail loudly, never': 1, + '7. No pnpm-workspace.yaml at all → same loud failure.': 1, + '8. Stamped by its own build → fresh, and the pass line says what it now': 6, + '9. THE POINT OF THE WHOLE CHANGE: a source edit after the build is stale,': 6, + '10. mtime is NOT the criterion — the whole reason PR #5863 refused to do': 1, + '11. Absence of the freshness input is red, not a shrug (#4690): a dist': 4, + '12. A stamp that is not a sha256 (truncated, hand-written, half-flushed)': 1, + '13. Declared = enforced, in BOTH directions. An amplifier whose build': 2, + '14. Every other way the freshness half can lose its subject is red too.': 4, + '15. The hash reads the inputs it claims to. A global build input (from': 3, + '16. Existence outranks freshness: a workspace that is not built reports': 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 = 16; + +// The key an assertion is filed under when no battery is open. It is not a +// declared battery, so it reds by the same set difference rather than silently +// inflating whichever battery happened to run last. +const UNATTRIBUTED_BATTERY = '(no battery open)'; + const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); /** @@ -590,8 +634,23 @@ function stamp(root, cwd, amplifiers = AMPLIFIERS) { 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 failures = []; const expect = (label, actual, wanted) => { + registerCase(); if (actual !== wanted) failures.push(`${label}: expected ${JSON.stringify(wanted)}, got ${JSON.stringify(actual)}`); }; @@ -643,6 +702,7 @@ function selfTest() { try { // 1. Built workspace → green, and the count reflects what was inspected. + battery('1. Built workspace → green, and the count reflects what was inspected.'); const built = fixture('built', { installed: true }); write(built, 'packages/a/package.json', JSON.stringify({ name: '@f/a', exports: { '.': { types: './dist/index.d.mts', default: './dist/index.mjs' } } })); write(built, 'packages/a/dist/index.mjs', 'export {};'); @@ -654,6 +714,7 @@ function selfTest() { expect('built/fix', v.fix, 'pnpm build'); // 2. Unbuilt package → red, named, with the plain build fix. + battery('2. Unbuilt package → red, named, with the plain build fix.'); const unbuilt = fixture('unbuilt', { installed: true }); write(unbuilt, 'packages/a/package.json', JSON.stringify({ name: '@f/a', exports: { '.': './dist/index.mjs' } })); write(unbuilt, 'packages/b/package.json', JSON.stringify({ name: '@f/b', main: 'dist/index.js' })); @@ -676,6 +737,7 @@ function selfTest() { expect('built/one-line', green.text.trim().split('\n').length, 1); // 3. Dependencies absent → the one-line fix has to install first. + battery('3. Dependencies absent → the one-line fix has to install first.'); const fresh = fixture('fresh'); write(fresh, 'packages/a/package.json', JSON.stringify({ name: '@f/a', main: 'dist/index.js' })); expect('fresh/fix', inspect(fresh, []).fix, 'pnpm install && pnpm build'); @@ -686,6 +748,7 @@ function selfTest() { // build-console.sh; declares no entry but package.json), @objectstack/docs // (no entry point, filtered out of `pnpm build`), and the examples // (entry is a .ts source file). See header. + battery('4. Exclusions: a package is only judged on an entry point under dist/.'); const excluded = fixture('excluded', { installed: true }); write(excluded, 'packages/console/package.json', JSON.stringify({ name: '@f/console', exports: { './package.json': './package.json' }, files: ['dist'] })); write(excluded, 'packages/docs/package.json', JSON.stringify({ name: '@f/docs', scripts: { build: 'next build' } })); @@ -695,6 +758,7 @@ function selfTest() { expect('excluded/missing', v.missing.length, 0); // 5. Nested and literal member patterns both expand. + battery('5. Nested and literal member patterns both expand.'); const nested = fixture('nested', { members: ['packages/plugins/*', 'packages/spec'], installed: true }); write(nested, 'packages/plugins/driver-sql/package.json', JSON.stringify({ name: '@f/driver-sql', main: 'dist/index.js' })); write(nested, 'packages/spec/package.json', JSON.stringify({ name: '@f/spec', main: 'dist/index.js' })); @@ -704,10 +768,12 @@ function selfTest() { // 6. A member pattern this gate cannot expand must fail loudly, never // silently cover fewer packages. + battery('6. A member pattern this gate cannot expand must fail loudly, never'); const opaque = fixture('opaque', { members: ['packages/**'], installed: true }); expect('opaque/throws', threwCoverage(() => inspect(opaque, [])), 'CoverageError'); // 7. No pnpm-workspace.yaml at all → same loud failure. + battery('7. No pnpm-workspace.yaml at all → same loud failure.'); const rootless = path.join(tmp, 'rootless'); mkdirSync(rootless, { recursive: true }); expect('rootless/throws', threwCoverage(() => inspect(rootless, [])), 'CoverageError'); @@ -716,6 +782,7 @@ function selfTest() { // 8. Stamped by its own build → fresh, and the pass line says what it now // vouches for AND what it still does not. + battery('8. Stamped by its own build → fresh, and the pass line says what it now'); const stampedRoot = amplifierFixture('stamped'); expect('stamp/exit-code', capture(() => stamp(stampedRoot, path.join(stampedRoot, 'packages/spec'), ['packages/spec'])).code, 0); v = inspect(stampedRoot, ['packages/spec']); @@ -728,6 +795,7 @@ function selfTest() { // 9. THE POINT OF THE WHOLE CHANGE: a source edit after the build is stale, // red, and named — with one fix, and #5726 named as the reason. + battery('9. THE POINT OF THE WHOLE CHANGE: a source edit after the build is stale,'); write(stampedRoot, 'packages/spec/src/index.ts', 'export const token = 2;\n'); v = inspect(stampedRoot, ['packages/spec']); expect('stale/state', v.freshness[0]?.state, 'stale'); @@ -742,6 +810,7 @@ function selfTest() { // this half. A source file touched into the future with byte-identical // content stays fresh; an mtime comparison would red here, and reds like // that are how gates get switched off. + battery('10. mtime is NOT the criterion — the whole reason PR #5863 refused to do'); const untouched = amplifierFixture('untouched'); capture(() => stamp(untouched, path.join(untouched, 'packages/spec'), ['packages/spec'])); const touched = path.join(untouched, 'packages/spec/src/index.ts'); @@ -752,6 +821,7 @@ function selfTest() { // 11. Absence of the freshness input is red, not a shrug (#4690): a dist // built before this stamp existed is exactly #5726's tree. + battery('11. Absence of the freshness input is red, not a shrug (#4690): a dist'); const unstamped = amplifierFixture('unstamped'); v = inspect(unstamped, ['packages/spec']); expect('unstamped/state', v.freshness[0]?.state, 'unstamped'); @@ -762,6 +832,7 @@ function selfTest() { // 12. A stamp that is not a sha256 (truncated, hand-written, half-flushed) // is unverifiable, and unverifiable is red — never "close enough". + battery('12. A stamp that is not a sha256 (truncated, hand-written, half-flushed)'); const garbled = amplifierFixture('garbled'); capture(() => stamp(garbled, path.join(garbled, 'packages/spec'), ['packages/spec'])); write(garbled, 'packages/spec/dist/' + STAMP_BASENAME, 'not-a-hash\n'); @@ -771,11 +842,13 @@ function selfTest() { // script stopped stamping must fail loudly (otherwise this gate passes // on any dist forever), and --stamp from an unlisted package must refuse // (otherwise a stamp is written that nobody reads). + battery('13. Declared = enforced, in BOTH directions. An amplifier whose build'); const unstamping = amplifierFixture('unstamping', { build: 'tsup' }); expect('drift/build-script-lost-stamp', threwCoverage(() => inspect(unstamping, ['packages/spec'])), 'CoverageError'); expect('drift/stamp-refuses-unlisted', capture(() => stamp(stampedRoot, path.join(stampedRoot, 'packages/spec'), [])).code, 1); // 14. Every other way the freshness half can lose its subject is red too. + battery('14. Every other way the freshness half can lose its subject is red too.'); const noMember = amplifierFixture('no-member'); expect('coverage/not-a-member', threwCoverage(() => inspect(noMember, ['packages/nonexistent'])), 'CoverageError'); const noSrc = amplifierFixture('no-src'); @@ -792,6 +865,7 @@ function selfTest() { // turbo.json) and the package manifest both move it; the stamp file // itself, living inside dist, does not — otherwise stamping would // invalidate the stamp it just wrote. + battery('15. The hash reads the inputs it claims to. A global build input (from'); const inputs = amplifierFixture('inputs'); const specDir = path.join(inputs, 'packages/spec'); const base = buildInputHash(inputs, specDir); @@ -806,6 +880,7 @@ function selfTest() { // 16. Existence outranks freshness: a workspace that is not built reports // ONE precondition, and it is the build — not two. + battery('16. Existence outranks freshness: a workspace that is not built reports'); const halfBuilt = amplifierFixture('half-built'); capture(() => stamp(halfBuilt, path.join(halfBuilt, 'packages/spec'), ['packages/spec'])); write(halfBuilt, 'packages/other/package.json', JSON.stringify({ name: '@f/other', main: 'dist/index.js' })); @@ -824,6 +899,52 @@ function selfTest() { // that consolidated onto it folds in its checks. failures.push(...workspaceEnumeratorSelfTest({ root: ROOT })); + // ── The floor: every declared battery RAN, and ran its cases (#13489) ─── + // + // Evaluated after every battery has had its chance and BEFORE the verdict, so + // the success line below can only be printed by a run in which the set of + // batteries that registered assertions EQUALS the set declared. A set + // difference names WHICH battery stopped; a count says only that something did. + const floorFailure = (message) => { + failures.push(message); + }; + const declaredBatteries = Object.keys(SELF_TEST_BATTERIES); + let floorBreached = false; + if (declaredBatteries.length < SELF_TEST_BATTERY_FLOOR) { + floorBreached = true; + floorFailure( + `SELF_TEST_BATTERIES declares ${declaredBatteries.length} batteries, below the pinned ` + + `${SELF_TEST_BATTERY_FLOOR} — a battery deleted from the roster takes its own floor with it.`, + ); + } + for (const [name, count] of 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(`\n✗ check:dev-prereqs --self-test — ${failures.length} failure(s)\n`); for (const f of failures) console.error(` ${f}`); diff --git a/scripts/check-dispatcher-error-vocabulary.mjs b/scripts/check-dispatcher-error-vocabulary.mjs index 9d4b84924f..817f023914 100644 --- a/scripts/check-dispatcher-error-vocabulary.mjs +++ b/scripts/check-dispatcher-error-vocabulary.mjs @@ -223,6 +223,36 @@ import { findViolations } from './check-error-code-casing.mjs'; import { join, relative, dirname, resolve } 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({ + '[#14626] THE NESTED TEMPLATE, across all FOUR shared textual primitives.': 242, + '[#13790] The INLINE literal EXPRESSION at an object-literal `code:`.': 40, +}); + +// 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 ROOT = resolve(new URL('..', import.meta.url).pathname); const SCAN_ROOT = 'packages'; const SKIP_DIRS = new Set(['node_modules', 'dist', '.git', '.turbo', 'coverage', 'build']); @@ -2829,9 +2859,24 @@ export function checkDoorTyping({ doorSource, files }) { 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); + }; + battery('[#14626] THE NESTED TEMPLATE, across all FOUR shared textual primitives.'); + const fail = []; let cases = 0; - const ok = (cond, what) => { cases += 1; if (!cond) fail.push(what); }; + const ok = (cond, what) => { registerCase(); cases += 1; if (!cond) fail.push(what); }; // Each published SHAPE matches what it claims to. const samples = { @@ -4420,6 +4465,7 @@ function selfTest() { // negative with a positive control, because "the shape is silent" and "the // shape is dead" are the same output. // ------------------------------------------------------------------ + battery('[#13790] The INLINE literal EXPRESSION at an object-literal `code:`.'); { const C = INLINE_LITERAL_EXPRESSION_CENSUS; const REL = 'packages/x/src/a.ts'; @@ -4747,6 +4793,52 @@ function selfTest() { } } + // ── The floor: every declared battery RAN, and ran its cases (#13489) ─── + // + // Evaluated after every battery has had its chance and BEFORE the verdict, so + // the success line below can only be printed by a run in which the set of + // batteries that registered assertions EQUALS the set declared. A set + // difference names WHICH battery stopped; a count says only that something did. + const floorFailure = (message) => { + fail.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 (fail.length) { console.error('check-dispatcher-error-vocabulary --self-test FAILED:'); for (const f of fail) console.error(` - ${f}`); diff --git a/scripts/check-doc-frontmatter.mjs b/scripts/check-doc-frontmatter.mjs index 771b8a4a2e..a090167353 100644 --- a/scripts/check-doc-frontmatter.mjs +++ b/scripts/check-doc-frontmatter.mjs @@ -261,6 +261,45 @@ const { parse, parseDocument } = await requireDependency('yaml', () => import('y 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 corpus\'s ordinary shape is CLEAN': 5, + '(2) THE defect, verbatim': 5, + '(3) other ways the block fails to parse': 3, + '(4) the block that is not there': 3, + '(5) the two keys `pageSchema` types': 11, + '(6) REFUSALS, against real directories': 12, + '(6b) THE per-root floor: an empty root refuses ON ITS OWN': 10, + '(7) the extraction agrees with the docs build\'s OWN extractor': 10, + '(9) the blog root\'s OWN keys, each observed firing': 16, + '(10) ROOTS is pinned to `apps/docs/source.config.ts`': 8, + '(8) WIRING: the gate and its self-test really run in CI': 2, +}); + +// DELETING an entry silences that battery's floor exactly as effectively as +// zeroing it, so the roster's own size is pinned too. +const SELF_TEST_BATTERY_FLOOR = 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 = resolve(fileURLToPath(import.meta.url), '..'); const REPO_ROOT = resolve(HERE, '..'); @@ -864,9 +903,24 @@ export function main(roots = ROOTS) { 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 = (ok, what) => { + registerCase(); checked++; if (!ok) failures.push(what); }; @@ -895,6 +949,7 @@ export async function selfTest() { }; // ── (1) the corpus's ordinary shape is CLEAN ───────────────────────────── + battery('(1) the corpus\'s ordinary shape is CLEAN'); assert(kinds(page('title: A\ndescription: B')).length === 0, 'a well-formed page is clean'); assert( kinds('---\r\ntitle: A\r\ndescription: B\r\n---\r\nBody\r\n').length === 0, @@ -916,6 +971,7 @@ export async function selfTest() { // ── (2) THE defect, verbatim ───────────────────────────────────────────── // The `description` that reached `Build Docs` on the card, character for // character. Everything else in this battery is a generalisation of it. + battery('(2) THE defect, verbatim'); const THE_DEFECT = page( 'title: Declarative Endpoints\n' + 'description: Expose your app to systems outside the platform by declaring an apis: endpoint as metadata.', @@ -937,11 +993,13 @@ export async function selfTest() { ); // ── (3) other ways the block fails to parse ────────────────────────────── + battery('(3) other ways the block fails to parse'); assert(kinds(page('title: A\n\tdescription: B'))[0] === 'frontmatter-parse', 'a tab as indentation is a parse failure'); assert(kinds(page('title: A\n description: B'))[0] === 'frontmatter-parse', 'a stray indent is a parse failure'); assert(kinds(page('- a\n- b'))[0] === 'frontmatter-not-a-mapping', 'a sequence where a mapping belongs is named as such'); // ── (4) the block that is not there ────────────────────────────────────── + battery('(4) the block that is not there'); assert(kinds('# A page with no frontmatter\n')[0] === 'frontmatter-missing', 'no block at all is named directly'); assert(kinds('---\n---\nBody\n')[0] === 'frontmatter-missing', 'an EMPTY block does not match the extractor either'); assert( @@ -950,6 +1008,7 @@ export async function selfTest() { ); // ── (5) the two keys `pageSchema` types ────────────────────────────────── + battery('(5) the two keys `pageSchema` types'); assert(kinds(page('description: B'))[0] === 'title-missing', 'a missing title is caught'); const numTitle = only(page('title: 42\ndescription: B')); assert(numTitle?.kind === 'title-not-a-string', 'an unquoted numeric title is caught'); @@ -974,6 +1033,7 @@ export async function selfTest() { assert(kinds(page('title: 1\ndescription: [x]')).length === 2, 'both key violations are reported in one pass'); // ── (6) REFUSALS, against real directories ─────────────────────────────── + battery('(6) REFUSALS, against real directories'); const { mkdirSync, mkdtempSync, rmSync, symlinkSync, writeFileSync } = await import('node:fs'); const { tmpdir } = await import('node:os'); const tmp = mkdtempSync(join(tmpdir(), 'doc-frontmatter-')); @@ -1075,6 +1135,7 @@ export async function selfTest() { // arithmetic rather than as intent -- the surviving root is deliberately // LARGE, and the assertion below records that the run refused while the // global count was 12. A union gate is green in exactly that state. + battery('(6b) THE per-root floor: an empty root refuses ON ITS OWN'); const rootA = join(tmp, 'root-a'); // stands in for content/docs const rootB = join(tmp, 'root-b'); // stands in for content/blog mkdirSync(rootA); @@ -1127,6 +1188,7 @@ export async function selfTest() { assert(quietly(() => main([])) === 1, 'main() refuses when no roots are declared at all'); // ── (7) the extraction agrees with the docs build's OWN extractor ────── + battery('(7) the extraction agrees with the docs build\'s OWN extractor'); const { createRequire: cr } = await import('node:module'); const { pathToFileURL } = await import('node:url'); let extract = null; @@ -1175,6 +1237,7 @@ export async function selfTest() { } // ── (9) the blog root's OWN keys, each observed firing ────────────────── + battery('(9) the blog root\'s OWN keys, each observed firing'); const bkinds = (s) => judgeSource(s, BLOG_KEYS).violations.map((v) => v.kind); const bonly = (s) => { const v = judgeSource(s, BLOG_KEYS).violations; @@ -1237,6 +1300,7 @@ export async function selfTest() { // ── (10) ROOTS is pinned to `apps/docs/source.config.ts` ──────────────── // This card exists because a root was added there and nothing noticed. The // parity below is what stops a THIRD one arriving unowned. + battery('(10) ROOTS is pinned to `apps/docs/source.config.ts`'); let config = null; try { config = readFileSync(join(REPO_ROOT, 'apps/docs/source.config.ts'), 'utf8'); @@ -1292,6 +1356,7 @@ export async function selfTest() { // ── (8) WIRING: the gate and its self-test really run in CI ────────────── // A gate that exists and is not scheduled is the same dormant shape from the // other side. Asserted against the workflow text, not remembered. + battery('(8) WIRING: the gate and its self-test really run in CI'); const SELF = 'scripts/check-doc-frontmatter.mjs'; let lint = null; try { @@ -1304,6 +1369,52 @@ export async function selfTest() { assert(lint.includes(`node ${SELF} --self-test`), 'wiring: lint.yml runs the --self-test leg too'); } + // ── The floor: every declared battery RAN, and ran its cases (#13489) ─── + // + // Evaluated after every battery has had its chance and BEFORE the verdict, so + // the success line below can only be printed by a run in which the set of + // batteries that registered assertions EQUALS the set declared. A set + // difference names WHICH battery stopped; a count says only that something did. + const floorFailure = (message) => { + failures.push(message); + }; + const declaredBatteries = Object.keys(SELF_TEST_BATTERIES); + let floorBreached = false; + if (declaredBatteries.length < SELF_TEST_BATTERY_FLOOR) { + floorBreached = true; + floorFailure( + `SELF_TEST_BATTERIES declares ${declaredBatteries.length} batteries, below the pinned ` + + `${SELF_TEST_BATTERY_FLOOR} — a battery deleted from the roster takes its own floor with it.`, + ); + } + for (const [name, count] of 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-doc-frontmatter --self-test — ${failures.length} of ${checked} assertion(s) failed\n`); for (const f of failures) console.error(` • ${f}`); diff --git a/scripts/check-docs-locale-catch-all.mjs b/scripts/check-docs-locale-catch-all.mjs index 5d41e272e2..daad1e5ece 100644 --- a/scripts/check-docs-locale-catch-all.mjs +++ b/scripts/check-docs-locale-catch-all.mjs @@ -133,6 +133,49 @@ import { dirname, join } from 'node:path'; import { fileURLToPath } from 'node:url'; import { isEntrypoint } from './invoked-as.mjs'; +// ── The self-test's own battery roster and floor (#13489) ────────────────── +// +// `failures.length === 0` used to be this self-test's ONLY success condition, so +// "every case held" and "the cases never ran" printed the same line. Closed the +// way PR #13487 validated on check-doc-authoring: what is pinned is the +// registered NAMES, not a number. Every section opens with `battery('')`, +// every assertion is attributed to the battery most recently opened, and the +// floor requires the OPENED set to equal the DECLARED set with each battery at +// or above its own count. +// +// ⛔ A pinned TOTAL is not the repair: a battery dropping from 9 cases to 3 +// keeps a total "right" the moment a sibling grows. +// +// The counts are a FLOOR, not an equality — adding cases is ordinary work and +// must not red. A battery BELOW its floor means cases stopped running; the +// remedy is to find what stopped registering. +const SELF_TEST_BATTERIES = Object.freeze({ + '1. GREEN: the real shape passes, and the summary names its own scope.': 4, + '2. RED: the guard call deleted -- the exact regression this gate exists for.': 1, + '3. RED: the guard present but AFTER rendering starts.': 1, + '4. RED: the predicate stops reading the declared locales.': 1, + '5. RED: a NEW unguarded top-level dynamic segment -- the class, not the file.': 2, + '6. The condition is LIVE, not decorative: a matcher that DOES cover dotted': 3, + '6b. RED, THE ABLATION FROM THE PROXY SIDE. Nothing but the matcher moves:': 3, + '6c. GREEN control: the limb fires on the BREAK, not on the flag. This': 3, + '7. RED: a matcher that stops rewriting the dotless probe is reported, not': 1, + '8. RED: an uncompilable matcher is loud, never treated as inert.': 1, + '9. RED -- THE ABLATION. The marker keeps its position and loses only its': 3, + '10. GREEN control: the marker\'s name is free, so a differently-named dotted marker': 2, + '11. RED: the marker dropped entirely -- the URL now ends in a page slug.': 1, + '12. RED: the array literal is intact but no longer reaches the END of the': 1, + '13. RED: the builder is gone. An unreadable input is never a pass.': 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 = 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)'; + /** Paths a crawler probes by default -- the reachable half of the defect. */ const DOTTED_PROBES = ['/ads.txt', '/security.txt', '/sitemap_index.xml', '/anything.html']; @@ -546,15 +589,31 @@ function writeFixture( const SELF_TEST_VERDICT = 'check-docs-locale-catch-all 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 = []; let checked = 0; const assert = (ok, what) => { + registerCase(); checked += 1; if (!ok) failures.push(what); }; const dir = mkdtempSync(join(tmpdir(), 'docs-locale-catch-all-')); try { // 1. GREEN: the real shape passes, and the summary names its own scope. + battery('1. GREEN: the real shape passes, and the summary names its own scope.'); let paths = writeFixture(dir); let run = checkApp(paths); assert(run.findings.length === 0, `clean fixture must be silent -- got ${JSON.stringify(run.findings)}`); @@ -566,11 +625,13 @@ function selfTest() { ); // 2. RED: the guard call deleted -- the exact regression this gate exists for. + battery('2. RED: the guard call deleted -- the exact regression this gate exists for.'); paths = writeFixture(dir, { layout: FIXTURE_LAYOUT.replace(' if (!isSupportedLanguage(lang)) notFound();\n', '') }); run = checkApp(paths); assert(run.findings.length === 1 && /never calls/.test(run.findings[0]), `deleting the guard must be reported once -- got ${JSON.stringify(run.findings)}`); // 3. RED: the guard present but AFTER rendering starts. + battery('3. RED: the guard present but AFTER rendering starts.'); paths = writeFixture(dir, { layout: FIXTURE_LAYOUT .replace(' if (!isSupportedLanguage(lang)) notFound();\n', '') @@ -580,11 +641,13 @@ function selfTest() { assert(run.findings.length === 1 && /AFTER it has started rendering/.test(run.findings[0]), `a guard behind the return must be reported -- got ${JSON.stringify(run.findings)}`); // 4. RED: the predicate stops reading the declared locales. + battery('4. RED: the predicate stops reading the declared locales.'); paths = writeFixture(dir, { i18n: FIXTURE_I18N.replace('return i18n.languages.includes(value);', 'return true;') }); run = checkApp(paths); assert(run.findings.length === 1 && /does not read/.test(run.findings[0]), `a predicate that stopped reading i18n.languages must be reported -- got ${JSON.stringify(run.findings)}`); // 5. RED: a NEW unguarded top-level dynamic segment -- the class, not the file. + battery('5. RED: a NEW unguarded top-level dynamic segment -- the class, not the file.'); paths = writeFixture(dir, { extra: { name: '[slug]', layout: 'export default function S({ children }) { return children; }\n' } }); run = checkApp(paths); assert(run.findings.length === 1 && /app\/\[slug\]\//.test(run.findings[0]), `a new unguarded segment must be reported -- got ${JSON.stringify(run.findings)}`); @@ -597,6 +660,7 @@ function selfTest() { // widening takes every `og:image` to 404, so the OG limb reports it here // and the run as a whole is red. Before that limb existed this fixture // was silent, which is the hole this case now pins from both sides. + battery('6. The condition is LIVE, not decorative: a matcher that DOES cover dotted'); paths = writeFixture(dir, { proxy: `export const config = { matcher: ['/((?!api|_next/static).*)'] };\n`, layout: FIXTURE_LAYOUT.replace(' if (!isSupportedLanguage(lang)) notFound();\n', ''), @@ -617,6 +681,7 @@ function selfTest() { // other limb is satisfied and the two conditional limbs have relaxed // themselves. The surface is broken anyway, and this is the reading that // says so -- taken from the built URL, not from either side alone. + battery('6b. RED, THE ABLATION FROM THE PROXY SIDE. Nothing but the matcher moves:'); paths = writeFixture(dir, { proxy: `export const config = { matcher: ['/((?!api|_next/static).*)'] };\n` }); run = checkApp(paths); assert(run.stats.ogFinalSegmentDotted === true, 'the marker must be untouched in the proxy-side ablation'); @@ -631,6 +696,7 @@ function selfTest() { // relaxes exactly as in 6 -- but still excludes the `/og/` prefix, so // the cards are still served and there is nothing to report. A limb // wired to `dottedBypassesProxy` instead of to the URL would cry here. + battery('6c. GREEN control: the limb fires on the BREAK, not on the flag. This'); paths = writeFixture(dir, { proxy: `export const config = { matcher: ['/((?!api|_next/static|og/).*)'] };\n`, layout: FIXTURE_LAYOUT.replace(' if (!isSupportedLanguage(lang)) notFound();\n', ''), @@ -642,11 +708,13 @@ function selfTest() { // 7. RED: a matcher that stops rewriting the dotless probe is reported, not // silently read as "everything bypasses". + battery('7. RED: a matcher that stops rewriting the dotless probe is reported, not'); paths = writeFixture(dir, { proxy: `export const config = { matcher: ['/docs/(.*)'] };\n` }); run = checkApp(paths); assert(run.findings.some((f) => /no longer rewrites/.test(f)), `a matcher that stops covering the dotless probe must be reported -- got ${JSON.stringify(run.findings)}`); // 8. RED: an uncompilable matcher is loud, never treated as inert. + battery('8. RED: an uncompilable matcher is loud, never treated as inert.'); paths = writeFixture(dir, { proxy: `export const config = { matcher: ['/((?!unclosed.*)'] };\n` }); run = checkApp(paths); assert(run.findings.length === 1 && /does not compile/.test(run.findings[0]), `an uncompilable matcher must be reported -- got ${JSON.stringify(run.findings)}`); @@ -655,6 +723,7 @@ function selfTest() { // dot. Nothing else in the tree moves: the route still slices off the // last segment, every type still checks, every page still renders. This // is the whole reason the limb exists, so it is observed failing here. + battery('9. RED -- THE ABLATION. The marker keeps its position and loses only its'); paths = writeFixture(dir, { pageSource: FIXTURE_SOURCE.replace(`'image.png'`, `'image'`) }); run = checkApp(paths); assert( @@ -666,12 +735,14 @@ function selfTest() { // 10. GREEN control: the marker's name is free, so a differently-named dotted marker // is GREEN -- the gate must pin the dot, not the filename. + battery('10. GREEN control: the marker\'s name is free, so a differently-named dotted marker'); paths = writeFixture(dir, { pageSource: FIXTURE_SOURCE.replace(`'image.png'`, `'card.jpeg'`) }); run = checkApp(paths); assert(run.findings.length === 0, `a renamed but still-dotted marker must stay green -- got ${JSON.stringify(run.findings)}`); assert(run.stats.ogMarker === 'card.jpeg', `the renamed marker must be read back -- got ${summarise(run.stats)}`); // 11. RED: the marker dropped entirely -- the URL now ends in a page slug. + battery('11. RED: the marker dropped entirely -- the URL now ends in a page slug.'); paths = writeFixture(dir, { pageSource: FIXTURE_SOURCE.replace(`, 'image.png'`, '') }); run = checkApp(paths); assert( @@ -682,6 +753,7 @@ function selfTest() { // 12. RED: the array literal is intact but no longer reaches the END of the // URL, so its last element is not the final segment. Checking the // literal alone here would report GREEN over a broken surface. + battery('12. RED: the array literal is intact but no longer reaches the END of the'); paths = writeFixture(dir, { pageSource: FIXTURE_SOURCE.replace(`\${segments.join('/')}\``, `\${segments.join('/')}/card\``), }); @@ -692,6 +764,7 @@ function selfTest() { ); // 13. RED: the builder is gone. An unreadable input is never a pass. + battery('13. RED: the builder is gone. An unreadable input is never a pass.'); paths = writeFixture(dir, { pageSource: 'export function somethingElse() {}\n' }); run = checkApp(paths); assert( @@ -702,6 +775,52 @@ function selfTest() { rmSync(dir, { recursive: true, force: true }); } + // ── The floor: every declared battery RAN, and ran its cases (#13489) ─── + // + // Evaluated after every battery has had its chance and BEFORE the verdict, so + // the success line below can only be printed by a run in which the set of + // batteries that registered assertions EQUALS the set declared. A set + // difference names WHICH battery stopped; a count says only that something did. + const floorFailure = (message) => { + failures.push(message); + }; + const declaredBatteries = Object.keys(SELF_TEST_BATTERIES); + let floorBreached = false; + if (declaredBatteries.length < SELF_TEST_BATTERY_FLOOR) { + floorBreached = true; + floorFailure( + `SELF_TEST_BATTERIES declares ${declaredBatteries.length} batteries, below the pinned ` + + `${SELF_TEST_BATTERY_FLOOR} — a battery deleted from the roster takes its own floor with it.`, + ); + } + for (const [name, count] of 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-docs-locale-catch-all --self-test -- ${failures.length} failure(s)\n`); for (const failure of failures) console.error(` • ${failure}`); diff --git a/scripts/check-docs-section-name.mjs b/scripts/check-docs-section-name.mjs index 6770b2a6e6..402f764ebe 100644 --- a/scripts/check-docs-section-name.mjs +++ b/scripts/check-docs-section-name.mjs @@ -217,6 +217,56 @@ import { fencedBlocks } from './check-react-page-adapter-contract.mjs'; import { isEntrypoint } from './invoked-as.mjs'; import { blank, scanSource } 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 happy path: a real tree, real fences, real floors': 3, + 'THE case: one planted nameless section reds the real sweep': 4, + 'the spelling that defeated #10709: `sections: [{` on one line': 2, + 'the spelling that defeated #10579: a multi-line literal': 1, + 'negative controls: the population must NOT grow by fabrication': 2, + 'the YAML arm (#11887) — the two cases that used to sit here, INVERTED': 14, + '⭐ #13880 selector ③: a singular `section:` mapping, nameless': 7, + 'a singular `section:` at the document ROOT, not nested': 2, + 'negative controls: the widened `sections?:` prefilter must not': 2, + '⭐ #13880 selector ④: a `sections: [...]` array in a JSON-family fence': 8, + 'a `jsonc` fence with a `// <-` line comment and a trailing comma': 1, + 'an ELIDED placeholder array: judged as ZERO entries, not skipped': 4, + 'negative controls: a `json` fence with no `sections` key': 2, + '⭐ a DUPLICATE-KEY fence: a complete tree, judged anyway': 5, + '⭐ a SYNTAX-error fence: counted, printed, NEVER judged': 5, + 'negative controls: the YAML population must not grow by fabrication ─': 4, + 'classifyYamlFence, directly': 4, + 'the YAML refusals': 2, + 'the JSON-family refusals (#13880)': 2, + 'a quoted key cannot escape the rule': 3, + 'the refusals: each is a broken selector wearing a pass': 6, + 'the matcher, directly': 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 = 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 HERE = dirname(fileURLToPath(import.meta.url)); const REPO_ROOT = resolve(HERE, '..'); @@ -1278,10 +1328,25 @@ function baseFixtureFiles(extra = {}) { let selfTestReachedVerdict = false; export function selfTest() { + // The battery ledger this self-test's floor is evaluated against (#13489). + // `battery()` opens a battery; every assertion below is attributed to the one + // most recently opened, so a section that stops running stops registering and + // names ITSELF at the floor rather than going quiet. + const 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 trees = []; /** @param {string} name @param {unknown} actual @param {unknown} expected */ const t = (name, actual, expected) => { + registerCase(); const ok = JSON.stringify(actual) === JSON.stringify(expected); cases.push({ name, ok, detail: ok ? '' : `got ${JSON.stringify(actual)}, want ${JSON.stringify(expected)}` }); }; @@ -1302,6 +1367,7 @@ export function selfTest() { try { // ── the happy path: a real tree, real fences, real floors ─────────────── + battery('the happy path: a real tree, real fences, real floors'); const clean = tree(baseFixtureFiles()); t('a clean corpus passes', run(clean, quiet), EXIT_CLEAN); const cleanSweep = sweep(clean); @@ -1309,6 +1375,7 @@ export function selfTest() { t('...over the anchors plus the extras', new Set(cleanSweep.sites.map((s) => s.file)).size, 8); // ── THE case: one planted nameless section reds the real sweep ────────── + battery('THE case: one planted nameless section reds the real sweep'); const planted = tree( baseFixtureFiles({ [`${DOCS_ROOT}/extra/a.mdx`]: @@ -1322,6 +1389,7 @@ export function selfTest() { t('...carrying its label into the message', plantedSweep.findings[0]?.label ?? NONE, 'Nameless'); // ── the spelling that defeated #10709: `sections: [{` on one line ─────── + battery('the spelling that defeated #10709: `sections: [{` on one line'); const inline = tree( baseFixtureFiles({ [`${DOCS_ROOT}/extra/b.mdx`]: `# b\n\n${FENCE}ts\nconst f = { sections: [{ label: 'Inline' }, { name: 'ok', label: 'Ok' }] };\n${FENCE}\n`, @@ -1331,6 +1399,7 @@ export function selfTest() { t('...and the inline page reds', run(inline, quiet), EXIT_VIOLATIONS); // ── the spelling that defeated #10579: a multi-line literal ───────────── + battery('the spelling that defeated #10579: a multi-line literal'); const multi = tree( baseFixtureFiles({ [`${DOCS_ROOT}/extra/c.mdx`]: @@ -1340,6 +1409,7 @@ export function selfTest() { t('a multi-line literal is judged (the #10579 miss)', sweep(multi).findings.length, 1); // ── negative controls: the population must NOT grow by fabrication ────── + battery('negative controls: the population must NOT grow by fabrication'); const negatives = tree( baseFixtureFiles({ [`${DOCS_ROOT}/extra/d.mdx`]: @@ -1363,6 +1433,7 @@ export function selfTest() { // The arm makes both of those wrong by design. They are REPLACED rather // than deleted, in place, so the inversion is legible to the next reader // instead of looking like coverage that quietly went missing. + battery('the YAML arm (#11887) — the two cases that used to sit here, INVERTED'); t('the base fixture clears the YAML floors', cleanSweep.yamlSites.length, 24); t('...over two `sections:` fences per anchor page plus two singular-`section:` extras', cleanSweep.yamlFences, 8); t('...and the nested `layout: sections:` mappings are reached too', cleanSweep.yamlSites.filter((s) => /^b/.test(String(s.label ?? '').toLowerCase())).length, 9); @@ -1399,6 +1470,7 @@ export function selfTest() { // The exact shape `concept.mdx:426` carried live: `section:` (not // `sections:`) whose value is a mapping, nested under an unrelated outer // key -- the `- section:` sequence-item cardinality. + battery('⭐ #13880 selector ③: a singular `section:` mapping, nameless'); const yamlSingularNameless = tree( baseFixtureFiles({ [`${DOCS_ROOT}/extra/yaml-case.mdx`]: @@ -1416,6 +1488,7 @@ export function selfTest() { t('...and the real sweep goes RED on it', run(yamlSingularNameless, quiet), EXIT_VIOLATIONS); // ── a singular `section:` at the document ROOT, not nested ────────────── + battery('a singular `section:` at the document ROOT, not nested'); const yamlSingularRootNameless = tree( baseFixtureFiles({ [`${DOCS_ROOT}/extra/yaml-case.mdx`]: `# e\n\n${FENCE}yaml\nsection:\n label: Nameless\n fields: [a]\n${FENCE}\n`, @@ -1435,6 +1508,7 @@ export function selfTest() { // a scalar `section:` value (no mapping to judge -- the doc-pages.mdx:257 // control this card's own triage named: "...from the previous section:" // is prose outside any fence and never reaches this regex at all). + battery('negative controls: the widened `sections?:` prefilter must not'); const yamlSingularNegatives = tree( baseFixtureFiles({ [`${DOCS_ROOT}/extra/yaml-case.mdx`]: @@ -1450,6 +1524,7 @@ export function selfTest() { t('...and no YAML mapping either', yamlSingularNegSweep.yamlSites.length, cleanSweep.yamlSites.length); // ── ⭐ #13880 selector ④: a `sections: [...]` array in a JSON-family fence + battery('⭐ #13880 selector ④: a `sections: [...]` array in a JSON-family fence'); const jsonNameless = tree( baseFixtureFiles({ [`${DOCS_ROOT}/extra/json-case.mdx`]: @@ -1476,6 +1551,7 @@ export function selfTest() { // ── a `jsonc` fence with a `// <-` line comment and a trailing comma ──── // (the exact shape `concept.mdx`'s own "Final Merged Layout" fence used) // -- proves this arm tolerates what `JSON.parse` would refuse. + battery('a `jsonc` fence with a `// <-` line comment and a trailing comma'); const jsonWithComments = tree( baseFixtureFiles({ [`${DOCS_ROOT}/extra/json-case.mdx`]: @@ -1486,6 +1562,7 @@ export function selfTest() { // ── an ELIDED placeholder array: judged as ZERO entries, not skipped ──── // (`forms.mdx:183`'s real shape: `"sections": [/* … */]`) + battery('an ELIDED placeholder array: judged as ZERO entries, not skipped'); const jsonElided = tree( baseFixtureFiles({ [`${DOCS_ROOT}/extra/json-case.mdx`]: `# e\n\n${FENCE}json\n{\n "sections": [/* … */]\n}\n${FENCE}\n`, @@ -1498,6 +1575,7 @@ export function selfTest() { t('...so the gate stays clean', run(jsonElided, quiet), EXIT_CLEAN); // ── negative controls: a `json` fence with no `sections` key ──────────── + battery('negative controls: a `json` fence with no `sections` key'); const jsonNegatives = tree( baseFixtureFiles({ [`${DOCS_ROOT}/extra/json-case.mdx`]: @@ -1513,6 +1591,7 @@ export function selfTest() { // into one fence. A `toJS()` walk would collapse these to ONE `layout:` // and never see the second block; the AST keeps both pairs. On the real // corpus this is not hypothetical — two pages do it. + battery('⭐ a DUPLICATE-KEY fence: a complete tree, judged anyway'); const dupKey = tree( baseFixtureFiles({ [`${DOCS_ROOT}/extra/yaml-case.mdx`]: @@ -1531,6 +1610,7 @@ export function selfTest() { // This is the decision #10830 deferred and #11887 made. It has NO live // population on the tree the arm landed against, so this fixture is the // only thing holding the boundary — which is exactly why it is here. + battery('⭐ a SYNTAX-error fence: counted, printed, NEVER judged'); const badYaml = tree( baseFixtureFiles({ [`${DOCS_ROOT}/extra/yaml-case.mdx`]: @@ -1545,6 +1625,7 @@ export function selfTest() { t('...and the gate stays clean rather than fabricating from a guessed tree', run(badYaml, quiet), EXIT_CLEAN); // ── negative controls: the YAML population must not grow by fabrication ─ + battery('negative controls: the YAML population must not grow by fabrication ─'); const yamlNegatives = tree( baseFixtureFiles({ [`${DOCS_ROOT}/extra/yaml-case.mdx`]: @@ -1570,12 +1651,14 @@ export function selfTest() { t('...and produces no finding — it carries no key to require', scalarSweep.findings.length, 0); // ── classifyYamlFence, directly ──────────────────────────────────────── + battery('classifyYamlFence, directly'); t('a clean fence is judged', classifyYamlFence('sections:\n - name: a\n').judged, true); t('a duplicate key is judged', classifyYamlFence('a:\n b: 1\na:\n b: 2\n').judged, true); t('a tab indent is NOT judged', classifyYamlFence('a:\n\tb: 1\n').judged, false); t('an empty fence is NOT judged', classifyYamlFence('').judged, false); // ── the YAML refusals ────────────────────────────────────────────────── + battery('the YAML refusals'); t( 'a corpus whose YAML fences evaporated refuses', run( @@ -1591,6 +1674,7 @@ export function selfTest() { ); // ── the JSON-family refusals (#13880) ──────────────────────────────────── + battery('the JSON-family refusals (#13880)'); t( 'a JSON census anchor kept as a file but emptied of json fences refuses', // `YAML_CENSUS_ANCHORS[0]` doubles as `JSON_CENSUS_ANCHORS[0]`; drop @@ -1608,6 +1692,7 @@ export function selfTest() { ); // ── a quoted key cannot escape the rule ──────────────────────────────── + battery('a quoted key cannot escape the rule'); const quoted = tree( baseFixtureFiles({ [`${DOCS_ROOT}/extra/a.mdx`]: `# a\n\n${FENCE}ts\nconst f = { 'sections': [{ 'label': 'Quoted' }, { 'name': 'ok', 'label': 'Ok' }] };\n${FENCE}\n`, @@ -1619,6 +1704,7 @@ export function selfTest() { t('...and it is the unnamed one', quotedSweep.findings[0]?.label ?? NONE, 'Quoted'); // ── the refusals: each is a broken selector wearing a pass ────────────── + battery('the refusals: each is a broken selector wearing a pass'); t('a missing docs root refuses', run(join(tree({}), 'nowhere'), quiet), EXIT_REFUSED); t('an empty docs root refuses', run(tree({ [`${DOCS_ROOT}/.keep`]: '' }), quiet), EXIT_REFUSED); t( @@ -1650,6 +1736,7 @@ export function selfTest() { ); // ── the matcher, directly ────────────────────────────────────────────── + battery('the matcher, directly'); const src = "const a = { s: ['[', {x: 1}] };"; t('a bracket inside a string does not move the match', matchBracket(project(src).codeOnly, src.indexOf('[')), src.lastIndexOf(']') + 1); t('an unbalanced array returns -1', matchBracket(project('const a = [1, 2').codeOnly, 10), -1); @@ -1657,6 +1744,52 @@ export function selfTest() { for (const root of trees) 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) => { + cases.push({ name: message, ok: false, detail: '' }); + }; + 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 cases) if (!c.ok) console.error(` ✗ ${c.name} — ${c.detail}`); if (failed.length) { diff --git a/scripts/check-engine-split-ratio.mjs b/scripts/check-engine-split-ratio.mjs index 85e77ea559..d92d218666 100644 --- a/scripts/check-engine-split-ratio.mjs +++ b/scripts/check-engine-split-ratio.mjs @@ -76,6 +76,36 @@ import { fileURLToPath } from 'node:url'; import { historyHorizon } from './pm/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({ + 'pure rendering': 5, + 'real repos: the defect, then both legs of the guard': 11, +}); + +// DELETING an entry silences that battery's floor exactly as effectively as +// zeroing it, so the roster's own size is pinned too. +const SELF_TEST_BATTERY_FLOOR = 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 __dirname = dirname(fileURLToPath(import.meta.url)); const ENGINE_CORE = ['packages/objectql/src/engine.ts', 'packages/objectql/src/registry.ts']; @@ -221,8 +251,23 @@ 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) { console.log(` ✓ ${name}`); return; } failures += 1; console.log(` ✗ ${name}${detail ? `\n ${detail}` : ''}`); @@ -230,6 +275,7 @@ function selfTest() { const day = 24 * 60 * 60 * 1000; // ── pure rendering ──────────────────────────────────────────────────────── + battery('pure rendering'); t('a complete clone reports its horizon as complete, with the tip', renderHorizon({ shallow: false, tip: '2026-08-21' }).includes('complete clone') && renderHorizon({ shallow: false, tip: '2026-08-21' }).includes('2026-08-21')); @@ -250,6 +296,7 @@ function selfTest() { !/%/.test(refusal), refusal); // ── real repos: the defect, then both legs of the guard ─────────────────── + battery('real repos: the defect, then both legs of the guard'); const root = mkdtempSync(join(tmpdir(), 'engine-split-selftest-')); const g = (args, cwd) => execFileSync('git', args, { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }); const self = fileURLToPath(import.meta.url); @@ -336,6 +383,53 @@ 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; + console.log(` ✗ ${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.', + ); + } + console.log(failures === 0 ? `\ncheck-engine-split-ratio --self-test: all cases passed.` : `\ncheck-engine-split-ratio --self-test: ${failures} FAILED.`); diff --git a/scripts/check-error-status-conformance.mjs b/scripts/check-error-status-conformance.mjs index b2b9625ec9..605a41d954 100644 --- a/scripts/check-error-status-conformance.mjs +++ b/scripts/check-error-status-conformance.mjs @@ -114,6 +114,57 @@ import { maskComments } from './js-comment-mask.mjs'; import { join, relative } 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({ + '1 — THE regression pin: the real pre-#8963 pages go RED, naming 422.': 2, + '2 — POSITIVE CONTROL for the zero-hit case: the instrument must be SEEING a': 3, + '3 — a green run over a fixture with NO producer must NOT report reconciled': 2, + '4 — direction B: a doc claim no producer can reach.': 2, + '5 — section headings are `covered`: they absolve direction A, and never': 2, + '6 — constant resolution: a class that names its code and status.': 1, + '6b — object-member and MAP[OBJ.key] resolution.': 1, + '7 — an UNRESOLVABLE declaration is reported, never silently dropped.': 1, + '8 — an ambiguous identifier is refused, not guessed.': 1, + '9 — the two `sendError` doors, and the mapper\'s `{ status, body }` terminal.': 1, + '10 — the door map contributes explicit entries only, never the bucket fallback.': 2, + '11 — the ratchet-authority convention holds on the weakening remedy only.': 2, + '12 — the vocabulary bound: a ledger code is derived but not reconciled.': 1, + '13 — comments are NOT producers. Both halves matter: a docblock narrating a': 2, + '14 — a computed-key status table beside the code table it keys on, the': 1, + '15 — a LEDGER code the docs publish a status for is reconciled, in both': 2, + '16 — the extension is load-bearing, not decorative: the SAME ledger code': 1, + '17 — the surviving bound: a ledger code NO page publishes a status for stays': 1, + '18 — the unreadable census: a heading that names a code in an unrecognised': 3, + '19 — `**HTTP Status:**` is honoured on the CATALOG too (it was read on the': 2, + '20 — the ungraded census: an entry the parser READ but for which no page': 3, + '21 — nowPinned, the PRODUCER branch: a baselined code that GAINS a': 2, + '22 — nowPinned, the DOC-REMOVED branch: the #9266/#9563 counterfactual,': 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 = 23; + +// The key an assertion is filed under when no battery is open. It is not a +// declared battery, so it reds by the same set difference rather than silently +// inflating whichever battery happened to run last. +const UNATTRIBUTED_BATTERY = '(no battery open)'; + const SCAN_ROOT = 'packages'; const SKIP_DIRS = new Set(['node_modules', 'dist', '.git', '.turbo', 'coverage', 'build', 'fixtures']); const ERRORS_ZOD = 'packages/spec/src/api/errors.zod.ts'; @@ -773,11 +824,26 @@ function runFixture({ files, handling, catalog, members }) { 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 failures = []; - const check = (name, ok, detail) => { if (!ok) failures.push(`${name}${detail ? ` — ${detail}` : ''}`); }; + const check = (name, ok, detail) => { registerCase(); if (!ok) failures.push(`${name}${detail ? ` — ${detail}` : ''}`); }; const members = ['MISSING_REQUIRED_FIELD', 'VALIDATION_ERROR', 'TIMEOUT']; // 1 — THE regression pin: the real pre-#8963 pages go RED, naming 422. + battery('1 — THE regression pin: the real pre-#8963 pages go RED, naming 422.'); const pre = runFixture({ files: { 'a/errors.ts': CBP_ERROR_CLASS, 'a/routes.ts': CBP_400_DOOR }, handling: PRE_8963_HANDLING, catalog: PRE_8963_CATALOG, members, @@ -792,6 +858,7 @@ function selfTest() { // status it accepts, not merely finding nothing. Same fixture, post-#8963 // text: zero findings AND a non-zero count of reconciled (code, status) // pairs, so "0 findings" can never be reported by a blind run. + battery('2 — POSITIVE CONTROL for the zero-hit case: the instrument must be SEEING a'); const post = runFixture({ files: { 'a/errors.ts': CBP_ERROR_CLASS, 'a/routes.ts': CBP_400_DOOR }, handling: POST_8963_HANDLING, catalog: POST_8963_CATALOG, members, @@ -805,12 +872,14 @@ function selfTest() { // 3 — a green run over a fixture with NO producer must NOT report reconciled // pairs (the blind-run inverse of case 2). + battery('3 — a green run over a fixture with NO producer must NOT report reconciled'); const blind = runFixture({ files: { 'a/x.ts': 'export const nothing = 1;' }, handling: '', catalog: PRE_8963_CATALOG, members }); check('3 no producers ⇒ no reconciled pairs', blind.reconciledPairs === 0 && blind.reconciledCodes === 0); check('3b documented-but-unproduced lands in the census, not in a failure', blind.unpinned.includes('MISSING_REQUIRED_FIELD') && blind.emittedNotDocumented.length === 0); // 4 — direction B: a doc claim no producer can reach. + battery('4 — direction B: a doc claim no producer can reach.'); const dirB = runFixture({ files: { 'a/e.ts': 'export class E extends Error {\n readonly code = \'TIMEOUT\';\n readonly status = 504;\n}' }, handling: '#### `TIMEOUT`\n**HTTP Status:** 500 \n', catalog: '', members, @@ -823,6 +892,7 @@ function selfTest() { // 5 — section headings are `covered`: they absolve direction A, and never // drive direction B. + battery('5 — section headings are `covered`: they absolve direction A, and never'); const sec = runFixture({ files: { 'a/e.ts': 'export class E extends Error {\n readonly code = \'VALIDATION_ERROR\';\n readonly statusCode = 428;\n}' }, handling: '', catalog: '## Request Errors (405/428)\n\n### `VALIDATION_ERROR`\n', members, @@ -831,6 +901,7 @@ function selfTest() { check('5b a section heading never demands reachability', sec.documentedNotReachable.length === 0); // 6 — constant resolution: a class that names its code and status. + battery('6 — constant resolution: a class that names its code and status.'); const consts = runFixture({ files: { 'a/c.ts': `export const REFUSAL_CODE = 'VALIDATION_ERROR';\nexport const REFUSAL_STATUS = 400;`, @@ -842,6 +913,7 @@ function selfTest() { consts.reconciledPairs === 1 && consts.emittedNotDocumented.length === 0, JSON.stringify(consts.emittedNotDocumented)); // 6b — object-member and MAP[OBJ.key] resolution. + battery('6b — object-member and MAP[OBJ.key] resolution.'); const mapped = runFixture({ files: { 'a/c.ts': `export const CODES = { timeout: 'TIMEOUT' };\nexport const STATUS = { TIMEOUT: 504 };`, @@ -852,6 +924,7 @@ function selfTest() { check('6b MAP[OBJ.key] resolves', mapped.reconciledPairs === 1 && mapped.unresolved.length === 0, JSON.stringify(mapped.unresolved)); // 7 — an UNRESOLVABLE declaration is reported, never silently dropped. + battery('7 — an UNRESOLVABLE declaration is reported, never silently dropped.'); const opaque = runFixture({ files: { 'a/e.ts': 'export class E extends Error {\n readonly code = lookupCode(x);\n readonly status = lookupStatus(x);\n}' }, handling: '', catalog: '', members, @@ -859,10 +932,12 @@ function selfTest() { check('7 an unresolved declaration is reported', opaque.unresolved.length === 1, JSON.stringify(opaque.unresolved)); // 8 — an ambiguous identifier is refused, not guessed. + battery('8 — an ambiguous identifier is refused, not guessed.'); const amb = buildConstantIndex(new Map([['a.ts', `const S = 400;`], ['b.ts', `const S = 500;`]])); check('8 an ambiguous constant refuses to resolve', resolveStatus('S', amb) === undefined); // 9 — the two `sendError` doors, and the mapper's `{ status, body }` terminal. + battery('9 — the two `sendError` doors, and the mapper\'s `{ status, body }` terminal.'); const doors = runFixture({ files: { 'a/a.ts': `sendError(res, 503, 'SERVICE_UNAVAILABLE', 'down');`, @@ -878,11 +953,13 @@ function selfTest() { [...doors.emitted.keys()].join(',')); // 10 — the door map contributes explicit entries only, never the bucket fallback. + battery('10 — the door map contributes explicit entries only, never the bucket fallback.'); const door = deriveDoorMap(`export const HttpStatusErrorCodeMap: Record = {\n 400: 'VALIDATION_ERROR',\n 504: 'TIMEOUT',\n};\n`); check('10 the door map is parsed', door.length === 2 && door.some((d) => d.code === 'TIMEOUT' && d.status === 504)); check('10b the bucket fallback contributes nothing', !door.some((d) => d.status === 415 || d.status === 507)); // 11 — the ratchet-authority convention holds on the weakening remedy only. + battery('11 — the ratchet-authority convention holds on the weakening remedy only.'); check('11 the baseline-expanding remedy is marked maintainer-only', RATCHET_EXPANSION_OFFER.test(newUnpinnedMessage('X')) && newUnpinnedMessage('X').includes(RATCHET_AUTHORITY_MARKER)); check('11b both ratchet-DOWN remedies stay the author\'s own', @@ -890,6 +967,7 @@ function selfTest() { && !nowPinnedDocRemovedMessage('X').includes(RATCHET_AUTHORITY_MARKER)); // 12 — the vocabulary bound: a ledger code is derived but not reconciled. + battery('12 — the vocabulary bound: a ledger code is derived but not reconciled.'); const ledger = runFixture({ files: { 'a/e.ts': 'export class E extends Error {\n readonly code = \'SETTINGS_LOCKED\';\n readonly statusCode = 409;\n}' }, handling: '', catalog: '', members: ['VALIDATION_ERROR'], @@ -901,6 +979,7 @@ function selfTest() { // fixed bug must not mint a finding, and the real declaration two lines // down must still be read. Both sentences below are the real ones this // gate first tripped over on `main`. + battery('13 — comments are NOT producers. Both halves matter: a docblock narrating a'); const prose = runFixture({ files: { 'a/n.ts': @@ -921,6 +1000,7 @@ function selfTest() { // 14 — a computed-key status table beside the code table it keys on, the // `external-errors.ts` shape. + battery('14 — a computed-key status table beside the code table it keys on, the'); const computed = runFixture({ files: { 'a/c.ts': @@ -949,6 +1029,7 @@ function selfTest() { // 15 — a LEDGER code the docs publish a status for is reconciled, in both // directions, without appearing in `StandardErrorCode`. + battery('15 — a LEDGER code the docs publish a status for is reconciled, in both'); const ledgerDoc = runFixture({ files: { 'a/meta.ts': META_PRODUCER }, handling: '', catalog: META_CATALOG, members: ['VALIDATION_ERROR'], @@ -965,6 +1046,7 @@ function selfTest() { // 16 — the extension is load-bearing, not decorative: the SAME ledger code // goes red when the published status is not one the runtime can emit. // This is the defect the ledger half was previously blind to. + battery('16 — the extension is load-bearing, not decorative: the SAME ledger code'); const ledgerDrift = runFixture({ files: { 'a/meta.ts': "sendError(res, 409, 'INVALID_REQUEST', 'drifted');" }, handling: '', catalog: META_CATALOG, members: ['VALIDATION_ERROR'], @@ -977,6 +1059,7 @@ function selfTest() { // 17 — the surviving bound: a ledger code NO page publishes a status for stays // OUT of the vocabulary (derived, counted, not reconciled). The residual // is a subtraction, so this must not drift into a per-code assertion. + battery('17 — the surviving bound: a ledger code NO page publishes a status for stays'); const ledgerSilent = runFixture({ files: { 'a/e.ts': "export class E extends Error {\n readonly code = 'SETTINGS_LOCKED';\n readonly statusCode = 409;\n}" }, handling: '', catalog: META_CATALOG, members: ['VALIDATION_ERROR'], @@ -988,6 +1071,7 @@ function selfTest() { // 18 — the unreadable census: a heading that names a code in an unrecognised // shape is REPORTED, never silently dropped. Both refusal reasons. + battery('18 — the unreadable census: a heading that names a code in an unrecognised'); const oddShape = runFixture({ files: {}, handling: '', catalog: '## Errors (400)\n\n### `INVALID_REQUEST`: unrecognised type spelling\n', members: ['VALIDATION_ERROR'], @@ -1010,6 +1094,7 @@ function selfTest() { // 19 — `**HTTP Status:**` is honoured on the CATALOG too (it was read on the // handling page only), and a section heading still only ever COVERS. + battery('19 — `**HTTP Status:**` is honoured on the CATALOG too (it was read on the'); const catalogClaim = runFixture({ files: { 'a/e.ts': "export class E extends Error {\n readonly code = 'TIMEOUT';\n readonly status = 504;\n}" }, handling: '', catalog: '## Server Errors (5xx)\n\n### `TIMEOUT`\n**HTTP Status:** 500 \n', members: ['TIMEOUT'], @@ -1023,6 +1108,7 @@ function selfTest() { // 20 — the ungraded census: an entry the parser READ but for which no page // publishes a status in a graded shape is reported, and does NOT fail. // This is the `## Batch Operation Errors` shape on the live catalog. + battery('20 — the ungraded census: an entry the parser READ but for which no page'); const ungradedFx = runFixture({ files: {}, handling: '', catalog: '## Batch Operation Errors\n\n### `TRANSACTION_FAILED`\n**Cause:** the transaction rolled back.\n', @@ -1040,6 +1126,7 @@ function selfTest() { // 21 — nowPinned, the PRODUCER branch: a baselined code that GAINS a // producer while remaining documented is named a producer, never a // doc removal. + battery('21 — nowPinned, the PRODUCER branch: a baselined code that GAINS a'); const producerCase = runFixture({ files: { 'a/e.ts': "export class E extends Error {\n readonly code = 'TRANSACTION_FAILED';\n readonly status = 500;\n}" }, handling: '#### `TRANSACTION_FAILED`\n**HTTP Status:** 500 \n', catalog: '', members: ['TRANSACTION_FAILED'], @@ -1060,6 +1147,7 @@ function selfTest() { // the real cause the old single-cause message misdiagnosed as "a // producer now declares its status" when the `## Batch Operation // Errors` entries were deleted on `main`. + battery('22 — nowPinned, the DOC-REMOVED branch: the #9266/#9563 counterfactual,'); const docRemovedCase = runFixture({ files: {}, handling: '', catalog: '', members: ['TRANSACTION_FAILED'] }); const docRemovedFindings = nowPinned({ baselined: ['TRANSACTION_FAILED'], unpinned: docRemovedCase.unpinned, @@ -1073,6 +1161,52 @@ function selfTest() { && !nowPinnedDocRemovedMessage('TRANSACTION_FAILED').includes('a producer now declares')); const CASES = 40; + // ── 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) { for (const f of failures) console.error(` x self-test: ${f}`); console.error(`\n✗ check-error-status-conformance --self-test: ${failures.length}/${CASES} case(s) failed.\n`); diff --git a/scripts/check-i18n-stale-fill.mjs b/scripts/check-i18n-stale-fill.mjs index e5e0d8e5e6..26494eb116 100644 --- a/scripts/check-i18n-stale-fill.mjs +++ b/scripts/check-i18n-stale-fill.mjs @@ -136,6 +136,36 @@ import { fileURLToPath } from 'node:url'; import { findExtractConfigs, flagsFromDocstring } from './i18n-bundle-surface.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({ + 'Verdict 2 — UNSERVED PROVENANCE': 24, + 'Verdict 2, the EMPTY-LEDGER cases': 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 = 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)'; + /** * Every read is anchored to the script's own location, never the cwd (#10907): * a cwd-relative population and a cwd-relative baseline empty TOGETHER, and the @@ -504,8 +534,24 @@ function discoverProvenanceServing() { 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); + }; + battery('Verdict 2 — UNSERVED PROVENANCE'); + let failures = 0; const expect = (what, ok) => { + registerCase(); if (!ok) { failures += 1; console.error(` ✗ ${what}`); @@ -708,6 +754,7 @@ function selfTest() { // discrimination is driven here rather than inferred from a green run. The // verdict compares through this same `ratchet`, so these three cases ARE the // verdict's decision procedure, not a model of it. + battery('Verdict 2, the EMPTY-LEDGER cases'); const emptyClean = ratchet([], []); expect( 'EMPTY ledger + every set serving its companion ⇒ no finding (the gate passes, correctly)', @@ -726,6 +773,53 @@ function selfTest() { stale.removed.length === 1 && stale.added.length === 0, ); + // ── The floor: every declared battery RAN, and ran its cases (#13489) ─── + // + // Evaluated after every battery has had its chance and BEFORE the verdict, so + // the success line below can only be printed by a run in which the set of + // batteries that registered assertions EQUALS the set declared. A set + // difference names WHICH battery stopped; a count says only that something did. + const floorFailure = (message) => { + failures += 1; + console.error(` ✗ ${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.', + ); + } + console.log(failures === 0 ? '\ncheck-i18n-stale-fill: self-test OK\n' : `\ncheck-i18n-stale-fill: self-test FAILED (${failures})\n`); selfTestReachedVerdict = true; process.exit(failures === 0 ? 0 : 1); diff --git a/scripts/check-init-service-contract.mjs b/scripts/check-init-service-contract.mjs index 22a11b2c17..70dd6f2716 100644 --- a/scripts/check-init-service-contract.mjs +++ b/scripts/check-init-service-contract.mjs @@ -85,6 +85,53 @@ import { requireDefaultExport } from './import-prerequisite.mjs'; const ts = await requireDefaultExport('typescript', () => import('typescript'), import.meta.url); import { parseSourceFile } from './ts-parse.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 #4420 pre-fix shape MUST be caught: dependencies = [], no': 3, + '2. The post-#4460 shape (declared tolerance via optionalDependencies) passes.': 1, + '3. A hard dependency on the provider passes.': 1, + '4. requiresServices naming the service passes.': 1, + '5. getService inside a hook callback registered during init is DEFERRED —': 1, + '6. getService in start() is the sanctioned best-effort pattern — not flagged.': 1, + '7. A service with no workspace provider has nothing to order against.': 1, + '8. Object-literal plugins (the createApiRegistryPlugin shape) are scanned too.': 1, + '9. Self-provided services are not edges.': 1, + '10. The walk is transitive: init → helperA → helperB → getService, plus a': 1, + '11. A dynamic (non-literal) service name cannot be judged statically —': 1, + '12. Recursion between helpers must terminate.': 1, + '13. #4772 VERBATIM, pre-fix (`f2eb85007^`, packages/plugins/plugin-auth/src/auth-plugin.ts:346):': 5, + '14. The same call without the optional-call / cast noise — a plain': 1, + '15. Declaring it discharges the obligation — the remedy the message prints': 1, + '16. `getServiceScoped` resolves through the very same': 2, + '17. Every exemption is decided by WHEN the call runs, not by which accessor': 1, + '18. `--list` and the problem text must quote the accessor actually called.': 1, + '19. Nothing outside the vocabulary is invented: a lookup-shaped call on a': 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 = 19; + +// 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)), '..'); const DECLARATION_FIELDS = ['dependencies', 'optionalDependencies', 'requiresServices', 'providesServices']; @@ -506,7 +553,21 @@ function list() { const SELF_TEST_VERDICT = 'check-init-service-contract self-test reached its verdict'; function selfTest() { - const assert = (cond, msg) => { if (!cond) { console.error('✗ self-test: ' + msg); process.exit(1); } }; + // 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 assert = (cond, msg) => { registerCase(); if (!cond) { console.error('✗ self-test: ' + msg); process.exit(1); } }; const auditSource = (code) => { const src = parseSourceFile('fixture.ts', code); @@ -526,6 +587,7 @@ function selfTest() { // optionalDependencies, no requiresServices — and the getService('manifest') // is NOT in init()'s own body but in a private helper init() calls, wrapped // in a best-effort try/catch. Exactly what shipped the data-loss bug. + battery('1. The #4420 pre-fix shape MUST be caught: dependencies = [], no'); { const { problems } = auditSource(PROVIDER + ` export class AutomationServicePlugin implements Plugin { @@ -555,6 +617,7 @@ function selfTest() { } // 2. The post-#4460 shape (declared tolerance via optionalDependencies) passes. + battery('2. The post-#4460 shape (declared tolerance via optionalDependencies) passes.'); { const { problems } = auditSource(PROVIDER + ` export class AutomationServicePlugin implements Plugin { @@ -568,6 +631,7 @@ function selfTest() { } // 3. A hard dependency on the provider passes. + battery('3. A hard dependency on the provider passes.'); { const { problems } = auditSource(PROVIDER + ` export class ApprovalsPlugin implements Plugin { @@ -580,6 +644,7 @@ function selfTest() { } // 4. requiresServices naming the service passes. + battery('4. requiresServices naming the service passes.'); { const { problems } = auditSource(PROVIDER + ` export class AuthPlugin implements Plugin { @@ -593,6 +658,7 @@ function selfTest() { // 5. getService inside a hook callback registered during init is DEFERRED — // it runs after every init(), so it is not an init-ordering edge. + battery('5. getService inside a hook callback registered during init is DEFERRED —'); { const { problems } = auditSource(PROVIDER + ` export class LatePlugin implements Plugin { @@ -606,6 +672,7 @@ function selfTest() { } // 6. getService in start() is the sanctioned best-effort pattern — not flagged. + battery('6. getService in start() is the sanctioned best-effort pattern — not flagged.'); { const { problems } = auditSource(PROVIDER + ` export class StartConsumerPlugin implements Plugin { @@ -618,6 +685,7 @@ function selfTest() { } // 7. A service with no workspace provider has nothing to order against. + battery('7. A service with no workspace provider has nothing to order against.'); { const { problems } = auditSource(` export class HostConsumerPlugin implements Plugin { @@ -629,6 +697,7 @@ function selfTest() { } // 8. Object-literal plugins (the createApiRegistryPlugin shape) are scanned too. + battery('8. Object-literal plugins (the createApiRegistryPlugin shape) are scanned too.'); { const { problems } = auditSource(PROVIDER + ` export function createBadPlugin(): Plugin { @@ -642,6 +711,7 @@ function selfTest() { } // 9. Self-provided services are not edges. + battery('9. Self-provided services are not edges.'); { const { problems } = auditSource(` export class SelfPlugin implements Plugin { @@ -658,6 +728,7 @@ function selfTest() { // 10. The walk is transitive: init → helperA → helperB → getService, plus a // same-file free function. + battery('10. The walk is transitive: init → helperA → helperB → getService, plus a'); { const { problems } = auditSource(PROVIDER + ` function seedThings(ctx: PluginContext) { ctx.getService('data').insert('t', {}); } @@ -673,6 +744,7 @@ function selfTest() { // 11. A dynamic (non-literal) service name cannot be judged statically — // ignored here; the runtime describeInitOrderFault diagnostics cover it. + battery('11. A dynamic (non-literal) service name cannot be judged statically —'); { const { problems } = auditSource(PROVIDER + ` export class DynamicPlugin implements Plugin { @@ -684,6 +756,7 @@ function selfTest() { } // 12. Recursion between helpers must terminate. + battery('12. Recursion between helpers must terminate.'); { const { problems } = auditSource(PROVIDER + ` export class LoopPlugin implements Plugin { @@ -718,6 +791,7 @@ function selfTest() { // via an optional call on a cast `ctx`, inside a best-effort try/catch, and // the plugin's declarations cover `data`/`manifest`/objectql — never `cache`. // Before `getServiceAsync` joined the vocabulary this audited GREEN. + battery('13. #4772 VERBATIM, pre-fix (`f2eb85007^`, packages/plugins/plugin-auth/src/auth-plugin.ts:346):'); { const code = CACHE_PROVIDER + ` export class AuthPlugin implements Plugin { @@ -760,6 +834,7 @@ function selfTest() { // 14. The same call without the optional-call / cast noise — a plain // `await ctx.getServiceAsync('cache')` is the same undeclared edge. + battery('14. The same call without the optional-call / cast noise — a plain'); { const { problems } = auditSource(CACHE_PROVIDER + ` export class PlainAsyncPlugin implements Plugin { @@ -772,6 +847,7 @@ function selfTest() { // 15. Declaring it discharges the obligation — the remedy the message prints // works for the async accessor exactly as it does for the sync one. + battery('15. Declaring it discharges the obligation — the remedy the message prints'); { const { problems } = auditSource(CACHE_PROVIDER + ` export class DeclaredAsyncPlugin implements Plugin { @@ -786,6 +862,7 @@ function selfTest() { // 16. `getServiceScoped` resolves through the very same // `pluginLoader.getService(name, scopeId)` as `getServiceAsync`, so it // carries the identical hazard and the identical verdict. + battery('16. `getServiceScoped` resolves through the very same'); { const { problems } = auditSource(CACHE_PROVIDER + ` export class ScopedPlugin implements Plugin { @@ -799,6 +876,7 @@ function selfTest() { // 17. Every exemption is decided by WHEN the call runs, not by which accessor // made it: start() is still the sanctioned best-effort seam. + battery('17. Every exemption is decided by WHEN the call runs, not by which accessor'); { const { problems } = auditSource(CACHE_PROVIDER + ` export class LateAsyncPlugin implements Plugin { @@ -813,6 +891,7 @@ function selfTest() { // 18. `--list` and the problem text must quote the accessor actually called. // Hardcoding `getService('X')` made the edge list rename every reader — // a machine-readable surface that lies about the code it describes. + battery('18. `--list` and the problem text must quote the accessor actually called.'); { const { edges } = auditSource(CACHE_PROVIDER + ` export class MixedPlugin implements Plugin { @@ -834,6 +913,7 @@ function selfTest() { // 19. Nothing outside the vocabulary is invented: a lookup-shaped call on a // name `packages/core` does not expose to plugins stays unjudged. + battery('19. Nothing outside the vocabulary is invented: a lookup-shaped call on a'); { const { problems } = auditSource(CACHE_PROVIDER + ` export class ProbePlugin implements Plugin { @@ -844,6 +924,53 @@ function selfTest() { assert(problems.length === 0, 'hasService is not in the vocabulary (not plugin-reachable in packages/core)'); } + // ── 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('✗ self-test: ' + 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.', + ); + process.exit(1); + } + console.log('✓ self-test: 19 cases'); return SELF_TEST_VERDICT; diff --git a/scripts/check-keyed-text-bounds.mjs b/scripts/check-keyed-text-bounds.mjs index f046ddc268..cdf0b08aa0 100644 --- a/scripts/check-keyed-text-bounds.mjs +++ b/scripts/check-keyed-text-bounds.mjs @@ -173,6 +173,43 @@ import { fileURLToPath } from 'node:url'; import { isEntrypoint } from './invoked-as.mjs'; import { blank, scanSource } 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 emitter-derived family': 3, + 'the detector FIRES': 15, + 'the detector STAYS SILENT': 8, + 'refusals: the shapes it will not guess at': 11, + 'the allowlist mechanism, driven on synthetic objects': 10, + 'the vacuity floors': 5, + 'provenance: the record must stay reproducible, and visibly so': 7, + 'the watch-hint declaration vs the repo-wide walk': 4, + 'package attribution, which the per-package allowlist rests on': 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 = 9; + +// The key an assertion is filed under when no battery is open. It is not a +// declared battery, so it reds by the same set difference rather than silently +// inflating whichever battery happened to run last. +const UNATTRIBUTED_BATTERY = '(no battery open)'; + const HERE = dirname(fileURLToPath(import.meta.url)); const REPO_ROOT = resolve(HERE, '..'); @@ -1153,8 +1190,23 @@ function objectFile(body) { let selfTestReachedVerdict = false; export function selfTest() { + // The battery ledger this self-test's floor is evaluated against (#13489). + // `battery()` opens a battery; every assertion below is attributed to the one + // most recently opened, so a section that stops running stops registering and + // names ITSELF at the floor rather than going quiet. + const 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) { console.log(` ok ${name}`); return; } failures += 1; console.error(` FAIL ${name}${detail === undefined ? '' : ` -- ${detail}`}`); @@ -1173,6 +1225,7 @@ export function selfTest() { try { // ── the emitter-derived family ──────────────────────────────────────── + battery('the emitter-derived family'); const fam = oneObject(`{ name: 'o', fields: {}, indexes: [] }`); t('the text family is read off the emitter, and matches this gate\'s witness', familyProblem(fam.family) === null, JSON.stringify(fam.family)); @@ -1189,6 +1242,7 @@ export function selfTest() { JSON.stringify(movedFamily.family)); // ── the detector FIRES ──────────────────────────────────────────────── + battery('the detector FIRES'); for (const type of EXPECTED_TEXT_FAMILY) { const r = oneObject(`{ name: 'o', fields: { c: { type: '${type}' } }, indexes: [{ fields: ['c'] }] }`); const found = offendersOf(r); @@ -1224,6 +1278,7 @@ export function selfTest() { offendersOf(codeSecondArg).length === 1, JSON.stringify(codeSecondArg.refusals)); // ── the detector STAYS SILENT ───────────────────────────────────────── + battery('the detector STAYS SILENT'); const bounded = oneObject(`{ name: 'o', fields: { c: Field.text({ maxLength: 255 }) }, indexes: [{ fields: ['c'] }] }`); t('a bounded keyed text column is NOT a finding', offendersOf(bounded).length === 0); @@ -1253,6 +1308,7 @@ export function selfTest() { braceInString.refusals.length === 0 && offendersOf(braceInString).length === 0, JSON.stringify(braceInString.refusals)); // ── refusals: the shapes it will not guess at ───────────────────────── + battery('refusals: the shapes it will not guess at'); const unknownBuilder = oneObject(`{ name: 'o', fields: { c: Field.mystery({}) }, indexes: [{ fields: ['c'] }] }`); t('an UNKNOWN `Field.` on a keyed column REFUSES rather than passing', unknownBuilder.refusals.length === 1 && /does not know/.test(unknownBuilder.refusals[0].message), @@ -1304,6 +1360,7 @@ export function selfTest() { // ── the allowlist mechanism, driven on synthetic objects ────────────── // ALLOWLIST is empty against the real tree, so the excusing branch is never // taken there and would rot unexecuted. Both outcomes are driven here. + battery('the allowlist mechanism, driven on synthetic objects'); const syntheticObjects = [{ name: 'o', file: 'packages/p/src/a.object.ts', @@ -1339,6 +1396,7 @@ export function selfTest() { staleAllowlistRows(syntheticObjects, [{ ...goodRow, why: ' ' }]).length === 1); // ── the vacuity floors ──────────────────────────────────────────────── + battery('the vacuity floors'); const empty = run({}); t('an EMPTY tree trips a floor rather than reporting clean', floorProblem(empty.counts) !== null, JSON.stringify(empty.counts)); @@ -1359,6 +1417,7 @@ export function selfTest() { // when the RECORD stops being a self-contained, reproducible claim: a ref // that is not a ref, a quotation that restated the ref instead of reading // it, or a pass line that stopped showing the reader both censuses. + battery('provenance: the record must stay reproducible, and visibly so'); t('PROVENANCE — the record carries the ref it was measured on, inside the frozen record', typeof MEASURED.ref === 'string' && /^[0-9a-f]{7,40}$/.test(MEASURED.ref) && Object.isFrozen(MEASURED), JSON.stringify(MEASURED.ref)); @@ -1404,6 +1463,7 @@ export function selfTest() { 'the pass path in main() no longer calls provenanceLine — the record would stop being reconciled in the log'); // ── the watch-hint declaration vs the repo-wide walk ───────────────── + battery('the watch-hint declaration vs the repo-wide walk'); const outsideHints = run({ 'tools/stray.object.ts': objectFile(`{ name: 'o', fields: { c: Field.text({ maxLength: 5 }) }, indexes: [{ fields: ['c'] }] }`) }); t('an object file OUTSIDE every watch hint is still SWEPT -- the walk is repo-wide', outsideHints.relFiles.includes('tools/stray.object.ts') && outsideHints.objects.length === 1, @@ -1416,6 +1476,7 @@ export function selfTest() { ROOT_DIR_WATCH_HINTS.every((h) => h.includes('/'))); // ── package attribution, which the per-package allowlist rests on ───── + battery('package attribution, which the per-package allowlist rests on'); t('packageOf attributes a plugin path to the plugin', packageOf('packages/plugins/plugin-audit/src/objects/x.object.ts') === 'packages/plugins/plugin-audit'); t('packageOf attributes a service path to the service', @@ -1428,6 +1489,53 @@ export function selfTest() { rmSync(tmp, { recursive: true, force: true }); } + // ── The floor: every declared battery RAN, and ran its cases (#13489) ─── + // + // Evaluated after every battery has had its chance and BEFORE the verdict, so + // the success line below can only be printed by a run in which the set of + // batteries that registered assertions EQUALS the set declared. A set + // difference names WHICH battery stopped; a count says only that something did. + const floorFailure = (message) => { + failures += 1; + console.error(` FAIL ${message}`); + }; + const declaredBatteries = Object.keys(SELF_TEST_BATTERIES); + let floorBreached = false; + if (declaredBatteries.length < SELF_TEST_BATTERY_FLOOR) { + floorBreached = true; + floorFailure( + `SELF_TEST_BATTERIES declares ${declaredBatteries.length} batteries, below the pinned ` + + `${SELF_TEST_BATTERY_FLOOR} — a battery deleted from the roster takes its own floor with it.`, + ); + } + for (const [name, count] of 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(`\n${failures === 0 ? 'PASS' : 'FAIL'} check-keyed-text-bounds --self-test (${failures} failure(s))`); selfTestReachedVerdict = true; return failures === 0 ? 0 : 1; diff --git a/scripts/check-merge-queue-triage-outcome.mjs b/scripts/check-merge-queue-triage-outcome.mjs index 92d1c66cc1..2449f5f97a 100644 --- a/scripts/check-merge-queue-triage-outcome.mjs +++ b/scripts/check-merge-queue-triage-outcome.mjs @@ -126,6 +126,41 @@ import { requireDependency } from './import-prerequisite.mjs'; const { isMap, isSeq, parseDocument } = await requireDependency('yaml', () => import('yaml'), import.meta.url); import { isEntrypoint } from './invoked-as.mjs'; +// ── The self-test's own battery roster and floor (#13489) ────────────────── +// +// `failures.length === 0` used to be this self-test's ONLY success condition, so +// "every case held" and "the cases never ran" printed the same line. Closed the +// way PR #13487 validated on check-doc-authoring: what is pinned is the +// registered NAMES, not a number. Every section opens with `battery('')`, +// every assertion is attributed to the battery most recently opened, and the +// floor requires the OPENED set to equal the DECLARED set with each battery at +// or above its own count. +// +// ⛔ A pinned TOTAL is not the repair: a battery dropping from 9 cases to 3 +// keeps a total "right" the moment a sibling grows. +// +// The counts are a FLOOR, not an equality — adding cases is ordinary work and +// must not red. A battery BELOW its floor means cases stopped running; the +// remedy is to find what stopped registering. +const SELF_TEST_BATTERIES = Object.freeze({ + '1. The unmutated shipped script must be green -- otherwise every red below': 1, + '2. Every mutation must be REACHED and must turn the battery red, in the': 8, + 'The dual. A scenario named here must survive the mutation untouched': 106, + '3. A script that does not compile is caught before any scenario runs.': 1, + '4. Missing input is a failure, never a pass (#4690).': 1, + '5. The corpus is the evidence. A fixture that quietly vanished would leave': 6, + '6. Wiring. A check nobody runs is the #4449 shape this repo keeps paying for.': 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 WORKFLOW = '.github/workflows/merge-queue-triage.yml'; const JOB = 'triage'; const SELF = 'scripts/check-merge-queue-triage-outcome.mjs'; @@ -1465,6 +1500,20 @@ const MUTATIONS = [ const SELF_TEST_VERDICT = 'check-merge-queue-triage-outcome 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 root = repoRoot(); const { source, problems } = extractScript(root); if (problems.length > 0) { @@ -1474,16 +1523,18 @@ async function selfTest() { const failures = []; let checked = 0; - const assert = (cond, msg) => { checked++; if (!cond) failures.push(msg); }; + const assert = (cond, msg) => { registerCase(); checked++; if (!cond) failures.push(msg); }; // 1. The unmutated shipped script must be green -- otherwise every red below // proves nothing about the mutation. + battery('1. The unmutated shipped script must be green -- otherwise every red below'); const clean = await judge(source, root); assert(clean.failures.length === 0, `the shipped script is green before any mutation, got: ${clean.failures.map((f) => `[${f.id}] ${f.message}`).join(' | ')}`); // 2. Every mutation must be REACHED and must turn the battery red, in the // scenarios it names. + battery('2. Every mutation must be REACHED and must turn the battery red, in the'); for (const m of MUTATIONS) { assert(source.includes(m.from), `${m.id}: its anchor is present in the shipped script (a no-op mutation proves nothing)`); if (!source.includes(m.from)) continue; @@ -1499,6 +1550,7 @@ async function selfTest() { // that is what makes its GREEN a reading rather than an absence, and it is // the only assertion that can fail a detector which simply reds on // everything. + battery('The dual. A scenario named here must survive the mutation untouched'); for (const id of m.keepGreen ?? []) { assert(!red.failures.some((f) => f.id === id), `${m.id}: scenario ${id} must NOT move -- it is the control for this mutation, ` @@ -1507,15 +1559,18 @@ async function selfTest() { } // 3. A script that does not compile is caught before any scenario runs. + battery('3. A script that does not compile is caught before any scenario runs.'); const broken = await judge(`${source}\nconst github = 1;`, root); assert(broken.failures.length === 1 && broken.failures[0].id === 'C0', 'a non-compiling script is reported as C0, once'); // 4. Missing input is a failure, never a pass (#4690). + battery('4. Missing input is a failure, never a pass (#4690).'); const gone = extractScript(join(root, 'scripts')); assert(gone.source === null && gone.problems.length === 1, 'a missing workflow file is an input problem, not a pass'); // 5. The corpus is the evidence. A fixture that quietly vanished would leave // every extraction scenario asserting over an empty string. + battery('5. The corpus is the evidence. A fixture that quietly vanished would leave'); for (const name of ['plugin-dev-timeout.job-log.txt', 'plugin-dev-assertion.job-log.txt', 'incident-32333709633-published-excerpt.job-log.txt']) { assert(existsSync(join(root, FIXTURES, name)), `corpus: ${FIXTURES}/${name} exists`); } @@ -1529,6 +1584,7 @@ async function selfTest() { 'corpus: the two captures share a byte-identical FAIL line -- that identity IS the defect limb (1) exists for'); // 6. Wiring. A check nobody runs is the #4449 shape this repo keeps paying for. + battery('6. Wiring. A check nobody runs is the #4449 shape this repo keeps paying for.'); const lint = join(root, '.github', 'workflows', 'lint.yml'); assert(existsSync(lint), 'wiring: .github/workflows/lint.yml exists -- it is where this check runs'); if (existsSync(lint)) { @@ -1537,6 +1593,52 @@ async function selfTest() { assert(body.includes(`${SELF} --self-test`), 'wiring: lint.yml runs the --self-test half too'); } + // ── The floor: every declared battery RAN, and ran its cases (#13489) ─── + // + // Evaluated after every battery has had its chance and BEFORE the verdict, so + // the success line below can only be printed by a run in which the set of + // batteries that registered assertions EQUALS the set declared. A set + // difference names WHICH battery stopped; a count says only that something did. + const floorFailure = (message) => { + failures.push(message); + }; + const declaredBatteries = Object.keys(SELF_TEST_BATTERIES); + let floorBreached = false; + if (declaredBatteries.length < SELF_TEST_BATTERY_FLOOR) { + floorBreached = true; + floorFailure( + `SELF_TEST_BATTERIES declares ${declaredBatteries.length} batteries, below the pinned ` + + `${SELF_TEST_BATTERY_FLOOR} — a battery deleted from the roster takes its own floor with it.`, + ); + } + for (const [name, count] of 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-merge-queue-triage-outcome --self-test -- ${failures.length} failure(s)\n`); for (const f of failures) console.error(` • ${f}`); diff --git a/scripts/check-overlay-whitelist-table.mjs b/scripts/check-overlay-whitelist-table.mjs index 0daaee2832..ae6f2f5f18 100644 --- a/scripts/check-overlay-whitelist-table.mjs +++ b/scripts/check-overlay-whitelist-table.mjs @@ -188,6 +188,53 @@ 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 POSITIVE CONTROL: the pre-#11750 table must produce exactly 4.': 6, + '2. The corrected table must be green on BOTH legs.': 3, + '3. A ONE-LEGGED gate would have shipped 3 of the 4. Pinned so that': 1, + '4. AST vs REGEX on the fixture: the numbers must differ, in the': 3, + '5. Comments are not entries. `grep -c` over the fixture inflates both': 2, + '6. MULTI-TYPE CELL control: five names in one cell become five types.': 2, + '7. A mismatch inside a MULTI-TYPE cell is caught per type, not per row.': 1, + '8. STRUCTURAL refusals. Each must be RED, none may read as clean.': 7, + '9. REGISTRY structural refusals -- the route-around clause.': 7, + '10. A duplicated type in the table is caught.': 1, + '11. A retired type left behind in the table is caught.': 1, + '12. An entry that OMITS the optional flag counts as false, not as a': 1, + '13a. Number-word reading: both spellings, the compound, and the refusal.': 20, + '13b. POSITIVE CONTROL for green: a sentence true of FIXTURE_REGISTRY': 3, + '13c. THE CARD\'S SCENARIO, both halves — and each isolated by the other': 2, + '13d. Digits and words are interchangeable on both claims, and dropping the': 3, + '13e. EVERY match is checked, not just the first. A second copy of the same': 1, + '13f. STRUCTURAL refusals. An absent or unreadable claim is RED — draining': 5, + '13g. The refusals above must be refusals, not silent empties: a fixture': 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 = 19; + +// 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.replace(/\/$/, ''); const REGISTRY_FILE = 'packages/spec/src/kernel/metadata-plugin.zod.ts'; @@ -847,12 +894,28 @@ function run(registryText, tableText) { let selfTestReachedVerdict = false; export function selfTest() { + // The battery ledger this self-test's floor is evaluated against (#13489). + // `battery()` opens a battery; every assertion below is attributed to the one + // most recently opened, so a section that stops running stops registering and + // names ITSELF at the floor rather than going quiet. + const 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, ok, detail = '') => { + registerCase(); if (!ok) failures.push(`${name}${detail ? ` — ${detail}` : ''}`); }; // ---- 1. THE POSITIVE CONTROL: the pre-#11750 table must produce exactly 4. + battery('1. THE POSITIVE CONTROL: the pre-#11750 table must produce exactly 4.'); const before = run(FIXTURE_REGISTRY, FIXTURE_TABLE_BEFORE); check('before/structure-clean', before.structure.length === 0, JSON.stringify(before.structure)); const beforeTotal = before.leg1.length + before.leg2.length; @@ -875,6 +938,7 @@ export function selfTest() { ); // ---- 2. The corrected table must be green on BOTH legs. + battery('2. The corrected table must be green on BOTH legs.'); const after = run(FIXTURE_REGISTRY, FIXTURE_TABLE_AFTER); check('after/structure-clean', after.structure.length === 0, JSON.stringify(after.structure)); check('after/leg1=0', after.leg1.length === 0, JSON.stringify(after.leg1)); @@ -882,10 +946,12 @@ export function selfTest() { // ---- 3. A ONE-LEGGED gate would have shipped 3 of the 4. Pinned so that // deleting leg 2 cannot pass this battery. + battery('3. A ONE-LEGGED gate would have shipped 3 of the 4. Pinned so that'); check('leg2 is load-bearing (3 != 4)', before.leg1.length !== beforeTotal); // ---- 4. AST vs REGEX on the fixture: the numbers must differ, in the // direction the header documents. + battery('4. AST vs REGEX on the fixture: the numbers must differ, in the'); const regexEntryCount = (FIXTURE_REGISTRY.match(/^ {2}\{ type: '/gm) || []).length; const astEntryCount = before.entries.length; check('fixture/ast=14', astEntryCount === 14, `got ${astEntryCount}`); @@ -901,6 +967,7 @@ export function selfTest() { // ---- 5. Comments are not entries. `grep -c` over the fixture inflates both // flags; the AST count must be the smaller, correct one. + battery('5. Comments are not entries. `grep -c` over the fixture inflates both'); const grepTrue = (FIXTURE_REGISTRY.match(new RegExp(`${COL_FLAG}: true`, 'g')) || []).length; const astTrue = before.entries.filter((e) => e.allowOrgOverride).length; check('fixture/ast true-set = 5', astTrue === 5, `got ${astTrue}`); @@ -911,6 +978,7 @@ export function selfTest() { ); // ---- 6. MULTI-TYPE CELL control: five names in one cell become five types. + battery('6. MULTI-TYPE CELL control: five names in one cell become five types.'); const multi = readTable(FIXTURE_TABLE_AFTER); const rowWith5 = multi.rows.find((r) => r.types.length === 5); check( @@ -925,6 +993,7 @@ export function selfTest() { `${flatCount} types / ${multi.rows.length} rows`); // ---- 7. A mismatch inside a MULTI-TYPE cell is caught per type, not per row. + battery('7. A mismatch inside a MULTI-TYPE cell is caught per type, not per row.'); const oneWrongInCell = FIXTURE_TABLE_AFTER.replace( `| \`view\`, \`dashboard\`, \`report\`, \`email_template\`, \`translation\` | ${YES} |`, `| \`view\`, \`dashboard\`, \`report\`, \`email_template\`, \`translation\`, \`hook\` | ${YES} |`, @@ -937,6 +1006,7 @@ export function selfTest() { ); // ---- 8. STRUCTURAL refusals. Each must be RED, none may read as clean. + battery('8. STRUCTURAL refusals. Each must be RED, none may read as clean.'); const structural = [ ['renamed heading', FIXTURE_TABLE_AFTER.replace(HEADING, '## Overlay whitelist')], ['no table', `${HEADING}\n\nJust prose now.\n\n## Next\n`], @@ -958,6 +1028,7 @@ export function selfTest() { } // ---- 9. REGISTRY structural refusals -- the route-around clause. + battery('9. REGISTRY structural refusals -- the route-around clause.'); const registryStructural = [ ['spread element', FIXTURE_REGISTRY.replace(" { type: 'agent'", ' ...EXTRA_ENTRIES,\n { type: \'agent\'')], [ @@ -983,6 +1054,7 @@ export function selfTest() { } // ---- 10. A duplicated type in the table is caught. + battery('10. A duplicated type in the table is caught.'); const dup = run( FIXTURE_REGISTRY, FIXTURE_TABLE_AFTER.replace(`| \`job\` | ${NO} |`, `| \`job\` | ${NO} |\n| \`job\` | ${NO} |`), @@ -994,6 +1066,7 @@ export function selfTest() { ); // ---- 11. A retired type left behind in the table is caught. + battery('11. A retired type left behind in the table is caught.'); const retired = run(FIXTURE_REGISTRY, FIXTURE_TABLE_AFTER.replace('| `job` |', '| `validation` |')); check( 'a table row for an undeclared type is caught', @@ -1003,6 +1076,7 @@ export function selfTest() { // ---- 12. An entry that OMITS the optional flag counts as false, not as a // structural refusal (the schema's documented default). + battery('12. An entry that OMITS the optional flag counts as false, not as a'); const omitted = readRegistry( FIXTURE_REGISTRY.replace(`{ type: 'agent', label: 'AI Agent', supportsOverlay: false, ${COL_FLAG}: false, loadOrder: 90 }`, `{ type: 'agent', label: 'AI Agent', supportsOverlay: false, loadOrder: 90 }`), @@ -1022,6 +1096,7 @@ export function selfTest() { // 13a. Number-word reading: both spellings, the compound, and the refusal. // `null` is the refusal channel and must never collide with 0. + battery('13a. Number-word reading: both spellings, the compound, and the refusal.'); const tokenCases = [ ['0', 0], ['5', 5], ['27', 27], ['28', 28], ['zero', 0], ['five', 5], ['Five', 5], ['six', 6], ['nineteen', 19], ['twenty', 20], @@ -1040,6 +1115,7 @@ export function selfTest() { // 13b. POSITIVE CONTROL for green: a sentence true of FIXTURE_REGISTRY // parses cleanly, yields BOTH claims, and drifts zero. + battery('13b. POSITIVE CONTROL for green: a sentence true of FIXTURE_REGISTRY'); const proseOk = readProseCounts(proseSentence('five', 14)); check('prose/ok structure-clean', proseOk.findings.length === 0, JSON.stringify(proseOk.findings)); check( @@ -1054,6 +1130,7 @@ export function selfTest() { // 13c. THE CARD'S SCENARIO, both halves — and each isolated by the other // staying right, so neither red can be produced by the wrong claim. + battery('13c. THE CARD\'S SCENARIO, both halves — and each isolated by the other'); const staleTotal = compareProse(before.entries, readProseCounts(proseSentence('five', 27)).claims); check( 'prose/a stale TOTAL is caught, alone', @@ -1069,6 +1146,7 @@ export function selfTest() { // 13d. Digits and words are interchangeable on both claims, and dropping the // bold from `**complete**` is not a false red. + battery('13d. Digits and words are interchangeable on both claims, and dropping the'); check( 'prose/digit true-set accepted', compareProse(before.entries, readProseCounts(proseSentence('5', 14)).claims).length === 0, @@ -1084,6 +1162,7 @@ export function selfTest() { // 13e. EVERY match is checked, not just the first. A second copy of the same // claim elsewhere on the page is a second hand-kept copy. + battery('13e. EVERY match is checked, not just the first. A second copy of the same'); const twoCopies = readProseCounts( `${proseSentence('five', 14)}\n\nElsewhere: of the 27 types in \`${REGISTRY_CONST}\`, most are inert.`, ); @@ -1096,6 +1175,7 @@ export function selfTest() { // 13f. STRUCTURAL refusals. An absent or unreadable claim is RED — draining // this leg to vacuum is precisely the failure it exists to stop. + battery('13f. STRUCTURAL refusals. An absent or unreadable claim is RED — draining'); const proseStructural = [ ['claim sentence deleted', 'The table above is the whitelist. Nothing else to say.'], ['total claim missing', `Those five are the **complete** \`${COL_FLAG}: true\` set.`], @@ -1110,11 +1190,58 @@ export function selfTest() { // 13g. The refusals above must be refusals, not silent empties: a fixture // that refuses must also surrender no claim it could not check. + battery('13g. The refusals above must be refusals, not silent empties: a fixture'); check( 'prose/an unreadable count yields NO claim (never a default of 0)', readProseCounts(proseSentence('five', 'several')).claims.every((c) => c.what !== 'total'), ); + // ── 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('\n✗ check-overlay-whitelist-table self-test failed:\n'); for (const f of failures) console.error(` - ${f}`); diff --git a/scripts/check-quick-reference-counts.mjs b/scripts/check-quick-reference-counts.mjs index 8e219152c2..ce2b2de37f 100644 --- a/scripts/check-quick-reference-counts.mjs +++ b/scripts/check-quick-reference-counts.mjs @@ -103,6 +103,56 @@ import { readFileSync, readdirSync, existsSync } from 'node:fs'; 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({ + '1. POSITIVE — the measurement itself, per section, on BOTH numbers. A': 2, + '2. POSITIVE — a wrong N is caught, and the message names the section and': 5, + '3. POSITIVE — the M-ASSERTION, and the case that makes this gate more than': 4, + '4. POSITIVE — a deleted table row is caught.': 2, + '5. POSITIVE — the SINGULAR "(1 of 1 schema)" heading is a real section.': 2, + '6. POSITIVE — a section ends at the next `##` even when that heading has no': 1, + '7. POSITIVE — the heading FORMAT changing is loud, not silent. Both halves': 2, + '8. POSITIVE — the OLD `(N schemas)` spelling is refused rather than silently': 1, + '9. POSITIVE — a counted section whose table vanished.': 2, + '10. POSITIVE — a page the parser no longer recognises at all fails loudly': 2, + '11. POSITIVE — two tables in one counted section is ambiguous, not silently': 1, + '12. POSITIVE — a bare unlinked name is refused. This is the shape Shared\'s': 1, + '13. POSITIVE — a row that documents its protocol outside the reference tree': 1, + '14. POSITIVE — and the mirror: marking a row that IS in its own tree hides a': 1, + '15. POSITIVE — a row pointing at a reference page that no longer exists.': 1, + '16. POSITIVE — #6319\'s actual defect, caught by LINK rather than by count: a': 1, + '17. POSITIVE — one page listed twice inflates N while every count still': 1, + '18. POSITIVE — a category directory that is neither sectioned nor declared.': 2, + '19. POSITIVE — the declared page count of an unsectioned category is checked': 1, + '20. POSITIVE — declaring a category as unsectioned while it HAS a section is': 2, + '21. POSITIVE — a declared directory that does not exist. The symmetric': 1, + '22. POSITIVE — deleting the whole block is loud. Without this, "state the': 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 = 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 ROOT = new URL('..', import.meta.url).pathname.replace(/\/$/, ''); const TARGET = 'content/docs/getting-started/quick-reference.mdx'; const REFERENCES = 'content/docs/references'; @@ -538,8 +588,23 @@ const GOOD_PAGE = [ 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 failures = []; const expect = (label, got, want) => { + registerCase(); const g = JSON.stringify(got); const w = JSON.stringify(want); if (g !== w) failures.push(` ✗ ${label}: expected ${w}, got ${g}`); @@ -550,6 +615,7 @@ function selfTest() { // gutted checker that returns nothing fails here, and so does one that // stops resolving the directory side: the fourth column is the catalog's // real size, not the page's claim about it. + battery('1. POSITIVE — the measurement itself, per section, on BOTH numbers. A'); { const { sections, findings } = checkPage(GOOD_PAGE, GOOD_CATALOG); expect( @@ -568,6 +634,7 @@ function selfTest() { // 2. POSITIVE — a wrong N is caught, and the message names the section and // BOTH numbers. A gate that merely says "mismatch" fails this. + battery('2. POSITIVE — a wrong N is caught, and the message names the section and'); { const bad = GOOD_PAGE.replace('## Data Protocol (3 of 4 schemas)', '## Data Protocol (5 of 4 schemas)'); const { findings } = checkPage(bad, GOOD_CATALOG); @@ -583,6 +650,7 @@ function selfTest() { // only the directory disagrees. Delete the `declaredTotal !== pages.length` // comparison in checkPage and this case measures zero findings — which is // exactly the green-forever state #6530 documented. + battery('3. POSITIVE — the M-ASSERTION, and the case that makes this gate more than'); { const bad = GOOD_PAGE.replace('## Data Protocol (3 of 4 schemas)', '## Data Protocol (3 of 9 schemas)'); const { findings } = checkPage(bad, GOOD_CATALOG); @@ -593,6 +661,7 @@ function selfTest() { } // 4. POSITIVE — a deleted table row is caught. + battery('4. POSITIVE — a deleted table row is caught.'); { const bad = GOOD_PAGE.replace( '| **[Object](/docs/references/data/object)** | `object.zod.ts` | Object | Object defs |\n', @@ -607,6 +676,7 @@ function selfTest() { // #6319's report came from a plural-only regex; under one, this page yields // no finding at all because QA is not a section and its row is charged to // Data. + battery('5. POSITIVE — the SINGULAR "(1 of 1 schema)" heading is a real section.'); { const bad = GOOD_PAGE.replace('## QA Protocol (1 of 1 schema)', '## QA Protocol (2 of 1 schema)'); const { findings } = checkPage(bad, GOOD_CATALOG); @@ -619,6 +689,7 @@ function selfTest() { // Declarative Endpoints rule table is charged to QA. Asserted as a measured // value (1), not as "no findings" — the leaky scanner of #6319 would // measure 5 here. + battery('6. POSITIVE — a section ends at the next `##` even when that heading has no'); { const { sections } = checkPage(GOOD_PAGE, GOOD_CATALOG); const qa = sections.find((s) => s.title === 'QA Protocol'); @@ -628,6 +699,7 @@ function selfTest() { // 7. POSITIVE — the heading FORMAT changing is loud, not silent. Both halves // matter: the section stops being counted (so nothing is compared) and the // gate must say so — including that its category then has no section. + battery('7. POSITIVE — the heading FORMAT changing is loud, not silent. Both halves'); { const bad = GOOD_PAGE.replace('## Data Protocol (3 of 4 schemas)', '## Data Protocol - 3 schemas'); const { findings } = checkPage(bad, GOOD_CATALOG); @@ -638,6 +710,7 @@ function selfTest() { // 8. POSITIVE — the OLD `(N schemas)` spelling is refused rather than silently // accepted. Without this, #6530's migration could be reverted one heading // at a time and the M-assertion would quietly stop applying to it. + battery('8. POSITIVE — the OLD `(N schemas)` spelling is refused rather than silently'); { const bad = GOOD_PAGE.replace('## Data Protocol (3 of 4 schemas)', '## Data Protocol (3 schemas)'); const { findings } = checkPage(bad, GOOD_CATALOG); @@ -645,6 +718,7 @@ function selfTest() { } // 9. POSITIVE — a counted section whose table vanished. + battery('9. POSITIVE — a counted section whose table vanished.'); { const bad = [ '## Data Protocol (2 of 4 schemas)', @@ -665,6 +739,7 @@ function selfTest() { // 10. POSITIVE — a page the parser no longer recognises at all fails loudly // instead of passing with zero comparisons. + battery('10. POSITIVE — a page the parser no longer recognises at all fails loudly'); { const { findings } = checkPage('# Quick Reference Guide\n\nnothing here.\n', GOOD_CATALOG); expect('unrecognised page is reported', has(findings, /no "## \(N of M schemas\)" sections found at all/), true); @@ -673,6 +748,7 @@ function selfTest() { // 11. POSITIVE — two tables in one counted section is ambiguous, not silently // summed. + battery('11. POSITIVE — two tables in one counted section is ambiguous, not silently'); { const bad = GOOD_PAGE.replace( '| **[Object](/docs/references/data/object)** | `object.zod.ts` | Object | Object defs |', @@ -690,6 +766,7 @@ function selfTest() { // 12. POSITIVE — a bare unlinked name is refused. This is the shape Shared's // `Connector Auth` row had before #6530. + battery('12. POSITIVE — a bare unlinked name is refused. This is the shape Shared\'s'); { const bad = GOOD_PAGE.replace( '| **[Field](/docs/references/data/field)** | `field.zod.ts` | Field | Field types |', @@ -701,6 +778,7 @@ function selfTest() { // 13. POSITIVE — a row that documents its protocol outside the reference tree // must carry the marker; unmarked, it silently claims to be one of the M. + battery('13. POSITIVE — a row that documents its protocol outside the reference tree'); { const bad = GOOD_PAGE.replace( '| **[Events](/docs/kernel/events)** ↗ |', @@ -712,6 +790,7 @@ function selfTest() { // 14. POSITIVE — and the mirror: marking a row that IS in its own tree hides a // page from the coverage the heading advertises. + battery('14. POSITIVE — and the mirror: marking a row that IS in its own tree hides a'); { const bad = GOOD_PAGE.replace( '| **[Field](/docs/references/data/field)** |', @@ -723,6 +802,7 @@ function selfTest() { // 15. POSITIVE — a row pointing at a reference page that no longer exists. // Nothing else on the page changes, so only the directory can catch it. + battery('15. POSITIVE — a row pointing at a reference page that no longer exists.'); { const bad = GOOD_PAGE.replace('/docs/references/data/object)', '/docs/references/data/ghost)'); const { findings } = checkPage(bad, GOOD_CATALOG); @@ -731,6 +811,7 @@ function selfTest() { // 16. POSITIVE — #6319's actual defect, caught by LINK rather than by count: a // row that migrated into the wrong section. The counts still agree. + battery('16. POSITIVE — #6319\'s actual defect, caught by LINK rather than by count: a'); { const bad = GOOD_PAGE.replace('/docs/references/data/object)', '/docs/references/qa/testing)'); const { findings } = checkPage(bad, GOOD_CATALOG); @@ -739,6 +820,7 @@ function selfTest() { // 17. POSITIVE — one page listed twice inflates N while every count still // agrees with every other count. + battery('17. POSITIVE — one page listed twice inflates N while every count still'); { const bad = GOOD_PAGE.replace('/docs/references/data/object)', '/docs/references/data/field)'); const { findings } = checkPage(bad, GOOD_CATALOG); @@ -747,6 +829,7 @@ function selfTest() { // 18. POSITIVE — a category directory that is neither sectioned nor declared. // This is the category-level half of #6530's drift. + battery('18. POSITIVE — a category directory that is neither sectioned nor declared.'); { const bad = GOOD_PAGE.replace('| [`studio`](/docs/references/studio) | 3 | Designer-facing metadata. |\n', ''); const { findings } = checkPage(bad, GOOD_CATALOG); @@ -756,6 +839,7 @@ function selfTest() { // 19. POSITIVE — the declared page count of an unsectioned category is checked // against the directory too, so `studio` growing a page is not silent. + battery('19. POSITIVE — the declared page count of an unsectioned category is checked'); { const bad = GOOD_PAGE.replace('](/docs/references/studio) | 3 |', '](/docs/references/studio) | 5 |'); const { findings } = checkPage(bad, GOOD_CATALOG); @@ -764,6 +848,7 @@ function selfTest() { // 20. POSITIVE — declaring a category as unsectioned while it HAS a section is // a contradiction, not a harmless duplicate. + battery('20. POSITIVE — declaring a category as unsectioned while it HAS a section is'); { const bad = GOOD_PAGE.replace('| `contracts` | 0 |', '| `data` | 4 |'); const { findings } = checkPage(bad, GOOD_CATALOG); @@ -773,6 +858,7 @@ function selfTest() { // 21. POSITIVE — a declared directory that does not exist. The symmetric // rot: the block outliving the tree it describes. + battery('21. POSITIVE — a declared directory that does not exist. The symmetric'); { const bad = GOOD_PAGE.replace('| `contracts` | 0 |', '| `gone` | 0 |'); const { findings } = checkPage(bad, GOOD_CATALOG); @@ -781,12 +867,59 @@ function selfTest() { // 22. POSITIVE — deleting the whole block is loud. Without this, "state the // curation on the page" is enforced only while someone keeps it there. + battery('22. POSITIVE — deleting the whole block is loud. Without this, "state the'); { const bad = GOOD_PAGE.split('## Categories Without a Section')[0] + '## Common Patterns\n'; const { findings } = checkPage(bad, GOOD_CATALOG); expect('missing block is reported', has(findings, /no "## Categories Without a Section" block found/), 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('\n✗ check-quick-reference-counts self-test failed:\n'); for (const f of failures) console.error(f); diff --git a/scripts/check-ratchet-remedy-authority.mjs b/scripts/check-ratchet-remedy-authority.mjs index 54f604a19a..d7590f8aa4 100644 --- a/scripts/check-ratchet-remedy-authority.mjs +++ b/scripts/check-ratchet-remedy-authority.mjs @@ -109,6 +109,54 @@ 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({ + '(1) The lexer. A regex literal containing a quote must not desync it — the': 1, + '(2) Message boundaries. Two unrelated diagnostics must not compose into one': 1, + '(3) Concatenation is ONE message. The mirror of (2): if `+`-joined literals': 1, + '(4) Offer grammar, word order A: the registry FOLLOWS a preposition.': 1, + '(5) Offer grammar, word order B — #8540 miss ①. The fixture carries NO': 1, + '(6) The dot. A registry named by PATH must match — the measured `[^.;]` bug.': 1, + '(7) The descriptive-modal guard — the measured regen-artifacts.mjs case.': 1, + '(8) Stage 2 discriminates. Same offer shape, no testimony → not a ratchet.': 1, + '(9) Stage 2 reaches a real ratchet, by each limb, so (8) is not vacuous.': 2, + '(10) NON-CIRCULARITY. The authority token must never be its own anchor: a': 1, + '(11) Refusal, BOUND shape — check-adr-links.mjs / check-driver-memory-census.mjs.': 1, + '(12) Refusal, PREDICATION shape — check-type-source-resolution.mjs / check-test-source-alias.mjs.': 1, + '(13) Refusal DISCRIMINATES. A marking gate\'s closing discouragement is not a': 1, + '(14) End-to-end: an anchored, unrefused, unmarked offer is a VIOLATION. This': 1, + '(15) …and the same text carrying the token classifies as MARKED. Paired with': 1, + '(16) The corpus-scale positive control, asserted here as well as in the run:': 1, + '(18) Compliance is carried in AUTHOR-FACING text, not in commentary. Found by': 1, + '(19) …and the mirror: the token in a string literal DOES count. Paired with': 1, + '(17) This gate must not be an instance of its own convention.': 1, + '(20) The declared population, held to the walk in BOTH directions (#13813)': 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 = 20; + +// 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, '..'); const SCRIPTS_DIR = join(REPO_ROOT, 'scripts'); @@ -1019,8 +1067,22 @@ function main() { 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 failures = []; - const expect = (label, cond) => { if (!cond) failures.push(label); }; + const expect = (label, cond) => { registerCase(); if (!cond) failures.push(label); }; // ⛔ THE OFFER VERB IS HOISTED OUT OF EVERY FIXTURE THAT CARRIES TESTIMONY. // A fixture spelling `add …` inline, beside the words "shrink-only", would make @@ -1033,6 +1095,7 @@ function selfTest() { // (1) The lexer. A regex literal containing a quote must not desync it — the // measured first-cut bug, which silently swapped comment and string space for // the rest of the file. + battery('(1) The lexer. A regex literal containing a quote must not desync it — the'); const lexFixture = 'const RE = /[\'"]/g; // add an entry to THE_LEDGER\nconst m = "real message";\n'; const lexed = authorFacingMessages(lexFixture); expect('lexer — a regex literal containing a quote does not desync comment/string space ' @@ -1041,6 +1104,7 @@ function selfTest() { // (2) Message boundaries. Two unrelated diagnostics must not compose into one // offer — the measured check-adr-anchors.mjs false positive. + battery('(2) Message boundaries. Two unrelated diagnostics must not compose into one'); const twoMessages = 'push(`first: add \\`.vN\\` to the filename.`);\npush(`second: the KNOWN_THING entry is stale.`);\n'; expect('message boundary — a verb in one diagnostic does not reach a registry named in the ' + 'NEXT diagnostic (the measured check-adr-anchors.mjs false positive)', @@ -1048,6 +1112,7 @@ function selfTest() { // (3) Concatenation is ONE message. The mirror of (2): if `+`-joined literals // were split, every real multi-part remedy would lose its testimony. + battery('(3) Concatenation is ONE message. The mirror of (2): if `+`-joined literals'); expect('message boundary — literals joined only by `+` stay ONE message, so a remedy built ' + 'from three literals keeps the testimony written beside it', authorFacingMessages(`const m = '${ADD} an entry to ' + 'the ledger. It is ' + 'shrink-only.';`).length === 1); @@ -1055,6 +1120,7 @@ function selfTest() { const offersIn = (src) => findOffers(src); // (4) Offer grammar, word order A: the registry FOLLOWS a preposition. + battery('(4) Offer grammar, word order A: the registry FOLLOWS a preposition.'); expect('offer grammar — word order "… to … REGISTRY" is an offer', offersIn(`const m = 'Fix it properly. Or ${ADD} a MEASURED entry to scripts/x.baseline.json saying why not.';`).length > 0); @@ -1065,12 +1131,14 @@ function selfTest() { // draft of this fixture read "… entry in the ledger", which the prototype's // defect would have matched happily — the assertion would have passed while // pinning nothing. Fixtures for a word-order bug have to be word-ordered. + battery('(5) Offer grammar, word order B — #8540 miss ①. The fixture carries NO'); expect('offer grammar — a registry named BEFORE the noun, with no preposition leading to it, is ' + 'an offer (#8540 miss ①: the prototype demanded "add … to/in … REGISTRY" and so missed ' + 'check-type-check-coverage.mjs entirely)', offersIn(`const TEST_DEBT = {};\nconst m = 'Fix it properly. Or ${ADD} a TEST_DEBT entry saying why not.';`).length > 0); // (6) The dot. A registry named by PATH must match — the measured `[^.;]` bug. + battery('(6) The dot. A registry named by PATH must match — the measured `[^.;]` bug.'); const byPath = offersIn("const m = 'add it to scripts/role-word-baseline.json to admit the case.';"); expect('offer grammar — an offer naming its ledger by PATH is matched (a gap class excluding ' + 'the dot silently loses every path-named registry while the gate still reports clean)', @@ -1084,6 +1152,7 @@ function selfTest() { // modal guard was never consulted at all. Mutation-testing caught it: deleting // the guard left this assertion green. An assertion whose fixture cannot reach // the mechanism it names is worse than no assertion, because it reads as cover. + battery('(7) The descriptive-modal guard — the measured regen-artifacts.mjs case.'); expect('offer grammar — "can WIDEN it" DESCRIBES a hazard and is not an offer ' + '(the measured regen-artifacts.mjs false positive)', offersIn("const m = 'a SHRINK-ONLY ratchet. Regenerating it can WIDEN it — a fresh gap gets a " @@ -1092,6 +1161,7 @@ function selfTest() { // (8) Stage 2 discriminates. Same offer shape, no testimony → not a ratchet. // This is what keeps the ~20 declaration registries out, and it is what makes // (9) worth having: an anchor that fired on everything would keep (9) green. + battery('(8) Stage 2 discriminates. Same offer shape, no testimony → not a ratchet.'); const declSrc = `const INHERIT_JUSTIFIED = []; const m = 'write model: inherit AND ${ADD} an entry to INHERIT_JUSTIFIED saying why.';`; const declOffers = findOffers(declSrc); if (declOffers.length === 0) { @@ -1104,6 +1174,7 @@ function selfTest() { } // (9) Stage 2 reaches a real ratchet, by each limb, so (8) is not vacuous. + battery('(9) Stage 2 reaches a real ratchet, by each limb, so (8) is not vacuous.'); const shrinkSrc = `const m = '${ADD} a MEASURED entry to the baseline saying why not. That baseline is shrink-only.';`; const shrinkOffers = findOffers(shrinkSrc); expect('stage 2 — the SHRINK limb anchors an offer whose message testifies the registry only shrinks', @@ -1117,6 +1188,7 @@ function selfTest() { // (10) NON-CIRCULARITY. The authority token must never be its own anchor: a // detector anchored by the compliance token can only ever examine gates that // already comply, and can never report a violation. + battery('(10) NON-CIRCULARITY. The authority token must never be its own anchor: a'); const tokenOnly = `const m = 'add an entry to the ledger. ${RATCHET_AUTHORITY_MARKER}, not a co-equal option.';`; const tokenOffers = findOffers(tokenOnly); if (tokenOffers.length === 0) { @@ -1129,23 +1201,27 @@ function selfTest() { } // (11) Refusal, BOUND shape — check-adr-links.mjs / check-driver-memory-census.mjs. + battery('(11) Refusal, BOUND shape — check-adr-links.mjs / check-driver-memory-census.mjs.'); expect('refusal — a negation bound to the verb is a refusal ("do not add it to …"), the shape ' + 'check-adr-links.mjs and check-driver-memory-census.mjs use', offerIsRefused({ context: 'fix the link; do not add it to KNOWN_DEAD_TARGETS to make this green.' })); // (12) Refusal, PREDICATION shape — check-type-source-resolution.mjs / check-test-source-alias.mjs. + battery('(12) Refusal, PREDICATION shape — check-type-source-resolution.mjs / check-test-source-alias.mjs.'); expect('refusal — an act named as subject and denied is a refusal ("widening the registry entry ' + 'is not the fix"), the shape the two registry gates use', offerIsRefused({ context: 'Add the rules to its tsconfig.json — widening the registry entry is not the fix.' })); // (13) Refusal DISCRIMINATES. A marking gate's closing discouragement is not a // refusal; if it were, any gate could shed the token by appending a sentence. + battery('(13) Refusal DISCRIMINATES. A marking gate\'s closing discouragement is not a'); expect('refusal — a marking gate\'s closing discouragement is NOT a refusal ("do not take this ' + 'path to get CI green" negates `take`, not the expanding act)', !offerIsRefused({ context: 'add a MEASURED entry to the baseline saying why not — do not take this path to get CI green.' })); // (14) End-to-end: an anchored, unrefused, unmarked offer is a VIOLATION. This // is the assertion that proves the gate can fail at all. + battery('(14) End-to-end: an anchored, unrefused, unmarked offer is a VIOLATION. This'); const violation = `const m = 'Fix it properly. Or ${ADD} a MEASURED entry to the baseline saying why not. That baseline is shrink-only.';`; expect('end-to-end — an anchored, unrefused offer with no authority token classifies as UNMARKED ' + '(proves the gate discriminates rather than approving every script it reads)', @@ -1153,6 +1229,7 @@ function selfTest() { // (15) …and the same text carrying the token classifies as MARKED. Paired with // (14) by construction: exactly one of the two can fire on a broken predicate. + battery('(15) …and the same text carrying the token classifies as MARKED. Paired with'); expect('end-to-end — the same offer carrying the authority token classifies as MARKED', classify(`const T = '${RATCHET_AUTHORITY_MARKER}';\n${violation}`).verdict === 'marked'); @@ -1160,6 +1237,7 @@ function selfTest() { // the sweep must still REACH every instance the control names. This is the // assertion that fails when the detector goes blind — the failure mode that a // control-less detector reports as a clean run. + battery('(16) The corpus-scale positive control, asserted here as well as in the run:'); const results = sweep(); const unreached = Object.entries(CONTROL) .filter(([, d]) => d.expect === 'marked' || d.expect === 'refused') @@ -1172,6 +1250,7 @@ function selfTest() { // (18) Compliance is carried in AUTHOR-FACING text, not in commentary. Found by // reverse verification: stripping the token from a real gate left this detector // green, because that gate's header mentions the token in a comment. + battery('(18) Compliance is carried in AUTHOR-FACING text, not in commentary. Found by'); const commentaryOnly = `// this gate marks the path ${RATCHET_AUTHORITY_MARKER} per #8435\n` + `const m = 'Fix it properly. Or ${ADD} a MEASURED entry to the baseline saying why not. That baseline is shrink-only.';`; expect('compliance — a gate that only MENTIONS the token in a comment does not count as carrying ' @@ -1181,6 +1260,7 @@ function selfTest() { // (19) …and the mirror: the token in a string literal DOES count. Paired with // (18) by construction, so exactly one of the two can fire on a broken check. + battery('(19) …and the mirror: the token in a string literal DOES count. Paired with'); const inLiteral = `const T = '${RATCHET_AUTHORITY_MARKER}';\n` + `const m = 'Fix it properly. Or ${ADD} a MEASURED entry to the baseline saying why not. That baseline is shrink-only.';`; expect('compliance — the token declared as a string literal DOES count as carrying it, which is ' @@ -1188,6 +1268,7 @@ function selfTest() { classify(inLiteral).verdict === 'marked'); // (17) This gate must not be an instance of its own convention. + battery('(17) This gate must not be an instance of its own convention.'); expect('self-classification — this gate is NOT an instance of the convention it enforces (its ' + 'control is a declaration registry, and its offer-shaped quotes live in comments)', results.get(SELF_FILE) !== undefined && results.get(SELF_FILE).verdict === 'excluded'); @@ -1207,6 +1288,7 @@ function selfTest() { // "it names the root" is true of `scripts/**`, which is the FALSE spelling // here. What is asserted is that the hints are a function of the two constants // the walk is a function of — move the read and this reds, in this file. + battery('(20) The declared population, held to the walk in BOTH directions (#13813)'); const CORPUS_ROOT = relative(REPO_ROOT, SCRIPTS_DIR).split('\\').join('/'); expect('declaration — one hint per admitted extension, each the flat-directory glob under the ' + `very root the walk reads from (declared: ${JSON.stringify(ROOT_DIR_WATCH_HINTS)}, root: ` @@ -1265,6 +1347,52 @@ function selfTest() { + 'nothing, and a dead declaration prints as the same silence as declaring nothing)', !ROOT_DIR_WATCH_HINTS.some((h) => h.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) => { + 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) { for (const f of failures) console.error(` x self-test: ${f}`); console.error(`\ncheck-ratchet-remedy-authority --self-test: ${failures.length} failure(s).\n`); diff --git a/scripts/check-required-contexts.mjs b/scripts/check-required-contexts.mjs index cfaae92c85..fb53bd743a 100644 --- a/scripts/check-required-contexts.mjs +++ b/scripts/check-required-contexts.mjs @@ -191,6 +191,56 @@ import { fileURLToPath } from 'node:url'; import { invokes, shellCommands } from './check-shard-attestation.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 checked-in state is green': 2, + '(3) THE PIN: reverse verification, one per workflow file': 7, + '(2) the job disappearing entirely': 2, + '(4) growing a matrix on a registered job': 2, + '(5) continue-on-error': 4, + '(6) the merge_group trigger': 3, + '(7) a path-filtered pull_request trigger': 4, + '(7b) a `types:` list that drops a GitHub default': 6, + '(9) the shadowing collision, on the live specimen': 3, + '(8) a registry that lists one context twice': 1, + '(10) a `carries` string that embeds a step count': 3, + 'missing input is a failure, never a pass (#4690)': 6, + 'the `on:` key under both YAML schemas': 3, + 'instruction surfaces (#9491): the stale-name scan': 28, + 'the dispatch-gates declaration (#9979)': 6, + 'the wiring: this gate must actually run on every PR': 28, + 'the live mode stays OFF the required path': 3, + '…and the standing caller it DOES have (#9678)': 7, + 'the recognizer itself, in BOTH directions': 10, + '…and the NARROW recognizer beside it, in both directions': 6, + 'is the pin WIRED into the required job?': 3, + 'the wiring recognizer, in BOTH directions': 13, +}); + +// DELETING an entry silences that battery's floor exactly as effectively as +// zeroing it, so the roster's own size is pinned too. +const SELF_TEST_BATTERY_FLOOR = 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)'; + /** * The branch-protection-required check contexts, as this repository declares * them. Each entry is `` + `` → the exact check-run name. @@ -1583,9 +1633,24 @@ async function main() { const SELF_TEST_VERDICT = 'check-required-contexts 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 = []; let checked = 0; const assert = (condition, description) => { + registerCase(); checked += 1; if (!condition) failures.push(description); }; @@ -1620,6 +1685,7 @@ async function selfTest() { }; // ── the checked-in state is green ───────────────────────────────────────── + battery('the checked-in state is green'); const baseline = await scanWorkflows(root); assert(baseline.problems.length === 0, `the checked-in workflows pass the pin — got ${JSON.stringify(baseline.problems)}`); assert(baseline.pinned.length === REQUIRED_CONTEXTS.length, `every registered context is reached and pinned (${baseline.pinned.length}/${REQUIRED_CONTEXTS.length})`); @@ -1633,6 +1699,7 @@ async function selfTest() { // this fixture doubles as the regression test for that rename: reverting // lint.yml alone, without this registry, is exactly the half-landed state // the #9325 sequencing exists to prevent, and it must be red. + battery('(3) THE PIN: reverse verification, one per workflow file'); const renamedGateJob = fixture('rename the gate-family job back to ESLint', 'lint.yml', (s) => s.replace(' name: Lint & Repo Gates\n', ' name: ESLint\n'), ); @@ -1663,6 +1730,7 @@ async function selfTest() { // Re-pointed from `console-pin` to `temporal-conformance` when #9533 dropped // the console-pin row: a fixture must mutate a job the registry still // REGISTERS, or it asserts nothing while reading exactly as before. + battery('(2) the job disappearing entirely'); const droppedJob = fixture('drop the temporal-conformance job', 'ci.yml', (s) => s.replace('\n temporal-conformance:\n', '\n temporal-conformance-disabled:\n'), ); @@ -1673,6 +1741,7 @@ async function selfTest() { // ── (4) growing a matrix on a registered job ────────────────────────────── // Re-pointed from `build-docs` for the same reason as (2) above. + battery('(4) growing a matrix on a registered job'); const matrixed = fixture('matrix on build-core', 'ci.yml', (s) => s.replace(' build-core:\n name: Build Core\n', ' build-core:\n name: Build Core\n strategy:\n matrix:\n shard: [1, 2]\n'), ); @@ -1682,6 +1751,7 @@ async function selfTest() { ); // ── (5) continue-on-error ──────────────────────────────────────────────── + battery('(5) continue-on-error'); const soft = fixture('continue-on-error on typecheck', 'lint.yml', (s) => s.replace(' typecheck:\n name: TypeScript Type Check\n', ' typecheck:\n name: TypeScript Type Check\n continue-on-error: true\n'), ); @@ -1696,6 +1766,7 @@ async function selfTest() { assert(softFalse.problems.length === 0, `continue-on-error: false ⇒ green — got ${JSON.stringify(softFalse.problems)}`); // ── (6) the merge_group trigger ────────────────────────────────────────── + battery('(6) the merge_group trigger'); const noQueue = fixture('drop merge_group from lint.yml', 'lint.yml', (s) => s.replace('\n merge_group:\n', '\n')); assert( noQueue.problems.some((p) => p.includes('merge_group') && p.includes('Lint & Repo Gates') && p.includes('TypeScript Type Check')), @@ -1707,6 +1778,7 @@ async function selfTest() { ); // ── (7) a path-filtered pull_request trigger ───────────────────────────── + battery('(7) a path-filtered pull_request trigger'); const pathFiltered = fixture('paths: on ci.yml', 'ci.yml', (s) => s.replace(' pull_request:\n branches:\n - main\n', " pull_request:\n branches:\n - main\n paths:\n - 'packages/**'\n"), ); @@ -1726,6 +1798,7 @@ async function selfTest() { // is the same permanent-pending wedge as a `paths:` filter (#8304). The // no-`types:`-at-all ⇒ green half is the checked-in baseline itself, // asserted green at the top of this self-test. + battery('(7b) a `types:` list that drops a GitHub default'); const droppedReopened = fixture('grow a types: list that omits reopened onto ci.yml', 'ci.yml', (s) => s.replace(' pull_request:\n branches:\n - main\n', ' pull_request:\n types: [opened, synchronize]\n branches:\n - main\n'), ); @@ -1753,6 +1826,7 @@ async function selfTest() { // and its aggregate gate is named `Test Core`. Dropping the suffix makes two // jobs publish one context, and the surviving conclusion is whichever // finished last — a shard could satisfy the aggregate's required gate. + battery('(9) the shadowing collision, on the live specimen'); const collided = fixture('collide the shard name with the gate name', 'ci.yml', (s) => s.replace('name: Test Core (${{ matrix.shard }}/6)', 'name: Test Core'), ); @@ -1768,6 +1842,7 @@ async function selfTest() { ); // ── (8) a registry that lists one context twice ────────────────────────── + battery('(8) a registry that lists one context twice'); const doubled = judge({ registry: [...REQUIRED_CONTEXTS, { workflow: 'ci.yml', job: 'build-docs', context: 'Build Core' }], workflows: new Map(Object.entries(sources).map(([f, text]) => [f, { doc: parse(text) }])), @@ -1778,6 +1853,7 @@ async function selfTest() { // The rot mode #9103 recorded, now with an assertion on it. Both spellings // the registry actually used are exercised, since the parenthesised form is // the one a future author is most likely to reintroduce. + battery('(10) a `carries` string that embeds a step count'); const workflowsFor = () => new Map(Object.entries(sources).map(([f, text]) => [f, { doc: parse(text) }])); for (const spelling of ['the whole check:* gate family (25 steps)', 'the type-check family, 33 steps']) { const stale = judge({ @@ -1797,6 +1873,7 @@ async function selfTest() { ); // ── missing input is a failure, never a pass (#4690) ───────────────────── + battery('missing input is a failure, never a pass (#4690)'); assert(judge({ registry: [], workflows: new Map() }).problems.length === 1, 'an empty registry ⇒ red, never a silent tick'); assert( judge({ registry: REQUIRED_CONTEXTS, workflows: new Map() }).problems.some((p) => p.includes('was never read')), @@ -1834,6 +1911,7 @@ async function selfTest() { // Same document, two parser verdicts. Reading only one spelling would make // every trigger assertion vacuous the day the parser's schema changes — and // vacuous means GREEN, which is the direction that never gets noticed. + battery('the `on:` key under both YAML schemas'); const triggerDoc = { push: {}, pull_request: {}, merge_group: null }; assert(triggersOf({ on: triggerDoc }) === triggerDoc, "the YAML 1.2 spelling (string key 'on') is read"); assert(triggersOf({ [true]: triggerDoc }) === triggerDoc, 'the YAML 1.1 spelling (boolean key true) is read'); @@ -1847,6 +1925,7 @@ async function selfTest() { // when this scan landed; a fixture anchored into the line it rewrites would // have let THIS self-test eject that PR from the merge queue — the exact // block-the-sitting failure the budget design exists to avoid. + battery('instruction surfaces (#9491): the stale-name scan'); const CHECKLIST_SURFACE = '.claude/skills/pm-dispatch/references/review-checklist.md'; const surfaceSources = Object.fromEntries( INSTRUCTION_SURFACES.map((s) => [s.file, readFileSync(join(root, s.file), 'utf8')]), @@ -2144,6 +2223,7 @@ async function selfTest() { // shows up only as a dev dispatched on an AGENTS.md card with this gate // absent from the brief — on the surface that carries `mustName` for all six // required contexts. + battery('the dispatch-gates declaration (#9979)'); assert( INSTRUCTION_SURFACES.map((s) => s.file) .filter((f) => !f.includes('/')) @@ -2199,6 +2279,7 @@ async function selfTest() { // unrun — #4690 with a network call attached. Everything below is offline: // synthetic rulesets for each direction, plus one frozen copy of the real // 2026-08-18 reading so the shape this code parses is the shape GitHub sends. + battery('the wiring: this gate must actually run on every PR'); const RULESET_SNAPSHOT = { id: 12119582, name: 'main', @@ -2357,6 +2438,7 @@ async function selfTest() { // reddens on the very PR that carries the repo half of a rename, deadlocking // the two-step. Wiring it is a maintainer decision, and it goes red HERE // first rather than silently in the queue. + battery('the live mode stays OFF the required path'); { for (const [file, text] of Object.entries(sources)) { assert( @@ -2380,6 +2462,7 @@ async function selfTest() { // .github/workflows tree is swept rather than the two files `sources` // carries: a second caller appearing in some third workflow is exactly the // thing the absences above are guarding against, and they cannot see it. + battery('…and the standing caller it DOES have (#9678)'); const workflowDir = join(root, '.github', 'workflows'); // A document this sweep could not READ is not a document with nothing in // it (#4690), and the recognizer now needs a parse to answer at all. So an @@ -2510,6 +2593,7 @@ async function selfTest() { // under test — parse, then lex each `run:`, then look for the flag. One // seam, so the recognizer can be swapped for the old line filter under // reverse verification without touching a single fixture. + battery('the recognizer itself, in BOTH directions'); const wired = (source) => wiresLiveRead(parse(source)); const workflowFixture = (...lines) => lines.join('\n'); const stepFixture = (...runLines) => @@ -2653,6 +2737,7 @@ async function selfTest() { // the two recognizers are pinned TOGETHER here because the point is that // they deliberately disagree. Reading these cases as "one of them is wrong" // is the mistake this block exists to prevent; see `invokesLiveRead`. + battery('…and the NARROW recognizer beside it, in both directions'); const invoked = (source) => invokesLiveRead(parse(source)); // (j′) the measured defect: the patrol's own `echo` about the flag. Spelled @@ -2714,6 +2799,7 @@ async function selfTest() { // `jobs.lint.steps` and asks each step's `run:` whether it INVOKES the // script; the three text shapes that used to satisfy this are catalogued // there and pinned below in both directions. + battery('is the pin WIRED into the required job?'); const lintDoc = parse(sources['lint.yml']); const lintJob = lintDoc?.jobs?.lint; // #4690: "no readable `lint` job" must be a NAMED failure, not a zero that @@ -2749,6 +2835,7 @@ async function selfTest() { // self-test gets is loosened. So every "prose" case below is paired with the // same wiring made REAL and asserted as wiring, and the checked-in lint.yml // is asserted above as the sixth live limb. + battery('the wiring recognizer, in BOTH directions'); const lintFixture = (...jobLines) => ['name: Lint', 'on:', ' push: {}', 'jobs:', ...jobLines].join('\n'); const lintJobFixture = (...stepLines) => lintFixture(' lint:', ' runs-on: ubuntu-latest', ' steps:', ...stepLines); @@ -2866,6 +2953,52 @@ 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) => { + 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-required-contexts --self-test — ${failures.length} failure(s)\n`); for (const failure of failures) console.error(` • ${failure}`); diff --git a/scripts/check-route-envelope.mjs b/scripts/check-route-envelope.mjs index 8fa687373b..0a444a2510 100644 --- a/scripts/check-route-envelope.mjs +++ b/scripts/check-route-envelope.mjs @@ -147,6 +147,61 @@ 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) A `//` inside a string truncated the rest of the line for the regex': 4, + '(2) `c.req.json()` READS a request. The regex version counted it as two': 3, + 'The sibling-`code` dialect (#7035, counted since #7295)': 11, + 'Dispatcher domains': 5, + 'Plugin-mounted Hono routes (#9267)': 13, + 'Surface 4 (#9813): the same grammar under express receivers': 4, + 'The #9389 ruling: an exemption is a CLOSED list': 1, + '(1) A NEW FILE carrying one. Undeclared is an ERROR, never a default — the': 1, + '(2) …and the same body added INSIDE a file that is already ruled exempt.': 2, + '(3) At exactly the ruled count it is green — the exemption does its job.': 1, + '(4) BELOW the count is red too. An enumeration that over-counts describes a': 1, + '(5) Tracked drift and a ruled boundary are exclusive claims about one': 1, + '(6) An exemption with nothing to exempt is the file-level waiver again,': 1, + '(7) NEGATIVE: the ordinary tracked-drift path is untouched by all of the': 1, + 'The vendor-wire state (maintainer ruling 2026-08-21, #10554)': 1, + '(1) Accepted at the pinned count, with the conforming three-part note.': 1, + '(2) REJECTED without a note — the ruling makes the note mandatory.': 1, + '(3) REJECTED with a note that does not name all three parties — free prose': 1, + '(4) REJECTED beside `ratchet` — a conversion that can never happen is not': 1, + '(5) REJECTED beside `exempt` — one body sits on exactly ONE ruled boundary': 1, + '(6) WIDENING carries the authority marker (#8435): a second vendor-shaped': 2, + '(7) BELOW the count is red in the shrink direction, and shrinking needs': 1, + '(8) A vendor-wire declaration over NOTHING is the standing-waiver shape,': 1, + '(9) NEGATIVE: the #9389 exempt diagnostics are untouched by the new state —': 1, + 'The read/write discriminator (#9937) — BOTH directions': 8, + 'The walk (#9937): reproduce before believing': 8, + 'The declared-vs-discovered correspondence (#11920)': 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 = 27; + +// 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)), '..'); /** @@ -2100,7 +2155,22 @@ function audit() { const SELF_TEST_VERDICT = 'check-route-envelope self-test reached its verdict'; function selfTest() { - const assert = (cond, msg) => { if (!cond) { console.error('✗ self-test: ' + msg); process.exit(1); } }; + // 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('(1) A `//` inside a string truncated the rest of the line for the regex'); + + const assert = (cond, msg) => { registerCase(); if (!cond) { console.error('✗ self-test: ' + msg); process.exit(1); } }; const sound = ` function sendError(res, s, code, message) { res.status(s).json({ success: false, error: { code, message } }); } @@ -2144,6 +2214,7 @@ function selfTest() { // (2) `c.req.json()` READS a request. The regex version counted it as two // unenveloped responses in hmr-routes.ts. + battery('(2) `c.req.json()` READS a request. The regex version counted it as two'); r = scanSource(`const body = await c.req.json(); const b2 = await req.json();`); assert(r.responses === 0, `request reads must not count as responses → ${JSON.stringify(r)}`); @@ -2162,6 +2233,7 @@ function selfTest() { // ── The sibling-`code` dialect (#7035, counted since #7295) ─────────────── // A top-level `code` NEXT TO `error` instead of inside it. Same failure as // the bare string one key over: `body.error.code` reads `undefined`. + battery('The sibling-`code` dialect (#7035, counted since #7295)'); r = scanSource(`res.status(400).json({ error: 'Batch too large', code: 'BATCH_TOO_LARGE' });`); assert( r.siblingCode === 1 && r.stringError === 1, @@ -2221,6 +2293,7 @@ function selfTest() { // ── Dispatcher domains ──────────────────────────────────────────────────── // A domain that answers only through the helpers hand-builds nothing. + battery('Dispatcher domains'); let d = scanDomainSource(` if (m === 'GET') return { handled: true, response: deps.success(rows) }; if (m === 'DELETE') return { handled: true, response: deps.success({ ok: true }) }; @@ -2257,6 +2330,7 @@ function selfTest() { // ── Plugin-mounted Hono routes (#9267) ──────────────────────────────────── // Every case below is a shape measured in the repo when this surface was // added, not an invented one. + battery('Plugin-mounted Hono routes (#9267)'); // The conformant body: nothing to report, and the write site is still seen. let p = scanHonoRouteSource(` @@ -2344,6 +2418,7 @@ function selfTest() { // zero bodies under the default (Hono) receivers and as a bare body under // EXPRESS_RESPONSE_RECEIVERS — which is exactly how dispatcher-plugin.ts's // discovery bodies sat invisible beside a green surface 3. + battery('Surface 4 (#9813): the same grammar under express receivers'); const expressBare = `res.json({ data: await dispatcher.getDiscoveryInfo(prefix) });`; p = scanHonoRouteSource(expressBare, 'x.ts', EXPRESS_RESPONSE_RECEIVERS); assert( @@ -2375,6 +2450,7 @@ function selfTest() { // is declared. So these cases drive `auditPluginRouteModule`, which is why it // is a pure function; asserting only that `scanHonoRouteSource` still sees a // bare body would be pinning the half that was never in doubt. + battery('The #9389 ruling: an exemption is a CLOSED list'); // The shape the ruling covers, so every case below is the real one. const preAuth = `rawApp.get('/bootstrap-status', async (c) => c.json({ hasOwner: true }));`; @@ -2388,6 +2464,7 @@ function selfTest() { // (1) A NEW FILE carrying one. Undeclared is an ERROR, never a default — the // closed list at file granularity. A new pre-auth surface does not inherit the // ruling by resembling the three files it named. + battery('(1) A NEW FILE carrying one. Undeclared is an ERROR, never a default — the'); let probs = auditPluginRouteModule('packages/plugins/plugin-new/src/plugin.ts', undefined, scanOf(preAuth)); assert( probs.length === 1 && probs[0].includes('NOT DECLARED'), @@ -2397,6 +2474,7 @@ function selfTest() { // (2) …and the same body added INSIDE a file that is already ruled exempt. // This is the case a file-level waiver would have passed in silence, and the // reason `exempt` stays counted on this surface. + battery('(2) …and the same body added INSIDE a file that is already ruled exempt.'); probs = auditPluginRouteModule('x.ts', ruled, scanOf(`${preAuth}\n${preAuth}`)); assert( probs.length === 1 && probs[0].includes('CLOSED list'), @@ -2411,12 +2489,14 @@ function selfTest() { ); // (3) At exactly the ruled count it is green — the exemption does its job. + battery('(3) At exactly the ruled count it is green — the exemption does its job.'); probs = auditPluginRouteModule('x.ts', ruled, scanOf(preAuth)); assert(probs.length === 0, `a ruled-exempt module at its pinned count must pass → ${JSON.stringify(probs)}`); // (4) BELOW the count is red too. An enumeration that over-counts describes a // body the file no longer emits, and the reason is the deliverable here — a // reason nobody has to keep true decays into the waiver this is not. + battery('(4) BELOW the count is red too. An enumeration that over-counts describes a'); probs = auditPluginRouteModule('x.ts', ruled, scanOf(`c.json({ success: true, data });`)); assert( probs.length === 1 && probs[0].includes('fewer than pinned'), @@ -2426,6 +2506,7 @@ function selfTest() { // (5) Tracked drift and a ruled boundary are exclusive claims about one // number. The fixture carries a `note` so this asserts the exclusivity rule // alone rather than tripping the ratchet's own note requirement as well. + battery('(5) Tracked drift and a ruled boundary are exclusive claims about one'); probs = auditPluginRouteModule('x.ts', { ...ruled, ratchet: '#9364', note: 'n' }, scanOf(preAuth)); assert( probs.length === 1 && probs[0].includes('exclusive'), @@ -2435,6 +2516,7 @@ function selfTest() { // (6) An exemption with nothing to exempt is the file-level waiver again, // spelled as an empty one: it would sit dormant until the next bare body made // it retroactively cover something nobody ruled on. + battery('(6) An exemption with nothing to exempt is the file-level waiver again,'); probs = auditPluginRouteModule('x.ts', { exempt: 'because' }, scanOf(`c.json({ success: true, data });`)); assert( probs.length === 1 && probs[0].includes('pins no non-conforming body'), @@ -2444,6 +2526,7 @@ function selfTest() { // (7) NEGATIVE: the ordinary tracked-drift path is untouched by all of the // above — a ratchet gaining a body still says "raising the declared number is // not the fix", not the ruling's text. + battery('(7) NEGATIVE: the ordinary tracked-drift path is untouched by all of the'); probs = auditPluginRouteModule('y.ts', { unenveloped: 1, ratchet: '#9364', note: 'n' }, scanOf(`${preAuth}\n${preAuth}`)); assert( probs.length === 1 && probs[0].includes('Raising the declared number is not the fix') && @@ -2459,6 +2542,7 @@ function selfTest() { // the same closed-list property as the #9389 cases above and is driven // through the same pure function; the fixture is the adjudicated body itself // (better-auth's `{ session, user }`, escalated on PR #10352). + battery('The vendor-wire state (maintainer ruling 2026-08-21, #10554)'); const vendorBody = `rawApp.post('/admin/impersonate-user', async (ctx) => ctx.json({ session, user }));`; const vendorRuled = { unenveloped: 1, @@ -2468,10 +2552,12 @@ function selfTest() { assert(scanOf(vendorBody).unenveloped === 1, 'the fixture must be a bare vendor-shaped body'); // (1) Accepted at the pinned count, with the conforming three-part note. + battery('(1) Accepted at the pinned count, with the conforming three-part note.'); probs = auditPluginRouteModule('x.ts', vendorRuled, scanOf(vendorBody)); assert(probs.length === 0, `a vendor-wire module at its pinned count must pass → ${JSON.stringify(probs)}`); // (2) REJECTED without a note — the ruling makes the note mandatory. + battery('(2) REJECTED without a note — the ruling makes the note mandatory.'); probs = auditPluginRouteModule('x.ts', { unenveloped: 1, vendorWire: vendorRuled.vendorWire }, scanOf(vendorBody)); assert( probs.length === 1 && probs[0].includes('no `note`'), @@ -2480,6 +2566,7 @@ function selfTest() { // (3) REJECTED with a note that does not name all three parties — free prose // naming nobody re-checkable is the empty gesture the labels exist to refuse. + battery('(3) REJECTED with a note that does not name all three parties — free prose'); probs = auditPluginRouteModule('x.ts', { ...vendorRuled, note: 'better-auth needs this shape' }, scanOf(vendorBody)); assert( probs.length === 1 && probs[0].includes('all three parties'), @@ -2488,6 +2575,7 @@ function selfTest() { // (4) REJECTED beside `ratchet` — a conversion that can never happen is not // tracked drift, and an entry claiming both claims neither. + battery('(4) REJECTED beside `ratchet` — a conversion that can never happen is not'); probs = auditPluginRouteModule('x.ts', { ...vendorRuled, ratchet: '#9559' }, scanOf(vendorBody)); assert( probs.length === 1 && probs[0].includes('exclusive') && probs[0].includes('vendorWire'), @@ -2496,6 +2584,7 @@ function selfTest() { // (5) REJECTED beside `exempt` — one body sits on exactly ONE ruled boundary // (#9389's pre-auth class and this one were ruled on opposite populations). + battery('(5) REJECTED beside `exempt` — one body sits on exactly ONE ruled boundary'); probs = auditPluginRouteModule('x.ts', { ...vendorRuled, exempt: 'pre-auth' }, scanOf(vendorBody)); assert( probs.length === 1 && probs[0].includes('exclusive') && probs[0].includes('exempt'), @@ -2506,6 +2595,7 @@ function selfTest() { // body on a ruled entry is a decision for the maintainer, never a number to // raise — and the diagnostic must name the const-hoist as the forbidden move // rather than leave the next author to rediscover it as a fix. + battery('(6) WIDENING carries the authority marker (#8435): a second vendor-shaped'); probs = auditPluginRouteModule('x.ts', vendorRuled, scanOf(`${vendorBody}\n${vendorBody}`)); assert( probs.length === 1 && probs[0].includes('CLOSED list') && probs[0].includes(RATCHET_AUTHORITY_MARKER), @@ -2518,6 +2608,7 @@ function selfTest() { // (7) BELOW the count is red in the shrink direction, and shrinking needs // nobody's leave — the marker guards only the widening direction. + battery('(7) BELOW the count is red in the shrink direction, and shrinking needs'); probs = auditPluginRouteModule('x.ts', vendorRuled, scanOf(`c.json({ success: true, data });`)); assert( probs.length === 1 && probs[0].includes('fewer than pinned'), @@ -2526,6 +2617,7 @@ function selfTest() { // (8) A vendor-wire declaration over NOTHING is the standing-waiver shape, // refused the same way an empty exempt is. + battery('(8) A vendor-wire declaration over NOTHING is the standing-waiver shape,'); probs = auditPluginRouteModule( 'x.ts', { vendorWire: vendorRuled.vendorWire, note: vendorRuled.note }, @@ -2538,6 +2630,7 @@ function selfTest() { // (9) NEGATIVE: the #9389 exempt diagnostics are untouched by the new state — // an exempt widening still speaks the pre-auth ruling's text, not this one's. + battery('(9) NEGATIVE: the #9389 exempt diagnostics are untouched by the new state —'); probs = auditPluginRouteModule('x.ts', ruled, scanOf(`${preAuth}\n${preAuth}`)); assert( probs.length === 1 && probs[0].includes('#9389') && !probs[0].includes('vendorWire'), @@ -2553,6 +2646,7 @@ function selfTest() { // reddens on `@objectstack/client`. Both directions, and the reject direction // asserted POSITIVELY — `reads: 1`, not merely `bodies: 0`, which a scanner // that had simply stopped matching the receiver would also produce. + battery('The read/write discriminator (#9937) — BOTH directions'); // ACCEPT — the bare express write. p = scanHonoRouteSource(`res.json({ success: true, data });`, 'x.ts', EXPRESS_RESPONSE_RECEIVERS); @@ -2626,6 +2720,7 @@ function selfTest() { // the real tree, not a fixture. Everything the walk adds beyond that rests on // this: a walk that could not re-find the file somebody had already audited is // not one to trust on the files nobody has. + battery('The walk (#9937): reproduce before believing'); const walked = discoverExpressRoutes(); for (const file of Object.keys(EXPRESS_RESPONSE_MODULES)) { assert( @@ -2687,6 +2782,7 @@ function selfTest() { // declares `plugin-a`. Under exact equality that is two findings at once. // Under a basename credit it is silently zero, each missing finding covering // for the other. + battery('The declared-vs-discovered correspondence (#11920)'); const movedTo = 'packages/plugins/plugin-b/src/thing-routes.ts'; const declaredAt = 'packages/plugins/plugin-a/src/thing-routes.ts'; const moveTable = { [declaredAt]: { responses: 0, ok: 0, err: 0 } }; @@ -2772,6 +2868,53 @@ function selfTest() { 'no real discovered module resolves through declarationFor — the helper is not wired to the live table', ); + // ── 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('✗ self-test: ' + 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.', + ); + process.exit(1); + } + console.log('✓ check-route-envelope self-test passed'); return SELF_TEST_VERDICT; diff --git a/scripts/check-section-landing-index.mjs b/scripts/check-section-landing-index.mjs index 1bc0f5e5f1..00697dcf3c 100644 --- a/scripts/check-section-landing-index.mjs +++ b/scripts/check-section-landing-index.mjs @@ -105,6 +105,46 @@ import { fileURLToPath } from 'node:url'; import { isEntrypoint } from './invoked-as.mjs'; +// ── The self-test's own battery roster and floor (#13489) ────────────────── +// +// `failures.length === 0` used to be this self-test's ONLY success condition, so +// "every case held" and "the cases never ran" printed the same line. Closed the +// way PR #13487 validated on check-doc-authoring: what is pinned is the +// registered NAMES, not a number. Every section opens with `battery('')`, +// every assertion is attributed to the battery most recently opened, and the +// floor requires the OPENED set to equal the DECLARED set with each battery at +// or above its own count. +// +// ⛔ A pinned TOTAL is not the repair: a battery dropping from 9 cases to 3 +// keeps a total "right" the moment a sibling grows. +// +// The counts are a FLOOR, not an equality — adding cases is ordinary work and +// must not red. A battery BELOW its floor means cases stopped running; the +// remedy is to find what stopped registering. +const SELF_TEST_BATTERIES = Object.freeze({ + 'Both shapes, in sync, are silent': 2, + 'Set, direction A: a page in meta.json with no row': 2, + 'Set, direction B: a row for a page meta.json does not declare': 1, + 'Order': 2, + 'The real `ui` defect, reproduced: the shape this gate was written for': 1, + 'Curation survives: out-of-section links are ignored, not counted': 1, + 'A sub-heading does not end the block (the `ui` "### Recipes" shape)': 2, + 'Masking: a fence cannot end the block, and neither fences nor MDX': 3, + 'meta.json shapes: index and group labels are not pages': 1, + 'Opt-in: a landing page with no heading is skipped, not judged': 1, + 'Refusals: none of these may be reported OK': 8, + 'The real run() path, over a temp fixture on disk': 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 = 12; + +// The key an assertion is filed under when no battery is open. It is not a +// declared battery, so it reds by the same set difference rather than silently +// inflating whichever battery happened to run last. +const UNATTRIBUTED_BATTERY = '(no battery open)'; + const HERE = dirname(fileURLToPath(import.meta.url)); const repoRoot = () => join(HERE, '..'); @@ -438,9 +478,24 @@ function main() { const SELF_TEST_VERDICT = 'check-section-landing-index 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 = []; let checked = 0; const assert = (cond, what) => { + registerCase(); checked++; if (!cond) failures.push(what); }; @@ -461,22 +516,26 @@ function selfTest() { `\n\n## Related\n\n- x\n`; // ── Both shapes, in sync, are silent ────────────────────────────────────── + battery('Both shapes, in sync, are silent'); assert(one(section('m', ['a', 'b', 'c'], bullets('m', ['a', 'b', 'c']))).length === 0, 'a synced BULLET list is reported OK'); assert(one(section('m', ['a', 'b', 'c'], cards('m', ['a', 'b', 'c']))).length === 0, 'a synced CARD grid is reported OK'); // ── Set, direction A: a page in meta.json with no row ───────────────────── + battery('Set, direction A: a page in meta.json with no row'); for (const shape of [bullets, cards]) { const p = one(section('m', ['a', 'b', 'c'], shape('m', ['a', 'c']))); assert(p.length === 1 && /omits 1 page\(s\)/.test(p[0]) && /: b\./.test(p[0]), `a MISSING page is named (${shape === bullets ? 'bullets' : 'cards'})`); } // ── Set, direction B: a row for a page meta.json does not declare ───────── + battery('Set, direction B: a row for a page meta.json does not declare'); { const p = one(section('m', ['a', 'b'], bullets('m', ['a', 'b', 'gone']), ['a', 'b', 'gone'])); assert(p.length === 1 && /does not declare/.test(p[0]) && /gone/.test(p[0]), 'an UNDECLARED row is named'); } // ── Order ───────────────────────────────────────────────────────────────── + battery('Order'); { const p = one(section('m', ['a', 'b', 'c'], bullets('m', ['b', 'a', 'c']))); assert(p.length === 1 && /wrong order/.test(p[0]), 'a REORDERED block fails'); @@ -484,6 +543,7 @@ function selfTest() { } // ── The real `ui` defect, reproduced: the shape this gate was written for ── + battery('The real `ui` defect, reproduced: the shape this gate was written for'); { const uiMeta = ['apps', 'pages', 'react-pages', 'views', 'actions', 'dashboards', 'reports', 'translations', 'forms', 'doc-pages', 'setup-app']; const uiHas = ['apps', 'views', 'pages', 'dashboards', 'forms', 'doc-pages', 'setup-app']; @@ -493,12 +553,14 @@ function selfTest() { } // ── Curation survives: out-of-section links are ignored, not counted ────── + battery('Curation survives: out-of-section links are ignored, not counted'); { const text = `# T\n\n## ${INDEX_HEADING}\n\n- [a](/docs/m/a)\n- [b](/docs/m/b)\n- Spec: [x](/docs/protocol/kernel/plugin-spec)\n- Ref: [y](/docs/references/kernel)\n\n## Related\n`; assert(one(section('m', ['a', 'b'], text)).length === 0, 'FOREIGN links are ignored, never counted or ordered'); } // ── A sub-heading does not end the block (the `ui` "### Recipes" shape) ─── + battery('A sub-heading does not end the block (the `ui` "### Recipes" shape)'); { const text = `# T\n\n## ${INDEX_HEADING}\n\n\n \n\n\n### Recipes\n\n\n \n\n\n## Related\n`; assert(one(section('m', ['a', 'b'], text)).length === 0, 'a `###` sub-heading does NOT truncate the block'); @@ -508,6 +570,7 @@ function selfTest() { // ── Masking: a fence cannot end the block, and neither fences nor MDX ───── // comments may satisfy a row. + battery('Masking: a fence cannot end the block, and neither fences nor MDX'); { const fenced = `# T\n\n## ${INDEX_HEADING}\n\n- [a](/docs/m/a)\n\n\`\`\`md\n## Not a heading\n\`\`\`\n\n- [b](/docs/m/b)\n\n## Related\n`; assert(one(section('m', ['a', 'b'], fenced)).length === 0, 'a `## ` line INSIDE a fence does not end the block'); @@ -520,6 +583,7 @@ function selfTest() { } // ── meta.json shapes: index and group labels are not pages ──────────────── + battery('meta.json shapes: index and group labels are not pages'); { const s = { id: 'm', @@ -531,6 +595,7 @@ function selfTest() { } // ── Opt-in: a landing page with no heading is skipped, not judged ───────── + battery('Opt-in: a landing page with no heading is skipped, not judged'); { const r = judge({ sections: [{ id: 'curated', metaText: JSON.stringify({ pages: ['index', 'a', 'b'] }), indexText: `# T\n\n## For Implementers\n\n- [a](/docs/curated/a)\n`, filesOnDisk: ['a', 'b'] }], minSections: 0 }); assert(r.problems.length === 0 && r.skipped.includes('curated') && !r.covered.includes('curated'), @@ -538,6 +603,7 @@ function selfTest() { } // ── Refusals: none of these may be reported OK ──────────────────────────── + battery('Refusals: none of these may be reported OK'); { const empty = one(section('m', ['a'], `# T\n\n## ${INDEX_HEADING}\n\nProse, no links.\n\n## Related\n`)); assert(empty.length === 1 && /links to no/.test(empty[0]) && /#4690/.test(empty[0]), 'an EMPTY index block is refused'); @@ -563,6 +629,7 @@ function selfTest() { } // ── The real run() path, over a temp fixture on disk ────────────────────── + battery('The real run() path, over a temp fixture on disk'); const dir = mkdtempSync(join(tmpdir(), 'section-landing-')); try { const mk = (id, pages, indexText) => { @@ -591,6 +658,52 @@ function selfTest() { rmSync(dir, { recursive: true, force: true }); } + // ── The floor: every declared battery RAN, and ran its cases (#13489) ─── + // + // Evaluated after every battery has had its chance and BEFORE the verdict, so + // the success line below can only be printed by a run in which the set of + // batteries that registered assertions EQUALS the set declared. A set + // difference names WHICH battery stopped; a count says only that something did. + const floorFailure = (message) => { + failures.push(message); + }; + const declaredBatteries = Object.keys(SELF_TEST_BATTERIES); + let floorBreached = false; + if (declaredBatteries.length < SELF_TEST_BATTERY_FLOOR) { + floorBreached = true; + floorFailure( + `SELF_TEST_BATTERIES declares ${declaredBatteries.length} batteries, below the pinned ` + + `${SELF_TEST_BATTERY_FLOOR} — a battery deleted from the roster takes its own floor with it.`, + ); + } + for (const [name, count] of 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-section-landing-index --self-test -- ${failures.length} failure(s)\n`); for (const f of failures) console.error(` • ${f}`); diff --git a/scripts/check-stall-guard-budget.mjs b/scripts/check-stall-guard-budget.mjs index 802b17d0bc..ae55686316 100644 --- a/scripts/check-stall-guard-budget.mjs +++ b/scripts/check-stall-guard-budget.mjs @@ -204,6 +204,44 @@ import { maskComments } from './js-comment-mask.mjs'; import { commandWords, shellCommands } from './check-shard-attestation.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 shape the repo ships: W=10 C=20 T=30, slack exactly one window ─': 4, + '2. TIER 1: a cap at or above the budget is a guaranteed no-op': 3, + '3. TIER 2: the two one-line edits the card named': 5, + '4. An explicit cap is honoured, and it can RESCUE a short job': 2, + '5. The defaults are READ from the guard, not hardcoded here': 6, + '6. Budget resolution: step-level, missing, and unusable': 7, + '7. The selector: what must NOT count as a site': 7, + '8. Line numbers: attached when they can be trusted, never guessed': 1, + '9. Siblings on one job clock: what the census must and must NOT say': 13, + '10. The real repository -- the direction the fixtures cannot prove': 13, +}); + +// DELETING an entry silences that battery's floor exactly as effectively as +// zeroing it, so the roster's own size is pinned too. +const SELF_TEST_BATTERY_FLOOR = 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)'; + /** Refusal to measure, kept distinct from a finding (see check-agent-test-spelling). */ export const EXIT_REFUSED = 2; @@ -656,10 +694,25 @@ export function run(root, parseYaml, 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 { parse } = await requireDependency('yaml', () => import('yaml'), import.meta.url); const failures = []; let checked = 0; const assert = (name, ok, detail) => { + registerCase(); checked += 1; if (!ok) failures.push(detail ? `${name} -- ${detail}` : name); }; @@ -729,6 +782,7 @@ export async function selfTest() { try { // ── 1. The shape the repo ships: W=10 C=20 T=30, slack exactly one window ─ + battery('1. The shape the repo ships: W=10 C=20 T=30, slack exactly one window ─'); const green = fixture({ 'a.yml': workflow({ jobTimeout: 30, command: GUARDED }) }); const greenRun = drive(green); assert('the ci.yml shape (W=10, C=20, T=30) passes', greenRun.code === 0, greenRun.out); @@ -741,6 +795,7 @@ export async function selfTest() { assert('...with the cap DERIVED, not read from the command line', greenScan.sites[0].cap === 20 && greenScan.sites[0].capSource.includes('DEFAULT_CAP_MULTIPLE'), JSON.stringify(greenScan.sites[0])); // ── 2. TIER 1: a cap at or above the budget is a guaranteed no-op ──────── + battery('2. TIER 1: a cap at or above the budget is a guaranteed no-op'); const over = fixture({ 'a.yml': workflow({ jobTimeout: 30, command: 'node scripts/run-with-stall-guard.mjs --log x --stall-minutes 10 --stall-cap-minutes 40 -- pnpm test' }), }); @@ -754,6 +809,7 @@ export async function selfTest() { assert('a cap EQUAL to the job budget is red (the boundary is not below)', drive(equal).code === 1); // ── 3. TIER 2: the two one-line edits the card named ───────────────────── + battery('3. TIER 2: the two one-line edits the card named'); const tightened = fixture({ 'a.yml': workflow({ jobTimeout: 25, command: GUARDED }) }); const tightenedRun = drive(tightened); assert('lowering the JOB timeout 30 -> 25 under an unchanged guard is red', tightenedRun.code === 1, tightenedRun.out); @@ -769,6 +825,7 @@ export async function selfTest() { assert('a guard-wrapped step added to a SHORT job is red', drive(shortJob).code === 1); // ── 4. An explicit cap is honoured, and it can RESCUE a short job ──────── + battery('4. An explicit cap is honoured, and it can RESCUE a short job'); const explicit = fixture({ 'a.yml': workflow({ jobTimeout: 25, command: 'node scripts/run-with-stall-guard.mjs --log x --stall-minutes 10 --stall-cap-minutes 12 -- pnpm test' }), }); @@ -781,6 +838,7 @@ export async function selfTest() { // Same workflow as case 1 -- green under the repo's real DEFAULT_CAP_MULTIPLE // of 2, red under a guard declaring 3. A gate carrying its own copy of `2` // passes both, which is the drift this gate must not commit. + battery('5. The defaults are READ from the guard, not hardcoded here'); const otherMultiple = fixture({ 'a.yml': workflow({ jobTimeout: 30, command: GUARDED }) }, { capMultiple: 3 }); const otherRun = drive(otherMultiple); assert('the cap multiple is read from the guard: multiple 3 turns the same workflow red', otherRun.code === 1, otherRun.out); @@ -802,6 +860,7 @@ export async function selfTest() { assert('...and both missing declarations are named', /DEFAULT_CAP_MULTIPLE/.test(renamedRun.out) && /stallMinutes/.test(renamedRun.out), renamedRun.out); // ── 6. Budget resolution: step-level, missing, and unusable ────────────── + battery('6. Budget resolution: step-level, missing, and unusable'); const stepBound = fixture({ 'a.yml': workflow({ jobTimeout: 120, stepTimeout: 25, command: GUARDED }) }); const stepBoundRun = drive(stepBound); assert('a step-level timeout-minutes BINDS when it is tighter than the job\'s', stepBoundRun.code === 1, stepBoundRun.out); @@ -830,6 +889,7 @@ export async function selfTest() { assert('a window that is not a number REFUSES instead of guessing', unresolvableRun.code === EXIT_REFUSED, unresolvableRun.out); // ── 7. The selector: what must NOT count as a site ─────────────────────── + battery('7. The selector: what must NOT count as a site'); const commented = fixture({ 'a.yml': `name: fixture\non: push\njobs:\n probe:\n timeout-minutes: 5\n runs-on: ubuntu-latest\n steps:\n` + @@ -862,6 +922,7 @@ export async function selfTest() { assert('a missing guard script REFUSES', drive(noGuardFile).code === EXIT_REFUSED); // ── 8. Line numbers: attached when they can be trusted, never guessed ──── + battery('8. Line numbers: attached when they can be trusted, never guessed'); assert('a site carries the line its command sits on', greenScan.sites[0].line === 10, JSON.stringify(greenScan.sites[0])); // ── 9. Siblings on one job clock: what the census must and must NOT say ── @@ -871,6 +932,7 @@ export async function selfTest() { // importantly, pin the two ways of deriving it that are WRONG. Both wrong // ways are green against a single-sibling tree, so only fixtures shaped // like the mistake can hold them. + battery('9. Siblings on one job clock: what the census must and must NOT say'); // The positive case: two guarded steps, one job, one clock. const pair = fixture({ @@ -948,6 +1010,7 @@ export async function selfTest() { assert('...and the middle one is 2 of 3', /\(2 of 3 guarded steps in this job/.test(trioCensus[1]), trioCensus[1]); // ── 10. The real repository -- the direction the fixtures cannot prove ─── + battery('10. The real repository -- the direction the fixtures cannot prove'); const realDefaults = guardDefaults(repoRoot()); assert('the real guard still declares both defaults', Boolean(realDefaults.defaults), JSON.stringify(realDefaults.problems)); if (realDefaults.defaults) { @@ -998,6 +1061,52 @@ export async function selfTest() { for (const dir of roots) rmSync(dir, { recursive: true, force: true }); } + // ── The floor: every declared battery RAN, and ran its cases (#13489) ─── + // + // Evaluated after every battery has had its chance and BEFORE the verdict, so + // the success line below can only be printed by a run in which the set of + // batteries that registered assertions EQUALS the set declared. A set + // difference names WHICH battery stopped; a count says only that something did. + const floorFailure = (message) => { + failures.push(message); + }; + const declaredBatteries = Object.keys(SELF_TEST_BATTERIES); + let floorBreached = false; + if (declaredBatteries.length < SELF_TEST_BATTERY_FLOOR) { + floorBreached = true; + floorFailure( + `SELF_TEST_BATTERIES declares ${declaredBatteries.length} batteries, below the pinned ` + + `${SELF_TEST_BATTERY_FLOOR} — a battery deleted from the roster takes its own floor with it.`, + ); + } + for (const [name, count] of 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-stall-guard-budget --self-test -- ${failures.length} of ${checked} assertion(s) failed\n`); for (const f of failures) console.error(` • ${f}`); diff --git a/scripts/check-system-context-census.mjs b/scripts/check-system-context-census.mjs index 8c2c4e5c16..873e073139 100644 --- a/scripts/check-system-context-census.mjs +++ b/scripts/check-system-context-census.mjs @@ -167,6 +167,51 @@ import { isEntrypoint } from './invoked-as.mjs'; import { CORPUS_ROOTS, runCensus, siteKeys } from './isystem-census.mjs'; import { extractLineAnchors, extractPathCitations, resolveAnchorFile } from './doc-line-anchors.mjs'; +// ── The self-test's own battery roster and floor (#13489) ────────────────── +// +// `failures.length === 0` used to be this self-test's ONLY success condition, so +// "every case held" and "the cases never ran" printed the same line. Closed the +// way PR #13487 validated on check-doc-authoring: what is pinned is the +// registered NAMES, not a number. Every section opens with `battery('')`, +// every assertion is attributed to the battery most recently opened, and the +// floor requires the OPENED set to equal the DECLARED set with each battery at +// or above its own count. +// +// ⛔ A pinned TOTAL is not the repair: a battery dropping from 9 cases to 3 +// keeps a total "right" the moment a sibling grows. +// +// The counts are a FLOOR, not an equality — adding cases is ordinary work and +// must not red. A battery BELOW its floor means cases stopped running; the +// remedy is to find what stopped registering. +const SELF_TEST_BATTERIES = Object.freeze({ + 'the GREEN control: a page that is correct': 2, + '⭐ the RED that matters: a site the page never mentions': 1, + 'the deletion shape: the row stands, the site is gone': 3, + 'resolution': 3, + 'ledger': 3, + 'counts': 6, + '⭐ CRITERION: enforced means CENSUS-DERIVED, pinned over the REAL lists': 2, + 'the same criterion, behaviourally, on one page': 3, + '⛔ and the half that must NOT have moved: the contract still reds': 2, + 'absence is loud': 1, + '--fix': 2, + '⭐ #13490: the incident shape -- reads AND ledger citations BOTH shift': 2, + '⛔ the dangerous direction: the citation crosses onto a read anchor\'s line ─': 1, + '⭐ and the safety property, on the shape that now ACCEPTS': 2, + 'the refusal has to SHOW its work (both counts, both classes, the diff)': 2, + 'WIRING: this gate, and its self-test, really run in CI': 2, + 'POPULATION DECLARATION: what the dispatch derivation is told this gate reads': 6, +}); + +// DELETING an entry silences that battery's floor exactly as effectively as +// zeroing it, so the roster's own size is pinned too. +const SELF_TEST_BATTERY_FLOOR = 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 ROOT = join(fileURLToPath(new URL('.', import.meta.url)), '..'); export const PAGE = 'content/docs/permissions/system-context.mdx'; @@ -1216,8 +1261,23 @@ function fixtureUnenforcedTable({ linesTotal = 6, dropTestsRow = false, dated = 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) failures += 1; process.stdout.write(`${ok ? ' ok ' : ' FAIL'} ${name}${detail ? ` -- ${detail}` : ''}\n`); }; @@ -1234,11 +1294,13 @@ function selfTest() { }); // ── the GREEN control: a page that is correct ─────────────────────────────── + battery('the GREEN control: a page that is correct'); const green = run(fixturePage()); t('green control: a correct page reports nothing', green.problems.length === 0, green.problems.join(' | ')); t('green control: the fenced `pkg/a.ts:999` is not read as an anchor', green.stats.anchors === 2); // ── ⭐ the RED that matters: a site the page never mentions ────────────────── + battery('⭐ the RED that matters: a site the page never mentions'); const arrived = { ...FIXTURE_CENSUS, sites: [...FIXTURE_CENSUS.sites, { file: 'pkg/a.ts', line: 4, receiver: 'ctx', package: 'pkg', text: 'return DENY;' }], @@ -1250,6 +1312,7 @@ function selfTest() { ); // ── the deletion shape: the row stands, the site is gone ──────────────────── + battery('the deletion shape: the row stands, the site is gone'); const deleted = { ...FIXTURE_CENSUS, sites: [] }; const gone = run(fixturePage(), deleted); t('POPULATION: an empty census refuses rather than passing', gone.problems.some((p) => p.startsWith('[empty-census]'))); @@ -1269,6 +1332,7 @@ function selfTest() { t('ROT: a shifted anchor is caught from both sides', rotted.problems.length === 2, rotted.problems.join(' | ')); // ── resolution ───────────────────────────────────────────────────────────── + battery('resolution'); const ambiguous = run(fixturePage({ anchor: 'a.ts:2' })); t('RESOLUTION: a bare basename matching two files is refused', ambiguous.problems.some((p) => p.startsWith('[ambiguous-anchor]'))); const gonefile = run(fixturePage({ anchor: 'pkg/nope.ts:2' })); @@ -1277,6 +1341,7 @@ function selfTest() { t('RESOLUTION: a line past end of file is refused', overrun.problems.some((p) => p.startsWith('[out-of-range-anchor]'))); // ── ledger ───────────────────────────────────────────────────────────────── + battery('ledger'); const ledgerStale = evaluate({ pageText: fixturePage(), census: FIXTURE_CENSUS, @@ -1309,6 +1374,7 @@ function selfTest() { t('LEDGER: a row no anchor uses is a finding', ledgerUnused.problems.some((p) => p.startsWith('[ledger-row-unused]'))); // ── counts ───────────────────────────────────────────────────────────────── + battery('counts'); const countPage = fixturePage() + '\nit is a single boolean read at **1\ndistinct sites across 1 packages**.\n'; const countsOk = run(countPage, FIXTURE_CENSUS, FIXTURE_COUNTS); @@ -1371,6 +1437,7 @@ function selfTest() { // THEMSELVES, not over a fixture stand-in. That is the point: move a text count // back into the enforced list and the first case names it by id. A criterion // change with nothing watching it is how the next reader undoes it. + battery('⭐ CRITERION: enforced means CENSUS-DERIVED, pinned over the REAL lists'); const textDrifted = { ...FIXTURE_CENSUS, roleCounts: { ...FIXTURE_CENSUS.roleCounts, key: FIXTURE_CENSUS.roleCounts.key + 1 }, @@ -1395,6 +1462,7 @@ function selfTest() { ); // ── the same criterion, behaviourally, on one page ────────────────────────── + battery('the same criterion, behaviourally, on one page'); const countSentence = '\nit is a single boolean read at **1\ndistinct sites across 1 packages**.\n'; const okPage = fixturePage() + countSentence; const staleText = run( @@ -1432,6 +1500,7 @@ function selfTest() { ); // ── ⛔ and the half that must NOT have moved: the contract still reds ──────── + battery('⛔ and the half that must NOT have moved: the contract still reds'); const rottedToo = run( fixturePage({ anchor: 'pkg/a.ts:4' }) + countSentence + fixtureUnenforcedTable({ linesTotal: 999 }), FIXTURE_CENSUS, @@ -1458,10 +1527,12 @@ function selfTest() { ); // ── absence is loud ──────────────────────────────────────────────────────── + battery('absence is loud'); const noAnchors = run('---\ntitle: x\n---\n\nnothing here.\n'); t('ABSENCE: a page with no anchors refuses', noAnchors.problems.some((p) => p.startsWith('[no-anchors]'))); // ── --fix ────────────────────────────────────────────────────────────────── + battery('--fix'); const fixed = fixAnchors({ pageText: fixturePage({ anchor: 'pkg/a.ts:4' }), census: FIXTURE_CENSUS, @@ -1492,6 +1563,7 @@ function selfTest() { // `security-plugin.ts` (7 reads + 1 citation, all +20/+19, zero `isSystem` lines // added or removed) refused with "page anchors 8 distinct read line(s), census // finds 7", and `rest-server.ts` (6 + 2) with "7 ... finds 6". + battery('⭐ #13490: the incident shape -- reads AND ledger citations BOTH shift'); const bothShifted = fixAnchors({ pageText: fixturePage({ anchor: 'pkg/a.ts:1', helper: 'pkg/a.ts:6' }), census: FIXTURE_CENSUS, @@ -1533,6 +1605,7 @@ function selfTest() { // surviving anchor was mapped onto the read site -- leaving the page GREEN with // the two rows pointing at each other's lines. Both spellings survive either // way, so this case asserts which ROW holds which line. + battery('⛔ the dangerous direction: the citation crosses onto a read anchor\'s line ─'); const crossed = fixAnchors({ pageText: crossingPage({ read: 'pkg/b.ts:3', helper: 'pkg/b.ts:2' }), census: CROSSING_CENSUS, @@ -1553,6 +1626,7 @@ function selfTest() { // ⛔ The fix must not buy acceptance with the refusal. A read site ARRIVES while // the ledger citation shifts: the old counting arm and the new one both refuse // here, and that must stay true, or #13490 was closed by deleting the guard. + battery('⭐ and the safety property, on the shape that now ACCEPTS'); const grewWhileShifting = fixAnchors({ pageText: fixturePage({ anchor: 'pkg/a.ts:2', helper: 'pkg/a.ts:6' }), census: arrived, @@ -1585,6 +1659,7 @@ function selfTest() { // site" when nothing had, and the output gave no way to tell which case you were // in short of running the census in two trees by hand. `already anchored 8 of 9` // + one named target settles it; `0 of 9` says uniform displacement. + battery('the refusal has to SHOW its work (both counts, both classes, the diff)'); const refusalText = grewWhileShifting.refused[0] ?? ''; t( 'FIX #13490: the refusal states BOTH counts it compared and the ledger it set aside', @@ -1618,6 +1693,7 @@ function selfTest() { // `check-aggregator-roster` and `check-ci-filter-parity` set -- and, like the second // docs root that gate added, this needed NO workflow edit: `lint.yml` already invokes // both legs, and it is the repo's busiest file. + battery('WIRING: this gate, and its self-test, really run in CI'); const SELF = 'scripts/check-system-context-census.mjs'; let lintYml = null; try { @@ -1642,6 +1718,7 @@ function selfTest() { // the exact round this declaration was added to end. Both directions are derived // from `CORPUS_ROOTS`, never re-spelled: a corpus root added or dropped there has // to move this declaration or fail here. + battery('POPULATION DECLARATION: what the dispatch derivation is told this gate reads'); const declaredRoots = ROOT_DIR_WATCH_HINTS.map((h) => h.replace(/\/\*+$/, '')); t( 'POPULATION DECLARATION: every root the census walks is declared', @@ -1697,6 +1774,53 @@ function selfTest() { : `\ncheck-system-context-census --self-test: ${failures} case(s) FAILED\n` ); selfTestReachedVerdict = 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(` FAIL ${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.', + ); + } + return failures === 0 ? 0 : 1; } diff --git a/scripts/check-undeclared-dep-imports.mjs b/scripts/check-undeclared-dep-imports.mjs index cee9f445b8..ec42db46be 100644 --- a/scripts/check-undeclared-dep-imports.mjs +++ b/scripts/check-undeclared-dep-imports.mjs @@ -216,6 +216,43 @@ import { fileURLToPath } from 'node:url'; import { isEntrypoint } from './invoked-as.mjs'; import { blank, scanSource } 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 detector FIRES, once per import form': 10, + 'the detector STAYS SILENT where it must': 6, + 'the comment mask, in both directions': 3, + 'tsconfig exclusions are honoured, and only where declared': 2, + 'refusals': 8, + 'provenance: the record must stay reproducible, and visibly so': 8, + 'ledger reconciliation, in both directions': 10, + 'the declared watch hints stay equal to the real population': 2, + 'POSITIVE CONTROL on the real tree': 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 = 9; + +// The key an assertion is filed under when no battery is open. It is not a +// declared battery, so it reds by the same set difference rather than silently +// inflating whichever battery happened to run last. +const UNATTRIBUTED_BATTERY = '(no battery open)'; + const HERE = dirname(fileURLToPath(import.meta.url)); const REPO_ROOT = resolve(HERE, '..'); @@ -717,8 +754,22 @@ function makeTree(root, { manifest, files, workspace }) { 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) => { if (!ok) { failures += 1; console.error(` FAIL ${name}`); } else console.log(` ok ${name}`); }; + const t = (name, ok) => { registerCase(); if (!ok) { failures += 1; console.error(` FAIL ${name}`); } else console.log(` ok ${name}`); }; const tmp = mkdtempSync(join(tmpdir(), 'undeclared-dep-imports-')); const run = (spec) => { @@ -730,6 +781,7 @@ function selfTest() { try { // ── the detector FIRES, once per import form ──────────────────────────── + battery('the detector FIRES, once per import form'); const staticImport = run({ manifest: baseManifest, files: { 'src/a.ts': "import { x } from '@objectstack/undeclared';\n" } }); t('static value import of an undeclared package is a finding', staticImport.findings.length === 1 && staticImport.findings[0].dep === '@objectstack/undeclared' @@ -774,6 +826,7 @@ function selfTest() { devOnly.findings.length === 1 && devOnly.findings[0].devDeclared === true); // ── the detector STAYS SILENT where it must ───────────────────────────── + battery('the detector STAYS SILENT where it must'); const declared = run({ manifest: { ...baseManifest, dependencies: { '@objectstack/dep': 'workspace:*' } }, files: { 'src/a.ts': "import { x } from '@objectstack/dep';\n" }, @@ -812,6 +865,7 @@ function selfTest() { t('only `src/**` is read', outsideSrc.findings.length === 0); // ── the comment mask, in both directions ─────────────────────────────── + battery('the comment mask, in both directions'); const prose = run({ manifest: baseManifest, files: { @@ -837,6 +891,7 @@ function selfTest() { assembled.findings.length === 0 && assembled.assembled === 1); // ── tsconfig exclusions are honoured, and only where declared ────────── + battery('tsconfig exclusions are honoured, and only where declared'); const payload = run({ manifest: baseManifest, files: { @@ -858,6 +913,7 @@ function selfTest() { noExclusion.findings.length === 1); // ── refusals ─────────────────────────────────────────────────────────── + battery('refusals'); const emptyRoot = mkdtempSync(join(tmp, 'empty-')); t('REFUSAL — a root with no pnpm-workspace.yaml is fatal, never clean', typeof sweep(emptyRoot).fatal === 'string'); @@ -898,6 +954,7 @@ function selfTest() { // when the RECORD stops being a self-contained, reproducible claim: a ref // that is not a ref, a quotation that restated the ref instead of reading // it, or a pass line that stopped showing the reader both censuses. + battery('provenance: the record must stay reproducible, and visibly so'); t('PROVENANCE — the record carries the ref it was measured on, inside the frozen record', typeof MEASURED.ref === 'string' && /^[0-9a-f]{7,40}$/.test(MEASURED.ref) && Object.isFrozen(MEASURED), JSON.stringify(MEASURED.ref)); @@ -943,6 +1000,7 @@ function selfTest() { 'the pass path in main() no longer calls provenanceLine — the record would stop being reconciled in the log'); // ── ledger reconciliation, in both directions ───────────────────────── + battery('ledger reconciliation, in both directions'); const row = { pkg: '@objectstack/p', dep: '@objectstack/d', file: 'packages/p/src/a.ts', kind: 'optional-runtime-probe', why: 'x'.repeat(50) }; const dyn = { pkg: '@objectstack/p', dep: '@objectstack/d', file: 'packages/p/src/a.ts', form: 'dynamic', line: 1, spec: '@objectstack/d', devDeclared: false }; t('LEDGER — a matching dynamic finding is covered by its row', @@ -974,6 +1032,7 @@ function selfTest() { t('LEDGER — the shipped ledger is well formed', ledgerShapeProblems(LEDGER).length === 0); // ── the declared watch hints stay equal to the real population ──────── + battery('the declared watch hints stay equal to the real population'); const declaredGlobs = workspaceGlobs(readFileSync(join(REPO_ROOT, 'pnpm-workspace.yaml'), 'utf8')); const hintRoots = ROOT_DIR_WATCH_HINTS.map((h) => h.replace(/\/\*+$/, '')); t('WATCH HINTS — every `packages:` glob in pnpm-workspace.yaml sits under a declared hint', @@ -986,6 +1045,7 @@ function selfTest() { // A zero from this gate is only a reading if the instrument is seen working // on the tree it actually judges. The sweep must reach the real population // and must extract real specifiers; a matcher that has died reads as clean. + battery('POSITIVE CONTROL on the real tree'); const real = sweep(REPO_ROOT); t('POSITIVE CONTROL — the real sweep reaches its population and extracts specifiers', real.fatal === undefined && real.packages.length >= MIN_PACKAGES @@ -994,6 +1054,53 @@ function selfTest() { rmSync(tmp, { recursive: true, force: true }); } + // ── The floor: every declared battery RAN, and ran its cases (#13489) ─── + // + // Evaluated after every battery has had its chance and BEFORE the verdict, so + // the success line below can only be printed by a run in which the set of + // batteries that registered assertions EQUALS the set declared. A set + // difference names WHICH battery stopped; a count says only that something did. + const floorFailure = (message) => { + failures += 1; + console.error(` FAIL ${message}`); + }; + const declaredBatteries = Object.keys(SELF_TEST_BATTERIES); + let floorBreached = false; + if (declaredBatteries.length < SELF_TEST_BATTERY_FLOOR) { + floorBreached = true; + floorFailure( + `SELF_TEST_BATTERIES declares ${declaredBatteries.length} batteries, below the pinned ` + + `${SELF_TEST_BATTERY_FLOOR} — a battery deleted from the roster takes its own floor with it.`, + ); + } + for (const [name, count] of 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(`\n${failures === 0 ? 'PASS' : 'FAIL'} check-undeclared-dep-imports --self-test (${failures} failure(s))`); selfTestReachedVerdict = true; return failures === 0 ? 0 : 1; diff --git a/scripts/check-vendor-version-stamps.mjs b/scripts/check-vendor-version-stamps.mjs index 241aa513a4..a205b1070a 100644 --- a/scripts/check-vendor-version-stamps.mjs +++ b/scripts/check-vendor-version-stamps.mjs @@ -142,6 +142,40 @@ import { fileURLToPath } from 'node:url'; import { isEntrypoint } from './invoked-as.mjs'; +// ── The self-test's own battery roster and floor (#13489) ────────────────── +// +// `failures.length === 0` used to be this self-test's ONLY success condition, so +// "every case held" and "the cases never ran" printed the same line. Closed the +// way PR #13487 validated on check-doc-authoring: what is pinned is the +// registered NAMES, not a number. Every section opens with `battery('')`, +// every assertion is attributed to the battery most recently opened, and the +// floor requires the OPENED set to equal the DECLARED set with each battery at +// or above its own count. +// +// ⛔ A pinned TOTAL is not the repair: a battery dropping from 9 cases to 3 +// keeps a total "right" the moment a sibling grows. +// +// The counts are a FLOOR, not an equality — adding cases is ordinary work and +// must not red. A battery BELOW its floor means cases stopped running; the +// remedy is to find what stopped registering. +const SELF_TEST_BATTERIES = Object.freeze({ + 'Attribution: the half a naive gate gets wrong': 32, + 'Prose distance: the docs population\'s two buckets': 9, + 'Sentence scope': 6, + 'Nearest claimant wins': 5, + 'The measurement rule is POSITIONAL': 7, + 'Structure': 5, +}); + +// DELETING an entry silences that battery's floor exactly as effectively as +// zeroing it, so the roster's own size is pinned too. +const SELF_TEST_BATTERY_FLOOR = 6; + +// The key an assertion is filed under when no battery is open. It is not a +// declared battery, so it reds by the same set difference rather than silently +// inflating whichever battery happened to run last. +const UNATTRIBUTED_BATTERY = '(no battery open)'; + const REPO_ROOT = fileURLToPath(new URL('..', import.meta.url)); // ── Configuration ─────────────────────────────────────────────────────────── @@ -908,9 +942,25 @@ function collectFiles() { 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); + }; + battery('Attribution: the half a naive gate gets wrong'); + const failures = []; let ran = 0; const check = (name, ok, detail = '') => { + registerCase(); ran++; if (!ok) failures.push(`${name}${detail ? `\n ${detail}` : ''}`); }; @@ -1040,6 +1090,7 @@ function selfTest() { // must be caught and the other must be left alone; a detector that reports // the second is worse than no detector, because "fixing" it turns a true // sentence into a false one. + battery('Prose distance: the docs population\'s two buckets'); const vocab = new Set(['minimatch', 'better-call', '@better-auth/utils']); const sitesOf = (text, opts = {}) => findStampSites(text, family, { vocabulary: vocab, ...opts }); @@ -1113,6 +1164,7 @@ function selfTest() { JSON.stringify(sitesOf(twoNames))); // ── Sentence scope ─────────────────────────────────────────────────────── + battery('Sentence scope'); const stopped = 'better-auth mounts the route itself. Node 20.11.0 is the floor.'; check('a full stop ends the reach, however close the number is', sitesOf(stopped).every((s) => s.pkg === null), JSON.stringify(sitesOf(stopped))); @@ -1144,6 +1196,7 @@ function selfTest() { JSON.stringify(attributionBoundaries(runaway))); // ── Nearest claimant wins ──────────────────────────────────────────────── + battery('Nearest claimant wins'); const otherSpecifier = '// better-auth 1.7.2 peers an exact `better-call@1.3.7` in the rc line'; check('a `name@version` specifier binds its own version, not a watched name\'s', sitesOf(otherSpecifier).find((s) => s.version === '1.3.7')?.pkg === null, @@ -1170,6 +1223,7 @@ function selfTest() { })()); // ── The measurement rule is POSITIONAL ─────────────────────────────────── + battery('The measurement rule is POSITIONAL'); const measuredSomethingElse = [ '// Measured on the configuration the range *does* govern (better-auth\'s own', '// Kysely dialect: migrations, sign-up, sign-in, adapter find/update/delete),', @@ -1200,6 +1254,7 @@ function selfTest() { })()); // ── Structure ──────────────────────────────────────────────────────────── + battery('Structure'); check('every site carries the offset the positional rules need', sitesOf(attestation).every((s) => typeof s.pos === 'number')); @@ -1217,6 +1272,52 @@ function selfTest() { !/add (?:it|the file|this) to\s+\S*(?:baseline|ledger)/i.test(src), ); + // ── 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(`\ncheck-vendor-version-stamps --self-test: ${failures.length} failure(s).\n`); for (const f of failures) console.error(` ✗ ${f}`); diff --git a/scripts/docs-audit/affected-docs.mjs b/scripts/docs-audit/affected-docs.mjs index 0923d8c83a..965c95f05f 100644 --- a/scripts/docs-audit/affected-docs.mjs +++ b/scripts/docs-audit/affected-docs.mjs @@ -203,6 +203,62 @@ import { join, relative } from 'node:path'; // side-effect-free on import, so the no-install contract this script runs under holds. import { blank, maskComments, scanSource } 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 ADR-0049 ledger-entry exclusion (#12966)': 33, + 'the WALK\'s admission decision, against a fake tree (#11866)': 67, + 'the container qualifier every anchor case below runs under (#13713)': 10, + 'anchor PROVENANCE (#12824)': 12, + 'CONTAINER-QUALIFIED DATA-PROPERTY ANCHORS (option D, #13713)': 20, + 'the two ruled true positives, against the LIVE generated artifacts': 61, + 'A PARTIAL LEDGER READ MUST SAY SO (#9896)': 14, + 'why the verdict keys on the SPELLING and not on a shortfall': 4, + 'A NON-LITERAL `route:` IS A VERDICT TOO (#10500)': 10, + 'A DECLARED `client:` ON A ROW THAT WAS NEVER ASSEMBLED (#10636)': 26, + 'A LITERAL-UNION `route:` TYPE MEMBER IS NOT A ROW (#10793)': 16, + 'THE SAME TYPE MEMBER, IN THE TWO QUOTES THE RECOGNIZER DECLINES (#10901)': 42, + '(3) THE WINDOW DELIMITER (`nextRouteRe`) — the worst of the seven, and the reason this is': 1, + '(4) THE IN-WINDOW `client:` MATCH (`windowClientRe`). `window.match()` takes the FIRST': 2, + '(5) THE DECLINED SWEEP (`declinedIn`), which is the LOUD direction: a double-quoted': 2, + '(6) THE RAW SWEEP behind `outsideCode`. A `subroute:` in a COMMENT was reported to the': 1, + '(7) …and `codeLeads`, the other half of that pair, which has no behaviour of its own:': 1, + '(8) …AND THE EIGHTH SCAN\'S ANSWER IS UNCHANGED, which is the whole point: it is the one': 26, + 'the route bridge admits LEAF symbols only (#9294)': 15, + 'the handler-window scan reads CODE, never PROSE (#9432)': 8, + 'a registration BOUNDS the previous window even when its path is a variable (#9503)': 9, + 'the CLI command anchor kind (#9230)': 40, + 'the rule-block anchor kind (#9282)': 49, + 'THE PAIR MUST AGREE ON SCREAMING_SNAKE (#13471)': 9, + '`computedOn` (#9519): the record that names WHICH TREE the answer is about': 12, + 'the sdk bridge\'s REACH over the declared surface (#9572)': 11, + '#11178: WHY a row is unreachable, and the two causes that printed as one': 28, + '`causes` GETS THE SAME TREATMENT, AT BOTH ENDS (#11867)': 16, +}); + +// DELETING an entry silences that battery's floor exactly as effectively as +// zeroing it, so the roster's own size is pinned too. +const SELF_TEST_BATTERY_FLOOR = 28; + +// 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 repoRoot = execSync('git rev-parse --show-toplevel').toString().trim(); const args = process.argv.slice(2); const asJson = args.includes('--json'); @@ -2586,9 +2642,25 @@ function parseLedgerSource(text) { * filesystem lookup is injected as a fake tree. */ 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 ADR-0049 ledger-entry exclusion (#12966)'); + let failed = 0; let total = 0; const check = (fn, label, path, want, got) => { + registerCase(); total++; if (got !== want) { console.error(` ✗ self-test "${label}": ${path} → expected ${fn}=${JSON.stringify(want)}, got ${JSON.stringify(got)}`); @@ -2682,6 +2754,7 @@ function selfTest() { // `REGISTRAR_FILE_RE`, and a test double contributed production route tails — the exact // failure this is here to keep out. Verified RED against the pre-fix line: 4 registrar // files and 6 source files, against the 1 and 2 asserted here. + battery('the WALK\'s admission decision, against a fake tree (#11866)'); const fakeSrcTree = [ 'packages/foo/src/engine.ts', // plain implementation 'packages/foo/src/real-route.ts', // a genuine registrar — must survive @@ -2921,6 +2994,7 @@ function selfTest() { // `ui.json` collision — and the containers deliberately LEFT OUT (`MetaOverlayCacheKey`, // `DatasourceDef`, `SysScimConnectionBinding`, `RestServer`, …) are exactly the ones the // real map does not carry either. That absence is what the case-(c) pins below exercise. + battery('the container qualifier every anchor case below runs under (#13713)'); const selfTestSurface = buildContainerSurface( [ { entries: { ObjectSchemaBase: 'data/Object', DatasourceSchema: 'data/Datasource' }, collisions: [] }, @@ -2976,6 +3050,7 @@ function selfTest() { // the tool mints, the other is the most valuable one — disproven discriminator #1 on // the card is exactly that no syntactic test separates them. The container does, and // that is the field these rows now print. + battery('anchor PROVENANCE (#12824)'); const cacheStructSource = [ '/** The identity of one cached overlay read. */', 'export interface MetaOverlayCacheKey {', @@ -3038,6 +3113,7 @@ function selfTest() { // The DROP leg is a deliberate false negative, so it is pinned twice: once for the // anchor it removes and once for the ledger entry that names the removal. A drop no // field reports is one no reviewer can audit. + battery('CONTAINER-QUALIFIED DATA-PROPERTY ANCHORS (option D, #13713)'); const authorableUseSite = [ 'const DatasourceSchema = strictObject({', // maps to data/Datasource ' managedBy: z.literal("system"),', // an authorable key NAME, not of THIS type @@ -3151,6 +3227,7 @@ function selfTest() { // proves nothing about the map the tool actually reads: if `gen:declaration-map` or // `gen:schema` stops emitting what this reads, the qualification silently reverts to // pre-#13713 behaviour and no fixture pin would notice. These cases are what notice. + battery('the two ruled true positives, against the LIVE generated artifacts'); const live = liveContainerSurface(); check('liveContainerSurface', 'the generated artifacts are readable — a silent revert to pre-#13713 behaviour is itself the regression', 'available', true, live.available); check('liveContainerSurface', 'ObjectSchemaBase resolves — the `userActions` pin runs through case (a)', 'data/Object', 'data/Object', String(live.resolve('ObjectSchemaBase'))); @@ -3330,6 +3407,7 @@ function selfTest() { // Every one of those is ordinary TypeScript; the template-literal spellings are the // realistic ones, because the repo's formatter rewrites double quotes back to single and // leaves a template literal alone. + battery('A PARTIAL LEDGER READ MUST SAY SO (#9896)'); const partialSource = [ 'export const REST_ROUTE_LEDGER = [', " { route: 'GET /api/v1/meta/:type/:name/references', family: 'metadata', disposition: 'sdk', client: 'meta.getReferences' },", @@ -3393,6 +3471,7 @@ function selfTest() { // claim `meta.getTypes`. Measured on the real tree at a718ee3dd, backtick-quoting // `GET /api/v1/meta` in `rest-route-ledger.ts` did exactly this, and `clientRows` stayed // 221 — a count comparison cannot see it, which is why `declined` is what fires. + battery('why the verdict keys on the SPELLING and not on a shortfall'); const stealSource = [ 'export const REST_ROUTE_LEDGER = [', " { route: 'GET /api/v1/docs', family: 'ops', disposition: 'server-only' },", @@ -3422,6 +3501,7 @@ function selfTest() { // is inside a type declaration and a row never is — so the counter can widen without // billing that member as a row. Both directions are pinned here, because counting the // interface member is precisely how a fix here goes wrong. + battery('A NON-LITERAL `route:` IS A VERDICT TOO (#10500)'); const nonLiteralSource = [ 'export interface Entry { route: string; client: string }', 'export const L = [', @@ -3486,6 +3566,7 @@ function selfTest() { // four. The row DOES land in a verdict after #10500 (its `route:` is named as unread), so // what was wrong here is a sub-count, not a silence — and a denominator that omits a // declaration renders a partial read as more complete than it is. + battery('A DECLARED `client:` ON A ROW THAT WAS NEVER ASSEMBLED (#10636)'); const orphanSource = [ // The reject side lives on line 1, in the spelling that makes it hostile: a // literal-union TYPE member opens with the very quote this counter reads, so the quote @@ -3647,6 +3728,7 @@ function selfTest() { // ⚠️ PINNED IN BOTH DIRECTIONS, like the mask cases: a fix that reached the type member by // swallowing the table AFTER it — a brace match that ran away — would pass a test asserting // only that the phantom is gone, while silently dropping all 259 live rows. + battery('A LITERAL-UNION `route:` TYPE MEMBER IS NOT A ROW (#10793)'); const typeUnionSource = [ "export interface Entry { route: 'GET /api/v1/gone' | 'GET /api/v1/meta'; client: string }", 'export const L = [', @@ -3745,6 +3827,7 @@ function selfTest() { // code path, and were measured behaving identically — which is the reason to pin them // apart rather than to trust one for both: a later narrowing of that alternation would // otherwise take one of them with nothing to say so. + battery('THE SAME TYPE MEMBER, IN THE TWO QUOTES THE RECOGNIZER DECLINES (#10901)'); for (const [spelling, q] of [['double-quoted', '"'], ['backtick-quoted', '`']]) { const src = [ `export interface Entry { route: ${q}GET /api/v1/gone${q} | ${q}GET /api/v1/meta${q}; client: string }`, @@ -3990,6 +4073,7 @@ function selfTest() { // PHANTOM took it: a WRONG binding, on a path nobody mounts, which then joined the // UNREACHABLE population. A count comparison is blind to it by construction — the same // shape #10636 measured for the quote spellings, arriving through the key. + battery('(3) THE WINDOW DELIMITER (`nextRouteRe`) — the worst of the seven, and the reason this is'); const keyWindow = parseLedgerSource([ 'export const L = [', " { route: 'GET /api/v1/meta', family: 'metadata',", @@ -4005,6 +4089,7 @@ function selfTest() { // hit, so a `myclient:` written ahead of the real `client:` became the row's binding, and // the real one — spelled exactly the way this recognizer reads — fell through to #10636's // unclaimed sweep and was NAMED as a value no row read, on a ledger that binds it correctly. + battery('(4) THE IN-WINDOW `client:` MATCH (`windowClientRe`). `window.match()` takes the FIRST'); const keyClient = parseLedgerSource([ 'export const L = [', " { route: 'GET /api/v1/meta', family: 'metadata', disposition: 'sdk',", @@ -4021,6 +4106,7 @@ function selfTest() { // `subroute:` was billed as a `route:` value the parse FAILED to read, so it entered the // denominator, was NAMED with its line, and fired a PARTIAL-read verdict with exit 1 on a // wholly accurate ledger. A false red costs the same trust a false green does. + battery('(5) THE DECLINED SWEEP (`declinedIn`), which is the LOUD direction: a double-quoted'); const keyDeclined = parseLedgerSource([ 'export const L = [', ' { subroute: "GET /api/v1/gone", family: \'metadata\', disposition: \'sdk\' },', @@ -4036,6 +4122,7 @@ function selfTest() { // (6) THE RAW SWEEP behind `outsideCode`. A `subroute:` in a COMMENT was reported to the // reader as a lead sitting where the mask says code is not — a finding printed on every // `--bridge-coverage` run, naming something that is not a lead at all. + battery('(6) THE RAW SWEEP behind `outsideCode`. A `subroute:` in a COMMENT was reported to the'); const keyProse = parseLedgerSource([ "// The retired row read subroute: 'GET /api/v1/gone' before #1234.", 'export const L = [', @@ -4049,12 +4136,14 @@ function selfTest() { // only `codeLeads` anchored, a `subroute:` in CODE position would fall OUT of the filter and // be reported as prose — the card's own fixture, pinned here explicitly so the pair cannot // drift apart while every other fixture stays green. + battery('(7) …and `codeLeads`, the other half of that pair, which has no behaviour of its own:'); check('parseLedgerSource', 'and a `subroute:` in CODE position is not reported as one either', 'outsideCode', 0, unanchoredKey.outsideCode.length); // (8) …AND THE EIGHTH SCAN'S ANSWER IS UNCHANGED, which is the whole point: it is the one // that was already right. A `subroute:` whose value is not a string literal at all was never // billed as an unreadable declaration — before the anchor moved or after. + battery('(8) …AND THE EIGHTH SCAN\'S ANSWER IS UNCHANGED, which is the whole point: it is the one'); const keyUnreadable = parseLedgerSource([ 'export const L = [', " { subroute: ROUTES.gone, family: 'metadata', disposition: 'sdk' },", @@ -4295,6 +4384,7 @@ function selfTest() { // method must stay bridgeable, and the identifier scan must still SEE the qualifier — // that last one is what stops this block going green because the fixture drifted into // deriving no route at all. + battery('the route bridge admits LEAF symbols only (#9294)'); const serverSource = [ 'export class RestServer {', ' /**', @@ -4376,6 +4466,7 @@ function selfTest() { // must NOT bridge, the code-named leaf MUST, and the raw scan must still SEE the prose // name — that last one is the counterfactual, and it is what stops this block going green // because the fixture drifted into carrying no comment at all. + battery('the handler-window scan reads CODE, never PROSE (#9432)'); const commentaryRegistrar = [ 'export class RestServer {', ' private registerPublishRoutes() {', @@ -4481,6 +4572,7 @@ function selfTest() { // reaches this block: the fixture is hermetic and models the variable-path SHAPE, // which is still the shape the scan cannot see wherever it survives. Kept verbatim // for that reason — ⛔ do not "refresh" a hermetic fixture to match today's tree. + battery('a registration BOUNDS the previous window even when its path is a variable (#9503)'); const variablePathRegistrar = [ 'export class RestServer {', ' private registerStateRoutes() {', @@ -4555,6 +4647,7 @@ function selfTest() { // are pinned — the phrase that must now be derived, AND the bare token that must stay // dropped, because buying recall by loosening the shape guard is the one fix this card // rules out (19 pages → 49, measured). + battery('the CLI command anchor kind (#9230)'); const commandIdCases = [ // [path under the commands root, expected id, label] @@ -4649,6 +4742,7 @@ function selfTest() { // check". Pinned here in three directions: the expressions that must now be derived, // the CALLER-LIST names that must stay dropped (they took the specimen from 7 pages to // 27), and the honest-failure property that made the defect filable at all. + battery('the rule-block anchor kind (#9282)'); // Block detection. The tag is an opt-in, so an untagged block must contribute nothing // however well-formed it is. @@ -4775,6 +4869,7 @@ function selfTest() { // identifier (pinned in `shapeCases` above) while `literalAnchorsFromLines` declined to // mint any anchor from it. Pin the agreement itself, so neither side can drift back out // of step silently — a check on one predicate alone could not have caught this. + battery('THE PAIR MUST AGREE ON SCREAMING_SNAKE (#13471)'); for (const t of ['OS_CLOUD_URL', 'OS_MODE', 'OS_TENANCY_POSTURE', 'ERROR_CODE_LEDGER', 'FLOW_INPUT_SCHEMA_INVALID']) { check('isCodeShaped/isLiteralAnchorShape', 'the pair agrees on a SCREAMING_SNAKE token', t, true, isCodeShaped(t) === isLiteralAnchorShape(t) && isLiteralAnchorShape(t)); @@ -4801,6 +4896,7 @@ function selfTest() { // commit's parents must survive as a PAIR — that pair is the only durable handle on // an ephemeral `refs/pull/N/merge` tree — and "could not tell" must never be // flattened into "checked, clean". + battery('`computedOn` (#9519): the record that names WHICH TREE the answer is about'); const mergeParents = '097fe96e1228f7da71f87e8f5ed95ae2739b53f1 047457ca3a8757012043460b8ded6090cbc9b114'; const computedOnCases = [ // [label, want, got] @@ -4833,6 +4929,7 @@ function selfTest() { // A ledger record as `parseLedgerSource` returns one. Spelled through a helper so a // fixture cannot quietly omit the declared counts — `bridgeCoverageFrom` reads them with // no default on purpose, and a fixture that skipped them would throw rather than pass. + battery('the sdk bridge\'s REACH over the declared surface (#9572)'); const covLedger = (file, rows) => ({ file, rows, @@ -4890,6 +4987,7 @@ function selfTest() { // `56 of 56` (auth) and `46 of 87` (rest) render identically today and are not the same // finding: the first surface has NO in-repo registration site, so the discovery widening // the second one wants moves it by zero rows — measured, before this split existed. + battery('#11178: WHY a row is unreachable, and the two causes that printed as one'); const ceilSrc = [ ['r-registers.ts', "app.get({ path: '/api/v1/storage/upload/presigned' }, handler);"], ['r-comments.ts', "// path: '/api/v1/never/registered' — an illustration, not a registration\n"], @@ -5013,6 +5111,7 @@ function selfTest() { // at all (measured: zero occurrences of `causes` in that file). Either half alone is // the half-wired state — a ceiling nobody renders is cost paid for no reader, and a // render branch with no ceiling prints `unmeasured` in a nicer shape. + battery('`causes` GETS THE SAME TREATMENT, AT BOTH ENDS (#11867)'); check('emit', 'the ADVISORY path measures causes — it passes a ceiling, not just tails', 'affected-docs.mjs', true, /bridgeCoverageFrom\(ledgers, registrarByTail\.keys\(\), ceilingTailsFrom\(sourceFiles\)\.keys\(\)\)/.test(ownSource)); // ⚠️ READ THE CODE, NOT THE COMMENT THAT FORBIDS IT. These three pins are about what @@ -5148,6 +5247,53 @@ function selfTest() { } + // ── The floor: every declared battery RAN, and ran its cases (#13489) ─── + // + // Evaluated after every battery has had its chance and BEFORE the verdict, so + // the success line below can only be printed by a run in which the set of + // batteries that registered assertions EQUALS the set declared. A set + // difference names WHICH battery stopped; a count says only that something did. + const floorFailure = (message) => { + failed++; + console.error(` ✗ ${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 (failed) { console.error(`\n✗ affected-docs self-test failed (${failed} case(s)).`); process.exit(1); diff --git a/scripts/release-github-releases.mjs b/scripts/release-github-releases.mjs index 7b97ecdb02..bbd6fca550 100644 --- a/scripts/release-github-releases.mjs +++ b/scripts/release-github-releases.mjs @@ -80,6 +80,44 @@ import { fileURLToPath } from 'node:url'; import { isEntrypoint } from './invoked-as.mjs'; import { workspacePackages } from './workspace-enumerator.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 real #4900 repro: spec\'s 17.0.0-rc.2 section': 9, + '2. A short entry is passed through untouched': 3, + '3. Fence balancing when the cut lands inside a code block': 3, + '4. Surrogate pairs are never split': 3, + '5. Anchors and fence-aware heading parsing': 6, + '6. Target resolution: both producers, and neither': 7, + '7. Planning is per package, and a missing entry is loud': 6, + '8. Every package gets a release; existing ones are updated, not retried ─': 7, + '9. Idempotent re-run': 2, + '10. One package\'s failure does not abandon the others': 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 = 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 __dirname = dirname(fileURLToPath(import.meta.url)); const REPO_ROOT = resolve(__dirname, '..'); @@ -682,10 +720,25 @@ function stubFetch({ existing = {}, failCreateFor = new Set() } = {}) { const SELF_TEST_VERDICT = 'release-github-releases 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); + }; + /** @type {string[]} */ const failures = []; let assertions = 0; const assert = (cond, msg) => { + registerCase(); assertions += 1; if (!cond) failures.push(msg); }; @@ -698,6 +751,7 @@ async function selfTest() { const specChangelog = readFileSync(join(REPO_ROOT, 'packages/spec/CHANGELOG.md'), 'utf8'); // ── 1. The real #4900 repro: spec's 17.0.0-rc.2 section ──────────────────── + battery('1. The real #4900 repro: spec\'s 17.0.0-rc.2 section'); const rc2 = getChangelogEntry(specChangelog, '17.0.0-rc.2'); assert(rc2 !== null, 'the 17.0.0-rc.2 entry is found in packages/spec/CHANGELOG.md'); assert( @@ -734,6 +788,7 @@ async function selfTest() { ); // ── 2. A short entry is passed through untouched ─────────────────────────── + battery('2. A short entry is passed through untouched'); const small = getChangelogEntry(specChangelog, '16.1.0'); assert(small !== null && measure(small) < BODY_LIMIT, 'the 16.1.0 entry is under the limit to begin with'); const smallBuilt = buildReleaseBody({ @@ -746,6 +801,7 @@ async function selfTest() { assert(smallBuilt.body === small, 'an under-limit entry is byte-identical to the changelog section'); // ── 3. Fence balancing when the cut lands inside a code block ────────────── + battery('3. Fence balancing when the cut lands inside a code block'); const fenced = [ ...Array.from({ length: 20 }, (_, i) => `line ${i} ${'x'.repeat(44)}`), '```ts', @@ -767,6 +823,7 @@ async function selfTest() { ); // ── 4. Surrogate pairs are never split ───────────────────────────────────── + battery('4. Surrogate pairs are never split'); const astral = '🚀'.repeat(5_000); const astralBuilt = buildReleaseBody({ entry: astral, @@ -784,6 +841,7 @@ async function selfTest() { // ── 5. Anchors and fence-aware heading parsing ───────────────────────────── // Expectations below are github-slugger's own output for these inputs. + battery('5. Anchors and fence-aware heading parsing'); assert(headingAnchor('17.0.0-rc.2') === '1700-rc2', 'the version anchor matches GitHub heading slugs'); assert(headingAnchor('16.1.0') === '1610', 'a stable version anchor drops its dots'); assert(headingAnchor('1.2.3-beta.10') === '123-beta10', 'a prerelease anchor keeps only its hyphen'); @@ -798,6 +856,7 @@ async function selfTest() { assert(getChangelogEntry(tricky, '2.0.0') === null, 'a missing version yields null rather than a wrong section'); // ── 6. Target resolution: both producers, and neither ────────────────────── + battery('6. Target resolution: both producers, and neither'); const packages = listWorkspacePackages(); assert(packages.has('@objectstack/spec'), 'the workspace scan finds @objectstack/spec'); const fromPublished = resolveReleaseTargets({ @@ -828,6 +887,7 @@ async function selfTest() { ); // ── 7. Planning is per package, and a missing entry is loud ──────────────── + battery('7. Planning is per package, and a missing entry is loud'); const specPlan = planRelease({ target: { name: '@objectstack/spec', version: '17.0.0-rc.2', dir: join(REPO_ROOT, 'packages/spec') }, ...CTX, @@ -856,6 +916,7 @@ async function selfTest() { assert(threw, 'a version with no changelog entry fails loudly instead of releasing an empty body'); // ── 8. Every package gets a release; existing ones are updated, not retried ─ + battery('8. Every package gets a release; existing ones are updated, not retried ─'); const plans = ['@objectstack/spec', '@objectstack/cli', '@objectstack/runtime'].map((name) => { const plan = planRelease({ target: { name, version: '17.0.0-rc.2', dir: packages.get(name).dir }, ...CTX }); if (!('tagName' in plan)) throw new Error(`fixture package ${name} produced no plan`); @@ -905,6 +966,7 @@ async function selfTest() { ); // ── 9. Idempotent re-run ─────────────────────────────────────────────────── + battery('9. Idempotent re-run'); const allExist = stubFetch({ existing: Object.fromEntries(plans.map((p, i) => [p.tagName, 100 + i])), }); @@ -929,6 +991,7 @@ async function selfTest() { ); // ── 10. One package's failure does not abandon the others ────────────────── + battery('10. One package\'s failure does not abandon the others'); const partial = stubFetch({ failCreateFor: new Set(['@objectstack/cli@17.0.0-rc.2']) }); const partialResult = await publishReleases({ client: createReleasesClient({ @@ -954,6 +1017,52 @@ async function selfTest() { 'the failure carries the API status through to the log', ); + // ── 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(`✗ release-github-releases --self-test — ${failures.length} of ${assertions} assertion(s) failed\n`); for (const f of failures) console.error(` • ${f}`); diff --git a/scripts/sync-template-versions.mjs b/scripts/sync-template-versions.mjs index f00d969628..278901bdbc 100644 --- a/scripts/sync-template-versions.mjs +++ b/scripts/sync-template-versions.mjs @@ -100,6 +100,42 @@ import { fileURLToPath } from 'node:url'; import { dirname, join, relative, sep } 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({ + 'Control G: the fixture table covers every declared stamp': 1, + 'Control A: a CLEAN corpus is REACHED, and left byte-identical and UNWRITTEN': 20, + 'Control B: a MISSING stamp is a hard failure naming the path': 4, + 'Control C: ONE run names EVERY unstamped surface': 6, + 'Control D: a template with no @objectstack/* dependency exits 1': 2, + 'Control E: a stamp FILE that does not exist exits 1 naming it': 2, + 'Control F: an unparseable template package.json exits 1 naming it': 2, + 'Control H: zero templates refuses a vacuous green': 3, +}); + +// DELETING an entry silences that battery's floor exactly as effectively as +// zeroing it, so the roster's own size is pinned too. +const SELF_TEST_BATTERY_FLOOR = 8; + +// The key an assertion is filed under when no battery is open. It is not a +// declared battery, so it reds by the same set difference rather than silently +// inflating whichever battery happened to run last. +const UNATTRIBUTED_BATTERY = '(no battery open)'; + /** The repo this script lives in — resolved from the script, so cwd cannot lie. */ const root = dirname(dirname(fileURLToPath(import.meta.url))); @@ -541,9 +577,24 @@ function runFixture(script) { const SELF_TEST_VERDICT = 'sync-template-versions 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 = []; let checked = 0; const assert = (condition, message) => { + registerCase(); checked++; if (!condition) failures.push(message); }; @@ -565,6 +616,7 @@ function selfTest() { // this exact case was dead output in the exact case it was written for, and // the next author reads a harness crash instead of "you declared a stamp and // owe it a fixture body". Ordering is the whole fix; the case is unchanged. + battery('Control G: the fixture table covers every declared stamp'); assert( TEXT_STAMPS.length > 0 && TEXT_STAMPS.every((stamp) => typeof SELF_TEST_BODIES[stamp.key] === 'function'), 'every TEXT_STAMPS row has a fixture body, so a newly declared stamp cannot sit outside every case here — ' + @@ -588,6 +640,7 @@ function selfTest() { // must REPORT every surface it judged. mtimes are backdated first, because // rewriting a file with identical bytes is still a write — and a version // pass that rewrites clean files churns the release diff. + battery('Control A: a CLEAN corpus is REACHED, and left byte-identical and UNWRITTEN'); { const dir = fixtureDir('clean'); const script = buildFixture(dir); @@ -642,6 +695,7 @@ function selfTest() { // the string unchanged and only one of them is fine. This is the assertion // that tells a rewriter which has STOPPED REWRITING from one with nothing // to do, which is the observation the card was filed about. + battery('Control B: a MISSING stamp is a hard failure naming the path'); { const dir = fixtureDir('missing-stamp'); const script = buildFixture(dir); @@ -669,6 +723,7 @@ function selfTest() { // Problems are COLLECTED, not thrown at the first hit. A run that named // only the first would send a release engineer round the loop once per // broken surface, and this is the only place that contract is observed. + battery('Control C: ONE run names EVERY unstamped surface'); { const dir = fixtureDir('all-problems'); const script = buildFixture(dir); @@ -695,6 +750,7 @@ function selfTest() { // // Zero matches is the silent-skip shape: it reads exactly like "already in // lockstep" and means the opposite. + battery('Control D: a template with no @objectstack/* dependency exits 1'); { const dir = fixtureDir('no-stack-deps'); const script = buildFixture(dir); @@ -713,6 +769,7 @@ function selfTest() { } // ── Control E: a stamp FILE that does not exist exits 1 naming it ─────── + battery('Control E: a stamp FILE that does not exist exits 1 naming it'); { const dir = fixtureDir('missing-file'); const script = buildFixture(dir); @@ -728,6 +785,7 @@ function selfTest() { } // ── Control F: an unparseable template package.json exits 1 naming it ─── + battery('Control F: an unparseable template package.json exits 1 naming it'); { const dir = fixtureDir('bad-json'); const script = buildFixture(dir); @@ -746,6 +804,7 @@ function selfTest() { // `main()`'s own guard, which is a DIFFERENT code path from the throw in // `stampedPaths()` that #9648 covers: this one is the run refusing to // report success after rewriting nothing. + battery('Control H: zero templates refuses a vacuous green'); { const dir = fixtureDir('no-templates'); const script = buildFixture(dir, { templates: [] }); @@ -765,6 +824,52 @@ function selfTest() { rmSync(scratch, { 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.', + ); + } + reportFailures(); console.log( `✓ sync-template-versions --self-test: ${checked} assertions over temp fixtures, running the real CLI. ` +