From ca1e17bfd0923dc0ad2891d49bfc034dd1533ca0 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 3 Sep 2026 06:38:05 +0000 Subject: [PATCH] test(scripts): pin a battery roster floor on 18 class-1 self-tests Batch 1b of #13799: the PR #13487 roster-floor shape transplanted onto the `scripts/**` self-tests whose assertion sink is NOT a block-bodied helper inside the self-test body -- a concise arrow, or a module-scope function -- so batch 1 (PR #14851) could not transplant it verbatim. Per the batch-1 review ruling, the roster machinery lives at MODULE scope (SELF_TEST_BATTERIES / SELF_TEST_BATTERY_FLOOR / UNATTRIBUTED_BATTERY / battery() / registerCase() / batteryFloorFailures()), and each file's existing assertion sink is given a minimal block body that calls registerCase() and returns the original expression unchanged. No case is rewritten, none is reordered, and no assertion changes meaning: all 18 self-tests exit with the same code and byte-identical output before and after. What is pinned is the registered NAMES, not a total: every existing section banner opens a battery, every assertion is attributed to the battery most recently opened, the floor requires the OPENED set to equal the DECLARED set with each battery at or above its own count, and the roster's own size is pinned so deleting an entry cannot silence a floor quietly. None of the introduced helpers is named with a self-test spelling. That is deliberate and recorded beside them: `check:pm-dispatch-gates` anchors on a top-level declaration whose NAME spells self-test, and every such name owes a row in that gate's COMPOUND_ANCHOR_LEDGER. These helpers hold no fixtures to mask and read no path literal, so the accurate name is the one that says `battery`. One battery is pinned at its structural invariant rather than at today's count, with the reason written over the entry: check-plugin-teardown-shape's exclusions battery runs exactly one case per DELIBERATELY_EXCLUDED row, and promoting a name onto the teardown roster is a legitimate edit that shrinks that list. Census (`node scripts/measure-self-test-floor.mjs --json`): ROSTER 3 -> 21, NONE 158 -> 140; the set of files whose class changed equals this worklist exactly. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_015k1DVWthZyPS7xi1Q72YpK --- scripts/check-cli-command-ids.mjs | 128 +++++++++++++++- scripts/check-cli-test-child-env.mjs | 162 +++++++++++++++++++- scripts/check-cross-package-test-inputs.mjs | 126 ++++++++++++++- scripts/check-dual-build-cjs-loads.mjs | 142 ++++++++++++++++- scripts/check-entry-guard.mjs | 132 +++++++++++++++- scripts/check-live-db-isolation.mjs | 130 +++++++++++++++- scripts/check-parse-guard.mjs | 138 ++++++++++++++++- scripts/check-partof-closing-keyword.mjs | 130 +++++++++++++++- scripts/check-plugin-teardown-shape.mjs | 144 ++++++++++++++++- scripts/check-published-list-mirrors.mjs | 128 +++++++++++++++- scripts/check-runner-env-posture.mjs | 128 +++++++++++++++- scripts/check-single-claim-paths.mjs | 126 ++++++++++++++- scripts/check-tenant-audit-census.mjs | 120 ++++++++++++++- scripts/check-watch-hint-literal.mjs | 130 +++++++++++++++- scripts/import-prerequisite.mjs | 146 +++++++++++++++++- scripts/invoked-as.mjs | 116 +++++++++++++- scripts/qa/qa-rollup.mjs | 130 ++++++++++++++++ scripts/ts-parse.mjs | 136 +++++++++++++++- 18 files changed, 2375 insertions(+), 17 deletions(-) diff --git a/scripts/check-cli-command-ids.mjs b/scripts/check-cli-command-ids.mjs index 4fa1892dad..35c5fa2a02 100644 --- a/scripts/check-cli-command-ids.mjs +++ b/scripts/check-cli-command-ids.mjs @@ -113,6 +113,117 @@ import { fileURLToPath } from 'node:url'; import { execFileSync } from 'node:child_process'; import { isEntrypoint } from './invoked-as.mjs'; +// ── The self-test's own battery roster and floor (#13489) ────────────────── +// +// This self-test used to decide success by "no failure was recorded" and +// nothing else, 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. A set difference says WHICH +// battery stopped; a count says only that something did. +// +// 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. +// +// The machinery lives HERE, at module scope, rather than inside the self-test: +// this self-test's assertion sink is not a block-bodied helper in its body (it +// is a concise arrow, or a module-scope function), so there is no in-body +// helper to thread a per-run ledger through. Module scope is safe because the +// self-test runs once per process, and it is what lets the existing sink route +// through `registerCase()` with no case rewritten and no assertion changed. +const SELF_TEST_BATTERIES = Object.freeze({ + 'the id derivation, against a scratch tree (no repo state)': 12, + 'a KNOWN-BAD literal: a command id that resolves to nothing': 3, + 'the delimiter rule: the six measured noise shapes stay OUT': 6, + 'the exemption ledger is site-scoped, not blanket': 3, + 'the ledger self-retires: a listed entry that stops reproducing REDS': 3, + 'the dispatch-gates declaration (#12016\'s own landing obligation)': 5, + 'bin names come from declared data': 3, + 'the live repo returns a verdict, and it is green': 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 = 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)'; + +// ⚠️ None of these helpers is named with a self-test spelling, deliberately and +// on the record: `check:pm-dispatch-gates` anchors on a top-level declaration +// whose NAME spells self-test, and every such name owes a row in that gate's +// COMPOUND_ANCHOR_LEDGER. These are the battery ROSTER's machinery -- they hold +// no fixtures to mask and read no path literal -- so the accurate name is the +// one that says `battery`, not the one that would owe a ledger row for a role +// this code does not have. + +/** Cases registered per battery: `battery()` opens one, `registerCase()` files into it. */ +const batteryCases = new Map(); +let openBattery = null; + +/** Open a battery. Every assertion after this line is attributed to it. */ +function battery(name) { + openBattery = name; +} + +/** Called by the self-test's own assertion sink, once per assertion. */ +function registerCase() { + const name = openBattery ?? UNATTRIBUTED_BATTERY; + batteryCases.set(name, (batteryCases.get(name) ?? 0) + 1); +} + +/** + * The floor: every declared battery RAN, and ran its cases (#13489). + * + * Evaluated after every battery has had its chance and BEFORE the verdict, so + * the success line can only be printed by a run in which the set of batteries + * that registered assertions EQUALS the set declared. + */ +function batteryFloorFailures() { + const declared = Object.keys(SELF_TEST_BATTERIES); + const problems = []; + if (declared.length < SELF_TEST_BATTERY_FLOOR) { + problems.push( + `SELF_TEST_BATTERIES declares ${declared.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 batteryCases) { + if (declared.includes(name)) continue; + problems.push( + `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 declared) { + const count = batteryCases.get(name) ?? 0; + if (count >= SELF_TEST_BATTERIES[name]) continue; + problems.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]} — cases that used to run no longer do.`, + ); + } + if (problems.length) { + problems.push( + '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 problems; +} + const REPO_ROOT = join(dirname(fileURLToPath(import.meta.url)), '..'); const OCLIF_COMMANDS_DIR = 'src/commands'; @@ -453,9 +564,13 @@ let selfTestReachedVerdict = false; function selfTest() { const cases = []; - const t = (name, ok, detail = '') => cases.push({ name, ok, detail }); + const t = (name, ok, detail = '') => { + registerCase(); + return cases.push({ name, ok, detail }); + }; // -- the id derivation, against a scratch tree (no repo state) -------------- + battery('the id derivation, against a scratch tree (no repo state)'); const dir = mkdtempSync(join(tmpdir(), 'cli-cmd-ids-')); try { const cmds = join(dir, 'src', 'commands'); @@ -499,6 +614,7 @@ function selfTest() { } // -- a KNOWN-BAD literal: a command id that resolves to nothing ------------- + battery('a KNOWN-BAD literal: a command id that resolves to nothing'); const ids = new Set(['migrate apply', 'migrate', 'build']); const topics = new Set(['migrate']); const bad = literalsOn("throw new Error('run \"os migrate nonexistent-command\" first');", ['os']); @@ -508,6 +624,7 @@ function selfTest() { resolveId(literalsOn('`os migrate apply`', ['os'])[0].words, ids, topics) === 'migrate apply'); // -- the delimiter rule: the six measured noise shapes stay OUT ------------ + battery('the delimiter rule: the six measured noise shapes stay OUT'); t('Spanish prose ("envios diarios") is not a literal', literalsOn("label: 'Limite de envios diarios',", ['os']).length === 0); t('a Python import example is not a literal', literalsOn("import os from 'os';", ['os']).length === 0); t('an unquoted sentence is not a literal', literalsOn('`carry an os validate-clean security posture`,', ['os']).length === 0); @@ -516,11 +633,13 @@ function selfTest() { t('a bin name at a quote IS a literal', literalsOn('via "os migrate apply --allow-destructive".', ['os']).length === 1); // -- the exemption ledger is site-scoped, not blanket ---------------------- + battery('the exemption ledger is site-scoped, not blanket'); t('a declared fixture is exempt', isExempt('scripts/docs-audit/check-drift-comment.mjs', 'os demo')); t('the SAME text elsewhere is NOT exempt', !isExempt('packages/drivers/driver-sql/src/schema-drift.ts', 'os demo')); t('a DIFFERENT text in an exempt file is NOT exempt', !isExempt('scripts/docs-audit/check-drift-comment.mjs', 'os migrate gone')); // -- the ledger self-retires: a listed entry that stops reproducing REDS --- + battery('the ledger self-retires: a listed entry that stops reproducing REDS'); t('every ledger entry reproduces in the live scan', audit().stale.length === 0, audit().stale.map((e) => `${e.file} "${e.text}"`).join('; ')); t('a fabricated ledger entry would be reported stale', @@ -546,6 +665,7 @@ function selfTest() { // forever and pays itself out as a dev dispatched on a scripts/ card with this gate // missing from the brief. Both directions are pinned, and both matter — a missing // declaration is a silent gate, a surplus one is a lying gate. + battery('the dispatch-gates declaration (#12016\'s own landing obligation)'); const separatorless = POPULATION_ROOTS.filter((r) => !r.includes('/')); t('every whole-root population entry is declared as a subtree (a bare root is refused by ' + 'hintCovers as too generic, so it needs the `/**` spelling)', @@ -588,11 +708,13 @@ function selfTest() { && new Set(audit().resolved.filter((x) => x.file.startsWith('scripts/')).map((x) => x.file)).size >= 5); // -- bin names come from declared data ------------------------------------ + battery('bin names come from declared data'); t('oclif.bin is read', binNamesOf({ oclif: { bin: 'os' } }).includes('os')); t('bin keys join it', binNamesOf({ oclif: { bin: 'os' }, bin: { objectstack: './bin/run.js' } }).includes('objectstack')); t('a package with no oclif block declares no bins', binNamesOf({ bin: { foo: 'x' } }).length === 0); // -- the live repo returns a verdict, and it is green ---------------------- + battery('the live repo returns a verdict, and it is green'); const live = audit(); t('the live audit returns a verdict', live.refusal === null, live.refusal ?? ''); t('the live repo has at least one CLI package', live.refusal === null && live.clis.length >= 1); @@ -602,6 +724,10 @@ function selfTest() { live.refusal === null && live.resolved.some((x) => x.file === 'packages/drivers/driver-sql/src/schema-drift.ts' && x.text === 'os migrate multi-value-columns')); + // The floor runs BEFORE the verdict below, so a success line can only be + // printed by a run in which every declared battery registered its cases. + for (const message of batteryFloorFailures()) cases.push({ name: message, ok: false, detail: '' }); + const failed = cases.filter((c) => !c.ok); for (const c of failed) console.error(` ✗ ${c.name}${c.detail ? ` -- ${c.detail}` : ''}`); if (failed.length) { diff --git a/scripts/check-cli-test-child-env.mjs b/scripts/check-cli-test-child-env.mjs index 0f37839136..d290b48b5d 100644 --- a/scripts/check-cli-test-child-env.mjs +++ b/scripts/check-cli-test-child-env.mjs @@ -309,6 +309,134 @@ const ts = await requireDefaultExport('typescript', () => import('typescript'), import { isEntrypoint } from './invoked-as.mjs'; import { parseSourceFile } from './ts-parse.mjs'; +// ── The self-test's own battery roster and floor (#13489) ────────────────── +// +// This self-test used to decide success by "no failure was recorded" and +// nothing else, 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. A set difference says WHICH +// battery stopped; a count says only that something did. +// +// 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. +// +// The machinery lives HERE, at module scope, rather than inside the self-test: +// this self-test's assertion sink is not a block-bodied helper in its body (it +// is a concise arrow, or a module-scope function), so there is no in-body +// helper to thread a per-run ledger through. Module scope is safe because the +// self-test runs once per process, and it is what lets the existing sink route +// through `registerCase()` with no case rewritten and no assertion changed. +const SELF_TEST_BATTERIES = Object.freeze({ + '(1) the positive control: a bare spread in a spawner REDS': 1, + '(2) the childEnv() form is green': 1, + '(3) the negative control the whole precision claim rests on': 3, + '(4) reading ONE variable off the environment is never a finding': 4, + '(5) the other bulk spellings, so the rule is not spread-shaped': 6, + '(6) prose is not code, which is what an AST buys over a text scan': 3, + '(7) the spawner roster, every import spelling': 13, + '(8) the carve-out, and that it is SITE-scoped rather than file-scoped': 2, + '(8b) SITE NAMING (#12531): a callback arrow is named after its CALL': 10, + '(8c) THE MODIFIER FAMILY (#12545): a rostered vitest chain names a': 23, + '(9) RULE 2 (#11595): a spawn CALL that leaves its env undeclared': 6, + 'rule 2\'s blind spot is the one place a call-anchored rule could go': 16, + '(10) the ratchet, in every direction it must move': 6, + '(11) every refusal, each PAIRED with a tree that still answers': 4, + '(12) THE anti-vacuity leg: the real entry point, out of process': 3, + '(13) the unparseable leg: ts-parse ends the PROCESS': 1, + '(16) RULE 3 (#11464): the built entrypoint and a rerouting NODE_ENV': 0, + 'membership': 5, + 'the rule over the members': 12, + 'the one-hop spread resolution': 12, + 'the declaration registry, site-scoped like DELIBERATE': 2, + 'the three rules are INDEPENDENT': 3, + 'OUT OF PROCESS: rule 3 can actually fail a run': 2, + '(14) wiring. Unwiring the gate must redden HERE, not go quiet.': 3, + '(15) the live tree, as a case rather than as the run\'s only evidence': 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 = 25; + +// 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)'; + +// ⚠️ None of these helpers is named with a self-test spelling, deliberately and +// on the record: `check:pm-dispatch-gates` anchors on a top-level declaration +// whose NAME spells self-test, and every such name owes a row in that gate's +// COMPOUND_ANCHOR_LEDGER. These are the battery ROSTER's machinery -- they hold +// no fixtures to mask and read no path literal -- so the accurate name is the +// one that says `battery`, not the one that would owe a ledger row for a role +// this code does not have. + +/** Cases registered per battery: `battery()` opens one, `registerCase()` files into it. */ +const batteryCases = new Map(); +let openBattery = null; + +/** Open a battery. Every assertion after this line is attributed to it. */ +function battery(name) { + openBattery = name; +} + +/** Called by the self-test's own assertion sink, once per assertion. */ +function registerCase() { + const name = openBattery ?? UNATTRIBUTED_BATTERY; + batteryCases.set(name, (batteryCases.get(name) ?? 0) + 1); +} + +/** + * The floor: every declared battery RAN, and ran its cases (#13489). + * + * Evaluated after every battery has had its chance and BEFORE the verdict, so + * the success line can only be printed by a run in which the set of batteries + * that registered assertions EQUALS the set declared. + */ +function batteryFloorFailures() { + const declared = Object.keys(SELF_TEST_BATTERIES); + const problems = []; + if (declared.length < SELF_TEST_BATTERY_FLOOR) { + problems.push( + `SELF_TEST_BATTERIES declares ${declared.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 batteryCases) { + if (declared.includes(name)) continue; + problems.push( + `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 declared) { + const count = batteryCases.get(name) ?? 0; + if (count >= SELF_TEST_BATTERIES[name]) continue; + problems.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]} — cases that used to run no longer do.`, + ); + } + if (problems.length) { + problems.push( + '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 problems; +} + const HERE = dirname(fileURLToPath(import.meta.url)); const REPO_ROOT = resolve(HERE, '..'); @@ -1650,7 +1778,10 @@ let selfTestReachedVerdict = false; export function selfTest() { const cases = []; - const t = (name, ok, detail) => cases.push({ name, ok: Boolean(ok), detail }); + const t = (name, ok, detail) => { + registerCase(); + return cases.push({ name, ok: Boolean(ok), detail }); + }; const SELF = fileURLToPath(import.meta.url); const dir = mkdtempSync(join(tmpdir(), 'cli-test-child-env-')); @@ -1709,10 +1840,12 @@ export function selfTest() { try { // -- (1) the positive control: a bare spread in a spawner REDS ---------- + battery('(1) the positive control: a bare spread in a spawner REDS'); t('a bare process.env spread in a spawner file REDS', count('pos-spread', { 'a.e2e.test.ts': spawner(' execFile(\'x\', [], { cwd, env: { ...process.env, NO_COLOR: \'1\' } });') }) === 1); // -- (2) the childEnv() form is green ----------------------------------- + battery('(2) the childEnv() form is green'); t('the childEnv() form stays GREEN', count('neg-childenv', { 'a.e2e.test.ts': spawner(' execFile(\'x\', [], { cwd, env: childEnv({ NO_COLOR: \'1\' }) });') }) === 0); @@ -1720,6 +1853,7 @@ export function selfTest() { // A legitimate non-spawner bulk copy -- the ordinary save/restore around // an in-process mutation. This is the shape a package-wide scan would // flag, and flagging it is how a gate gets carved out into uselessness. + battery('(3) the negative control the whole precision claim rests on'); t('a save/restore bulk copy in a NON-spawner file stays GREEN (the negative control)', count('neg-nonspawner', { ...COMPANION, @@ -1733,6 +1867,7 @@ export function selfTest() { count('restore-only', { 'a.e2e.test.ts': spawner(' process.env = build();') }) === 0); // -- (4) reading ONE variable off the environment is never a finding ---- + battery('(4) reading ONE variable off the environment is never a finding'); t('process.env.HOME is a member read, not a bulk copy', count('read-dot', { 'a.e2e.test.ts': spawner(' const h = process.env.HOME;\n void h;') }) === 0); t("process.env['HOME'] is a member read too", @@ -1745,6 +1880,7 @@ export function selfTest() { }) === 0); // -- (5) the other bulk spellings, so the rule is not spread-shaped ----- + battery('(5) the other bulk spellings, so the rule is not spread-shaped'); t('Object.assign({}, process.env) is a bulk copy', count('bulk-assign', { 'a.e2e.test.ts': spawner(' const e = Object.assign({}, process.env, { NO_COLOR: \'1\' });\n void e;') }) === 1); t('Object.entries(process.env) is a bulk copy', @@ -1759,6 +1895,7 @@ export function selfTest() { count('bulk-two', { 'a.e2e.test.ts': spawner(' const a = { ...process.env };\n const b = { ...process.env };\n void a; void b;') }) === 2); // -- (6) prose is not code, which is what an AST buys over a text scan -- + battery('(6) prose is not code, which is what an AST buys over a text scan'); t('a docblock quoting the banned shape is not a finding', count('prose-block', { 'a.e2e.test.ts': spawner(' /** never write { ...process.env } here */\n void cwd;') }) === 0); t('a line comment quoting it is not a finding', @@ -1767,6 +1904,7 @@ export function selfTest() { count('prose-string', { 'a.e2e.test.ts': spawner(' const s = \'{ ...process.env }\';\n void s;') }) === 0); // -- (7) the spawner roster, every import spelling ---------------------- + battery('(7) the spawner roster, every import spelling'); for (const [mod, roster] of Object.entries(SPAWN_MODULES)) { for (const api of roster) { const src = `import { ${api} } from 'node:${mod}';\nvoid ${api};\nexport const e = { ...process.env };\n`; @@ -1785,6 +1923,7 @@ export function selfTest() { count('roster-non-spawn-member', { ...COMPANION, 'a.ts': 'import { ChildProcess } from \'node:child_process\';\nvoid ChildProcess;\nexport const e = { ...process.env };\n' }) === 0); // -- (8) the carve-out, and that it is SITE-scoped rather than file-scoped + battery('(8) the carve-out, and that it is SITE-scoped rather than file-scoped'); const chokePath = 'helpers/serve-process.ts'; const carved = scan('carve-hit', { [chokePath]: 'import { spawn } from \'node:child_process\';\nvoid spawn;\nexport function childEnv() {\n return { ...process.env };\n}\n', @@ -1805,6 +1944,7 @@ export function selfTest() { // An arrow passed DIRECTLY as a call argument matches none of the four // shapes the walk recognises, so before this it ran past `it(...)` and // `beforeAll(...)` to the source file and reported `(top-level)`. + battery('(8b) SITE NAMING (#12531): a callback arrow is named after its CALL'); /** The site names a one-file spawner tree attributes its bulk copies to. */ const siteNames = (name, sources) => JSON.stringify(scan(name, sources).findings?.map((row) => row.fn)); @@ -1862,6 +2002,7 @@ export function selfTest() { // what that case's comment was really protecting -- the refusal of // `promise.then` and `rows.map` -- is pinned outright further down, // where it is a measurement rather than a side effect. + battery('(8c) THE MODIFIER FAMILY (#12545): a rostered vitest chain names a'); t('an it.skip() block is named, not (top-level)', siteNames('site-mod-skip', { 'a.e2e.test.ts': suite(`it.skip('x', () => {\n${BULK}\n});`) }) @@ -1991,6 +2132,7 @@ export function selfTest() { // The reds here are the whole point of the rule, so they are pinned by // REASON as well as by count: "reds for some reason" would still pass if // the classifier collapsed every shape into one. + battery('(9) RULE 2 (#11595): a spawn CALL that leaves its env undeclared'); const undeclared = (name, sources) => audit(tree(name, sources)).envless ?? []; const reasons = (name, sources) => undeclared(name, sources).map((row) => row.reason); @@ -2019,6 +2161,7 @@ export function selfTest() { // An options object this scan cannot read is a FINDING, never a quiet pass // -- rule 2's blind spot is the one place a call-anchored rule could go // silent, which is the defect this rule exists to close. + battery('rule 2\'s blind spot is the one place a call-anchored rule could go'); t('an options object passed as an identifier REDS as unreadable, not green', JSON.stringify(reasons('envless-opaque-ident', { 'a.e2e.test.ts': spawner(' execFile(\'x\', [], opts);') })) === JSON.stringify([ENVLESS.OPAQUE])); @@ -2091,6 +2234,7 @@ export function selfTest() { }).map((row) => row.fn)) === names('it("spawns a probe")')); // -- (10) the ratchet, in every direction it must move ----------------- + battery('(10) the ratchet, in every direction it must move'); const one = [{ file: 'packages/cli/test/a.ts', fn: 'runCli', line: 1, text: 'x' }]; const two = [...one, { file: 'packages/cli/test/a.ts', fn: 'runOther', line: 2, text: 'x' }]; const allDeliberate = Object.keys(DELIBERATE).map((key) => { @@ -2113,6 +2257,7 @@ export function selfTest() { && judge([], allDeliberate, {}).missing.length === 0); // -- (11) every refusal, each PAIRED with a tree that still answers ---- + battery('(11) every refusal, each PAIRED with a tree that still answers'); const emptyRoot = tree('refuse-empty', { 'README.md': 'not a source\n' }); t('a population with no TypeScript source REFUSES, while a readable tree still answers', audit(emptyRoot).refusal !== null && audit(tree('refuse-empty-pair', READABLE)).refusal === null, @@ -2140,6 +2285,7 @@ export function selfTest() { // -- (12) THE anti-vacuity leg: the real entry point, out of process --- // "exits non-zero on a violation" is the claim, and a process cannot // observe its own exit status. These two run the real CLI. + battery('(12) THE anti-vacuity leg: the real entry point, out of process'); const redRoot = tree('oop-red', { 'a.e2e.test.ts': spawner(' execFile(\'x\', [], { cwd, env: { ...process.env, NO_COLOR: \'1\' } });'), }); @@ -2166,6 +2312,7 @@ export function selfTest() { JSON.stringify({ status: green.status, out: (green.stdout || '').trim() })); // -- (13) the unparseable leg: ts-parse ends the PROCESS --------------- + battery('(13) the unparseable leg: ts-parse ends the PROCESS'); const wreckRoot = tree('oop-wreck', { ...READABLE, 'wreck.ts': 'import { spawn } from \'node:child_process\';\n<<<<<<< HEAD\nvoid spawn;\n=======\nvoid 0;\n>>>>>>> other\n', @@ -2182,6 +2329,7 @@ export function selfTest() { // wrong is the more expensive error -- it is what puts a file that only // NAMES bin/run.js into a population it does not belong to, which is the // census error this card was dispatched with. + battery('(16) RULE 3 (#11464): the built entrypoint and a rerouting NODE_ENV'); /** * A file that binds the BUILT entrypoint and spawns it -- the population's @@ -2203,6 +2351,7 @@ export function selfTest() { const memberCount = (name, sources) => audit(tree(name, sources), DELIBERATE, {}).builtSpawns; // -- membership -------------------------------------------------------- + battery('membership'); t('a spawn through a const bound to bin/run.js is in the population', memberCount('member-binding', { 'a.e2e.test.ts': built('childEnv({ NODE_ENV: undefined })') }) === 1); t('a spawn naming bin/run.js as a bare literal in argv is in the population too', @@ -2236,6 +2385,7 @@ export function selfTest() { }) === 0); // -- the rule over the members ----------------------------------------- + battery('the rule over the members'); t('NODE_ENV: undefined -- what the population says today -- stays GREEN', reroutes('rule-unset', 'childEnv({ NO_COLOR: \'1\', NODE_ENV: undefined })').length === 0); t('NODE_ENV: void 0 is the same value with different punctuation', @@ -2287,6 +2437,7 @@ export function selfTest() { // Every real site in the population is `childEnv({ ..., NODE_ENV: // undefined, ...env })`, so without this hop the rule reads NOTHING it // is meant to read and reports four "gave up" findings instead. + battery('the one-hop spread resolution'); const SPREAD = 'childEnv({ NODE_ENV: undefined, ...env })'; t('a trailing spread resolved to a caller that overrides NOTHING stays GREEN', @@ -2332,6 +2483,7 @@ export function selfTest() { 'export const a = boot({ NODE_ENV: \'production\' });').length === 0); // -- the declaration registry, site-scoped like DELIBERATE ------------- + battery('the declaration registry, site-scoped like DELIBERATE'); const rerouteTree = tree('reroute-registry', { 'a.e2e.test.ts': built(SPREAD, 'it(\'first\', () => {\n boot({ NODE_ENV: \'development\' });\n});\n' @@ -2355,6 +2507,7 @@ export function selfTest() { })).missingReroute.length === 0); // -- the three rules are INDEPENDENT ------------------------------------ + battery('the three rules are INDEPENDENT'); const onlyRule3 = audit(tree('independent-3', { 'a.e2e.test.ts': built('childEnv({ NODE_ENV: \'development\' })'), }), DELIBERATE, {}); @@ -2382,6 +2535,7 @@ export function selfTest() { && bothTwoThree.rerouted[0].reason === REROUTE.INHERITED); // -- OUT OF PROCESS: rule 3 can actually fail a run -------------------- + battery('OUT OF PROCESS: rule 3 can actually fail a run'); const rerouteRoot = tree('oop-reroute', { 'a.e2e.test.ts': built('childEnv({ NODE_ENV: \'development\' })'), }); @@ -2400,6 +2554,7 @@ export function selfTest() { JSON.stringify({ status: rerouteGreen.status, out: (rerouteGreen.stdout || '').trim() })); // -- (14) wiring. Unwiring the gate must redden HERE, not go quiet. ---- + battery('(14) wiring. Unwiring the gate must redden HERE, not go quiet.'); const pkg = JSON.parse(readFileSync(join(REPO_ROOT, 'package.json'), 'utf8')); const alias = pkg.scripts?.['check:cli-test-child-env'] ?? ''; t('a package.json alias invokes this script', /check-cli-test-child-env\.mjs/.test(alias), alias); @@ -2408,6 +2563,7 @@ export function selfTest() { t('a lint job runs the alias', lintYml.includes('pnpm check:cli-test-child-env')); // -- (15) the live tree, as a case rather than as the run's only evidence + battery('(15) the live tree, as a case rather than as the run\'s only evidence'); const live = audit(REPO_ROOT); t('the live tree resolves a real population (not zero, not a refusal)', live.refusal === null && live.files > 0 && live.spawners > 0, @@ -2495,6 +2651,10 @@ export function selfTest() { rmSync(dir, { recursive: true, force: true }); } + // The floor runs BEFORE the verdict below, so a success line can only be + // printed by a run in which every declared battery registered its cases. + for (const message of batteryFloorFailures()) cases.push({ name: message, ok: false }); + const failed = cases.filter((c) => !c.ok); for (const c of failed) console.error(` ✗ ${c.name}${c.detail ? ` -- ${c.detail}` : ''}`); if (failed.length) { diff --git a/scripts/check-cross-package-test-inputs.mjs b/scripts/check-cross-package-test-inputs.mjs index d63fa85f5d..1f192a56ee 100644 --- a/scripts/check-cross-package-test-inputs.mjs +++ b/scripts/check-cross-package-test-inputs.mjs @@ -231,6 +231,116 @@ import { CROSS_PACKAGE_TEST_INPUTS } from './cross-package-test-inputs.mjs'; import { matchesAny, selfTest as globMatchSelfTest } from './glob-match.mjs'; import { isEntrypoint } from './invoked-as.mjs'; +// ── The self-test's own battery roster and floor (#13489) ────────────────── +// +// This self-test used to decide success by "no failure was recorded" and +// nothing else, 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. A set difference says WHICH +// battery stopped; a count says only that something did. +// +// 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. +// +// The machinery lives HERE, at module scope, rather than inside the self-test: +// this self-test's assertion sink is not a block-bodied helper in its body (it +// is a concise arrow, or a module-scope function), so there is no in-body +// helper to thread a per-run ledger through. Module scope is safe because the +// self-test runs once per process, and it is what lets the existing sink route +// through `registerCase()` with no case rewritten and no assertion changed. +const SELF_TEST_BATTERIES = Object.freeze({ + 'the LINE-SPANNING DECLARATION (#11093)': 38, + 'the ANCHOR seeds (#10029)': 14, + 'The radius roster, reconstructed rather than quoted (#9763)': 9, + 'the INTERPOLATING TEMPLATE argument (#11487)': 4, + 'the INTERPOLATING TEMPLATE argument, `NEW_URL_LITERAL` sibling (#12085) ─': 7, + 'the RESOLVER half (#10452)': 43, + 'the entry guard, driven for real': 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)'; + +// ⚠️ None of these helpers is named with a self-test spelling, deliberately and +// on the record: `check:pm-dispatch-gates` anchors on a top-level declaration +// whose NAME spells self-test, and every such name owes a row in that gate's +// COMPOUND_ANCHOR_LEDGER. These are the battery ROSTER's machinery -- they hold +// no fixtures to mask and read no path literal -- so the accurate name is the +// one that says `battery`, not the one that would owe a ledger row for a role +// this code does not have. + +/** Cases registered per battery: `battery()` opens one, `registerCase()` files into it. */ +const batteryCases = new Map(); +let openBattery = null; + +/** Open a battery. Every assertion after this line is attributed to it. */ +function battery(name) { + openBattery = name; +} + +/** Called by the self-test's own assertion sink, once per assertion. */ +function registerCase() { + const name = openBattery ?? UNATTRIBUTED_BATTERY; + batteryCases.set(name, (batteryCases.get(name) ?? 0) + 1); +} + +/** + * The floor: every declared battery RAN, and ran its cases (#13489). + * + * Evaluated after every battery has had its chance and BEFORE the verdict, so + * the success line can only be printed by a run in which the set of batteries + * that registered assertions EQUALS the set declared. + */ +function batteryFloorFailures() { + const declared = Object.keys(SELF_TEST_BATTERIES); + const problems = []; + if (declared.length < SELF_TEST_BATTERY_FLOOR) { + problems.push( + `SELF_TEST_BATTERIES declares ${declared.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 batteryCases) { + if (declared.includes(name)) continue; + problems.push( + `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 declared) { + const count = batteryCases.get(name) ?? 0; + if (count >= SELF_TEST_BATTERIES[name]) continue; + problems.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]} — cases that used to run no longer do.`, + ); + } + if (problems.length) { + problems.push( + '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 problems; +} + const HERE = dirname(fileURLToPath(import.meta.url)); const REPO_ROOT = resolve(HERE, '..'); @@ -1488,7 +1598,11 @@ const SELF_TEST_VERDICT = 'check-cross-package-test-inputs self-test reached its function selfTest() { const cases = []; - const ok = (label, cond) => cases.push({ label, cond }); + const ok = (label, cond) => { + registerCase(); + return cases.push({ label, cond }); + }; + battery('the LINE-SPANNING DECLARATION (#11093)'); // glob semantics -- driven from the shared module rather than restated here, // so this gate and `check:examples-live-imports` are pinned against ONE set @@ -1742,6 +1856,7 @@ function selfTest() { // pin a NAME are the load-bearing pair -- a case asserting only "it does not // flag" passes just as happily on a seed that resolved to NOTHING, which is // precisely the bug. + battery('the ANCHOR seeds (#10029)'); const FIND_UP_FN = 'function findUp(predicate: (dir: string) => boolean, what: string): string {\n' + ' let dir = process.cwd();\n' + @@ -1889,6 +2004,7 @@ function selfTest() { // produce, because a case asserting only "some path came out" would pass just // as happily on a wrong one, and a wrong name is a roster entry pointing at a // file nobody reads. + battery('The radius roster, reconstructed rather than quoted (#9763)'); const named = (src, depth, fileSegs) => [...scanPathExpressions(src, depth, fileSegs).files]; const listed = (src, depth, fileSegs) => [...scanPathExpressions(src, depth, fileSegs).dirs]; // `packages/create-objectstack/src/x.test.ts` — depth 1 below its package root. @@ -2007,6 +2123,7 @@ function selfTest() { // unreadable argument is safe, and a template read as readable was LESS // safe than unreadable. Same climb, same file, only the middle argument // differs from the unreadable-argument pair just above. + battery('the INTERPOLATING TEMPLATE argument (#11487)'); const TEMPLATE_SEED = 'const HERE = dirname(fileURLToPath(import.meta.url));\n'; const TEMPLATE_UNREADABLE = TEMPLATE_SEED + "const P = join(HERE, someVar, '../../other-pkg/src/y.ts');"; const TEMPLATE_INTERP = TEMPLATE_SEED + "const P = join(HERE, `${someVar}`, '../../other-pkg/src/y.ts');"; @@ -2037,6 +2154,7 @@ function selfTest() { // the same outcome as any other unrecognised seed shape, NOT the depth-kept // outcome `PATH_LITERAL`'s pair above pins. So this case must assert // "does not flag, no name" rather than "flags at the unreadable depth". + battery('the INTERPOLATING TEMPLATE argument, `NEW_URL_LITERAL` sibling (#12085) ─'); const URL_TEMPLATE_INTERP = 'const P = new URL(`../../other-pkg/${someVar}`, import.meta.url);'; ok( "(control) the same climb spelled with a real segment instead of interpolation still flags and is named — proves the case above isn't vacuous", @@ -2092,6 +2210,7 @@ function selfTest() { // blind spot it closes — so `@objectstack/*`, `node:*` and a plain package // name are each pinned NOT to flag, rather than trusting one case to stand // for the class. + battery('the RESOLVER half (#10452)'); const specOf = (src, depth, fileSegs) => [...scanPathExpressions(src, depth, fileSegs).imports].map((p) => resolveImportTarget(p)).filter((p) => p !== null); // `packages/cli/src/commands/x.contract.test.ts` — the #10452 specimen, two @@ -2304,6 +2423,7 @@ function selfTest() { // A spawned child is the only honest witness: the guard's answer depends on // what node puts in `process.argv[1]`, which cannot be modelled in-process. // Without this case the guard can be deleted as quietly as it was missing. + battery('the entry guard, driven for real'); const importProbe = spawnSync( process.execPath, ['--input-type=module', '-e', `await import(${JSON.stringify(pathToFileURL(fileURLToPath(import.meta.url)).href)});\nconsole.log('ALIVE');`], @@ -2318,6 +2438,10 @@ function selfTest() { importProbe.status === 0 && (importProbe.stdout || '').includes('ALIVE'), ); + // The floor runs BEFORE the verdict below, so a success line can only be + // printed by a run in which every declared battery registered its cases. + for (const message of batteryFloorFailures()) cases.push({ label: message, cond: false }); + const failed = cases.filter((c) => !c.cond); for (const c of cases) console.log(`${c.cond ? 'ok ' : 'FAIL'} ${c.label}`); if (failed.length) { diff --git a/scripts/check-dual-build-cjs-loads.mjs b/scripts/check-dual-build-cjs-loads.mjs index c33244f1f5..172308deae 100644 --- a/scripts/check-dual-build-cjs-loads.mjs +++ b/scripts/check-dual-build-cjs-loads.mjs @@ -219,6 +219,124 @@ import { fileURLToPath } from 'node:url'; import { isEntrypoint } from './invoked-as.mjs'; +// ── The self-test's own battery roster and floor (#13489) ────────────────── +// +// This self-test used to decide success by "no failure was recorded" and +// nothing else, 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. A set difference says WHICH +// battery stopped; a count says only that something did. +// +// 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. +// +// The machinery lives HERE, at module scope, rather than inside the self-test: +// this self-test's assertion sink is not a block-bodied helper in its body (it +// is a concise arrow, or a module-scope function), so there is no in-body +// helper to thread a per-run ledger through. Module scope is safe because the +// self-test runs once per process, and it is what lets the existing sink route +// through `registerCase()` with no case rewritten and no assertion changed. +const SELF_TEST_BATTERIES = Object.freeze({ + 'the declaration itself': 4, + 'the exports resolver': 8, + 'the types resolver: what a `require` consumer actually READS': 6, + 'module kind: the reason the invariant is not "must end in .d.cts"': 6, + 'diagnostics classification': 3, + 'the fixture tree': 7, + 'the ledger, both directions': 10, + 'AGREES: the cross-format behaviour probe, both directions': 5, + 'TYPED: the #13112 class, in both directions': 9, + 'TYPED_EXEMPTIONS, both directions': 8, + 'prerequisite, never a silent green': 1, + 'a built tree missing one declared target IS a finding': 1, + 'the vacuity floors, each driven to zero': 13, + 'provenance: the record must stay reproducible, and visibly so': 6, + 'the real ledger is well-formed and shrink-only in shape': 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 = 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)'; + +// ⚠️ None of these helpers is named with a self-test spelling, deliberately and +// on the record: `check:pm-dispatch-gates` anchors on a top-level declaration +// whose NAME spells self-test, and every such name owes a row in that gate's +// COMPOUND_ANCHOR_LEDGER. These are the battery ROSTER's machinery -- they hold +// no fixtures to mask and read no path literal -- so the accurate name is the +// one that says `battery`, not the one that would owe a ledger row for a role +// this code does not have. + +/** Cases registered per battery: `battery()` opens one, `registerCase()` files into it. */ +const batteryCases = new Map(); +let openBattery = null; + +/** Open a battery. Every assertion after this line is attributed to it. */ +function battery(name) { + openBattery = name; +} + +/** Called by the self-test's own assertion sink, once per assertion. */ +function registerCase() { + const name = openBattery ?? UNATTRIBUTED_BATTERY; + batteryCases.set(name, (batteryCases.get(name) ?? 0) + 1); +} + +/** + * The floor: every declared battery RAN, and ran its cases (#13489). + * + * Evaluated after every battery has had its chance and BEFORE the verdict, so + * the success line can only be printed by a run in which the set of batteries + * that registered assertions EQUALS the set declared. + */ +function batteryFloorFailures() { + const declared = Object.keys(SELF_TEST_BATTERIES); + const problems = []; + if (declared.length < SELF_TEST_BATTERY_FLOOR) { + problems.push( + `SELF_TEST_BATTERIES declares ${declared.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 batteryCases) { + if (declared.includes(name)) continue; + problems.push( + `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 declared) { + const count = batteryCases.get(name) ?? 0; + if (count >= SELF_TEST_BATTERIES[name]) continue; + problems.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]} — cases that used to run no longer do.`, + ); + } + if (problems.length) { + problems.push( + '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 problems; +} + const HERE = dirname(fileURLToPath(import.meta.url)); const REPO_ROOT = resolve(HERE, '..'); const SCAN_ROOT = 'packages'; @@ -1083,9 +1201,13 @@ let selfTestReachedVerdict = false; export async function selfTest() { const cases = []; - const t = (name, ok, detail) => cases.push({ name, ok: Boolean(ok), detail }); + const t = (name, ok, detail) => { + registerCase(); + return cases.push({ name, ok: Boolean(ok), detail }); + }; // ── the declaration itself ──────────────────────────────────────────────── + battery('the declaration itself'); const selfSrc = readFileSync(fileURLToPath(import.meta.url), 'utf8'); t('the watch hints are spelled as literals the extractor can read', ROOT_DIR_WATCH_HINTS.every((h) => selfSrc.includes(`'${h}'`))); t('the watch hints cover the manifest and the tsup config', ROOT_DIR_WATCH_HINTS.length === 2 && ROOT_DIR_WATCH_HINTS.every((h) => h.startsWith(`${SCAN_ROOT}/`))); @@ -1093,6 +1215,7 @@ export async function selfTest() { t('no hint collapses to the bare scan root', !ROOT_DIR_WATCH_HINTS.some((h) => h.replace(/\/\*+$/, '') === SCAN_ROOT)); // ── the exports resolver ────────────────────────────────────────────────── + battery('the exports resolver'); t('a plain string under `require` resolves', resolveRequireTarget({ types: './x.d.ts', import: './x.js', require: './x.cjs' }) === './x.cjs'); t('a NESTED {types, default} under `require` resolves (the @objectstack/spec spelling)', resolveRequireTarget({ import: { types: './a.d.mts', default: './a.mjs' }, require: { types: './a.d.ts', default: './a.js' } }) === './a.js'); t('an `import`-only entry contributes nothing', resolveRequireTarget({ types: './x.d.ts', import: './x.js' }) === null); @@ -1103,6 +1226,7 @@ export async function selfTest() { t('a wildcard subpath is skipped (no single file to probe)', requireEntries({ exports: { './x/*': { require: './x/*.cjs' } } }).length === 0); // ── the types resolver: what a `require` consumer actually READS ────────── + battery('the types resolver: what a `require` consumer actually READS'); t('THE #13112 shape: a SIBLING `types` answers for the require condition too', resolveRequireTypes({ types: './x.d.ts', import: './x.js', require: './x.cjs' }) === './x.d.ts'); t('a nested `types` inside the require branch wins over nothing else', @@ -1114,6 +1238,7 @@ export async function selfTest() { t('`browser` is not a types source', resolveRequireTypes({ browser: { types: './b.d.ts' }, require: { types: './n.d.cts', default: './n.cjs' } }) === './n.d.cts'); // ── module kind: the reason the invariant is not "must end in .d.cts" ──── + battery('module kind: the reason the invariant is not "must end in .d.cts"'); t('`.d.cts` is CommonJS whatever the package type', declarationModuleKind('./x.d.cts', true) === 'commonjs' && declarationModuleKind('./x.d.cts', false) === 'commonjs'); t('`.d.mts` is ESM whatever the package type', declarationModuleKind('./x.d.mts', false) === 'module'); t('GREEN CONTROL — `.d.ts` in a CJS-first package is CommonJS-flavoured and correct under require', @@ -1123,11 +1248,13 @@ export async function selfTest() { t('requireTypesFor falls back to the root `types` for a (main) row', requireTypesFor({ types: 'dist/index.d.ts' }, '(main)') === './dist/index.d.ts'); // ── diagnostics classification ──────────────────────────────────────────── + battery('diagnostics classification'); t('a SyntaxError diagnostic is a parse failure', isParseFailure("foo.cjs:1\nSyntaxError: Cannot use 'import.meta' outside a module")); t('a plain Error diagnostic is NOT a parse failure', !isParseFailure('Error: Vitest cannot be imported in a CommonJS module using require().')); t('firstErrorLine picks the error, not the source echo', firstErrorLine("dist/index.cjs:810\n const x = import.meta.url\nSyntaxError: Cannot use 'import.meta' outside a module") === "SyntaxError: Cannot use 'import.meta' outside a module"); // ── the fixture tree ────────────────────────────────────────────────────── + battery('the fixture tree'); const root = mkdtempSync(join(tmpdir(), 'dual-cjs-')); try { // The shape the TYPED invariant demands: each condition names its own @@ -1187,6 +1314,7 @@ export async function selfTest() { t('the load finding carries the real message', r1.findings.some((f) => f.includes('nope at load'))); // ── the ledger, both directions ─────────────────────────────────────────── + battery('the ledger, both directions'); const withLedger = { '@t/throws#.': { reason: 'declared for the self-test' } }; const r2 = await scan(root, withLedger, []); t('a ledgered load failure is declared, not a finding', !r2.findings.some((f) => f.startsWith('@t/throws#.')) && r2.ledgerHits.some((h) => h.startsWith('@t/throws#.')), JSON.stringify(r2.ledgerHits)); @@ -1216,6 +1344,7 @@ export async function selfTest() { && orphanLedgerRows({ 'b#.': {}, 'a#.': {} }, [{ id: 'a#.' }])[0].startsWith('b#.')); // ── AGREES: the cross-format behaviour probe, both directions ──────────── + battery('AGREES: the cross-format behaviour probe, both directions'); writePkg(root, 'agree', { name: '@t/agree', version: '0.0.0', ...dual }, { 'dist/index.js': "export const v = () => 'same';\n", 'dist/index.cjs': "exports.v = () => 'same';\n", @@ -1244,6 +1373,7 @@ export async function selfTest() { // // Every case below emits real declaration files, because the invariant is // partly "is it on disk" and a modelled file answers nothing. + battery('TYPED: the #13112 class, in both directions'); const siblingTypes = { type: 'module', exports: { '.': { types: './dist/index.d.ts', import: './dist/index.js', require: './dist/index.cjs' } } }; writePkg(root, 'sibling', { name: '@t/sibling', version: '0.0.0', ...siblingTypes }, { 'dist/index.js': 'export const ok = 1;\n', 'dist/index.cjs': 'exports.ok = 1;\n', @@ -1297,6 +1427,7 @@ export async function selfTest() { t('…and three of the fixtures are the reported ones', typedFindingCount === 3, String(typedFindingCount)); // ── TYPED_EXEMPTIONS, both directions ─────────────────────────────────── + battery('TYPED_EXEMPTIONS, both directions'); const exemptSibling = { '@t/sibling#.': { reason: 'self-test: a declared, measured reason long enough to be real' } }; const rExempt = await scan(root, empty, [], exemptSibling); t('an exempted TYPED entry is DECLARED, not a finding', @@ -1322,6 +1453,7 @@ export async function selfTest() { t('every shipped behaviour probe states why it exists', DUAL_FORMAT_BEHAVIOUR_PROBES.every((p) => typeof p.why === 'string' && p.why.length > 20)); // ── prerequisite, never a silent green ─────────────────────────────────── + battery('prerequisite, never a silent green'); const bare = mkdtempSync(join(tmpdir(), 'dual-cjs-bare-')); try { writePkg(bare, 'unbuilt', { name: '@t/unbuilt', version: '0.0.0', ...dual }, {}); @@ -1333,6 +1465,7 @@ export async function selfTest() { } // ── a built tree missing one declared target IS a finding ──────────────── + battery('a built tree missing one declared target IS a finding'); const half = mkdtempSync(join(tmpdir(), 'dual-cjs-half-')); try { writePkg(half, 'half', { name: '@t/half', version: '0.0.0', ...dual }, { 'dist/index.js': 'export const ok = 1;\n' }); @@ -1351,6 +1484,7 @@ export async function selfTest() { // driven down individually AND the measured tuple is asserted to clear them // all — a floor accidentally set above its own measurement would red every // real run, which is the opposite failure and just as invisible in review. + battery('the vacuity floors, each driven to zero'); const full = { entries: MEASURED.entries, packages: MEASURED.packages, cjsFiles: MEASURED.cjsFiles, probes: MEASURED.probes, typedJudged: MEASURED_TYPED.typedJudged }; t('FLOOR — the values in the records clear every floor', floorProblem(full) === null, JSON.stringify(floorProblem(full))); t('FLOOR — a dead manifest walk refuses', floorProblem({ ...full, entries: 0 }) !== null); @@ -1387,6 +1521,7 @@ export async function selfTest() { // point. They red 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 numbers. + battery('provenance: the record must stay reproducible, and visibly so'); t('PROVENANCE — the record carries the ref it was measured on', typeof MEASURED.ref === 'string' && /^[0-9a-f]{7,40}$/.test(MEASURED.ref), JSON.stringify(MEASURED.ref)); t('PROVENANCE — the refusal reads the ref from the record rather than restating it', @@ -1412,6 +1547,7 @@ export async function selfTest() { // ── the real ledger is well-formed and shrink-only in shape ─────────────── // The SHIPPED exemption table against the REAL population — the half a // fixture tree cannot give, and the one that goes stale silently. + battery('the real ledger is well-formed and shrink-only in shape'); const realEntries = collectEntries(REPO_ROOT); t('every shipped TYPED exemption names a live require entry point', Object.keys(TYPED_EXEMPTIONS).every((id) => realEntries.some((r) => r.id === id)), @@ -1433,6 +1569,10 @@ export async function selfTest() { orphanLedgerRows(realLedger, collectEntries(REPO_ROOT)).length === 0, JSON.stringify(orphanLedgerRows(realLedger, collectEntries(REPO_ROOT)))); + // The floor runs BEFORE the verdict below, so a success line can only be + // printed by a run in which every declared battery registered its cases. + for (const message of batteryFloorFailures()) cases.push({ name: message, ok: false }); + const failed = cases.filter((c) => !c.ok); for (const c of failed) console.error(` ✗ ${c.name}${c.detail ? ` — ${c.detail}` : ''}`); if (failed.length) { diff --git a/scripts/check-entry-guard.mjs b/scripts/check-entry-guard.mjs index b9e1209575..662851774e 100644 --- a/scripts/check-entry-guard.mjs +++ b/scripts/check-entry-guard.mjs @@ -75,6 +75,119 @@ 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) ────────────────── +// +// This self-test used to decide success by "no failure was recorded" and +// nothing else, 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. A set difference says WHICH +// battery stopped; a count says only that something did. +// +// 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. +// +// The machinery lives HERE, at module scope, rather than inside the self-test: +// this self-test's assertion sink is not a block-bodied helper in its body (it +// is a concise arrow, or a module-scope function), so there is no in-body +// helper to thread a per-run ledger through. Module scope is safe because the +// self-test runs once per process, and it is what lets the existing sink route +// through `registerCase()` with no case rewritten and no assertion changed. +const SELF_TEST_BATTERIES = Object.freeze({ + 'the eleven spellings this gate exists to reject': 13, + 'the canonical form is accepted': 2, + 'the predicate\'s own home may read argv': 2, + 'prose and payloads are not guards': 4, + 'the other idioms': 3, + 'the call shape': 3, + 'the line number is the one a reader can open': 1, + 'the second kind: an EXPORTING file whose top level runs on import': 17, + 'the slicer, driven directly': 4, + 'the dispatch-gates scan surface (#10784)': 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 = 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)'; + +// ⚠️ None of these helpers is named with a self-test spelling, deliberately and +// on the record: `check:pm-dispatch-gates` anchors on a top-level declaration +// whose NAME spells self-test, and every such name owes a row in that gate's +// COMPOUND_ANCHOR_LEDGER. These are the battery ROSTER's machinery -- they hold +// no fixtures to mask and read no path literal -- so the accurate name is the +// one that says `battery`, not the one that would owe a ledger row for a role +// this code does not have. + +/** Cases registered per battery: `battery()` opens one, `registerCase()` files into it. */ +const batteryCases = new Map(); +let openBattery = null; + +/** Open a battery. Every assertion after this line is attributed to it. */ +function battery(name) { + openBattery = name; +} + +/** Called by the self-test's own assertion sink, once per assertion. */ +function registerCase() { + const name = openBattery ?? UNATTRIBUTED_BATTERY; + batteryCases.set(name, (batteryCases.get(name) ?? 0) + 1); +} + +/** + * The floor: every declared battery RAN, and ran its cases (#13489). + * + * Evaluated after every battery has had its chance and BEFORE the verdict, so + * the success line can only be printed by a run in which the set of batteries + * that registered assertions EQUALS the set declared. + */ +function batteryFloorFailures() { + const declared = Object.keys(SELF_TEST_BATTERIES); + const problems = []; + if (declared.length < SELF_TEST_BATTERY_FLOOR) { + problems.push( + `SELF_TEST_BATTERIES declares ${declared.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 batteryCases) { + if (declared.includes(name)) continue; + problems.push( + `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 declared) { + const count = batteryCases.get(name) ?? 0; + if (count >= SELF_TEST_BATTERIES[name]) continue; + problems.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]} — cases that used to run no longer do.`, + ); + } + if (problems.length) { + problems.push( + '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 problems; +} + const HERE = resolve(fileURLToPath(import.meta.url), '..'); const REPO_ROOT = resolve(HERE, '..'); const SCRIPTS = HERE; @@ -631,10 +744,14 @@ let selfTestReachedVerdict = false; export function selfTest() { const cases = []; - const t = (name, ok, detail) => cases.push({ name, ok: Boolean(ok), detail }); + const t = (name, ok, detail) => { + registerCase(); + return cases.push({ name, ok: Boolean(ok), detail }); + }; const n = (src, opts) => scanFile('f.mjs', src, opts).length; // ── the eleven spellings this gate exists to reject ─────────────────────── + battery('the eleven spellings this gate exists to reject'); const SPELLINGS = [ "const invokedDirectly = process.argv[1] && resolve(process.argv[1]) === resolve(fileURLToPath(import.meta.url));", "const invokedDirectly = process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url);", @@ -665,10 +782,12 @@ export function selfTest() { ); // ── the canonical form is accepted ──────────────────────────────────────── + battery('the canonical form is accepted'); t('the canonical guard is accepted', n(`if (${CANONICAL}) { main(); }`) === 0); t('a file with no guard at all is accepted', n("console.log('hello');\n") === 0); // ── the predicate's own home may read argv ──────────────────────────────── + battery('the predicate\'s own home may read argv'); t( 'invoked-as.mjs itself may read process.argv[1]', n('return invokedAs(process.argv[1], fileURLToPath(u));', { isPredicateHome: true }) === 0, @@ -679,6 +798,7 @@ export function selfTest() { ); // ── prose and payloads are not guards ───────────────────────────────────── + battery('prose and payloads are not guards'); t('a process.argv[1] in a LINE COMMENT is not a guard', n('// process.argv[1] is left as typed\n') === 0); t('a process.argv[1] in a BLOCK COMMENT is not a guard', n('/**\n * process.argv[1] as typed\n */\n') === 0); t( @@ -691,11 +811,13 @@ export function selfTest() { ); // ── the other idioms ────────────────────────────────────────────────────── + battery('the other idioms'); t('require.main is rejected', n('if (require.main === module) {}') > 0); t('import.meta.main is rejected', n('if (import.meta.main) {}') > 0); t('process.mainModule is rejected', n('if (process.mainModule === module) {}') > 0); // ── the call shape ──────────────────────────────────────────────────────── + battery('the call shape'); t('isEntrypoint on someone else’s url is rejected', n('if (isEntrypoint(other.url)) {}') > 0); t('isEntrypoint(import.meta.url) is accepted', n('if (isEntrypoint(import.meta.url)) {}') === 0); t( @@ -704,6 +826,7 @@ export function selfTest() { ); // ── the line number is the one a reader can open ────────────────────────── + battery('the line number is the one a reader can open'); const multi = "line one\nline two\nconst g = process.argv[1] === x;\n"; t('a finding reports the line the guard is ON', scanFile('f.mjs', multi)[0]?.line === 3, JSON.stringify(scanFile('f.mjs', multi))); @@ -712,6 +835,7 @@ export function selfTest() { // Asserted POSITIVELY in both directions. This gate prints files SCANNED, not // files recognised, so "the count moved" is not evidence available to a reader // — recognition has to be pinned here or it is not pinned anywhere. + battery('the second kind: an EXPORTING file whose top level runs on import'); const u = (src) => importUnsafeStatements(src).length; const first = (src) => importUnsafeStatements(src)[0]; @@ -756,6 +880,7 @@ export function selfTest() { ); // ── the slicer, driven directly ────────────────────────────────────────── + battery('the slicer, driven directly'); t('an import declaration is ONE top-level statement', topLevelStatements(codeOnly("import { a, b } from 'x';\n")).length === 1); t('a destructuring declaration is ONE top-level statement', topLevelStatements(codeOnly('const { a } = f();\n')).length === 1); t('else continues the statement before it', topLevelStatements(codeOnly('if (a) { x(); } else { y(); }\n')).length === 1); @@ -769,6 +894,7 @@ export function selfTest() { // missing from the brief — which is the round that was actually paid. Both // directions are derived from the walked root rather than re-spelled, so // moving or renaming it cannot leave the declaration describing the old one. + battery('the dispatch-gates scan surface (#10784)'); const walkedRoot = relative(REPO_ROOT, SCRIPTS); const declaredRoots = ROOT_DIR_WATCH_HINTS.map((h) => h.replace(/\/\*+$/, '')); t( @@ -800,6 +926,10 @@ export function selfTest() { // is read would send readdirSync at a directory that does not exist. t('the declared form is NOT the walk root itself', !ROOT_DIR_WATCH_HINTS.includes(walkedRoot)); + // The floor runs BEFORE the verdict below, so a success line can only be + // printed by a run in which every declared battery registered its cases. + for (const message of batteryFloorFailures()) cases.push({ name: message, ok: false }); + const failed = cases.filter((c) => !c.ok); for (const c of failed) console.error(` ✗ ${c.name}${c.detail ? ` — ${c.detail}` : ''}`); if (failed.length) { diff --git a/scripts/check-live-db-isolation.mjs b/scripts/check-live-db-isolation.mjs index 18c6cef115..67a7a3a221 100644 --- a/scripts/check-live-db-isolation.mjs +++ b/scripts/check-live-db-isolation.mjs @@ -60,6 +60,118 @@ import { dirname, join, relative, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; import { isEntrypoint } from './invoked-as.mjs'; +// ── The self-test's own battery roster and floor (#13489) ────────────────── +// +// This self-test used to decide success by "no failure was recorded" and +// nothing else, 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. A set difference says WHICH +// battery stopped; a count says only that something did. +// +// 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. +// +// The machinery lives HERE, at module scope, rather than inside the self-test: +// this self-test's assertion sink is not a block-bodied helper in its body (it +// is a concise arrow, or a module-scope function), so there is no in-body +// helper to thread a per-run ledger through. Module scope is safe because the +// self-test runs once per process, and it is what lets the existing sink route +// through `registerCase()` with no case rewritten and no assertion changed. +const SELF_TEST_BATTERIES = Object.freeze({ + '1. the literal-in-the-DDL form': 1, + '2. the literal-behind-an-identifier form -- the pre-#10382 tree, exactly': 2, + '3. the fixed form -- derived from a call': 1, + '4. the Postgres spelling, both directions': 2, + '5. a loop variable is derived too -- driver-sql\'s globalSetup shape, which': 1, + '6. the detector must be able to see nothing, without that meaning "clean"': 1, + '7. comments are not source -- the reason codeOf exists': 1, + '8. the live-file needle must MATCH a real read and not the cell form': 2, + '9. `use strict` and friends are not live DDL': 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)'; + +// ⚠️ None of these helpers is named with a self-test spelling, deliberately and +// on the record: `check:pm-dispatch-gates` anchors on a top-level declaration +// whose NAME spells self-test, and every such name owes a row in that gate's +// COMPOUND_ANCHOR_LEDGER. These are the battery ROSTER's machinery -- they hold +// no fixtures to mask and read no path literal -- so the accurate name is the +// one that says `battery`, not the one that would owe a ledger row for a role +// this code does not have. + +/** Cases registered per battery: `battery()` opens one, `registerCase()` files into it. */ +const batteryCases = new Map(); +let openBattery = null; + +/** Open a battery. Every assertion after this line is attributed to it. */ +function battery(name) { + openBattery = name; +} + +/** Called by the self-test's own assertion sink, once per assertion. */ +function registerCase() { + const name = openBattery ?? UNATTRIBUTED_BATTERY; + batteryCases.set(name, (batteryCases.get(name) ?? 0) + 1); +} + +/** + * The floor: every declared battery RAN, and ran its cases (#13489). + * + * Evaluated after every battery has had its chance and BEFORE the verdict, so + * the success line can only be printed by a run in which the set of batteries + * that registered assertions EQUALS the set declared. + */ +function batteryFloorFailures() { + const declared = Object.keys(SELF_TEST_BATTERIES); + const problems = []; + if (declared.length < SELF_TEST_BATTERY_FLOOR) { + problems.push( + `SELF_TEST_BATTERIES declares ${declared.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 batteryCases) { + if (declared.includes(name)) continue; + problems.push( + `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 declared) { + const count = batteryCases.get(name) ?? 0; + if (count >= SELF_TEST_BATTERIES[name]) continue; + problems.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]} — cases that used to run no longer do.`, + ); + } + if (problems.length) { + problems.push( + '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 problems; +} + const REPO_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..'); const ROOTS = ['packages', 'apps', 'examples']; const SKIP_DIRS = new Set(['node_modules', 'dist', '.turbo', 'coverage', '.next', 'build']); @@ -250,13 +362,18 @@ const SELF_TEST_VERDICT = 'check-live-db-isolation self-test reached its verdict function selfTest() { const cases = []; const bt = String.fromCharCode(96); - const check = (label, ok) => cases.push({ label, ok }); + const check = (label, ok) => { + registerCase(); + return cases.push({ label, ok }); + }; // 1. the literal-in-the-DDL form + battery('1. the literal-in-the-DDL form'); const literalDdl = `await c.query(${bt}CREATE DATABASE IF NOT EXISTS \\${bt}conformance\\${bt}${bt});`; check('flags a database named by a literal', violationsIn(literalDdl).length === 1); // 2. the literal-behind-an-identifier form -- the pre-#10382 tree, exactly + battery('2. the literal-behind-an-identifier form -- the pre-#10382 tree, exactly'); const literalConst = `const DB = 'os_metadata_protocol_9381';\n` + `await c.query(${bt}CREATE DATABASE IF NOT EXISTS \\${bt}\${DB}\\${bt}${bt});\n` + @@ -268,6 +385,7 @@ function selfTest() { ); // 3. the fixed form -- derived from a call + battery('3. the fixed form -- derived from a call'); const derived = `const DB = currentLiveMysqlDatabase();\n` + `await c.query(${bt}CREATE DATABASE IF NOT EXISTS \\${bt}\${DB}\\${bt}${bt});\n` + @@ -275,6 +393,7 @@ function selfTest() { check('passes a database derived from a call', violationsIn(derived).length === 0); // 4. the Postgres spelling, both directions + battery('4. the Postgres spelling, both directions'); const pgLiteral = `await db.raw(${bt}create schema if not exists "public"${bt});`; const pgDerived = `const schema = currentLiveSchema();\n` + @@ -284,6 +403,7 @@ function selfTest() { // 5. a loop variable is derived too -- driver-sql's globalSetup shape, which // has no declaration for the gate to find and must not be flagged for it + battery('5. a loop variable is derived too -- driver-sql\'s globalSetup shape, which'); const loopVar = `for (const { schema } of liveSchemaLedger()) {\n` + ` await db.raw(${bt}create database if not exists \\${bt}\${schema}\\${bt}${bt});\n` + @@ -291,15 +411,18 @@ function selfTest() { check('passes an identifier with no local literal declaration', violationsIn(loopVar).length === 0); // 6. the detector must be able to see nothing, without that meaning "clean" + battery('6. the detector must be able to see nothing, without that meaning "clean"'); check('reports no violation in source with no live DDL', violationsIn('const x = 1;').length === 0); // 7. comments are not source -- the reason codeOf exists + battery('7. comments are not source -- the reason codeOf exists'); const inComment = `// CREATE DATABASE IF NOT EXISTS \\${bt}conformance\\${bt}\nconst x = 1;`; check('ignores DDL that appears only in a comment', violationsIn(codeOf(inComment)).length === 0); // 8. the live-file needle must MATCH a real read and not the cell form -- // without this, a needle that silently stopped matching would report a // clean scan of an empty population forever + battery('8. the live-file needle must MATCH a real read and not the cell form'); check( 'the live-file needle matches a direct env read', LIVE_ENV_READ.test('const U = process' + '.env.OS_TEST_MYSQL_URL;'), @@ -310,8 +433,13 @@ function selfTest() { ); // 9. `use strict` and friends are not live DDL + battery('9. `use strict` and friends are not live DDL'); check('does not flag a non-identifier use', violationsIn(`'use strict';`).length === 0); + // The floor runs BEFORE the verdict below, so a success line can only be + // printed by a run in which every declared battery registered its cases. + for (const message of batteryFloorFailures()) cases.push({ label: message, ok: false }); + const failed = cases.filter((c) => !c.ok); for (const c of cases) console.log(`${c.ok ? 'ok ' : 'FAIL'} ${c.label}`); if (failed.length > 0) { diff --git a/scripts/check-parse-guard.mjs b/scripts/check-parse-guard.mjs index e198c5684f..5bb4829b60 100644 --- a/scripts/check-parse-guard.mjs +++ b/scripts/check-parse-guard.mjs @@ -124,6 +124,122 @@ 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) ────────────────── +// +// This self-test used to decide success by "no failure was recorded" and +// nothing else, 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. A set difference says WHICH +// battery stopped; a count says only that something did. +// +// 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. +// +// The machinery lives HERE, at module scope, rather than inside the self-test: +// this self-test's assertion sink is not a block-bodied helper in its body (it +// is a concise arrow, or a module-scope function), so there is no in-body +// helper to thread a per-run ledger through. Module scope is safe because the +// self-test runs once per process, and it is what lets the existing sink route +// through `registerCase()` with no case rewritten and no assertion changed. +const SELF_TEST_BATTERIES = Object.freeze({ + 'the call this gate exists to catch, in each spelling': 4, + 'the OTHER two parser entry points, same gate': 5, + 'the CHECKED calls must not match the banned ones. The names share a': 2, + 'the shared /g regex is reused for a prefilter AND a scan; a stale': 2, + 'the finding is openable': 1, + 'prose and payloads are NOT call sites. Getting this wrong makes the': 5, + 'the sanctioned call is silent': 1, + 'the parser home is exempt, and ONLY the parser home': 3, + 'the out-of-tree census: the SAME scanner, a different verdict': 4, + 'the TIER: which of the three sentences the row is printed under. The': 11, + 'the printed block must not put a row under a sentence that is false of': 2, + 'the masker is load-bearing: prove it, rather than trusting it': 1, + 'the dispatch-gates scan surface (#10784)': 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 = 13; + +// The key an assertion is filed under when no battery is open. It is not a +// declared battery, so it reds by the same set difference rather than silently +// inflating whichever battery happened to run last. +const UNATTRIBUTED_BATTERY = '(no battery open)'; + +// ⚠️ None of these helpers is named with a self-test spelling, deliberately and +// on the record: `check:pm-dispatch-gates` anchors on a top-level declaration +// whose NAME spells self-test, and every such name owes a row in that gate's +// COMPOUND_ANCHOR_LEDGER. These are the battery ROSTER's machinery -- they hold +// no fixtures to mask and read no path literal -- so the accurate name is the +// one that says `battery`, not the one that would owe a ledger row for a role +// this code does not have. + +/** Cases registered per battery: `battery()` opens one, `registerCase()` files into it. */ +const batteryCases = new Map(); +let openBattery = null; + +/** Open a battery. Every assertion after this line is attributed to it. */ +function battery(name) { + openBattery = name; +} + +/** Called by the self-test's own assertion sink, once per assertion. */ +function registerCase() { + const name = openBattery ?? UNATTRIBUTED_BATTERY; + batteryCases.set(name, (batteryCases.get(name) ?? 0) + 1); +} + +/** + * The floor: every declared battery RAN, and ran its cases (#13489). + * + * Evaluated after every battery has had its chance and BEFORE the verdict, so + * the success line can only be printed by a run in which the set of batteries + * that registered assertions EQUALS the set declared. + */ +function batteryFloorFailures() { + const declared = Object.keys(SELF_TEST_BATTERIES); + const problems = []; + if (declared.length < SELF_TEST_BATTERY_FLOOR) { + problems.push( + `SELF_TEST_BATTERIES declares ${declared.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 batteryCases) { + if (declared.includes(name)) continue; + problems.push( + `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 declared) { + const count = batteryCases.get(name) ?? 0; + if (count >= SELF_TEST_BATTERIES[name]) continue; + problems.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]} — cases that used to run no longer do.`, + ); + } + if (problems.length) { + problems.push( + '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 problems; +} + const HERE = resolve(fileURLToPath(import.meta.url), '..'); const REPO_ROOT = resolve(HERE, '..'); const SCRIPTS = HERE; @@ -538,10 +654,14 @@ let selfTestReachedVerdict = false; export function selfTest() { const cases = []; - const t = (name, ok, detail) => cases.push({ name, ok: Boolean(ok), detail }); + const t = (name, ok, detail) => { + registerCase(); + return cases.push({ name, ok: Boolean(ok), detail }); + }; const hits = (src, opts) => scanFile('fixture.mjs', src, opts); // -- the call this gate exists to catch, in each spelling ------------------ + battery('the call this gate exists to catch, in each spelling'); t('a plain ts.createSourceFile is a finding', hits('const sf = ts.createSourceFile(f, text, ts.ScriptTarget.Latest, true);').length === 1); t('an aliased receiver is a finding too — the receiver is not part of the pattern', @@ -552,6 +672,7 @@ export function selfTest() { hits('ts.createSourceFile(a, b);\nts.createSourceFile(c, d);').length === 2); // -- the OTHER two parser entry points, same gate ------------------------- + battery('the OTHER two parser entry points, same gate'); t('a ts.createProgram is a finding', hits('const p = ts.createProgram([entry], OPTIONS);').length === 1); t('a ts.transpileModule is a finding', @@ -571,6 +692,7 @@ export function selfTest() { // -- the CHECKED calls must not match the banned ones. The names share a // prefix, so this is one word boundary away from banning the fix itself // and turning every converted call site red. --------------------------- + battery('the CHECKED calls must not match the banned ones. The names share a'); t('createProgramChecked is not a createProgram finding', hits('import { createProgramChecked } from "./ts-parse.mjs";\n' + 'const p = createProgramChecked(files, OPTIONS);').length === 0); @@ -580,6 +702,7 @@ export function selfTest() { // -- the shared /g regex is reused for a prefilter AND a scan; a stale // lastIndex would make the SECOND file with a call site read clean ------ + battery('the shared /g regex is reused for a prefilter AND a scan; a stale'); const twice = 'const sf = ts.createSourceFile(f, t);'; t('scanning the same source twice gives the same answer (no lastIndex carry-over)', hits(twice).length === 1 && hits(twice).length === 1); @@ -587,12 +710,14 @@ export function selfTest() { hits('const a = 1;').length === 0 && hits(twice).length === 1); // -- the finding is openable ---------------------------------------------- + battery('the finding is openable'); const located = hits('const x = 1;\nconst y = 2;\nconst sf = ts.createSourceFile(f, t);'); t('the finding carries the line number of the call', located.length === 1 && located[0].line === 3, JSON.stringify(located)); // -- prose and payloads are NOT call sites. Getting this wrong makes the // gate fabricate findings out of its own documentation. ---------------- + battery('prose and payloads are NOT call sites. Getting this wrong makes the'); t('a line comment naming the call is not a finding', hits('// ts.createSourceFile never throws\nconst a = 1;').length === 0); t('a block comment naming the call is not a finding', @@ -606,10 +731,12 @@ export function selfTest() { hits('const probe = `const sf = ts.createSourceFile(${f}, ${t});`;').length === 0); // -- the sanctioned call is silent ---------------------------------------- + battery('the sanctioned call is silent'); t('the canonical parseSourceFile call is not a finding', hits('import { parseSourceFile } from "./ts-parse.mjs";\nconst sf = parseSourceFile(file, text);').length === 0); // -- the parser home is exempt, and ONLY the parser home ------------------ + battery('the parser home is exempt, and ONLY the parser home'); t('the parser home may call it', hits('const sf = ts.createSourceFile(f, t);', { isParserHome: true }).length === 0); t('…and any other file may not', @@ -619,6 +746,7 @@ export function selfTest() { { isParserHome: true }).length === 0); // -- the out-of-tree census: the SAME scanner, a different verdict --------- + battery('the out-of-tree census: the SAME scanner, a different verdict'); const OUTSIDE = new Map([ ['packages/lint/src/validate-react-page-props.ts', 'sf = tsc.createSourceFile("page.tsx", src);'], ['packages/spec/scripts/build-api-surface.ts', 'const program = ts.createProgram(entries, o);'], @@ -648,6 +776,7 @@ export function selfTest() { // -- the TIER: which of the three sentences the row is printed under. The // census reported a real number under a reason that was false for most of // what it counted, so every branch is asserted, both ways. -------------- + battery('the TIER: which of the three sentences the row is printed under. The'); const tierAt = (rel, isTest = false) => tierOf(rel, isTest, pkgAt); t('a row in a package\'s unpublished scripts/ is tooling', tierAt('packages/spec/scripts/build-api-surface.ts') === 'tooling', @@ -698,6 +827,7 @@ export function selfTest() { // -- the printed block must not put a row under a sentence that is false of // it. Every tier the census can produce needs somewhere to be printed. -- + battery('the printed block must not put a row under a sentence that is false of'); t('every tier a row can carry has a printing tier that claims it', ['test', 'tooling', 'shipped'].every((x) => TIERS.some((r) => r.tier === x)), JSON.stringify(TIERS.map((r) => r.tier))); @@ -707,6 +837,7 @@ export function selfTest() { JSON.stringify(TIERS.filter((r) => r.why.join(' ').includes('cannot answer')).map((r) => r.tier))); // -- the masker is load-bearing: prove it, rather than trusting it -------- + battery('the masker is load-bearing: prove it, rather than trusting it'); t('codeOnly blanks a comment but keeps the line count', codeOnly('// gone\nconst a = 1;\n').split('\n').length === 3 && !codeOnly('// gone\nconst a = 1;\n').includes('gone')); @@ -718,6 +849,7 @@ export function selfTest() { // pays itself out as a dev dispatched on a scripts/ card with this gate // missing from the brief. Both directions are derived from the walked root // rather than re-spelled. + battery('the dispatch-gates scan surface (#10784)'); const walkedRoot = relative(REPO_ROOT, SCRIPTS); const declaredRoots = ROOT_DIR_WATCH_HINTS.map((h) => h.replace(/\/\*+$/, '')); t('the scan surface is declared for the root this gate actually walks', @@ -739,6 +871,10 @@ export function selfTest() { // is read would send readdirSync at a directory that does not exist. t('the declared form is NOT the walk root itself', !ROOT_DIR_WATCH_HINTS.includes(walkedRoot)); + // The floor runs BEFORE the verdict below, so a success line can only be + // printed by a run in which every declared battery registered its cases. + for (const message of batteryFloorFailures()) cases.push({ name: message, ok: false }); + const failed = cases.filter((c) => !c.ok); for (const c of failed) console.error(` x ${c.name}${c.detail ? ` — ${c.detail}` : ''}`); if (failed.length) { diff --git a/scripts/check-partof-closing-keyword.mjs b/scripts/check-partof-closing-keyword.mjs index 03afadf7ba..fffa169c60 100644 --- a/scripts/check-partof-closing-keyword.mjs +++ b/scripts/check-partof-closing-keyword.mjs @@ -129,6 +129,118 @@ import process from 'node:process'; import { h7PartOfWithClosingKeyword } from './pm/check-half-states.mjs'; import { isEntrypoint } from './invoked-as.mjs'; +// ── The self-test's own battery roster and floor (#13489) ────────────────── +// +// This self-test used to decide success by "no failure was recorded" and +// nothing else, 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. A set difference says WHICH +// battery stopped; a count says only that something did. +// +// 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. +// +// The machinery lives HERE, at module scope, rather than inside the self-test: +// this self-test's assertion sink is not a block-bodied helper in its body (it +// is a concise arrow, or a module-scope function), so there is no in-body +// helper to thread a per-run ledger through. Module scope is safe because the +// self-test runs once per process, and it is what lets the existing sink route +// through `registerCase()` with no case rewritten and no assertion changed. +const SELF_TEST_BATTERIES = Object.freeze({ + 'The measured arms. All three were read live on one throwaway PR, in one': 3, + 'The shapes that must stay green, so the gate does not tax correct PRs.': 4, + 'Delegation, not a second copy of the rule. Every body above must get': 1, + 'The failure message must carry the approved rewordings. This is the': 4, + 'Empty body: a verdict about a real input, and it must SAY so rather': 2, + 'Wiring absent: never clean, never a verdict about a PR.': 3, + 'Context reading: presence, not truthiness.': 4, + 'The wiring itself. A gate whose workflow step is deleted or whose': 6, + 'The predicate source this gate reuses must still be there to reuse.': 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)'; + +// ⚠️ None of these helpers is named with a self-test spelling, deliberately and +// on the record: `check:pm-dispatch-gates` anchors on a top-level declaration +// whose NAME spells self-test, and every such name owes a row in that gate's +// COMPOUND_ANCHOR_LEDGER. These are the battery ROSTER's machinery -- they hold +// no fixtures to mask and read no path literal -- so the accurate name is the +// one that says `battery`, not the one that would owe a ledger row for a role +// this code does not have. + +/** Cases registered per battery: `battery()` opens one, `registerCase()` files into it. */ +const batteryCases = new Map(); +let openBattery = null; + +/** Open a battery. Every assertion after this line is attributed to it. */ +function battery(name) { + openBattery = name; +} + +/** Called by the self-test's own assertion sink, once per assertion. */ +function registerCase() { + const name = openBattery ?? UNATTRIBUTED_BATTERY; + batteryCases.set(name, (batteryCases.get(name) ?? 0) + 1); +} + +/** + * The floor: every declared battery RAN, and ran its cases (#13489). + * + * Evaluated after every battery has had its chance and BEFORE the verdict, so + * the success line can only be printed by a run in which the set of batteries + * that registered assertions EQUALS the set declared. + */ +function batteryFloorFailures() { + const declared = Object.keys(SELF_TEST_BATTERIES); + const problems = []; + if (declared.length < SELF_TEST_BATTERY_FLOOR) { + problems.push( + `SELF_TEST_BATTERIES declares ${declared.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 batteryCases) { + if (declared.includes(name)) continue; + problems.push( + `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 declared) { + const count = batteryCases.get(name) ?? 0; + if (count >= SELF_TEST_BATTERIES[name]) continue; + problems.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]} — cases that used to run no longer do.`, + ); + } + if (problems.length) { + problems.push( + '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 problems; +} + const ROOT = new URL('..', import.meta.url).pathname; /** The predicate's home — this gate reuses it and must move when it moves. */ @@ -220,7 +332,10 @@ const SELF_TEST_VERDICT = 'check-partof-closing-keyword self-test reached its ve function selfTest() { const cases = []; - const t = (name, actual, expected) => cases.push([name, actual, expected]); + const t = (name, actual, expected) => { + registerCase(); + return cases.push([name, actual, expected]); + }; const verdict = (body, number = '1') => judge({ number, body }); // --- The measured arms. All three were read live on one throwaway PR, in one @@ -228,6 +343,7 @@ function selfTest() { // gained a closing link within seconds, and the fenced and inline targets // gained none. The predicate strips code before scanning; these three cases // are that measurement, kept executable. + battery('The measured arms. All three were read live on one throwaway PR, in one'); t( 'plain prose beside a Part-of declaration is a finding (the incident specimen)', verdict('Part of #8131 — the PM should close #8131 deliberately once #8136 lands.').exit, @@ -245,6 +361,7 @@ function selfTest() { ); // --- The shapes that must stay green, so the gate does not tax correct PRs. + battery('The shapes that must stay green, so the gate does not tax correct PRs.'); t( 'Part of one card while genuinely closing another is clean', verdict('Part of #8247\n\nFixes #8245').exit, @@ -265,6 +382,7 @@ function selfTest() { // --- Delegation, not a second copy of the rule. Every body above must get // the same verdict from this gate as from the shipped predicate; a fork would // pass the cases above and drift from the sweep on the next one. + battery('Delegation, not a second copy of the rule. Every body above must get'); const bodies = [ 'Part of #1 close #1', 'Part of #1\n\nFixes #2', @@ -286,6 +404,7 @@ function selfTest() { // --- The failure message must carry the approved rewordings. This is the // card's own requirement and the reason the predicate's sentence is reused // verbatim: an author reading a red check gets the fix, not just the verdict. + battery('The failure message must carry the approved rewordings. This is the'); const failed = verdict('Part of #8131 — close #8131 once the rest lands.').lines.join('\n'); t('the failure names the "not addressed here" rewording', failed.includes('is not addressed here'), true); t('the failure names the "out of scope" rewording', failed.includes('out of scope: #8131'), true); @@ -294,17 +413,20 @@ function selfTest() { // --- Empty body: a verdict about a real input, and it must SAY so rather // than look like a run that judged nothing. + battery('Empty body: a verdict about a real input, and it must SAY so rather'); const empty = verdict(''); t('an empty body is clean', empty.exit, EXIT_CLEAN); t('an empty body says it was judged, not skipped', empty.lines.join('\n').includes('empty body'), true); // --- Wiring absent: never clean, never a verdict about a PR. + battery('Wiring absent: never clean, never a verdict about a PR.'); const unwired = judge(readPrContext({})); t('no PR context at all exits NOT WIRED', unwired.exit, EXIT_NOT_WIRED); t('NOT WIRED says it judged nothing', unwired.lines.join('\n').includes('judged nothing'), true); t('NOT WIRED does not read as a clean board', unwired.lines.join('\n').includes('✓'), false); // --- Context reading: presence, not truthiness. + battery('Context reading: presence, not truthiness.'); t('a present but empty body is still wired', readPrContext({ PR_BODY: '' })?.body, ''); t('the PR number alone is enough to be wired', readPrContext({ PR_NUMBER: '42' })?.number, '42'); t('an absent body reads as the empty string', readPrContext({ PR_NUMBER: '42' })?.body, ''); @@ -312,6 +434,7 @@ function selfTest() { // --- The wiring itself. A gate whose workflow step is deleted or whose // trigger loses the edit activity is not a weaker gate, it is a silent one. + battery('The wiring itself. A gate whose workflow step is deleted or whose'); const wiringPath = join(ROOT, WIRING_WORKFLOW); const wiring = existsSync(wiringPath) ? readFileSync(wiringPath, 'utf8') : ''; t('the wiring workflow exists', wiring !== '', true); @@ -344,8 +467,13 @@ function selfTest() { ); // --- The predicate source this gate reuses must still be there to reuse. + battery('The predicate source this gate reuses must still be there to reuse.'); t('the predicate source exists', existsSync(join(ROOT, PREDICATE_SOURCE)), true); + // The floor runs BEFORE the verdict below, so a success line can only be + // printed by a run in which every declared battery registered its cases. + for (const message of batteryFloorFailures()) cases.push([message, false, true]); + let failedCount = 0; for (const [name, actual, expected] of cases) { const ok = JSON.stringify(actual) === JSON.stringify(expected); diff --git a/scripts/check-plugin-teardown-shape.mjs b/scripts/check-plugin-teardown-shape.mjs index b63cd9fe79..b68b2fde87 100644 --- a/scripts/check-plugin-teardown-shape.mjs +++ b/scripts/check-plugin-teardown-shape.mjs @@ -150,6 +150,129 @@ const ts = await requireDefaultExport('typescript', () => import('typescript'), import { isEntrypoint } from './invoked-as.mjs'; import { parseSourceFile } from './ts-parse.mjs'; +// ── The self-test's own battery roster and floor (#13489) ────────────────── +// +// This self-test used to decide success by "no failure was recorded" and +// nothing else, 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. A set difference says WHICH +// battery stopped; a count says only that something did. +// +// 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. +// +// The machinery lives HERE, at module scope, rather than inside the self-test: +// this self-test's assertion sink is not a block-bodied helper in its body (it +// is a concise arrow, or a module-scope function), so there is no in-body +// helper to thread a per-run ledger through. Module scope is safe because the +// self-test runs once per process, and it is what lets the existing sink route +// through `registerCase()` with no case rewritten and no assertion changed. +const SELF_TEST_BATTERIES = Object.freeze({ + 'the positive control: the REAL pre-#10375 revision': 1, + 'the negative control from the same file, as REPAIRED': 1, + 'the delegating alias, both directions': 3, + 'every roster name reds': 4, + 'the arrow-property spelling, which a method-only scan misses': 1, + 'a stored callback handle is not a declared teardown': 2, + // ⛔ NOT today's count (18). This battery runs exactly one case per + // `DELIBERATELY_EXCLUDED` row and has no structural case of its own, and the + // roster it reads is complementary to `TEARDOWN_ALIASES`: promoting one name + // from the excluded list onto the teardown roster is a legitimate edit that + // SHRINKS this list while growing the sibling battery below. A floor at 18 + // would redden that edit and train the next author to edit the floor. Pinned + // instead is the part that does not move with the list: this battery RAN and + // at least one excluded name was actually audited. + 'the exclusions, pinned as cases rather than asserted in prose': 1, + 'population boundaries': 5, + 'the ratchet, in both directions': 4, + 'refusals, each PAIRED with a tree that still returns a verdict': 4, + 'the unparseable leg, out of process: ts-parse ends the PROCESS': 2, + 'and the live tree agrees with the checked-in ratchet': 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 = 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)'; + +// ⚠️ None of these helpers is named with a self-test spelling, deliberately and +// on the record: `check:pm-dispatch-gates` anchors on a top-level declaration +// whose NAME spells self-test, and every such name owes a row in that gate's +// COMPOUND_ANCHOR_LEDGER. These are the battery ROSTER's machinery -- they hold +// no fixtures to mask and read no path literal -- so the accurate name is the +// one that says `battery`, not the one that would owe a ledger row for a role +// this code does not have. + +/** Cases registered per battery: `battery()` opens one, `registerCase()` files into it. */ +const batteryCases = new Map(); +let openBattery = null; + +/** Open a battery. Every assertion after this line is attributed to it. */ +function battery(name) { + openBattery = name; +} + +/** Called by the self-test's own assertion sink, once per assertion. */ +function registerCase() { + const name = openBattery ?? UNATTRIBUTED_BATTERY; + batteryCases.set(name, (batteryCases.get(name) ?? 0) + 1); +} + +/** + * The floor: every declared battery RAN, and ran its cases (#13489). + * + * Evaluated after every battery has had its chance and BEFORE the verdict, so + * the success line can only be printed by a run in which the set of batteries + * that registered assertions EQUALS the set declared. + */ +function batteryFloorFailures() { + const declared = Object.keys(SELF_TEST_BATTERIES); + const problems = []; + if (declared.length < SELF_TEST_BATTERY_FLOOR) { + problems.push( + `SELF_TEST_BATTERIES declares ${declared.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 batteryCases) { + if (declared.includes(name)) continue; + problems.push( + `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 declared) { + const count = batteryCases.get(name) ?? 0; + if (count >= SELF_TEST_BATTERIES[name]) continue; + problems.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]} — cases that used to run no longer do.`, + ); + } + if (problems.length) { + problems.push( + '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 problems; +} + const HERE = dirname(fileURLToPath(import.meta.url)); const REPO_ROOT = resolve(HERE, '..'); @@ -541,7 +664,10 @@ let selfTestReachedVerdict = false; export function selfTest() { const cases = []; - const t = (name, ok, detail) => cases.push({ name, ok: Boolean(ok), detail }); + const t = (name, ok, detail) => { + registerCase(); + return cases.push({ name, ok: Boolean(ok), detail }); + }; const SELF = fileURLToPath(import.meta.url); const dir = mkdtempSync(join(tmpdir(), 'teardown-shape-')); @@ -564,6 +690,7 @@ export function selfTest() { try { // -- the positive control: the REAL pre-#10375 revision ------------------ + battery('the positive control: the REAL pre-#10375 revision'); const show = spawnSync('git', ['show', `${POSITIVE_CONTROL.rev}:${POSITIVE_CONTROL.path}`], { cwd: REPO_ROOT, encoding: 'utf8', maxBuffer: 32 * 1024 * 1024, }); @@ -589,6 +716,7 @@ export function selfTest() { ); // -- the negative control from the same file, as REPAIRED ---------------- + battery('the negative control from the same file, as REPAIRED'); const repaired = readFileSync(join(REPO_ROOT, POSITIVE_CONTROL.path), 'utf8'); const postFix = tree('repaired', { [POSITIVE_CONTROL.path.slice(POPULATION_ROOT.length + 1)]: repaired }); const postFixResult = audit(postFix); @@ -599,6 +727,7 @@ export function selfTest() { ); // -- the delegating alias, both directions ------------------------------- + battery('the delegating alias, both directions'); const aliasForward = audit(tree('alias-forward', { 'a/src/p.ts': plugin('ForwardPlugin', ` async ${KERNEL_HOOK}(): Promise {}\n async stop(): Promise { await this.${KERNEL_HOOK}(); }`), })); @@ -615,6 +744,7 @@ export function selfTest() { t('destroy() with no alias at all stays GREEN', destroyOnly.findings.length === 0, JSON.stringify(destroyOnly.findings)); // -- every roster name reds -------------------------------------------- + battery('every roster name reds'); for (const alias of TEARDOWN_ALIASES) { const r = audit(tree(`roster-${alias}`, { 'a/src/p.ts': plugin('RosterPlugin', ` async ${alias}(): Promise {}`), @@ -623,6 +753,7 @@ export function selfTest() { } // -- the arrow-property spelling, which a method-only scan misses -------- + battery('the arrow-property spelling, which a method-only scan misses'); const arrowProp = audit(tree('arrow-prop', { 'a/src/p.ts': plugin('ArrowPlugin', ' stop = async (ctx: unknown): Promise => { void ctx; };'), })); @@ -630,6 +761,7 @@ export function selfTest() { arrowProp.findings.length === 1 && arrowProp.findings[0].alias === 'stop', JSON.stringify(arrowProp.findings)); // -- a stored callback handle is not a declared teardown ---------------- + battery('a stored callback handle is not a declared teardown'); const handleWithHook = audit(tree('handle-with-hook', { 'a/src/p.ts': plugin('HandlePlugin', ` private close?: () => Promise;\n async ${KERNEL_HOOK}(): Promise { await this.close?.(); }`), })); @@ -643,6 +775,7 @@ export function selfTest() { handleAlone.findings.length === 0, JSON.stringify(handleAlone.findings)); // -- the exclusions, pinned as cases rather than asserted in prose ------ + battery('the exclusions, pinned as cases rather than asserted in prose'); for (const name of Object.keys(DELIBERATELY_EXCLUDED)) { const r = audit(tree(`excluded-${name}`, { 'a/src/p.ts': plugin('ExcludedPlugin', ` async ${name}(): Promise {}`), @@ -651,6 +784,7 @@ export function selfTest() { } // -- population boundaries --------------------------------------------- + battery('population boundaries'); const notAPlugin = audit(tree('not-a-plugin', { 'a/src/p.ts': `import type { Plugin } from '@objectstack/core';\nexport class NotAPlugin implements Disposable {\n async stop(): Promise {}\n [Symbol.dispose]() {}\n}\nexport class RealPlugin implements Plugin {\n name = 'r';\n async init(): Promise {}\n async ${KERNEL_HOOK}(): Promise {}\n}\n`, })); @@ -680,6 +814,7 @@ export function selfTest() { t('a .d.ts declaration file is not scanned', declarations.refusal === null && declarations.findings.length === 0, JSON.stringify(declarations)); // -- the ratchet, in both directions ----------------------------------- + battery('the ratchet, in both directions'); const known = [{ file: 'a/src/p.ts', cls: 'P', alias: 'stop', repair: '#10371' }]; const exact = judge([{ file: 'a/src/p.ts', cls: 'P', alias: 'stop' }], known); t('a finding already on the known list is held, not fresh', exact.fresh.length === 0 && exact.stale.length === 0 && exact.held === 1); @@ -692,6 +827,7 @@ export function selfTest() { renamed.fresh.length === 1 && renamed.stale.length === 1); // -- refusals, each PAIRED with a tree that still returns a verdict ----- + battery('refusals, each PAIRED with a tree that still returns a verdict'); const emptyTree = tree('refuse-empty', {}); mkdirSync(join(emptyTree, POPULATION_ROOT), { recursive: true }); const emptyResult = audit(emptyTree); @@ -727,6 +863,7 @@ export function selfTest() { JSON.stringify(unreadable)); // -- the unparseable leg, out of process: ts-parse ends the PROCESS ----- + battery('the unparseable leg, out of process: ts-parse ends the PROCESS'); const wreckRoot = tree('refuse-unparseable', { ...READABLE, 'b/src/wreck.ts': 'export class WreckPlugin implements Plugin {\n<<<<<<< HEAD\n async stop() {}\n=======\n async destroy() {}\n>>>>>>> other\n}\n', @@ -742,6 +879,7 @@ export function selfTest() { JSON.stringify({ status: parseable.status, out: (parseable.stdout || '').trim() })); // -- and the live tree agrees with the checked-in ratchet --------------- + battery('and the live tree agrees with the checked-in ratchet'); const live = audit(REPO_ROOT); const liveJudgement = live.refusal ? null : judge(live.findings, KNOWN_TEARDOWN_UNREACHED); t('the live tree resolves a real population (not zero, not a refusal)', @@ -753,6 +891,10 @@ export function selfTest() { rmSync(dir, { recursive: true, force: true }); } + // The floor runs BEFORE the verdict below, so a success line can only be + // printed by a run in which every declared battery registered its cases. + for (const message of batteryFloorFailures()) cases.push({ name: message, ok: false }); + const failed = cases.filter((c) => !c.ok); for (const c of failed) console.error(` ✗ ${c.name}${c.detail ? ` -- ${c.detail}` : ''}`); if (failed.length) { diff --git a/scripts/check-published-list-mirrors.mjs b/scripts/check-published-list-mirrors.mjs index 434e1b10d3..2eae7ad178 100644 --- a/scripts/check-published-list-mirrors.mjs +++ b/scripts/check-published-list-mirrors.mjs @@ -138,6 +138,117 @@ import process from 'node:process'; import { isEntrypoint } from './invoked-as.mjs'; +// ── The self-test's own battery roster and floor (#13489) ────────────────── +// +// This self-test used to decide success by "no failure was recorded" and +// nothing else, 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. A set difference says WHICH +// battery stopped; a count says only that something did. +// +// 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. +// +// The machinery lives HERE, at module scope, rather than inside the self-test: +// this self-test's assertion sink is not a block-bodied helper in its body (it +// is a concise arrow, or a module-scope function), so there is no in-body +// helper to thread a per-run ledger through. Module scope is safe because the +// self-test runs once per process, and it is what lets the existing sink route +// through `registerCase()` with no case rewritten and no assertion changed. +const SELF_TEST_BATTERIES = Object.freeze({ + 'the positive control: the ordinary case is GREEN, and it reads the right block': 4, + 'direction 1: the constant grew and the doc did not (the card\'s own case)': 1, + 'direction 2: the doc publishes a spelling the scanner cannot see (wrong on arrival)': 1, + 'the nastiest drift: comment/prohibition prose only, which containment cannot see': 3, + 'the projection: a spelling keeps its comments, a note paragraph is not published': 9, + 'every unreadable state REFUSES rather than passing empty': 9, + 'the code side refuses just as loudly (the #11871 refactor shape)': 4, + 'the live table, which is what actually rots': 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 = 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)'; + +// ⚠️ None of these helpers is named with a self-test spelling, deliberately and +// on the record: `check:pm-dispatch-gates` anchors on a top-level declaration +// whose NAME spells self-test, and every such name owes a row in that gate's +// COMPOUND_ANCHOR_LEDGER. These are the battery ROSTER's machinery -- they hold +// no fixtures to mask and read no path literal -- so the accurate name is the +// one that says `battery`, not the one that would owe a ledger row for a role +// this code does not have. + +/** Cases registered per battery: `battery()` opens one, `registerCase()` files into it. */ +const batteryCases = new Map(); +let openBattery = null; + +/** Open a battery. Every assertion after this line is attributed to it. */ +function battery(name) { + openBattery = name; +} + +/** Called by the self-test's own assertion sink, once per assertion. */ +function registerCase() { + const name = openBattery ?? UNATTRIBUTED_BATTERY; + batteryCases.set(name, (batteryCases.get(name) ?? 0) + 1); +} + +/** + * The floor: every declared battery RAN, and ran its cases (#13489). + * + * Evaluated after every battery has had its chance and BEFORE the verdict, so + * the success line can only be printed by a run in which the set of batteries + * that registered assertions EQUALS the set declared. + */ +function batteryFloorFailures() { + const declared = Object.keys(SELF_TEST_BATTERIES); + const problems = []; + if (declared.length < SELF_TEST_BATTERY_FLOOR) { + problems.push( + `SELF_TEST_BATTERIES declares ${declared.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 batteryCases) { + if (declared.includes(name)) continue; + problems.push( + `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 declared) { + const count = batteryCases.get(name) ?? 0; + if (count >= SELF_TEST_BATTERIES[name]) continue; + problems.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]} — cases that used to run no longer do.`, + ); + } + if (problems.length) { + problems.push( + '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 problems; +} + const HERE = dirname(fileURLToPath(import.meta.url)); const REPO_ROOT = resolve(HERE, '..'); @@ -362,7 +473,10 @@ const SELF_TEST_VERDICT = 'check-published-list-mirrors self-test reached its ve async function selfTest() { const cases = []; - const ok = (label, cond) => cases.push({ label, cond }); + const ok = (label, cond) => { + registerCase(); + return cases.push({ label, cond }); + }; const SPEC = { id: 'fixture', module: 'scripts/nonexistent.mjs', constant: 'FIXTURE', doc: 'FIXTURE.md', heading: '### The mirror heading', lang: 'ts' }; const doc = (body) => [ @@ -391,6 +505,7 @@ async function selfTest() { const ENTRIES = ['const A = 1; // seed', ' ⛔ NOT a manifest name belonging to some OTHER package']; // ── the positive control: the ordinary case is GREEN, and it reads the right block ── + battery('the positive control: the ordinary case is GREEN, and it reads the right block'); const good = locateBlock(doc(FENCED), SPEC); ok('the ordinary case locates a block', Array.isArray(good.lines)); ok('and it is the block under the declared heading, not the decoy in a later section', !good.lines?.includes('const DECOY = 0;')); @@ -398,6 +513,7 @@ async function selfTest() { ok('and an exact copy judges clean', judge(ENTRIES, good.lines ?? []).length === 0); // ── direction 1: the constant grew and the doc did not (the card's own case) ── + battery('direction 1: the constant grew and the doc did not (the card\'s own case)'); ok( 'a spelling in the constant and absent from the doc is RED', judge([...ENTRIES, "const REPO = findUp((dir) => existsSync(join(dir, 'pnpm-workspace.yaml')));"], good.lines ?? []) @@ -405,12 +521,14 @@ async function selfTest() { ); // ── direction 2: the doc publishes a spelling the scanner cannot see (wrong on arrival) ── + battery('direction 2: the doc publishes a spelling the scanner cannot see (wrong on arrival)'); ok( 'a spelling published that the constant does not hold is RED', judge(ENTRIES, [...(good.lines ?? []), 'const P = process.cwd();']).some((p) => p.includes('PUBLISHED but not in the constant')), ); // ── the nastiest drift: comment/prohibition prose only, which containment cannot see ── + battery('the nastiest drift: comment/prohibition prose only, which containment cannot see'); ok( "a COMMENT-only drift is RED (round 1 and round 2 were both comment prose)", judge(['const A = 1; // seed (ESM)', ENTRIES[1]], good.lines ?? []).some((p) => p.includes('differs')), @@ -422,6 +540,7 @@ async function selfTest() { ok('trailing whitespace alone is NOT a difference (the one stated slack)', judge(ENTRIES, [`${ENTRIES[0]} `, `${ENTRIES[1]}\t`]).length === 0); // ── the projection: a spelling keeps its comments, a note paragraph is not published ── + battery('the projection: a spelling keeps its comments, a note paragraph is not published'); const RAW = [ 'const A = 1; // seed', ' // continued here', @@ -447,6 +566,7 @@ async function selfTest() { ); // ── every unreadable state REFUSES rather than passing empty ── + battery('every unreadable state REFUSES rather than passing empty'); ok('a renamed heading REFUSES', locateBlock(doc(FENCED).replace(SPEC.heading, '### A different heading entirely'), SPEC).refusal?.includes('heading not found')); ok('a duplicated heading REFUSES as ambiguous', locateBlock(`${doc(FENCED)}\n${SPEC.heading}\n`, SPEC).refusal?.includes('occurs 2 times')); ok('a re-tagged fence REFUSES', locateBlock(doc(['```js', ...FENCED.slice(1)]), SPEC).refusal?.includes('no ````ts` fence')); @@ -458,12 +578,14 @@ async function selfTest() { ok('a heading spec that is not a heading REFUSES', locateBlock(doc(FENCED), { ...SPEC, heading: 'not a heading' }).refusal?.includes('not a markdown heading')); // ── the code side refuses just as loudly (the #11871 refactor shape) ── + battery('the code side refuses just as loudly (the #11871 refactor shape)'); ok('a renamed or vanished constant REFUSES', validateEntries(undefined, SPEC).refusal?.includes('not exported')); ok('an EMPTY constant REFUSES', validateEntries([], SPEC).refusal?.includes('EMPTY')); ok('a non-array constant REFUSES', validateEntries('a string', SPEC).refusal?.includes('not an array')); ok('a non-string entry REFUSES', validateEntries(['fine', 42], SPEC).refusal?.includes('not a string')); // ── the live table, which is what actually rots ── + battery('the live table, which is what actually rots'); ok('the mirror table is not empty', MIRRORS.length >= 1); for (const spec of MIRRORS) { const live = readDoc(spec); @@ -480,6 +602,10 @@ async function selfTest() { ok(`live: the block still LOCATES in ${spec.doc} (heading + fence, never a line number)`, Array.isArray(located.lines)); } + // The floor runs BEFORE the verdict below, so a success line can only be + // printed by a run in which every declared battery registered its cases. + for (const message of batteryFloorFailures()) cases.push({ label: message, cond: false }); + const failed = cases.filter((c) => !c.cond); for (const c of cases) console.log(`${c.cond ? 'ok ' : 'FAIL'} ${c.label}`); if (failed.length) { diff --git a/scripts/check-runner-env-posture.mjs b/scripts/check-runner-env-posture.mjs index f880a2f737..28ed071a91 100644 --- a/scripts/check-runner-env-posture.mjs +++ b/scripts/check-runner-env-posture.mjs @@ -78,6 +78,117 @@ import { fileURLToPath } from 'node:url'; import { scanSource, blank } from './js-comment-mask.mjs'; import { isEntrypoint } from './invoked-as.mjs'; +// ── The self-test's own battery roster and floor (#13489) ────────────────── +// +// This self-test used to decide success by "no failure was recorded" and +// nothing else, 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. A set difference says WHICH +// battery stopped; a count says only that something did. +// +// 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. +// +// The machinery lives HERE, at module scope, rather than inside the self-test: +// this self-test's assertion sink is not a block-bodied helper in its body (it +// is a concise arrow, or a module-scope function), so there is no in-body +// helper to thread a per-run ledger through. Module scope is safe because the +// self-test runs once per process, and it is what lets the existing sink route +// through `registerCase()` with no case rewritten and no assertion changed. +const SELF_TEST_BATTERIES = Object.freeze({ + 'Detection: the spellings an author actually reaches for.': 13, + 'The line number points at the real line, offsets preserved by the mask.': 1, + 'NOT flagged: the deployment signal, which is the whole point.': 2, + 'NOT flagged: prose and payloads. A gate that cannot tell them apart': 4, + 'Longer identifiers must not be split by the word boundaries.': 3, + 'Population.': 7, + 'Wiring. Unwiring the gate must redden HERE rather than go quiet.': 3, + 'The corpus itself, as a case rather than as the run\'s only evidence.': 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 = 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)'; + +// ⚠️ None of these helpers is named with a self-test spelling, deliberately and +// on the record: `check:pm-dispatch-gates` anchors on a top-level declaration +// whose NAME spells self-test, and every such name owes a row in that gate's +// COMPOUND_ANCHOR_LEDGER. These are the battery ROSTER's machinery -- they hold +// no fixtures to mask and read no path literal -- so the accurate name is the +// one that says `battery`, not the one that would owe a ledger row for a role +// this code does not have. + +/** Cases registered per battery: `battery()` opens one, `registerCase()` files into it. */ +const batteryCases = new Map(); +let openBattery = null; + +/** Open a battery. Every assertion after this line is attributed to it. */ +function battery(name) { + openBattery = name; +} + +/** Called by the self-test's own assertion sink, once per assertion. */ +function registerCase() { + const name = openBattery ?? UNATTRIBUTED_BATTERY; + batteryCases.set(name, (batteryCases.get(name) ?? 0) + 1); +} + +/** + * The floor: every declared battery RAN, and ran its cases (#13489). + * + * Evaluated after every battery has had its chance and BEFORE the verdict, so + * the success line can only be printed by a run in which the set of batteries + * that registered assertions EQUALS the set declared. + */ +function batteryFloorFailures() { + const declared = Object.keys(SELF_TEST_BATTERIES); + const problems = []; + if (declared.length < SELF_TEST_BATTERY_FLOOR) { + problems.push( + `SELF_TEST_BATTERIES declares ${declared.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 batteryCases) { + if (declared.includes(name)) continue; + problems.push( + `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 declared) { + const count = batteryCases.get(name) ?? 0; + if (count >= SELF_TEST_BATTERIES[name]) continue; + problems.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]} — cases that used to run no longer do.`, + ); + } + if (problems.length) { + problems.push( + '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 problems; +} + const HERE = resolve(fileURLToPath(import.meta.url), '..'); const ROOT = resolve(HERE, '..'); @@ -243,10 +354,14 @@ let selfTestReachedVerdict = false; export function selfTest() { const cases = []; - const t = (name, actual, expected) => cases.push([name, actual, expected]); + const t = (name, actual, expected) => { + registerCase(); + return cases.push([name, actual, expected]); + }; const tokens = (src) => findRunnerEnvReads(src).map((h) => h.token); // --- Detection: the spellings an author actually reaches for. + battery('Detection: the spellings an author actually reaches for.'); t('member access', tokens('if (env.VITEST) return 1;'), ['VITEST']); t('process.env member', tokens('const x = process.env.VITEST;'), ['VITEST']); t('optional chain', tokens('const x = process?.env?.VITEST;'), ['VITEST']); @@ -262,25 +377,30 @@ export function selfTest() { t('two reads on one line are both reported', tokens('env.VITEST || env.TEST;'), ['VITEST', 'TEST']); // --- The line number points at the real line, offsets preserved by the mask. + battery('The line number points at the real line, offsets preserved by the mask.'); t('line number survives masking', findRunnerEnvReads('// x\n/* y */\nif (env.VITEST) {}').map((h) => h.line), [3]); // --- NOT flagged: the deployment signal, which is the whole point. + battery('NOT flagged: the deployment signal, which is the whole point.'); t('NODE_ENV is not a runner variable', tokens("if (env.NODE_ENV === 'test') return 1;"), []); t("the string 'test' is not the token TEST", tokens("if (mode === 'test') return 1;"), []); // --- NOT flagged: prose and payloads. A gate that cannot tell them apart // forces the explanation to be deleted, which is how this comes back. + battery('NOT flagged: prose and payloads. A gate that cannot tell them apart'); t('a line comment quoting the banned line', tokens('// if (env.VITEST || x) return 1;'), []); t('a block comment quoting it', tokens('/**\n * if (env.VITEST) return 1;\n */\nconst a = 1;'), []); t('a string payload naming it', tokens("const s = 'VITEST';"), []); t('a template payload naming it', tokens('const s = `TEST=${x}`;'), []); // --- Longer identifiers must not be split by the word boundaries. + battery('Longer identifiers must not be split by the word boundaries.'); t('MANIFEST is not TEST', tokens('const MANIFEST = 1;'), []); t('TEST_TIMEOUT is not TEST', tokens('const TEST_TIMEOUT = 1;'), []); t('LATEST is not TEST', tokens('const LATEST = 1;'), []); // --- Population. + battery('Population.'); t('product source counts', isProductSource('packages/services/service-settings/src/local-crypto-provider.ts'), true); t('a unit test beside it does not', isProductSource('packages/services/service-settings/src/local-crypto-provider.test.ts'), false); t('an e2e in a test dir does not', isProductSource('packages/cli/test/helpers/serve-process.ts'), false); @@ -290,6 +410,7 @@ export function selfTest() { t('a package script outside src/ does not', isProductSource('packages/spec/scripts/build-schemas.mjs'), false); // --- Wiring. Unwiring the gate must redden HERE rather than go quiet. + battery('Wiring. Unwiring the gate must redden HERE rather than go quiet.'); const pkg = JSON.parse(readFileSync(join(ROOT, 'package.json'), 'utf8')); t('a package.json alias invokes this script', /check-runner-env-posture\.mjs/.test(pkg.scripts?.['check:runner-env-posture'] ?? ''), true); t('...and runs the self-test with it', /--self-test/.test(pkg.scripts?.['check:runner-env-posture'] ?? ''), true); @@ -297,8 +418,13 @@ export function selfTest() { t('a lint job runs the alias', lintYml.includes('pnpm check:runner-env-posture'), true); // --- The corpus itself, as a case rather than as the run's only evidence. + battery('The corpus itself, as a case rather than as the run\'s only evidence.'); t('today\'s tree is clean', scanTree().length, 0); + // The floor runs BEFORE the verdict below, so a success line can only be + // printed by a run in which every declared battery registered its cases. + for (const message of batteryFloorFailures()) cases.push([message, false, true]); + let failed = 0; for (const [name, actual, expected] of cases) { const ok = JSON.stringify(actual) === JSON.stringify(expected); diff --git a/scripts/check-single-claim-paths.mjs b/scripts/check-single-claim-paths.mjs index c2d2730436..603afaaa1f 100644 --- a/scripts/check-single-claim-paths.mjs +++ b/scripts/check-single-claim-paths.mjs @@ -148,6 +148,116 @@ import { join } from 'node:path'; import process from 'node:process'; import { isEntrypoint } from './invoked-as.mjs'; +// ── The self-test's own battery roster and floor (#13489) ────────────────── +// +// This self-test used to decide success by "no failure was recorded" and +// nothing else, 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. A set difference says WHICH +// battery stopped; a count says only that something did. +// +// 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. +// +// The machinery lives HERE, at module scope, rather than inside the self-test: +// this self-test's assertion sink is not a block-bodied helper in its body (it +// is a concise arrow, or a module-scope function), so there is no in-body +// helper to thread a per-run ledger through. Module scope is safe because the +// self-test runs once per process, and it is what lets the existing sink route +// through `registerCase()` with no case rewritten and no assertion changed. +const SELF_TEST_BATTERIES = Object.freeze({ + 'The declared list\'s own invariants. The list is the whole key, so a': 6, + 'The common path: a PR touching nothing declared is clean and SAYS it': 2, + 'First come, first served. Both arms, because failing the wrong one is': 11, + 'The failure has to carry the remedy, not just the verdict.': 3, + 'UNDETERMINED is its own answer. It must never read as clean, and it': 5, + 'Wiring absent: never clean, never an accusation.': 5, + 'The short-circuit. This is the property that makes the gate affordable,': 22, +}); + +// DELETING an entry silences that battery's floor exactly as effectively as +// zeroing it, so the roster's own size is pinned too. +const SELF_TEST_BATTERY_FLOOR = 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)'; + +// ⚠️ None of these helpers is named with a self-test spelling, deliberately and +// on the record: `check:pm-dispatch-gates` anchors on a top-level declaration +// whose NAME spells self-test, and every such name owes a row in that gate's +// COMPOUND_ANCHOR_LEDGER. These are the battery ROSTER's machinery -- they hold +// no fixtures to mask and read no path literal -- so the accurate name is the +// one that says `battery`, not the one that would owe a ledger row for a role +// this code does not have. + +/** Cases registered per battery: `battery()` opens one, `registerCase()` files into it. */ +const batteryCases = new Map(); +let openBattery = null; + +/** Open a battery. Every assertion after this line is attributed to it. */ +function battery(name) { + openBattery = name; +} + +/** Called by the self-test's own assertion sink, once per assertion. */ +function registerCase() { + const name = openBattery ?? UNATTRIBUTED_BATTERY; + batteryCases.set(name, (batteryCases.get(name) ?? 0) + 1); +} + +/** + * The floor: every declared battery RAN, and ran its cases (#13489). + * + * Evaluated after every battery has had its chance and BEFORE the verdict, so + * the success line can only be printed by a run in which the set of batteries + * that registered assertions EQUALS the set declared. + */ +function batteryFloorFailures() { + const declared = Object.keys(SELF_TEST_BATTERIES); + const problems = []; + if (declared.length < SELF_TEST_BATTERY_FLOOR) { + problems.push( + `SELF_TEST_BATTERIES declares ${declared.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 batteryCases) { + if (declared.includes(name)) continue; + problems.push( + `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 declared) { + const count = batteryCases.get(name) ?? 0; + if (count >= SELF_TEST_BATTERIES[name]) continue; + problems.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]} — cases that used to run no longer do.`, + ); + } + if (problems.length) { + problems.push( + '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 problems; +} + const ROOT = new URL('..', import.meta.url).pathname; /** The wiring that gives this gate a PR to judge. */ @@ -386,7 +496,10 @@ let selfTestReachedVerdict = false; function selfTest() { const cases = []; - const t = (name, actual, expected) => cases.push([name, actual, expected]); + const t = (name, actual, expected) => { + registerCase(); + return cases.push([name, actual, expected]); + }; const other = (number, extra = {}) => ({ number, @@ -400,6 +513,7 @@ function selfTest() { // --- The declared list's own invariants. The list is the whole key, so a // careless edit to it is the realistic way this gate becomes noise. + battery('The declared list\'s own invariants. The list is the whole key, so a'); t('every declared entry names a path', SINGLE_CLAIM_PATHS.every((e) => typeof e.path === 'string' && e.path.length > 0), true); t('every declared entry carries a reason', SINGLE_CLAIM_PATHS.every((e) => typeof e.reason === 'string' && e.reason.trim().length > 0), true); t('the declared list holds no duplicates', new Set(declaredPaths()).size, declaredPaths().length); @@ -416,12 +530,14 @@ function selfTest() { // --- The common path: a PR touching nothing declared is clean and SAYS it // judged, so a green here is never confused with a run that did no work. + battery('The common path: a PR touching nothing declared is clean and SAYS it'); const untouched = verdict({ claimed: [] }); t('a PR touching no declared path is clean', untouched.exit, EXIT_CLEAN); t('the clean verdict says what it checked', untouched.lines.join('\n').includes('declared at-most-one-writer path'), true); // --- First come, first served. Both arms, because failing the wrong one is // the failure mode that would make this gate worse than nothing. + battery('First come, first served. Both arms, because failing the wrong one is'); const later = verdict({ claimed: ['.objectui-sha'], others: [other(100)] }); t('an EARLIER open PR on a declared path fails THIS PR', later.exit, EXIT_CONFLICT); t('the failure names the earlier PR by number', later.lines.join('\n').includes('#100'), true); @@ -451,6 +567,7 @@ function selfTest() { ); // --- The failure has to carry the remedy, not just the verdict. + battery('The failure has to carry the remedy, not just the verdict.'); const failed = later.lines.join('\n'); t('the failure says the card-keyed gate cannot see this', failed.includes('Duplicate Fix Guard'), true); t('the failure tells the reader to close one PR', failed.includes('close it'), true); @@ -458,6 +575,7 @@ function selfTest() { // --- UNDETERMINED is its own answer. It must never read as clean, and it // must never turn an author red for a pagination limit. + battery('UNDETERMINED is its own answer. It must never read as clean, and it'); const undet = verdict({ claimed: ['.objectui-sha'], others: [], undetermined: [{ number: 777 }] }); t('an unwalkable file list does not turn this PR red', undet.exit, EXIT_CLEAN); t('an unwalkable file list is reported as UNDETERMINED', undet.lines.join('\n').includes('UNDETERMINED'), true); @@ -466,6 +584,7 @@ function selfTest() { t('a clean run with nothing undetermined emits no warning', earlier.lines.join('\n').includes('::warning::'), false); // --- Wiring absent: never clean, never an accusation. + battery('Wiring absent: never clean, never an accusation.'); const unwired = judge(readPrContext({})); t('no PR context at all exits NOT WIRED', unwired.exit, EXIT_NOT_WIRED); t('NOT WIRED says it judged nothing', unwired.lines.join('\n').includes('judged nothing'), true); @@ -476,6 +595,7 @@ function selfTest() { // --- The short-circuit. This is the property that makes the gate affordable, // and it is invisible in the verdict layer, so it is pinned here against a // recording fake API. Fixture paths name a tree that exists in no repo. + battery('The short-circuit. This is the property that makes the gate affordable,'); const calls = []; const fakeApi = (files) => async (path) => { calls.push(path); @@ -539,6 +659,10 @@ function selfTest() { t('the card-keyed duplicate gate still exists', sibling !== '', true); t('...and still asks its own question', sibling.includes('No other open PR may claim the same issue'), true); + // The floor runs BEFORE the verdict below, so a success line can only be + // printed by a run in which every declared battery registered its cases. + for (const message of batteryFloorFailures()) cases.push([message, false, true]); + let failedCount = 0; for (const [name, actual, expected] of cases) { const ok = JSON.stringify(actual) === JSON.stringify(expected); diff --git a/scripts/check-tenant-audit-census.mjs b/scripts/check-tenant-audit-census.mjs index e7152c84c2..f006a9463c 100644 --- a/scripts/check-tenant-audit-census.mjs +++ b/scripts/check-tenant-audit-census.mjs @@ -96,6 +96,113 @@ import { 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) ────────────────── +// +// This self-test used to decide success by "no failure was recorded" and +// nothing else, 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. A set difference says WHICH +// battery stopped; a count says only that something did. +// +// 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. +// +// The machinery lives HERE, at module scope, rather than inside the self-test: +// this self-test's assertion sink is not a block-bodied helper in its body (it +// is a concise arrow, or a module-scope function), so there is no in-body +// helper to thread a per-run ledger through. Module scope is safe because the +// self-test runs once per process, and it is what lets the existing sink route +// through `registerCase()` with no case rewritten and no assertion changed. +const SELF_TEST_BATTERIES = Object.freeze({ + 'A DRIFT': 5, + 'B PROSE': 4, + '⭐ THE SPLIT': 9, + 'refusals': 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 = 4; + +// The key an assertion is filed under when no battery is open. It is not a +// declared battery, so it reds by the same set difference rather than silently +// inflating whichever battery happened to run last. +const UNATTRIBUTED_BATTERY = '(no battery open)'; + +// ⚠️ None of these helpers is named with a self-test spelling, deliberately and +// on the record: `check:pm-dispatch-gates` anchors on a top-level declaration +// whose NAME spells self-test, and every such name owes a row in that gate's +// COMPOUND_ANCHOR_LEDGER. These are the battery ROSTER's machinery -- they hold +// no fixtures to mask and read no path literal -- so the accurate name is the +// one that says `battery`, not the one that would owe a ledger row for a role +// this code does not have. + +/** Cases registered per battery: `battery()` opens one, `registerCase()` files into it. */ +const batteryCases = new Map(); +let openBattery = null; + +/** Open a battery. Every assertion after this line is attributed to it. */ +function battery(name) { + openBattery = name; +} + +/** Called by the self-test's own assertion sink, once per assertion. */ +function registerCase() { + const name = openBattery ?? UNATTRIBUTED_BATTERY; + batteryCases.set(name, (batteryCases.get(name) ?? 0) + 1); +} + +/** + * The floor: every declared battery RAN, and ran its cases (#13489). + * + * Evaluated after every battery has had its chance and BEFORE the verdict, so + * the success line can only be printed by a run in which the set of batteries + * that registered assertions EQUALS the set declared. + */ +function batteryFloorFailures() { + const declared = Object.keys(SELF_TEST_BATTERIES); + const problems = []; + if (declared.length < SELF_TEST_BATTERY_FLOOR) { + problems.push( + `SELF_TEST_BATTERIES declares ${declared.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 batteryCases) { + if (declared.includes(name)) continue; + problems.push( + `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 declared) { + const count = batteryCases.get(name) ?? 0; + if (count >= SELF_TEST_BATTERIES[name]) continue; + problems.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]} — cases that used to run no longer do.`, + ); + } + if (problems.length) { + problems.push( + '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 problems; +} + import { BEGIN_MARKER, COUNTS, @@ -607,7 +714,11 @@ let selfTestReachedVerdict = false; export function selfTest() { const cases = []; - const t = (name, ok, detail) => cases.push({ name, ok: Boolean(ok), detail }); + const t = (name, ok, detail) => { + registerCase(); + return cases.push({ name, ok: Boolean(ok), detail }); + }; + battery('A DRIFT'); const census = runCensus(); const page = readFileSync(join(ROOT, PAGE), 'utf8'); @@ -649,6 +760,7 @@ export function selfTest() { // so a sentence-shaped needle silently replaces NOTHING and the case then // passes a clean page off as a drifted one. The gate matches with whitespace // collapsed; this self-test edits the raw file, and the two are not the same text. + battery('B PROSE'); const enforcedProse = (n) => `**${n} further sites**`; t('a stale hand-written number in the prose is a finding', check(page.replace( @@ -682,6 +794,7 @@ export function selfTest() { // unenforced value may move without a finding, and everything around it may // not. A gate that got this wrong in either direction would look identical on // a clean tree. + battery('⭐ THE SPLIT'); const scaleRow = UNENFORCED_SCALE_ROWS[0]; const drift = (text, delta) => text.replace( scaleRow.pattern, @@ -731,9 +844,14 @@ export function selfTest() { .some((p) => p.startsWith('[unenforced-prose-missing]'))); // ── refusals ─────────────────────────────────────────────────────────────── + battery('refusals'); t('an empty census refuses rather than certifying the artefacts', checkPage({ ...census, sites: [] }, page, counts).some((p) => p.startsWith('[empty-census]'))); + // The floor runs BEFORE the verdict below, so a success line can only be + // printed by a run in which every declared battery registered its cases. + for (const message of batteryFloorFailures()) cases.push({ name: message, ok: false }); + const failed = cases.filter((c) => !c.ok); for (const c of failed) console.error(` ✗ ${c.name}${c.detail ? ` -- ${c.detail}` : ''}`); if (failed.length > 0) { diff --git a/scripts/check-watch-hint-literal.mjs b/scripts/check-watch-hint-literal.mjs index 3528a18f03..20a024ad9f 100644 --- a/scripts/check-watch-hint-literal.mjs +++ b/scripts/check-watch-hint-literal.mjs @@ -146,6 +146,118 @@ 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) ────────────────── +// +// This self-test used to decide success by "no failure was recorded" and +// nothing else, 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. A set difference says WHICH +// battery stopped; a count says only that something did. +// +// 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. +// +// The machinery lives HERE, at module scope, rather than inside the self-test: +// this self-test's assertion sink is not a block-bodied helper in its body (it +// is a concise arrow, or a module-scope function), so there is no in-body +// helper to thread a per-run ledger through. Module scope is safe because the +// self-test runs once per process, and it is what lets the existing sink route +// through `registerCase()` with no case rewritten and no assertion changed. +const SELF_TEST_BATTERIES = Object.freeze({ + 'the computed spellings this gate exists to reject': 10, + 'shapes that are not a literal ARRAY': 3, + 'the literal spellings that must stay accepted': 6, + 'comments and prose are not declarations': 3, + 'EVERY rostered name is judged, not just the first': 10, + 'the per-name floor': 5, + 'discovery of an UNROSTERED spelling of the idiom': 5, + 'the empty population is refused, not passed': 1, + 'the live tree': 14, +}); + +// DELETING an entry silences that battery's floor exactly as effectively as +// zeroing it, so the roster's own size is pinned too. +const SELF_TEST_BATTERY_FLOOR = 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)'; + +// ⚠️ None of these helpers is named with a self-test spelling, deliberately and +// on the record: `check:pm-dispatch-gates` anchors on a top-level declaration +// whose NAME spells self-test, and every such name owes a row in that gate's +// COMPOUND_ANCHOR_LEDGER. These are the battery ROSTER's machinery -- they hold +// no fixtures to mask and read no path literal -- so the accurate name is the +// one that says `battery`, not the one that would owe a ledger row for a role +// this code does not have. + +/** Cases registered per battery: `battery()` opens one, `registerCase()` files into it. */ +const batteryCases = new Map(); +let openBattery = null; + +/** Open a battery. Every assertion after this line is attributed to it. */ +function battery(name) { + openBattery = name; +} + +/** Called by the self-test's own assertion sink, once per assertion. */ +function registerCase() { + const name = openBattery ?? UNATTRIBUTED_BATTERY; + batteryCases.set(name, (batteryCases.get(name) ?? 0) + 1); +} + +/** + * The floor: every declared battery RAN, and ran its cases (#13489). + * + * Evaluated after every battery has had its chance and BEFORE the verdict, so + * the success line can only be printed by a run in which the set of batteries + * that registered assertions EQUALS the set declared. + */ +function batteryFloorFailures() { + const declared = Object.keys(SELF_TEST_BATTERIES); + const problems = []; + if (declared.length < SELF_TEST_BATTERY_FLOOR) { + problems.push( + `SELF_TEST_BATTERIES declares ${declared.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 batteryCases) { + if (declared.includes(name)) continue; + problems.push( + `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 declared) { + const count = batteryCases.get(name) ?? 0; + if (count >= SELF_TEST_BATTERIES[name]) continue; + problems.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]} — cases that used to run no longer do.`, + ); + } + if (problems.length) { + problems.push( + '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 problems; +} + const HERE = dirname(fileURLToPath(import.meta.url)); const REPO_ROOT = resolve(HERE, '..'); @@ -445,7 +557,10 @@ let selfTestReachedVerdict = false; export function selfTest() { const cases = []; - const t = (name, ok, detail) => cases.push({ name, ok: Boolean(ok), detail }); + const t = (name, ok, detail) => { + registerCase(); + return cases.push({ name, ok: Boolean(ok), detail }); + }; const [DIR_NAME] = DECL_NAMES; /** A fixture that never spells the declaration verbatim in THIS file's text. */ const decl = (rhs, extra = '', name = DIR_NAME) => `${extra}const ${name} = ${rhs};\n`; @@ -455,6 +570,7 @@ export function selfTest() { const accepted = (src) => verdict(src) !== null && verdict(src).ok === true; // -- the computed spellings this gate exists to reject --------------------- + battery('the computed spellings this gate exists to reject'); const COMPUTED = [ 'ROOTS.map((r) => `${r}/**`)', '[`${SCAN_ROOT}/**`]', @@ -483,12 +599,14 @@ export function selfTest() { rejected(decl('[`${SCAN_ROOT}/**`]', `// hint: 'packages/**'\n`))); // -- shapes that are not a literal ARRAY ----------------------------------- + battery('shapes that are not a literal ARRAY'); t('an empty declaration is rejected -- it names no subtree', rejected(decl('[]'))); t('a bare string declaration is rejected', rejected(decl("'scripts/**'"))); t('two declaration sites are refused rather than judged', rejected(decl("['a/**']") + decl("['b/**']"))); // -- the literal spellings that must stay accepted ------------------------- + battery('the literal spellings that must stay accepted'); t('the canonical spelling is accepted', accepted(decl("['scripts/**']"))); t('an exported declaration is accepted', accepted(decl("['a/**', \"b/**\"]", 'export '))); t('a multi-line array with a trailing comma is accepted', @@ -501,6 +619,7 @@ export function selfTest() { JSON.stringify(verdict(decl("['a/**', 'b/**']")).hints) === '["a/**","b/**"]'); // -- comments and prose are not declarations ------------------------------- + battery('comments and prose are not declarations'); t('a COMMENTED-OUT computed declaration does not shadow the real one', accepted(`// ${decl('ROOTS.map((r) => r)')}${decl("['skills/**']")}`)); t('a file that only MENTIONS the constant is not a declarer', @@ -514,6 +633,7 @@ export function selfTest() { // literal spelling. Each is exercised through the SAME computed spelling that // the first name rejects, so a name that is rostered but not wired would show // up here as an acceptance rather than as a missing case. + battery('EVERY rostered name is judged, not just the first'); for (const name of DECL_NAMES) { t(`a computed ${name} declaration is rejected`, rejected(decl('[`${SCAN_ROOT}/**`]', '', name)), name); @@ -531,6 +651,7 @@ export function selfTest() { // ⭐ The vacuity trap the widening would otherwise CREATE. A global floor // passes all three of these: the population is large and healthy, and one // name is silently gone. + battery('the per-name floor'); t('an empty population reports EVERY rostered name as missing', missingNames([]).join() === DECL_NAMES.join()); const oneNameGone = DECL_NAMES.slice(1).map((name) => ({ rel: 'g.mjs', name, ok: true, hints: ['a/**'] })); @@ -545,6 +666,7 @@ export function selfTest() { missingNames(DECL_NAMES.map((name) => ({ rel: 'g.mjs', name, ok: false, why: 'x' }))).length === 0); // -- discovery of an UNROSTERED spelling of the idiom ---------------------- + battery('discovery of an UNROSTERED spelling of the idiom'); const strayName = `SOME_OTHER_${IDIOM_SUFFIX}`; t('a declaration under an unrostered name ending in the idiom suffix is discovered', unrosteredNames(decl("['a/**']", '', strayName)).join() === strayName); @@ -558,10 +680,12 @@ export function selfTest() { unrosteredNames(`// ${decl("['a/**']", '', strayName)}`).length === 0); // -- the empty population is refused, not passed --------------------------- + battery('the empty population is refused, not passed'); t('an empty file list produces no rows, which the floor above refuses', audit([]).rows.length === 0 && missingNames(audit([]).rows).length === DECL_NAMES.length); // -- the live tree --------------------------------------------------------- + battery('the live tree'); const { rows: live, strays } = audit(walk(REPO_ROOT)); const here = 'scripts/check-watch-hint-literal.mjs'; t('the live sweep finds a real population, not a broken one', live.length >= 30, @@ -596,6 +720,10 @@ export function selfTest() { .every((r) => /^packages\/[^/]+\/scripts\//.test(r.rel)), live.filter((r) => !r.rel.startsWith('scripts/')).map((r) => r.rel).join(' · ')); + // The floor runs BEFORE the verdict below, so a success line can only be + // printed by a run in which every declared battery registered its cases. + for (const message of batteryFloorFailures()) cases.push({ name: message, ok: false }); + const failed = cases.filter((c) => !c.ok); for (const c of failed) console.error(` ✗ ${c.name}${c.detail ? ` -- ${c.detail}` : ''}`); if (failed.length) { diff --git a/scripts/import-prerequisite.mjs b/scripts/import-prerequisite.mjs index 41bb31647a..3d272f0b35 100644 --- a/scripts/import-prerequisite.mjs +++ b/scripts/import-prerequisite.mjs @@ -84,6 +84,126 @@ import { fileURLToPath, pathToFileURL } from 'node:url'; import { WORKSPACE_SCOPE, workspaceBuildFix } from './cli-build-prerequisite.mjs'; import { isEntrypoint } from './invoked-as.mjs'; +// ── The self-test's own battery roster and floor (#13489) ────────────────── +// +// This self-test used to decide success by "no failure was recorded" and +// nothing else, 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. A set difference says WHICH +// battery stopped; a count says only that something did. +// +// 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. +// +// The machinery lives HERE, at module scope, rather than inside the self-test: +// this self-test's assertion sink is not a block-bodied helper in its body (it +// is a concise arrow, or a module-scope function), so there is no in-body +// helper to thread a per-run ledger through. Module scope is safe because the +// self-test runs once per process, and it is what lets the existing sink route +// through `registerCase()` with no case rewritten and no assertion changed. +const SELF_TEST_BATTERIES = Object.freeze({ + 'the specifier → package reduction': 6, + 'branch 1: nothing there at all': 2, + 'branch 2: workspace package present, never built': 4, + 'branch 3: a subpath import of the same unbuilt package': 1, + 'branch 4: third party present but incomplete': 2, + 'branch 5: the package is whole, so the miss came from inside it': 3, + 'branch 6: a built workspace package': 1, + 'branch 7: resolved-then-threw is not ours': 1, + 'the entry-point probe\'s own deferrals': 3, + 'a LOCAL module that reaches for an absent package': 3, + 'a local module whose failure text names nothing': 1, + 'the message parser on its own': 3, + 'SELF-REFERENCE: a package\'s own gate importing it by name': 4, + 'findPackageDir walks upward, as node does': 2, + 'the printed COMMAND: a path the reader can run, not a name': 9, + 'the inherited pipe-shape advisory': 5, + 'the exit-code CLASS, and the advisory that must move with it': 10, +}); + +// DELETING an entry silences that battery's floor exactly as effectively as +// zeroing it, so the roster's own size is pinned too. +const SELF_TEST_BATTERY_FLOOR = 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)'; + +// ⚠️ None of these helpers is named with a self-test spelling, deliberately and +// on the record: `check:pm-dispatch-gates` anchors on a top-level declaration +// whose NAME spells self-test, and every such name owes a row in that gate's +// COMPOUND_ANCHOR_LEDGER. These are the battery ROSTER's machinery -- they hold +// no fixtures to mask and read no path literal -- so the accurate name is the +// one that says `battery`, not the one that would owe a ledger row for a role +// this code does not have. + +/** Cases registered per battery: `battery()` opens one, `registerCase()` files into it. */ +const batteryCases = new Map(); +let openBattery = null; + +/** Open a battery. Every assertion after this line is attributed to it. */ +function battery(name) { + openBattery = name; +} + +/** Called by the self-test's own assertion sink, once per assertion. */ +function registerCase() { + const name = openBattery ?? UNATTRIBUTED_BATTERY; + batteryCases.set(name, (batteryCases.get(name) ?? 0) + 1); +} + +/** + * The floor: every declared battery RAN, and ran its cases (#13489). + * + * Evaluated after every battery has had its chance and BEFORE the verdict, so + * the success line can only be printed by a run in which the set of batteries + * that registered assertions EQUALS the set declared. + */ +function batteryFloorFailures() { + const declared = Object.keys(SELF_TEST_BATTERIES); + const problems = []; + if (declared.length < SELF_TEST_BATTERY_FLOOR) { + problems.push( + `SELF_TEST_BATTERIES declares ${declared.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 batteryCases) { + if (declared.includes(name)) continue; + problems.push( + `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 declared) { + const count = batteryCases.get(name) ?? 0; + if (count >= SELF_TEST_BATTERIES[name]) continue; + problems.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]} — cases that used to run no longer do.`, + ); + } + if (problems.length) { + problems.push( + '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 problems; +} + /** `pnpm install` at the repo root — the one remedy for an absent dependency. */ export const INSTALL_FIX = 'pnpm install'; @@ -604,10 +724,14 @@ let selfTestReachedVerdict = false; export function selfTest() { const cases = []; - const t = (name, ok, detail) => cases.push({ name, ok: Boolean(ok), detail }); + const t = (name, ok, detail) => { + registerCase(); + return cases.push({ name, ok: Boolean(ok), detail }); + }; const MNF = (msg) => Object.assign(new Error(msg), { code: 'ERR_MODULE_NOT_FOUND' }); // ── the specifier → package reduction ───────────────────────────────────── + battery('the specifier → package reduction'); t('a bare specifier is its own package', packageNameOf('yaml') === 'yaml'); t('a subpath is dropped', packageNameOf('yaml/util') === 'yaml'); t('a scoped package keeps both segments', packageNameOf('@objectstack/spec') === '@objectstack/spec'); @@ -641,11 +765,13 @@ export function selfTest() { const at = (spec, msg) => classifyImportFailure(spec, MNF(msg ?? `Cannot find package '${spec}'`), dir); // ── branch 1: nothing there at all ────────────────────────────────────── + battery('branch 1: nothing there at all'); const absent = at('totally-absent-fixture'); t('an absent package is not-installed', absent.kind === 'not-installed', absent.kind); t('an absent package prescribes install', absent.fix === INSTALL_FIX, absent.fix); // ── branch 2: workspace package present, never built ──────────────────── + battery('branch 2: workspace package present, never built'); const unbuilt = at('@objectstack/unbuilt-fixture'); t('a linked-but-unbuilt workspace package is workspace-unbuilt', unbuilt.kind === 'workspace-unbuilt', unbuilt.kind); t('an unbuilt workspace package prescribes a BUILD, never an install', @@ -656,6 +782,7 @@ export function selfTest() { t('unbuilt and not-installed are DIFFERENT verdicts', unbuilt.kind !== absent.kind && unbuilt.fix !== absent.fix); // ── branch 3: a subpath import of the same unbuilt package ────────────── + battery('branch 3: a subpath import of the same unbuilt package'); const unbuiltSub = classifyImportFailure( '@objectstack/unbuilt-fixture/system', Object.assign(new Error('no exports main'), { code: 'ERR_PACKAGE_PATH_NOT_EXPORTED' }), @@ -664,12 +791,14 @@ export function selfTest() { t('a subpath of an unbuilt workspace package classifies the same way', unbuiltSub.kind === 'workspace-unbuilt', unbuiltSub.kind); // ── branch 4: third party present but incomplete ──────────────────────── + battery('branch 4: third party present but incomplete'); const partial = at('partial-fixture'); t('a third party missing its entry is broken, NOT unbuilt', partial.kind === 'broken', partial.kind); t('a broken install says so rather than blaming the tree', partial.headline.includes('installed but incomplete'), partial.headline); // ── branch 5: the package is whole, so the miss came from inside it ────── + battery('branch 5: the package is whole, so the miss came from inside it'); const inner = at('whole-fixture', "Cannot find package 'some-transitive-dep' imported from /x/whole-fixture/index.js"); t('a whole package whose OWN import failed is dependency-missing', inner.kind === 'dependency-missing', inner.kind); t('dependency-missing quotes what node actually said', @@ -678,15 +807,18 @@ export function selfTest() { !inner.headline.includes('is not installed'), inner.headline); // ── branch 6: a built workspace package ───────────────────────────────── + battery('branch 6: a built workspace package'); const built = at('@objectstack/built-fixture', "Cannot find package 'inner-dep'"); t('a BUILT workspace package is never reported unbuilt', built.kind !== 'workspace-unbuilt', built.kind); // ── branch 7: resolved-then-threw is not ours ─────────────────────────── + battery('branch 7: resolved-then-threw is not ours'); const threw = classifyImportFailure('whole-fixture', new SyntaxError('Unexpected token'), dir); t('a package that resolved and threw is broken with no headline (rethrown)', threw.kind === 'broken' && threw.headline === '', `${threw.kind}/${threw.headline}`); // ── the entry-point probe's own deferrals ─────────────────────────────── + battery('the entry-point probe\'s own deferrals'); t('an unreadable manifest defers rather than guessing', 'unknown' in entryPointOnDisk(join(dir, 'no-such-package'))); const noEntry = mk('no-entry-fixture', { name: 'no-entry-fixture' }); @@ -699,6 +831,7 @@ export function selfTest() { // The shape three ratchet gates have: they import '../eslint.config.mjs', // and that file imports '@typescript-eslint/parser'. Naming the local module // as "not installed" would be nonsense; naming the package is the diagnosis. + battery('a LOCAL module that reaches for an absent package'); const viaLocal = classifyImportFailure( '../eslint.config.mjs', MNF("Cannot find package '@typescript-eslint/parser' imported from /repo/eslint.config.mjs"), @@ -713,11 +846,13 @@ export function selfTest() { !viaLocal.headline.includes('eslint.config'), viaLocal.headline); // ── a local module whose failure text names nothing ───────────────────── + battery('a local module whose failure text names nothing'); const opaque = classifyImportFailure('../eslint.config.mjs', MNF('something else entirely'), dir); t('an unrecognisable local failure defers rather than inventing a package', opaque.kind === 'broken' && opaque.headline === '', `${opaque.kind}/${opaque.headline}`); // ── the message parser on its own ─────────────────────────────────────── + battery('the message parser on its own'); t('a package miss is read out of the message', missingPackageFromMessage("Cannot find package 'yaml' imported from /x") === 'yaml'); t('a scoped package miss keeps its scope', missingPackageFromMessage("Cannot find package '@typescript-eslint/parser' imported from /x") === '@typescript-eslint/parser'); @@ -728,6 +863,7 @@ export function selfTest() { // `packages/lint/scripts/*.mjs` imports '@objectstack/lint'. There is no // node_modules link for that — node resolves it through the enclosing // package.json — so a node_modules-only walk reports it uninstalled. + battery('SELF-REFERENCE: a package\'s own gate importing it by name'); const selfPkgDir = join(dir, 'self-pkg'); mkdirSync(join(selfPkgDir, 'scripts'), { recursive: true }); writeFileSync( @@ -755,6 +891,7 @@ export function selfTest() { findPackageDir('@objectstack/no-exports', join(noExports, 'scripts')) === ''); // ── findPackageDir walks upward, as node does ─────────────────────────── + battery('findPackageDir walks upward, as node does'); const deep = join(dir, 'a', 'b', 'c'); mkdirSync(deep, { recursive: true }); t('a nested importer finds a package hoisted above it', @@ -773,6 +910,7 @@ export function selfTest() { // That shape is the whole reason the marker is the workspace manifest, and // the negative control below is what turns that from a preference into a // measurement. + battery('the printed COMMAND: a path the reader can run, not a name'); const wt = join(dir, 'objectstack-issue-fixture'); mkdirSync(join(wt, 'scripts'), { recursive: true }); mkdirSync(join(wt, 'packages', 'lint', 'scripts'), { recursive: true }); @@ -851,6 +989,7 @@ export function selfTest() { // print. The clauses are the four the advisory is for; the negative one is // the load-bearing one, since the wrong claim it excludes reads perfectly // plausible and shipped once already. + battery('the inherited pipe-shape advisory'); const advisory = prerequisiteNotMetText( new URL('file:///repo/scripts/check-fixture-gate.mjs').href, { headline: 'h', detail: ['d'], fix: 'f' }, @@ -882,6 +1021,7 @@ export function selfTest() { // constant reading 3, every consumer green (they all treat any non-zero as // failure, so 1-instead-of-3 is invisible to all of them), and a message that // still reads perfectly right. Nothing else in this repo would notice. + battery('the exit-code CLASS, and the advisory that must move with it'); const hardcodesExitCall = (fn) => /process\.exit\(\s*\d/.test(fn.toString()); const spellsALiteralCode = (fn) => /Exit code \d/.test(fn.toString()); t('the refusal exits through the named constant, never a literal', @@ -922,6 +1062,10 @@ export function selfTest() { t('the advisory carries NO stale spelling of the old code', !/Exit code 1\b/.test(advisory), advisory); + // The floor runs BEFORE the verdict below, so a success line can only be + // printed by a run in which every declared battery registered its cases. + for (const message of batteryFloorFailures()) cases.push({ name: message, ok: false }); + const failed = cases.filter((c) => !c.ok); for (const c of failed) console.error(` ✗ ${c.name}${c.detail ? ` -- ${c.detail}` : ''}`); if (failed.length) { diff --git a/scripts/invoked-as.mjs b/scripts/invoked-as.mjs index 913329c0fd..60700de081 100644 --- a/scripts/invoked-as.mjs +++ b/scripts/invoked-as.mjs @@ -100,6 +100,111 @@ import { tmpdir } from 'node:os'; import { join, relative, resolve } from 'node:path'; import { fileURLToPath, pathToFileURL } from 'node:url'; +// ── The self-test's own battery roster and floor (#13489) ────────────────── +// +// This self-test used to decide success by "no failure was recorded" and +// nothing else, 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. A set difference says WHICH +// battery stopped; a count says only that something did. +// +// 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. +// +// The machinery lives HERE, at module scope, rather than inside the self-test: +// this self-test's assertion sink is not a block-bodied helper in its body (it +// is a concise arrow, or a module-scope function), so there is no in-body +// helper to thread a per-run ledger through. Module scope is safe because the +// self-test runs once per process, and it is what lets the existing sink route +// through `registerCase()` with no case rewritten and no assertion changed. +const SELF_TEST_BATTERIES = Object.freeze({ + 'the predicate, directly': 5, + 'the fixture: a probe reached three ways': 6, +}); + +// DELETING an entry silences that battery's floor exactly as effectively as +// zeroing it, so the roster's own size is pinned too. +const SELF_TEST_BATTERY_FLOOR = 2; + +// The key an assertion is filed under when no battery is open. It is not a +// declared battery, so it reds by the same set difference rather than silently +// inflating whichever battery happened to run last. +const UNATTRIBUTED_BATTERY = '(no battery open)'; + +// ⚠️ None of these helpers is named with a self-test spelling, deliberately and +// on the record: `check:pm-dispatch-gates` anchors on a top-level declaration +// whose NAME spells self-test, and every such name owes a row in that gate's +// COMPOUND_ANCHOR_LEDGER. These are the battery ROSTER's machinery -- they hold +// no fixtures to mask and read no path literal -- so the accurate name is the +// one that says `battery`, not the one that would owe a ledger row for a role +// this code does not have. + +/** Cases registered per battery: `battery()` opens one, `registerCase()` files into it. */ +const batteryCases = new Map(); +let openBattery = null; + +/** Open a battery. Every assertion after this line is attributed to it. */ +function battery(name) { + openBattery = name; +} + +/** Called by the self-test's own assertion sink, once per assertion. */ +function registerCase() { + const name = openBattery ?? UNATTRIBUTED_BATTERY; + batteryCases.set(name, (batteryCases.get(name) ?? 0) + 1); +} + +/** + * The floor: every declared battery RAN, and ran its cases (#13489). + * + * Evaluated after every battery has had its chance and BEFORE the verdict, so + * the success line can only be printed by a run in which the set of batteries + * that registered assertions EQUALS the set declared. + */ +function batteryFloorFailures() { + const declared = Object.keys(SELF_TEST_BATTERIES); + const problems = []; + if (declared.length < SELF_TEST_BATTERY_FLOOR) { + problems.push( + `SELF_TEST_BATTERIES declares ${declared.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 batteryCases) { + if (declared.includes(name)) continue; + problems.push( + `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 declared) { + const count = batteryCases.get(name) ?? 0; + if (count >= SELF_TEST_BATTERIES[name]) continue; + problems.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]} — cases that used to run no longer do.`, + ); + } + if (problems.length) { + problems.push( + '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 problems; +} + /** * Is `entryArg` -- a `process.argv[1]` -- the module at `selfPath`? * @@ -169,11 +274,15 @@ let selfTestReachedVerdict = false; export function selfTest() { const cases = []; - const t = (name, ok, detail) => cases.push({ name, ok: Boolean(ok), detail }); + const t = (name, ok, detail) => { + registerCase(); + return cases.push({ name, ok: Boolean(ok), detail }); + }; const SELF = fileURLToPath(import.meta.url); // ── the predicate, directly ──────────────────────────────────────────────── + battery('the predicate, directly'); t('an absent argv[1] is not this module -- the `node --eval` importer', !invokedAs(undefined, SELF) && !invokedAs('', SELF)); t('an exact path is this module', invokedAs(SELF, SELF)); t('a relative path resolving to this module is this module', invokedAs(relative(process.cwd(), SELF), SELF)); @@ -181,6 +290,7 @@ export function selfTest() { t('an entry path that cannot be read is not this module (no throw)', !invokedAs(resolve(SELF, '..', 'no-such-file-here.mjs'), SELF)); // ── the fixture: a probe reached three ways ──────────────────────────────── + battery('the fixture: a probe reached three ways'); const dir = mkdtempSync(join(tmpdir(), 'invoked-as-')); try { // A directory whose name needs percent-encoding, because two of the @@ -252,6 +362,10 @@ export function selfTest() { rmSync(dir, { recursive: true, force: true }); } + // The floor runs BEFORE the verdict below, so a success line can only be + // printed by a run in which every declared battery registered its cases. + for (const message of batteryFloorFailures()) cases.push({ name: message, ok: false }); + const failed = cases.filter((c) => !c.ok); for (const c of failed) console.error(` ✗ ${c.name}${c.detail ? ` -- ${c.detail}` : ''}`); if (failed.length) { diff --git a/scripts/qa/qa-rollup.mjs b/scripts/qa/qa-rollup.mjs index bbea74521d..1e20fa8964 100755 --- a/scripts/qa/qa-rollup.mjs +++ b/scripts/qa/qa-rollup.mjs @@ -117,6 +117,120 @@ import { fileURLToPath } from 'node:url'; import { dirname, join } from 'node:path'; import { isEntrypoint } from '../invoked-as.mjs'; +// ── The self-test's own battery roster and floor (#13489) ────────────────── +// +// This self-test used to decide success by "no failure was recorded" and +// nothing else, 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. A set difference says WHICH +// battery stopped; a count says only that something did. +// +// 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. +// +// The machinery lives HERE, at module scope, rather than inside the self-test: +// this self-test's assertion sink is not a block-bodied helper in its body (it +// is a concise arrow, or a module-scope function), so there is no in-body +// helper to thread a per-run ledger through. Module scope is safe because the +// self-test runs once per process, and it is what lets the existing sink route +// through `registerCase()` with no case rewritten and no assertion changed. +const SELF_TEST_BATTERIES = Object.freeze({ + 'every real shape parses': 5, + 'the ONE judged/total spelling': 5, + 'the ONE NOT-RUN spelling': 7, + 'selector kinds': 3, + 'every RETIRED phrasing is rejected, by the right reason': 16, + 'malformed titles are REPORTED, never silently dropped': 5, + 'the judged cell now has ONE provenance': 2, + 'the legacy boundary reads created_at, never the title': 4, + 'freshness is three-valued': 8, + 'latest-per-selector wins': 4, + 'the render must never hide a record': 19, +}); + +// 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)'; + +// ⚠️ None of these helpers is named with a self-test spelling, deliberately and +// on the record: `check:pm-dispatch-gates` anchors on a top-level declaration +// whose NAME spells self-test, and every such name owes a row in that gate's +// COMPOUND_ANCHOR_LEDGER. These are the battery ROSTER's machinery -- they hold +// no fixtures to mask and read no path literal -- so the accurate name is the +// one that says `battery`, not the one that would owe a ledger row for a role +// this code does not have. + +/** Cases registered per battery: `battery()` opens one, `registerCase()` files into it. */ +const batteryCases = new Map(); +let openBattery = null; + +/** Open a battery. Every assertion after this line is attributed to it. */ +function battery(name) { + openBattery = name; +} + +/** Called by the self-test's own assertion sink, once per assertion. */ +function registerCase() { + const name = openBattery ?? UNATTRIBUTED_BATTERY; + batteryCases.set(name, (batteryCases.get(name) ?? 0) + 1); +} + +/** + * The floor: every declared battery RAN, and ran its cases (#13489). + * + * Evaluated after every battery has had its chance and BEFORE the verdict, so + * the success line can only be printed by a run in which the set of batteries + * that registered assertions EQUALS the set declared. + */ +function batteryFloorFailures() { + const declared = Object.keys(SELF_TEST_BATTERIES); + const problems = []; + if (declared.length < SELF_TEST_BATTERY_FLOOR) { + problems.push( + `SELF_TEST_BATTERIES declares ${declared.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 batteryCases) { + if (declared.includes(name)) continue; + problems.push( + `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 declared) { + const count = batteryCases.get(name) ?? 0; + if (count >= SELF_TEST_BATTERIES[name]) continue; + problems.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]} — cases that used to run no longer do.`, + ); + } + if (problems.length) { + problems.push( + '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 problems; +} + /** The verdict vocabulary, in report order. */ export const VERDICTS = ['PASS', 'PARTIAL', 'FAIL', 'BLOCKED', 'NOT-RUN']; @@ -871,6 +985,7 @@ export const RETIRED_TITLES = [ ]; function assert(cond, msg, failures) { + registerCase(); if (!cond) failures.push(msg); return cond ? 1 : 0; } @@ -886,6 +1001,7 @@ async function selfTest() { let checked = 0; // --- every real shape parses ------------------------------------------- + battery('every real shape parses'); for (const title of FIXTURE_TITLES) { const p = parseRunTitle(title); checked += assert(p.ok, `fixture must parse: ${title}\n -> ${p.ok ? '' : p.reason}`, failures); @@ -894,6 +1010,7 @@ async function selfTest() { const byIndex = FIXTURE_TITLES.map(parseRunTitle); // --- the ONE judged/total spelling -------------------------------------- + battery('the ONE judged/total spelling'); checked += assert(byIndex[0].judged === 18 && byIndex[0].total === 18, '"(18/18)" -> 18/18', failures); checked += assert(byIndex[1].judged === 9 && byIndex[1].total === 20, '"(9/20)" -> 9/20', failures); checked += assert(byIndex[2].judged === 1 && byIndex[2].total === 12, '"(1/12)" -> 1/12', failures); @@ -902,6 +1019,7 @@ async function selfTest() { checked += assert(byIndex[3].selector === 'priority:P0', 'non-area selector survives intact', failures); // --- the ONE NOT-RUN spelling ------------------------------------------- + battery('the ONE NOT-RUN spelling'); checked += assert(byIndex[1].counts['NOT-RUN'] === 10, 'segment "10 NOT-RUN" counts as NOT-RUN', failures); checked += assert(byIndex[2].counts['NOT-RUN'] === 11, 'segment "11 NOT-RUN" counts as NOT-RUN', failures); checked += assert(byIndex[3].counts.BLOCKED === 2, 'BLOCKED parsed', failures); @@ -914,6 +1032,7 @@ async function selfTest() { checked += assert(byIndex[1].counts.FAIL === 0, 'declared 0 FAIL is present AND zero', failures); // --- selector kinds ----------------------------------------------------- + battery('selector kinds'); checked += assert(classifySelector('tier2c:browser-2') === 'tier', 'tier selector classified', failures); checked += assert(classifySelector('priority:P0') === 'priority', 'priority selector classified', failures); checked += assert(classifySelector('studio-authoring') === 'area', 'area selector classified', failures); @@ -921,6 +1040,7 @@ async function selfTest() { // --- every RETIRED phrasing is rejected, by the right reason ------------- // This is the half that holds the ruling: a parser re-widened to accept one // of these passes every happy-path assertion above. + battery('every RETIRED phrasing is rejected, by the right reason'); for (const [title, expectedReason] of RETIRED_TITLES) { const p = parseRunTitle(title); checked += assert( @@ -931,6 +1051,7 @@ async function selfTest() { } // --- malformed titles are REPORTED, never silently dropped -------------- + battery('malformed titles are REPORTED, never silently dropped'); const bad = [ ['', 'empty'], ['Some other issue title', 'not a run record'], @@ -946,16 +1067,19 @@ async function selfTest() { // --- the judged cell now has ONE provenance ----------------------------- // The record's own mandatory parenthetical. Nothing is inferred from the // checklist, because there is no longer a record that declares no total. + battery('the judged cell now has ONE provenance'); checked += assert(judgedCell(byIndex[0]) === '18/18', 'judged cell is the declared judged/total', failures); checked += assert(judgedCell(byIndex[2]) === '1/12', 'a 1-of-12 run renders as 1/12, never as complete', failures); // --- the legacy boundary reads created_at, never the title -------------- + battery('the legacy boundary reads created_at, never the title'); checked += assert(predatesContract('2026-08-18T03:18:26Z') === true, 'a 2026-08-18 record predates the contract', failures); checked += assert(predatesContract(`${CANONICAL_FROM}T00:00:00Z`) === false, 'a record created on the effective date is under the contract', failures); checked += assert(predatesContract('2026-09-01T00:00:00Z') === false, 'a later record is under the contract', failures); checked += assert(predatesContract(null) === false, 'a missing created_at is not treated as legacy', failures); // --- freshness is three-valued ----------------------------------------- + battery('freshness is three-valued'); const repoShallow = { shallow: true, boundaryDate: '2026-08-16' }; const repoFull = { shallow: false, boundaryDate: null }; const target = { sha: 'e4e5c6e3aaaaaaaa', ref: 'origin/main' }; @@ -1014,6 +1138,7 @@ async function selfTest() { ); // --- latest-per-selector wins ------------------------------------------ + battery('latest-per-selector wins'); const recs = [ { number: 7695, parsed: { selector: 'studio-authoring', selectorKind: 'area', date: '2026-08-11' } }, { number: 9353, parsed: { selector: 'studio-authoring', selectorKind: 'area', date: '2026-08-17' } }, @@ -1040,6 +1165,7 @@ async function selfTest() { checked += assert(sameDay.latest[0].number === 101, 'same-day tie -> higher issue number wins', failures); // --- the render must never hide a record -------------------------------- + battery('the render must never hide a record'); const md = renderMarkdown({ computedOn: { targetRef: 'origin/main', @@ -1121,6 +1247,10 @@ async function selfTest() { ); checked += assert(emptyMd.includes('#7627'), 'the legacy record is still listed when the table is empty', failures); + // The floor runs BEFORE the verdict below, so a success line can only be + // printed by a run in which every declared battery registered its cases. + for (const message of batteryFloorFailures()) failures.push(message); + if (failures.length > 0) { console.error(`✗ qa-rollup --self-test — ${failures.length} failure(s)\n`); for (const f of failures) console.error(` - ${f}`); diff --git a/scripts/ts-parse.mjs b/scripts/ts-parse.mjs index 5da949a769..856cca68ed 100644 --- a/scripts/ts-parse.mjs +++ b/scripts/ts-parse.mjs @@ -148,6 +148,121 @@ const ts = await requireDefaultExport('typescript', () => import('typescript'), import { isEntrypoint } from './invoked-as.mjs'; +// ── The self-test's own battery roster and floor (#13489) ────────────────── +// +// This self-test used to decide success by "no failure was recorded" and +// nothing else, 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. A set difference says WHICH +// battery stopped; a count says only that something did. +// +// 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. +// +// The machinery lives HERE, at module scope, rather than inside the self-test: +// this self-test's assertion sink is not a block-bodied helper in its body (it +// is a concise arrow, or a module-scope function), so there is no in-body +// helper to thread a per-run ledger through. Module scope is safe because the +// self-test runs once per process, and it is what lets the existing sink route +// through `registerCase()` with no case rewritten and no assertion changed. +const SELF_TEST_BATTERIES = Object.freeze({ + 'a clean source still parses, and the tree is usable': 1, + 'THE case: each measured wreck refuses instead of scoring clean': 6, + 'the refusal carries a location a reader can open': 1, + 'ScriptKind, both directions. This is the shape that hides in a green': 4, + 'the refusal is NOT swallowable, which is why it exits rather than': 1, + 'the census has a numerator': 1, + 'and the report is armed by the first PARSE, not by the IMPORT. Both': 2, + 'ts.createProgram: the syntax lives behind a SECOND call': 4, + 'ts.transpileModule: the quietest of the three': 4, + 'the refusal is not swallowable on the new doors either': 1, + 'the census names which door each source came through': 1, + 'the diagnostics reader itself, in-process': 4, +}); + +// DELETING an entry silences that battery's floor exactly as effectively as +// zeroing it, so the roster's own size is pinned too. +const SELF_TEST_BATTERY_FLOOR = 12; + +// The key an assertion is filed under when no battery is open. It is not a +// declared battery, so it reds by the same set difference rather than silently +// inflating whichever battery happened to run last. +const UNATTRIBUTED_BATTERY = '(no battery open)'; + +// ⚠️ None of these helpers is named with a self-test spelling, deliberately and +// on the record: `check:pm-dispatch-gates` anchors on a top-level declaration +// whose NAME spells self-test, and every such name owes a row in that gate's +// COMPOUND_ANCHOR_LEDGER. These are the battery ROSTER's machinery -- they hold +// no fixtures to mask and read no path literal -- so the accurate name is the +// one that says `battery`, not the one that would owe a ledger row for a role +// this code does not have. + +/** Cases registered per battery: `battery()` opens one, `registerCase()` files into it. */ +const batteryCases = new Map(); +let openBattery = null; + +/** Open a battery. Every assertion after this line is attributed to it. */ +function battery(name) { + openBattery = name; +} + +/** Called by the self-test's own assertion sink, once per assertion. */ +function registerCase() { + const name = openBattery ?? UNATTRIBUTED_BATTERY; + batteryCases.set(name, (batteryCases.get(name) ?? 0) + 1); +} + +/** + * The floor: every declared battery RAN, and ran its cases (#13489). + * + * Evaluated after every battery has had its chance and BEFORE the verdict, so + * the success line can only be printed by a run in which the set of batteries + * that registered assertions EQUALS the set declared. + */ +function batteryFloorFailures() { + const declared = Object.keys(SELF_TEST_BATTERIES); + const problems = []; + if (declared.length < SELF_TEST_BATTERY_FLOOR) { + problems.push( + `SELF_TEST_BATTERIES declares ${declared.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 batteryCases) { + if (declared.includes(name)) continue; + problems.push( + `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 declared) { + const count = batteryCases.get(name) ?? 0; + if (count >= SELF_TEST_BATTERIES[name]) continue; + problems.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]} — cases that used to run no longer do.`, + ); + } + if (problems.length) { + problems.push( + '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 problems; +} + /** * The exit status of a refusal. Distinct from 1 ("this gate found violations") * so a reader can tell "there is nothing to report" from "I could not read it". @@ -523,7 +638,10 @@ let selfTestReachedVerdict = false; export function selfTest() { const cases = []; - const t = (name, ok, detail) => cases.push({ name, ok: Boolean(ok), detail }); + const t = (name, ok, detail) => { + registerCase(); + return cases.push({ name, ok: Boolean(ok), detail }); + }; const SELF = fileURLToPath(import.meta.url); // The conflict markers are BUILT rather than typed: a literal one in this @@ -573,11 +691,13 @@ export function selfTest() { ); // -- a clean source still parses, and the tree is usable ------------------ + battery('a clean source still parses, and the tree is usable'); const clean = parse(CLEAN); t('a clean source parses and returns a usable tree', clean.status === 0 && clean.out === 'PARSED 1', JSON.stringify(clean)); // -- THE case: each measured wreck refuses instead of scoring clean ------- + battery('THE case: each measured wreck refuses instead of scoring clean'); for (const [name, text] of [ ['merge-conflict markers', CONFLICTED], ['a truncated body', TRUNCATED], @@ -592,6 +712,7 @@ export function selfTest() { } // -- the refusal carries a location a reader can open -------------------- + battery('the refusal carries a location a reader can open'); const located = parse(CONFLICTED, 'packages/foo/src/bar.ts'); t('the refusal reports line:column and TypeScript’s own message', /packages\/foo\/src\/bar\.ts:2:1\s+Merge conflict marker encountered\./.test(located.err), @@ -599,6 +720,7 @@ export function selfTest() { // -- ScriptKind, both directions. This is the shape that hides in a green // gate rather than in a broken file, and it was LIVE on main. ---------- + battery('ScriptKind, both directions. This is the shape that hides in a green'); t('JSX in a .tsx file parses when the extension decides', parse(JSX, 'page.tsx').status === 0); t('…and the SAME source refuses when the call site forces ScriptKind.TS', @@ -611,6 +733,7 @@ export function selfTest() { // -- the refusal is NOT swallowable, which is why it exits rather than // throws: `try { parse } catch { continue }` is written in this repo // today, against a throw that never comes ---------------------------- + battery('the refusal is NOT swallowable, which is why it exits rather than'); const swallowed = run( `let caught = false;\n` + `try { parseSourceFile('t.ts', ${JSON.stringify(TRUNCATED)}); } catch { caught = true; }\n` @@ -621,6 +744,7 @@ export function selfTest() { JSON.stringify(swallowed)); // -- the census has a numerator ------------------------------------------ + battery('the census has a numerator'); const counted = run( `parseSourceFile('a.ts', 'const a = 1;');\n` + `parseSourceFile('a.ts', 'const b = 2;');\n` @@ -636,6 +760,7 @@ export function selfTest() { // directions, because only the pair is a claim: a library that writes // to your stderr because you imported it is the defect, and a census // that can no longer report is the over-correction. ------------------- + battery('and the report is armed by the first PARSE, not by the IMPORT. Both'); const reported = run( `parseSourceFile('a.ts', 'const a = 1;');\n`, { OS_TOOLING_PARSE_CENSUS: '1' }, @@ -650,6 +775,7 @@ export function selfTest() { JSON.stringify(importedOnly)); // -- ts.createProgram: the syntax lives behind a SECOND call ------------- + battery('ts.createProgram: the syntax lives behind a SECOND call'); const PROGRAM_OPTIONS = `{ noLib: true, skipLibCheck: true, noEmit: true, types: [],` + ` module: ts.ModuleKind.ESNext, target: ts.ScriptTarget.ESNext,` @@ -693,6 +819,7 @@ export function selfTest() { JSON.stringify({ status: progTransitive.status, err: progTransitive.err.slice(0, 300) })); // -- ts.transpileModule: the quietest of the three ---------------------- + battery('ts.transpileModule: the quietest of the three'); const transpile = (text, fileName = 'snippet.ts') => run( `import { transpileChecked } from ${JSON.stringify(pathToFileURL(SELF).href)};\n` @@ -726,6 +853,7 @@ export function selfTest() { raw.status === 0 && raw.out === '{"reported":0,"hasWreck":true}', JSON.stringify(raw)); // -- the refusal is not swallowable on the new doors either -------------- + battery('the refusal is not swallowable on the new doors either'); const swallowedTranspile = run( `import { transpileChecked } from ${JSON.stringify(pathToFileURL(SELF).href)};\n` + `let caught = false;\n` @@ -737,6 +865,7 @@ export function selfTest() { JSON.stringify(swallowedTranspile)); // -- the census names which door each source came through --------------- + battery('the census names which door each source came through'); const countedAll = run( `import { createProgramChecked, transpileChecked } from ${JSON.stringify(pathToFileURL(SELF).href)};\n` + `import { parseSourceFile as ps, parseCensus as pc } from ${JSON.stringify(pathToFileURL(SELF).href)};\n` @@ -751,6 +880,7 @@ export function selfTest() { JSON.stringify(countedAll)); // -- the diagnostics reader itself, in-process --------------------------- + battery('the diagnostics reader itself, in-process'); t('describeDiagnostics answers [] for a tree that parsed', describeDiagnostics(parseSourceFile('ok.ts', CLEAN)).length === 0); t('describeDiagnostics answers [] for a non-SourceFile rather than throwing', @@ -763,6 +893,10 @@ export function selfTest() { rmSync(dir, { recursive: true, force: true }); } + // The floor runs BEFORE the verdict below, so a success line can only be + // printed by a run in which every declared battery registered its cases. + for (const message of batteryFloorFailures()) cases.push({ name: message, ok: false }); + const failed = cases.filter((c) => !c.ok); for (const c of failed) console.error(` x ${c.name}${c.detail ? ` — ${c.detail}` : ''}`); if (failed.length) {