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. ` +