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 144cb2371b..ddafff5269 100644 --- a/scripts/check-watch-hint-literal.mjs +++ b/scripts/check-watch-hint-literal.mjs @@ -148,6 +148,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, '..'); @@ -447,7 +559,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`; @@ -457,6 +572,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}/**`]', @@ -485,12 +601,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', @@ -503,6 +621,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', @@ -516,6 +635,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); @@ -533,6 +653,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/**'] })); @@ -547,6 +668,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); @@ -560,10 +682,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, @@ -598,6 +722,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) {