diff --git a/packages/spec/scripts/check-browser-reachable-entries.ts b/packages/spec/scripts/check-browser-reachable-entries.ts index 2b69476bf7..c5e26994a9 100644 --- a/packages/spec/scripts/check-browser-reachable-entries.ts +++ b/packages/spec/scripts/check-browser-reachable-entries.ts @@ -715,6 +715,65 @@ function audit(): never { // Self-test — the shapes, not the corpus // --------------------------------------------------------------------------- +// Set by `selfTest()` only after its verdict line prints, and read at the +// dispatch at the foot of this file: a `return` that leaves the function above +// that line prints nothing, and here it does not even stop — control falls +// through to `audit()`, which on a built tree prints its own green line and +// exits 0. A self-test that never finished, reported as one that passed. +// ⛔ AN EXIT CODE IS NOT A HANDSHAKE; the flag is the thing an early return +// cannot carry with it. +let selfTestReachedVerdict = false; + +// ── The self-test's own battery roster and floor ─────────────────────────── +// +// `failures.length === 0` used to be this self-test's ONLY success condition, +// and the verdict line names three mechanisms rather than a count, so "every +// case held" and "the cases never ran" printed the same sentence. The shrink was +// measured on the sibling `check-exported-any.ts`, whose self-test has the same +// anatomy — deleting one name from a fixture name list de-registers that case +// and the run still prints its verdict byte-identically and still exits 0. +// +// Closed the way `scripts/check-agent-model-declared.mjs` and its TypeScript +// ports (`scripts/check-test-typecheck.mts`, `check-duration-unit-keys.ts` in +// this directory) closed it — COPIED and ⛔ never imported, because every +// self-test has to keep running standalone as +// `tsx scripts/check-browser-reachable-entries.ts --self-test`, and a shared +// assertion module would be one point of failure for every instrument at once. +// What is pinned is the registered NAMES, not a number. +// +// A BATTERY HERE IS A SECTION — the `// ──` groups this self-test was already +// written in. Each opens with `battery('')` and every `check()` after it +// is attributed to that name until the next one opens, so a section that stops +// running names ITSELF at the floor rather than going quiet. +// +// ⛔ A pinned TOTAL is not the repair — the refusal battery falling from 10 +// cases to 1 keeps a total "right" the moment a sibling grows — and ⛔ neither +// is a roster DERIVED from the run: a count taken from the cases that ran can +// never notice one that stopped. +// +// 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. +const SELF_TEST_BATTERIES: Readonly> = Object.freeze({ + 'the scanner FINDS real specifiers': 1, + 'the scanner does NOT fabricate specifiers out of prose or lookalikes': 1, + 'the walk: transitive over relative hops, both verdicts': 4, + 'the refusal: an unbuilt or stale tree is NOT MEASURED': 10, + 'the reconciliation: both directions': 4, + 'target resolution reads BOTH conditions': 1, + "the browser condition's targets, and only when declared (#11072)": 4, + 'what counts as Node-only (#11072)': 2, + 'the node-only verdict over a walked graph, BOTH directions (#11072)': 2, +}); + +// DELETING an entry silences that battery's floor exactly as effectively as +// zeroing it, so the roster's own size is pinned too. +const SELF_TEST_BATTERY_FLOOR = 9; + +// The key a case 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 open last. +const UNATTRIBUTED_BATTERY = '(no battery open)'; + /** * A green run over today's build proves only what today's build contains, and * two of the things this gate must do cannot be exercised by it at all: the tree @@ -722,14 +781,33 @@ function audit(): never { * transitive walk — the part that keeps the gate honest the day `splitting` is * turned on — would ship never having run. These fixtures are the contract. */ -function selfTest(): never { +function selfTest(): void { const failures: string[] = []; + // The battery ledger this self-test's floor is evaluated against. + // `battery()` opens a battery; every `check()` below is attributed to the one + // most recently opened. + // + // Registration is the FIRST statement of `check()`, before the line is + // printed and before `failures` moves, because the floor asserts REACH: a + // case that runs and FAILS still registers, and only a case that never runs + // at all goes missing from the ledger. Routing registration through the + // failure sink instead would register a case only when it failed — a fully + // green run would register 0 and every battery would read DID NOT RUN, the + // floor inverted rather than installed. + const seen = new Map(); + let openBattery: string | undefined; + const battery = (name: string): void => { + openBattery = name; + }; const check = (name: string, ok: boolean, detail = ''): void => { + const attributedTo = openBattery ?? UNATTRIBUTED_BATTERY; + seen.set(attributedTo, (seen.get(attributedTo) ?? 0) + 1); if (!ok) failures.push(`${name}${detail ? ` — ${detail}` : ''}`); console.log(` ${ok ? '✓' : '✗'} ${name}${!ok && detail ? ` — ${detail}` : ''}`); }; // ── The scanner: it must FIND real specifiers … ────────────────────────── + battery('the scanner FINDS real specifiers'); const real = [ `import { z } from 'zod';`, `export { a } from "./chunk-A.mjs";`, @@ -746,6 +824,7 @@ function selfTest(): never { ); // ── … and it must NOT fabricate them out of prose or lookalikes ────────── + battery('the scanner does NOT fabricate specifiers out of prose or lookalikes'); // The first case is REAL text from dist/index.mjs: a documentation string that // contains a complete import statement. A naive grep reports it as a link. const ghost = [ @@ -764,6 +843,7 @@ function selfTest(): never { ); // ── The walk: transitive over relative hops, both verdicts ─────────────── + battery('the walk: transitive over relative hops, both verdicts'); const tmp = mkdtempSync(join(tmpdir(), 'os-browser-reachable-')); try { const dist = join(tmp, 'dist'); @@ -813,6 +893,7 @@ function selfTest(): never { } // ── The refusal: an unbuilt or stale tree is NOT MEASURED ──────────────── + battery('the refusal: an unbuilt or stale tree is NOT MEASURED'); // Driven here rather than against this workspace, because the only way to // observe the refusal on the real tree is to break the real tree. Both // verdicts are pinned: a guard only ever seen green cannot be told apart from @@ -998,6 +1079,7 @@ function selfTest(): never { } // ── The reconciliation: both directions ────────────────────────────────── + battery('the reconciliation: both directions'); const emptyLedger = (): Ledger => ({ browserReachable: {}, unjudged: [], notAModule: [] }); let problems: string[] = []; @@ -1033,6 +1115,7 @@ function selfTest(): never { check('accepts a fully classified exports map', problems.length === 0, JSON.stringify(problems)); // ── Target resolution reads BOTH conditions ────────────────────────────── + battery('target resolution reads BOTH conditions'); const targets = targetsOf({ import: { types: './dist/x/index.d.mts', default: './dist/x/index.mjs' }, require: { types: './dist/x/index.d.ts', default: './dist/x/index.js' }, @@ -1044,6 +1127,7 @@ function selfTest(): never { ); // ── #11072: the browser condition's targets, and only when declared ────── + battery("the browser condition's targets, and only when declared (#11072)"); const conditioned = { browser: { import: { types: './dist/x/index.d.mts', default: './dist/browser/x/index.mjs' }, @@ -1077,6 +1161,7 @@ function selfTest(): never { ); // ── #11072: what counts as Node-only ───────────────────────────────────── + battery('what counts as Node-only (#11072)'); const nodeOnly = ['fs', 'node:fs', 'fs/promises', 'node:path', 'pg-connection-string', 'pg-connection-string/index.js']; const browserSafe = ['zod', 'zod/v4', './chunk.mjs', 'ai']; @@ -1092,6 +1177,7 @@ function selfTest(): never { ); // ── #11072: the node-only verdict over a walked graph, BOTH directions ─── + battery('the node-only verdict over a walked graph, BOTH directions (#11072)'); const nodeOnlyTmp = mkdtempSync(join(tmpdir(), 'os-browser-reachable-nodeonly-')); try { const dist = join(nodeOnlyTmp, 'dist'); @@ -1131,13 +1217,78 @@ function selfTest(): never { rmSync(nodeOnlyTmp, { recursive: true, force: true }); } - if (failures.length) { - console.error(`\n✗ self-test: ${failures.length} case(s) failed.`); + // ── The floor: every declared battery RAN, and ran its cases ───────────── + // + // 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 EQUALS the set declared, each at or above its own + // count. A set difference names WHICH battery stopped; a count says only that + // something did — and, before this block existed, not even that. + const floorProblems: string[] = []; + const declaredBatteries = Object.keys(SELF_TEST_BATTERIES); + if (declaredBatteries.length < SELF_TEST_BATTERY_FLOOR) { + floorProblems.push( + `SELF_TEST_BATTERIES declares ${declaredBatteries.length} batteries, below the pinned ` + + `${SELF_TEST_BATTERY_FLOOR} — a battery deleted from the roster takes its own floor with it.`, + ); + } + for (const [name, count] of seen) { + if (declaredBatteries.includes(name)) continue; + floorProblems.push( + `self-test battery "${name}" registered ${count} case(s) but is not declared in ` + + 'SELF_TEST_BATTERIES — a case attributed to no declared battery is one nothing floors.', + ); + } + for (const name of declaredBatteries) { + const count = seen.get(name) ?? 0; + if (count >= SELF_TEST_BATTERIES[name]) continue; + floorProblems.push( + 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]} — ${SELF_TEST_BATTERIES[name] - count} case(s) that used to run no longer do.`, + ); + } + if (floorProblems.length > 0) { + for (const problem of floorProblems) console.error(`✗ self-test floor: ${problem}`); + console.error( + '✗ self-test floor: A battery below its floor means cases STOPPED RUNNING — the battery is the ' + + 'bug, not the number. Find what stopped registering (a deleted case, a fixture the cases are ' + + 'built from that no longer exists, a guard that now skips) and restore it.', + ); + } + + if (failures.length || floorProblems.length) { + if (failures.length) console.error(`\n✗ self-test: ${failures.length} case(s) failed.`); process.exit(1); } - console.log('✅ self-test: scanner, transitive walk and ledger reconciliation all behave.'); - process.exit(0); + // The count is printed because a reader had to hand-tally the checkmarks to + // get one, and it is printed AFTER the floor rather than instead of it: the + // number is evidence, the floor is the proof. + const registered = [...seen.values()].reduce((a, b) => a + b, 0); + console.log( + '✅ self-test: scanner, transitive walk and ledger reconciliation all behave — ' + + `${registered} case(s) across ${declaredBatteries.length} batteries, every battery at or above ` + + 'its pinned floor.', + ); + selfTestReachedVerdict = true; } -if (SELF_TEST) selfTest(); +if (SELF_TEST) { + selfTest(); + // The handshake. Without it a `return` above the verdict prints nothing and + // does not even stop: control reaches `audit()` below, which on a built tree + // prints its own green line and exits 0 — a self-test that never finished, + // reported as one that passed. + if (!selfTestReachedVerdict) { + console.error( + '\n✗ check-browser-reachable-entries self-test: selfTest() returned without reaching its verdict,\n' + + 'so no verdict line was printed. Exiting 0 here would report a self-test that never\n' + + 'finished as a self-test that passed.\n', + ); + process.exit(1); + } + process.exit(0); +} audit(); diff --git a/packages/spec/scripts/check-dual-source-exports.ts b/packages/spec/scripts/check-dual-source-exports.ts index 5e3c0f0405..a23a1bb126 100644 --- a/packages/spec/scripts/check-dual-source-exports.ts +++ b/packages/spec/scripts/check-dual-source-exports.ts @@ -160,6 +160,62 @@ function makeProgram(files: string[], extra: ts.CompilerOptions = {}): ts.Progra }); } +// ── Self-test: the handshake flag ──────────────────────────────────────────── +// +// Set by `selfTest()` only after its verdict line prints, and read at the +// dispatch below: a `return` that leaves the function above that line prints +// nothing, and here it does not even stop — control falls through to the audit, +// which on a fresh dist prints its own green line and exits 0. A self-test that +// never finished, reported as one that passed. ⛔ AN EXIT CODE IS NOT A +// HANDSHAKE: `fail()` below exits 1 on its own, so the exit code stays +// load-bearing; the flag is the thing an early return cannot carry with it. +let selfTestReachedVerdict = false; + +// ── The self-test's own battery roster and floor ───────────────────────────── +// +// Reaching the `✅ self-test` line used to be this self-test's ONLY success +// condition, so "every case held" and "the cases never ran" printed the same +// line — no count, nothing to compare. The shrink was measured on this file's +// sibling `check-exported-any.ts`, whose self-test has the same anatomy: +// deleting one name from a fixture name list de-registers that case and the run +// still prints its verdict byte-identically and still exits 0. +// +// Closed the way `scripts/check-agent-model-declared.mjs` and its TypeScript +// ports (`scripts/check-test-typecheck.mts`, `check-duration-unit-keys.ts` in +// this very directory) closed it — COPIED and ⛔ never imported, because every +// self-test has to keep running standalone as +// `tsx scripts/check-dual-source-exports.ts --self-test`, and a shared +// assertion module would be one point of failure for every instrument at once. +// What is pinned is the registered NAMES, not a number. +// +// A BATTERY HERE IS A SECTION: this self-test is a sequence of assertions +// grouped by what they hold, so each group opens with `battery('')` and +// every `expect()` after it is attributed to that name until the next opens. +// +// ⛔ A pinned TOTAL is not the repair — a battery falling from 4 cases to 1 +// keeps a total "right" the moment a sibling grows — and ⛔ neither is a roster +// DERIVED from the run: a count taken from the cases that ran can never notice +// one that stopped. The two fixture name lists are exactly what a shrink +// deletes from, so their floors are their live lengths. +// +// 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. +const SELF_TEST_BATTERIES: Readonly> = Object.freeze({ + 'the fixture compiles: two entries over one shared declaration source': 1, + 'the fixture RESOLVES: the counts that keep both legs below non-vacuous': 2, + 'the RED leg: one name, two declarations — flagged in both faces of the trap': 2, + 'the GREEN leg: a re-export of ONE declaration, and a single-entry name, are NOT flagged': 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 = 4; + +// The key a case 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 open last. +const UNATTRIBUTED_BATTERY = '(no battery open)'; + // ── Self-test ──────────────────────────────────────────────────────────────── /** @@ -168,12 +224,35 @@ function makeProgram(files: string[], extra: ts.CompilerOptions = {}): ts.Progra * re-export must NOT be (a false positive drowns the signal in the ~80 * legitimate re-exports the real surface carries). */ -function selfTest(): never { +function selfTest(): void { const fail = (msg: string): never => { console.error(`✗ self-test: ${msg}`); process.exit(1); }; + // The battery ledger this self-test's floor is evaluated against. + // `battery()` opens a battery; every `expect()` 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. + // + // Registration is the FIRST statement of `expect()`, before the outcome is + // consulted, because the floor asserts REACH: a case that runs and FAILS + // still registers (and `fail()` exits loudly on its own), and only a case that + // never runs at all goes missing from the ledger. Routing registration + // through the failure path instead would register a case only when it failed + // — a fully green run would register 0 and every battery would read DID NOT + // RUN, the floor inverted rather than installed. + const seen = new Map(); + let openBattery: string | undefined; + const battery = (name: string): void => { + openBattery = name; + }; + const expect = (ok: boolean, msg: string | (() => string)): void => { + const attributedTo = openBattery ?? UNATTRIBUTED_BATTERY; + seen.set(attributedTo, (seen.get(attributedTo) ?? 0) + 1); + if (!ok) fail(typeof msg === 'function' ? msg() : msg); + }; + const dir = mkdtempSync(join(tmpdir(), 'spec-dual-source-')); try { // shared.ts — the single-source declarations both entries re-export. @@ -200,8 +279,12 @@ function selfTest(): never { const entries = { './a': join(dir, 'a.ts'), './b': join(dir, 'b.ts') }; const program = makeProgram(Object.values(entries)); + battery('the fixture compiles: two entries over one shared declaration source'); const syntactic = program.getSyntacticDiagnostics(); - if (syntactic.length > 0) fail(`fixture does not parse: ${ts.flattenDiagnosticMessageText(syntactic[0].messageText, ' ')}`); + expect( + syntactic.length === 0, + () => `fixture does not parse: ${ts.flattenDiagnosticMessageText(syntactic[0].messageText, ' ')}`, + ); const { findings, names, reExports } = scan(program, entries); const flagged = new Set(findings.map((f) => f.split(' — ')[0])); @@ -209,24 +292,94 @@ function selfTest(): never { // 6 distinct names (SharedType, sharedConst, TrueDup, Mixed, OnlyA, OnlyB). // Fewer means exports are not resolving, and every assertion below would // pass vacuously — the exact way a gate goes dormant. - if (names !== 6) fail(`saw ${names} export names, expected 6 — the fixture's modules are not resolving`); - if (reExports !== 2) fail(`saw ${reExports} re-exported names, expected 2 (SharedType, sharedConst) — alias resolution is broken`); + battery('the fixture RESOLVES: the counts that keep both legs below non-vacuous'); + expect(names === 6, `saw ${names} export names, expected 6 — the fixture's modules are not resolving`); + expect(reExports === 2, `saw ${reExports} re-exported names, expected 2 (SharedType, sharedConst) — alias resolution is broken`); + battery('the RED leg: one name, two declarations — flagged in both faces of the trap'); for (const name of ['TrueDup', 'Mixed']) { - if (!flagged.has(name)) fail(`missed \`${name}\` — two declarations share the name and the gate is DORMANT`); + expect(flagged.has(name), `missed \`${name}\` — two declarations share the name and the gate is DORMANT`); } + battery('the GREEN leg: a re-export of ONE declaration, and a single-entry name, are NOT flagged'); for (const name of ['SharedType', 'sharedConst', 'OnlyA', 'OnlyB']) { - if (flagged.has(name)) fail(`false positive on \`${name}\` — only same-name DIFFERENT-declaration exports may be flagged`); + expect(!flagged.has(name), `false positive on \`${name}\` — only same-name DIFFERENT-declaration exports may be flagged`); } } finally { rmSync(dir, { recursive: true, force: true }); } - console.log('✅ self-test: flags same-name different-declaration exports, and nothing else.'); - process.exit(0); + // ── The floor: every declared battery RAN, and ran its cases ────────────── + // + // 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 EQUALS the set declared, each at or above its own + // count. A set difference names WHICH battery stopped; a count says only that + // something did — and, before this block existed, not even that. + const floorProblems: string[] = []; + const declaredBatteries = Object.keys(SELF_TEST_BATTERIES); + if (declaredBatteries.length < SELF_TEST_BATTERY_FLOOR) { + floorProblems.push( + `SELF_TEST_BATTERIES declares ${declaredBatteries.length} batteries, below the pinned ` + + `${SELF_TEST_BATTERY_FLOOR} — a battery deleted from the roster takes its own floor with it.`, + ); + } + for (const [name, count] of seen) { + if (declaredBatteries.includes(name)) continue; + floorProblems.push( + `self-test battery "${name}" registered ${count} case(s) but is not declared in ` + + 'SELF_TEST_BATTERIES — a case attributed to no declared battery is one nothing floors.', + ); + } + for (const name of declaredBatteries) { + const count = seen.get(name) ?? 0; + if (count >= SELF_TEST_BATTERIES[name]) continue; + floorProblems.push( + 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]} — ${SELF_TEST_BATTERIES[name] - count} case(s) that used to run no longer do.`, + ); + } + if (floorProblems.length > 0) { + for (const problem of floorProblems) console.error(`✗ self-test floor: ${problem}`); + console.error( + '✗ self-test floor: A battery below its floor means cases STOPPED RUNNING — the battery is the ' + + 'bug, not the number. Find what stopped registering (a name deleted from a fixture name list, ' + + 'a guard that now skips, an early return) and restore it.', + ); + process.exit(1); + } + + // The count is printed because a reader had to hand-tally the assertions to + // get one, and it is printed AFTER the floor rather than instead of it: the + // number is evidence, the floor is the proof. + const registered = [...seen.values()].reduce((a, b) => a + b, 0); + console.log( + '✅ self-test: flags same-name different-declaration exports, and nothing else — ' + + `${registered} case(s) across ${declaredBatteries.length} batteries, every battery at or above ` + + 'its pinned floor.', + ); + selfTestReachedVerdict = true; } -if (SELF_TEST) selfTest(); +if (SELF_TEST) { + selfTest(); + // The handshake. Without it a `return` above the verdict prints nothing and + // does not even stop: control reaches the audit below, which on a fresh dist + // prints its own green line and exits 0 — a self-test that never finished, + // reported as one that passed. `fail()`'s own exit code stays load-bearing; + // this refuses to believe a SILENT success. + if (!selfTestReachedVerdict) { + console.error( + '\n✗ check-dual-source-exports self-test: selfTest() returned without reaching its verdict,\n' + + 'so no verdict line was printed. Exiting 0 here would report a self-test that never\n' + + 'finished as a self-test that passed.\n', + ); + process.exit(1); + } + process.exit(0); +} // ── Audit ──────────────────────────────────────────────────────────────────── diff --git a/packages/spec/scripts/check-error-code-provenance.ts b/packages/spec/scripts/check-error-code-provenance.ts index 9e74797b6d..4d6cc7b8f8 100644 --- a/packages/spec/scripts/check-error-code-provenance.ts +++ b/packages/spec/scripts/check-error-code-provenance.ts @@ -369,9 +369,86 @@ function run(report: boolean): number { // Self-test — the red leg, pinned per pattern and per waiver direction // --------------------------------------------------------------------------- +// Set by `selfTest()` only after its verdict line prints, and read at the +// dispatch at the foot of this file. +// +// ⛔ AN EXIT CODE IS NOT A HANDSHAKE, and in this file that is worth spelling +// out, because the code travels further than it does in the template this is +// copied from: `selfTest()` RETURNS a number and the dispatch hands it straight +// to `process.exit()`. A 0 rides that path just as happily when it comes from a +// `return` placed above the verdict — printing nothing, exiting 0, reporting a +// self-test that never finished as one that passed. The flag is the thing an +// early return cannot carry with it. +let selfTestReachedVerdict = false; + +// ── The self-test's own battery roster and floor ─────────────────────────── +// +// `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: the verdict +// names `STAMP_PATTERNS.length` and the waiver directions, neither of which +// moves when a `check()` call stops being reached. The shrink was measured on +// the sibling `check-exported-any.ts`, whose self-test has the same anatomy — +// deleting one name from a fixture name list de-registers that case and the run +// still prints its verdict byte-identically and still exits 0. +// +// Closed the way `scripts/check-agent-model-declared.mjs` and its TypeScript +// ports (`scripts/check-test-typecheck.mts`, `check-duration-unit-keys.ts` in +// this directory) closed it — COPIED and ⛔ never imported, because every +// self-test has to keep running standalone as +// `tsx scripts/check-error-code-provenance.ts --self-test`, and a shared +// assertion module would be one point of failure for every instrument at once. +// What is pinned is the registered NAMES, not a number. +// +// A BATTERY HERE IS A SECTION: this self-test is a sequence of `check()` calls +// grouped by what they hold, so each group opens with `battery('')` and +// every `check()` after it is attributed to that name until the next opens. +// +// ⛔ A pinned TOTAL is not the repair — a battery falling from 4 cases to 1 +// keeps a total "right" the moment a sibling grows — and ⛔ neither is a roster +// DERIVED from the run: a count taken from the cases that ran can never notice +// one that stopped. +// +// 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. +const SELF_TEST_BATTERIES: Readonly> = Object.freeze({ + 'each published STAMP pattern catches its own spelling': 4, + 'the population boundary and comment masking: what is NOT a site': 2, + 'the reconciliation: an unlisted stamper reddens, a listed one is green': 2, + 'a waiver admits EXACTLY its (package, code) pair': 2, + 'every stale-waiver direction reddens': 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 a case 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 open last. +const UNATTRIBUTED_BATTERY = '(no battery open)'; + function selfTest(): number { const failures: string[] = []; + // The battery ledger this self-test's floor is evaluated against. + // `battery()` opens a battery; every `check()` 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. + // + // Registration is the FIRST statement of `check()`, before the outcome is + // consulted, because the floor asserts REACH: a case that runs and FAILS + // still registers, and only a case that never runs at all goes missing from + // the ledger. Routing registration through the failure sink instead would + // register a case only when it failed — a fully green run would register 0 and + // every battery would read DID NOT RUN, the floor inverted rather than + // installed. + const seen = new Map(); + let openBattery: string | undefined; + const battery = (name: string): void => { + openBattery = name; + }; const check = (name: string, ok: boolean): void => { + const attributedTo = openBattery ?? UNATTRIBUTED_BATTERY; + seen.set(attributedTo, (seen.get(attributedTo) ?? 0) + 1); if (!ok) failures.push(name); }; // ⚠️ The fixture code spellings are REAL registered codes on purpose, driven @@ -398,6 +475,7 @@ function selfTest(): number { const ledger = { '@objectstack/owner': [CODE_A, CODE_B] } as const; // Each published pattern catches its spelling (red leg, per pattern). + battery('each published STAMP pattern catches its own spelling'); check( 'objlit catches a stamp', scanSourceText(`return { code: '${CODE_A}' };`, registered).some((h) => h.pattern === 'objlit'), @@ -416,6 +494,7 @@ function selfTest(): number { ); // Population boundary: a code outside the registered set is the sibling // gate's subject, never a site here. + battery('the population boundary and comment masking: what is NOT a site'); check( 'a code outside the registered set is out of population', scanSourceText(`return { code: '${CODE_OUT}' };`, registered).length === 0, @@ -426,6 +505,7 @@ function selfTest(): number { scanSourceText(`// answers { code: '${CODE_A}' } on refusal\nconst x = 1;`, registered).length === 0, ); // A synthetic unlisted stamper is caught THROUGH the real reconciliation. + battery('the reconciliation: an unlisted stamper reddens, a listed one is green'); { const { violations } = deriveFindings([site('@objectstack/rogue', CODE_A)], ledger, []); check('unlisted stamper is a violation', violations.length === 1); @@ -436,6 +516,7 @@ function selfTest(): number { check('listed stamper is green', violations.length === 0 && listed.length === 1); } // A waiver admits exactly its (package, code) pair — and only that pair. + battery('a waiver admits EXACTLY its (package, code) pair'); { const waiver: ProvenanceWaiver = { package: '@objectstack/rogue', @@ -454,6 +535,7 @@ function selfTest(): number { check('waiver does not admit a different code', other.violations.length === 1); } // Stale-waiver directions, each red. + battery('every stale-waiver direction reddens'); { const noSite = deriveFindings([], ledger, [{ package: '@objectstack/rogue', @@ -478,15 +560,80 @@ function selfTest(): number { check('row + waiver is dead weight', deadWeight.waiverProblems.some((p) => p.includes('dead weight'))); } - if (failures.length > 0) { - console.error(`self-test FAILED: ${failures.join('; ')}`); + // ── The floor: every declared battery RAN, and ran its cases ───────────── + // + // 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 EQUALS the set declared, each at or above its own + // count. A set difference names WHICH battery stopped; a count says only that + // something did — and, before this block existed, not even that. + const floorProblems: string[] = []; + const declaredBatteries = Object.keys(SELF_TEST_BATTERIES); + if (declaredBatteries.length < SELF_TEST_BATTERY_FLOOR) { + floorProblems.push( + `SELF_TEST_BATTERIES declares ${declaredBatteries.length} batteries, below the pinned ` + + `${SELF_TEST_BATTERY_FLOOR} — a battery deleted from the roster takes its own floor with it.`, + ); + } + for (const [name, count] of seen) { + if (declaredBatteries.includes(name)) continue; + floorProblems.push( + `self-test battery "${name}" registered ${count} case(s) but is not declared in ` + + 'SELF_TEST_BATTERIES — a case attributed to no declared battery is one nothing floors.', + ); + } + for (const name of declaredBatteries) { + const count = seen.get(name) ?? 0; + if (count >= SELF_TEST_BATTERIES[name]) continue; + floorProblems.push( + 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]} — ${SELF_TEST_BATTERIES[name] - count} case(s) that used to run no longer do.`, + ); + } + if (floorProblems.length > 0) { + for (const problem of floorProblems) console.error(`✗ self-test floor: ${problem}`); + console.error( + '✗ self-test floor: A battery below its floor means cases STOPPED RUNNING — the battery is the ' + + 'bug, not the number. Find what stopped registering (a deleted check, a guard that now skips, ' + + 'an early return) and restore it.', + ); + } + + if (failures.length > 0 || floorProblems.length > 0) { + if (failures.length > 0) console.error(`self-test FAILED: ${failures.join('; ')}`); return 1; } - console.log(`self-test OK — ${STAMP_PATTERNS.length} patterns and every waiver direction pinned`); + // The case count is printed because a reader had to hand-tally the `check()` + // calls to get one, and it is printed AFTER the floor rather than instead of + // it: the number is evidence, the floor is the proof. + const registeredCases = [...seen.values()].reduce((a, b) => a + b, 0); + console.log(`self-test OK — ${STAMP_PATTERNS.length} patterns and every waiver direction pinned; ` + + `${registeredCases} case(s) across ${declaredBatteries.length} batteries, every battery at or above its pinned floor`); + selfTestReachedVerdict = true; return 0; } if (process.argv[1] !== undefined && fileURLToPath(import.meta.url) === resolve(process.argv[1])) { const args = process.argv.slice(2); - process.exit(args.includes('--self-test') ? selfTest() : run(args.includes('--report'))); + if (args.includes('--self-test')) { + const selfTestCode = selfTest(); + // The handshake. Without it a `return` above the verdict prints nothing and + // hands back a 0 that travels `selfTest()` → `process.exit()` unchanged, + // reporting a self-test that never finished as one that passed. The + // self-test's own exit code stays load-bearing — this only refuses to + // believe a SILENT one. + if (!selfTestReachedVerdict) { + console.error( + '\n✗ check-error-code-provenance self-test: selfTest() returned without reaching its verdict,\n' + + 'so no verdict line was printed. Exiting 0 here would report a self-test that never\n' + + 'finished as a self-test that passed.\n', + ); + process.exit(1); + } + process.exit(selfTestCode); + } + process.exit(run(args.includes('--report'))); } diff --git a/packages/spec/scripts/check-exported-any.ts b/packages/spec/scripts/check-exported-any.ts index 7150b80a00..99c2fbe725 100644 --- a/packages/spec/scripts/check-exported-any.ts +++ b/packages/spec/scripts/check-exported-any.ts @@ -192,6 +192,65 @@ function makeProgram(files: string[], extra: ts.CompilerOptions = {}): ts.Progra }); } +// ── Self-test: the handshake flag ──────────────────────────────────────────── +// +// Set by `selfTest()` only after its verdict line prints, and read at the +// dispatch below: a `return` that leaves the function above that line prints +// nothing, and here it does not even stop — control falls through to the audit, +// which on a fresh dist prints its own green line and exits 0. A self-test that +// never finished, reported as one that passed. ⛔ AN EXIT CODE IS NOT A +// HANDSHAKE: `fail()` below exits 1 on its own, so the exit code stays +// load-bearing; the flag is the thing an early return cannot carry with it. +let selfTestReachedVerdict = false; + +// ── The self-test's own battery roster and floor ───────────────────────────── +// +// Reaching the `✅ self-test` line used to be this self-test's ONLY success +// condition, so "every case held" and "the cases never ran" printed the same +// line. Measured on THIS file rather than inherited from the sibling it is +// copied from: deleting `'InferredFromAnySchema'` from the RED-leg name list +// below de-registers one of the two type-half detection pins, and the run still +// prints that line byte-identically and still exits 0 — the +// 「a printed case count is EVIDENCE, NOT PROOF」 shape with the evidence +// missing as well, since this self-test printed no count at all. +// +// Closed the way `scripts/check-agent-model-declared.mjs` and its TypeScript +// ports (`scripts/check-test-typecheck.mts`, `check-duration-unit-keys.ts` in +// this very directory) closed it — COPIED and ⛔ never imported, because +// every self-test has to keep running standalone as +// `tsx scripts/check-exported-any.ts --self-test`, and a shared assertion +// module would be one point of failure for every instrument at once. What is +// pinned is the registered NAMES, not a number. +// +// A BATTERY HERE IS A SECTION: this self-test is a sequence of assertions +// grouped by what they hold, so each group opens with `battery('')` and +// every `expect()` after it is attributed to that name until the next opens. +// +// ⛔ A pinned TOTAL is not the repair — a battery falling from 8 cases to 1 +// keeps a total "right" the moment a sibling grows — and ⛔ neither is a roster +// DERIVED from the run: a count taken from the cases that ran can never notice +// one that stopped. The two fixture name lists are exactly what a shrink +// deletes from, so their floors are their live lengths. +// +// 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. +const SELF_TEST_BATTERIES: Readonly> = Object.freeze({ + 'the fixture compiles against the real zod': 1, + 'the fixture RESOLVES: the counts that keep every assertion below non-vacuous': 2, + 'the RED leg: an exported TYPE that IS `any` is flagged': 2, + 'the RED leg: an exported SCHEMA whose output is `any` is flagged': 1, + 'the GREEN leg: precise, `any`-CONTAINING and non-schema exports are NOT flagged': 8, +}); + +// DELETING an entry silences that battery's floor exactly as effectively as +// zeroing it, so the roster's own size is pinned too. +const SELF_TEST_BATTERY_FLOOR = 5; + +// The key a case 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 open last. +const UNATTRIBUTED_BATTERY = '(no battery open)'; + // ── Self-test ──────────────────────────────────────────────────────────────── /** @@ -200,12 +259,35 @@ function makeProgram(files: string[], extra: ts.CompilerOptions = {}): ts.Progra * forever, which is indistinguishable from "clean"), a false positive makes it * noise that someone will route around. */ -function selfTest(): never { +function selfTest(): void { const fail = (msg: string): never => { console.error(`✗ self-test: ${msg}`); process.exit(1); }; + // The battery ledger this self-test's floor is evaluated against. + // `battery()` opens a battery; every `expect()` 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. + // + // Registration is the FIRST statement of `expect()`, before the outcome is + // consulted, because the floor asserts REACH: a case that runs and FAILS + // still registers (and `fail()` exits loudly on its own), and only a case that + // never runs at all goes missing from the ledger. Routing registration + // through the failure path instead would register a case only when it failed + // — a fully green run would register 0 and every battery would read DID NOT + // RUN, the floor inverted rather than installed. + const seen = new Map(); + let openBattery: string | undefined; + const battery = (name: string): void => { + openBattery = name; + }; + const expect = (ok: boolean, msg: string | (() => string)): void => { + const attributedTo = openBattery ?? UNATTRIBUTED_BATTERY; + seen.set(attributedTo, (seen.get(attributedTo) ?? 0) + 1); + if (!ok) fail(typeof msg === 'function' ? msg() : msg); + }; + // Resolve the real zod so the fixture exercises the actual `ZodType` internals // this checker reads — a stub would keep passing after zod renamed them. const require = createRequire(import.meta.url); @@ -242,8 +324,12 @@ function selfTest(): never { paths: { zod: [zodDir], 'zod/*': [`${zodDir}/*`] }, }); + battery('the fixture compiles against the real zod'); const syntactic = program.getSyntacticDiagnostics(); - if (syntactic.length > 0) fail(`fixture does not parse: ${ts.flattenDiagnosticMessageText(syntactic[0].messageText, ' ')}`); + expect( + syntactic.length === 0, + () => `fixture does not parse: ${ts.flattenDiagnosticMessageText(syntactic[0].messageText, ' ')}`, + ); const { violations, types, schemas } = scan(program, { './fixture': fixture }, {}); const flagged = new Set(violations.map((v) => v.key.split(':')[1])); @@ -251,26 +337,97 @@ function selfTest(): never { // The fixture exports 6 type aliases and 4 schemas. A lower count means the // scan is not seeing them at all — which would make every assertion below // pass vacuously, the exact way a gate goes dormant. - if (types !== 6) fail(`saw ${types} exported types, expected 6 — the fixture's types are not resolving (zod unresolved?)`); - if (schemas !== 4) fail(`saw ${schemas} exported schemas, expected 4 — \`_output\` no longer resolves, so the schema half of this gate is DORMANT`); + battery('the fixture RESOLVES: the counts that keep every assertion below non-vacuous'); + expect(types === 6, `saw ${types} exported types, expected 6 — the fixture's types are not resolving (zod unresolved?)`); + expect(schemas === 4, `saw ${schemas} exported schemas, expected 4 — \`_output\` no longer resolves, so the schema half of this gate is DORMANT`); + battery('the RED leg: an exported TYPE that IS `any` is flagged'); for (const name of ['BareAny', 'InferredFromAnySchema']) { - if (!flagged.has(name)) fail(`missed exported type \`${name}\` — the type half of this gate is DORMANT`); + expect(flagged.has(name), `missed exported type \`${name}\` — the type half of this gate is DORMANT`); } - if (!flagged.has('AnySchema')) fail('missed `AnySchema` — the schema half of this gate is DORMANT'); + battery('the RED leg: an exported SCHEMA whose output is `any` is flagged'); + expect(flagged.has('AnySchema'), 'missed `AnySchema` — the schema half of this gate is DORMANT'); + battery('the GREEN leg: precise, `any`-CONTAINING and non-schema exports are NOT flagged'); for (const name of ['Precise', 'PreciseSchema', 'PlainSchema', 'InferredFromPlain', 'AnyInside', 'AnyArray', 'LooseSchema', 'NotASchema']) { - if (flagged.has(name)) fail(`false positive on \`${name}\` — only a type that IS \`any\` may be flagged`); + expect(!flagged.has(name), `false positive on \`${name}\` — only a type that IS \`any\` may be flagged`); } } finally { rmSync(dir, { recursive: true, force: true }); } - console.log('✅ self-test: detects `any` types and `any`-output schemas, and nothing else.'); - process.exit(0); + // ── The floor: every declared battery RAN, and ran its cases ────────────── + // + // 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 EQUALS the set declared, each at or above its own + // count. A set difference names WHICH battery stopped; a count says only that + // something did — and, before this block existed, not even that. + const floorProblems: string[] = []; + const declaredBatteries = Object.keys(SELF_TEST_BATTERIES); + if (declaredBatteries.length < SELF_TEST_BATTERY_FLOOR) { + floorProblems.push( + `SELF_TEST_BATTERIES declares ${declaredBatteries.length} batteries, below the pinned ` + + `${SELF_TEST_BATTERY_FLOOR} — a battery deleted from the roster takes its own floor with it.`, + ); + } + for (const [name, count] of seen) { + if (declaredBatteries.includes(name)) continue; + floorProblems.push( + `self-test battery "${name}" registered ${count} case(s) but is not declared in ` + + 'SELF_TEST_BATTERIES — a case attributed to no declared battery is one nothing floors.', + ); + } + for (const name of declaredBatteries) { + const count = seen.get(name) ?? 0; + if (count >= SELF_TEST_BATTERIES[name]) continue; + floorProblems.push( + 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]} — ${SELF_TEST_BATTERIES[name] - count} case(s) that used to run no longer do.`, + ); + } + if (floorProblems.length > 0) { + for (const problem of floorProblems) console.error(`✗ self-test floor: ${problem}`); + console.error( + '✗ self-test floor: A battery below its floor means cases STOPPED RUNNING — the battery is the ' + + 'bug, not the number. Find what stopped registering (a name deleted from a fixture name list, ' + + 'a guard that now skips, an early return) and restore it.', + ); + process.exit(1); + } + + // The count is printed because a reader had to hand-tally the assertions to + // get one, and it is printed AFTER the floor rather than instead of it: the + // number is evidence, the floor is the proof. + const registered = [...seen.values()].reduce((a, b) => a + b, 0); + console.log( + '✅ self-test: detects `any` types and `any`-output schemas, and nothing else — ' + + `${registered} case(s) across ${declaredBatteries.length} batteries, every battery at or above ` + + 'its pinned floor.', + ); + selfTestReachedVerdict = true; } -if (SELF_TEST) selfTest(); +if (SELF_TEST) { + selfTest(); + // The handshake. Without it a `return` above the verdict prints nothing and + // does not even stop: control reaches the audit below, which on a fresh dist + // prints its own green line and exits 0 — a self-test that never finished, + // reported as one that passed. `fail()`'s own exit code stays load-bearing; + // this refuses to believe a SILENT success. + if (!selfTestReachedVerdict) { + console.error( + '\n✗ check-exported-any self-test: selfTest() returned without reaching its verdict,\n' + + 'so no verdict line was printed. Exiting 0 here would report a self-test that never\n' + + 'finished as a self-test that passed.\n', + ); + process.exit(1); + } + process.exit(0); +} // ── Audit ────────────────────────────────────────────────────────────────────