From 54b433adbc6e215d30549c16a4763257d14df864 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 2 Sep 2026 13:58:29 +0000 Subject: [PATCH 1/2] fix(devx): guard the last two run-on-import gate scripts, ledger 2 -> 0 `check-changeset-no-major.mjs` and `check-empty-changeset.mjs` were the final two entries in the SHRINK-ONLY `KNOWN_IMPORT_UNSAFE` ledger, held back until the changesets-v3 epic released their file territory. Both take the inverted 4-line insert, so every existing dispatch branch keeps its indentation. Measured on this tree before the guards: - importing `check-changeset-no-major.mjs` for its exports ran the whole gate inside the importer and then `main()`'s trailing `process.exit(exitCode)` ended that process mid-import carrying status 0. The probe never reached the statement after its own `import()`, so a caller reading the status alone cannot tell it from a clean import. - importing `check-empty-changeset.mjs` ran the gate and wrote its verdict to the importer's stdout before returning the exports. After: both probes return their exports silently, exit 0, no foreign output. All three CLI modes (default, `--self-test`, `--list`) are byte-identical before and after on the same tree. Nothing in the repo imports either file -- every reference in `.github/**`, `package.json` and `scripts/**` spawns them as `node scripts/...` -- so neither guard silences a census. That is the sitting-3 `ts-parse.mjs` inversion check, applied and cleared. The ledger entries were deleted only after `check:entry-guard` named both STALE. The set is now empty and its SHRINK-ONLY doc comment is untouched: that comment is what keeps an empty set empty. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01WLJQhde67SeTccsmnBVarV --- scripts/check-changeset-no-major.mjs | 24 +++++++++++++++++++++++- scripts/check-empty-changeset.mjs | 23 ++++++++++++++++++++++- scripts/check-entry-guard.mjs | 5 +---- 3 files changed, 46 insertions(+), 6 deletions(-) diff --git a/scripts/check-changeset-no-major.mjs b/scripts/check-changeset-no-major.mjs index cd623e2fa7..f57e8087ad 100644 --- a/scripts/check-changeset-no-major.mjs +++ b/scripts/check-changeset-no-major.mjs @@ -354,6 +354,8 @@ import { tmpdir } from 'node:os'; import { dirname, join, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; +import { isEntrypoint } from './invoked-as.mjs'; + const __dirname = dirname(fileURLToPath(import.meta.url)); const REPO_ROOT = resolve(__dirname, '..'); @@ -1522,7 +1524,27 @@ function selfTest() { const argv = process.argv.slice(2); -if (argv.includes('--self-test')) { +/** + * The guard is INVERTED so the dispatch chain below keeps its indentation: + * the imported case is the empty first branch, and every mode that was here + * before is untouched in the `else if` chain. + * + * Measured before this landed: importing this module for its exports ran the + * whole gate inside the importer, and then `main()`'s trailing + * `process.exit(exitCode)` ended that process mid-import — carrying status 0. + * The importer never reached the statement after its own `import()`, and a + * caller reading the status alone cannot tell that apart from a clean import. + * + * Nothing imports this file today (every reference in `.github/**`, + * `package.json` and `scripts/**` spawns it as `node scripts/...`), so the + * guard silences no census: the only top-level statement it moves behind + * `isEntrypoint` is CLI dispatch. + */ +const invokedDirectly = isEntrypoint(import.meta.url); + +if (!invokedDirectly) { + // imported as a module — expose the exports and do nothing else +} else if (argv.includes('--self-test')) { if (selfTest() !== SELF_TEST_VERDICT) { console.error( '\n✗ check-changeset-no-major self-test: selfTest() returned without reaching its verdict,\n' diff --git a/scripts/check-empty-changeset.mjs b/scripts/check-empty-changeset.mjs index 18143fa8b8..cf431bb41d 100644 --- a/scripts/check-empty-changeset.mjs +++ b/scripts/check-empty-changeset.mjs @@ -143,6 +143,8 @@ import { tmpdir } from 'node:os'; import { dirname, join, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; +import { isEntrypoint } from './invoked-as.mjs'; + const __dirname = dirname(fileURLToPath(import.meta.url)); const REPO_ROOT = resolve(__dirname, '..'); @@ -1445,7 +1447,26 @@ function selfTest() { const argv = process.argv.slice(2); -if (argv.includes('--self-test')) { +/** + * The guard is INVERTED so the dispatch chain below keeps its indentation: + * the imported case is the empty first branch, and every mode that was here + * before is untouched in the `else if` chain. + * + * Measured before this landed: importing this module for its exports ran the + * whole gate inside the importer and wrote this gate's verdict to the + * importer's stdout before the exports came back — foreign output in a tool + * that never asked for it. + * + * Nothing imports this file today (every reference in `.github/**`, + * `package.json` and `scripts/**` spawns it as `node scripts/...`), so the + * guard silences no census: the only top-level statement it moves behind + * `isEntrypoint` is CLI dispatch. + */ +const invokedDirectly = isEntrypoint(import.meta.url); + +if (!invokedDirectly) { + // imported as a module — expose the exports and do nothing else +} else if (argv.includes('--self-test')) { if (selfTest() !== SELF_TEST_VERDICT) { console.error( '\n✗ check-empty-changeset self-test: selfTest() returned without reaching its verdict,\n' diff --git a/scripts/check-entry-guard.mjs b/scripts/check-entry-guard.mjs index b74a30ab08..b9e1209575 100644 --- a/scripts/check-entry-guard.mjs +++ b/scripts/check-entry-guard.mjs @@ -457,10 +457,7 @@ export function importUnsafeStatements(source) { * a line in here. An entry whose file has since been fixed fails as STALE and * names itself, which is what stops this from rotting into an allowlist. */ -const KNOWN_IMPORT_UNSAFE = new Set([ - 'scripts/check-changeset-no-major.mjs', - 'scripts/check-empty-changeset.mjs', -]); +const KNOWN_IMPORT_UNSAFE = new Set([]); /** * The SCAN SURFACE, written in the syntax `scripts/pm/dispatch-gates.mjs` can From d11a00a295b311a848bbcf019a5580ac05521997 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 2 Sep 2026 14:28:52 +0000 Subject: [PATCH 2/2] fix(pm): the #10784 ablation pins NOT MATCHED, not the spelling 'silent' The ablation strips check:entry-guard's declared subtree hint and asserts the brand-new-file verdict falls back. Its claim is that the subtree declaration is the load-bearing half -- i.e. that without it the file is NOT MATCHED -- but it spelled that as exactly `'silent'`. `'silent'` was only ever the residual because check-entry-guard.mjs's KNOWN_IMPORT_UNSAFE roster still contributed two path literals as hints. That ledger is SHRINK-ONLY and reaching zero is its GOAL, so the sitting that emptied it left the stripped hint set bare, the residual became `'undetermined'`, and this case went red over a gate that had not changed at all. The pin was calibrated against a debt ledger designed to disappear. Both verdicts are not-matched and either one proves the subtree hint is load-bearing, so both are accepted -- spelled as an explicit pair rather than `!== 'matched'`, so a NEW verdict value added later cannot slip through as a pass. The vacuity guard (`undeclared.hints.length < entry.hints.length`) is untouched, the case label now says NOT MATCHED rather than 'silent', and the residual verdict is carried into the failure detail so a future red names what it actually got. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01WLJQhde67SeTccsmnBVarV --- scripts/pm/dispatch-gates.mjs | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/scripts/pm/dispatch-gates.mjs b/scripts/pm/dispatch-gates.mjs index 7d11d5dc5a..3a42cbfc5b 100644 --- a/scripts/pm/dispatch-gates.mjs +++ b/scripts/pm/dispatch-gates.mjs @@ -12150,20 +12150,34 @@ function selfTest() { JSON.stringify({ verdict: verdict?.verdict, hints: entry?.hints }), ); // The ablation, run in-place: strip the declared SUBTREE from the live hint - // set and the verdict must fall back to what it was before this landed. + // set and the verdict must fall back to NOT MATCHED — that is the whole + // claim, since a brand-new file is nameable only through the subtree half. // Without it the case above could pass through any hint that happened to // cover the probe, and the reader could not tell which half was load-bearing. + // + // WHICH not-matched verdict it lands on is not fixed, and pinning one + // spelling was a latent trap: for `check:entry-guard` the residual depends + // on whether its KNOWN_IMPORT_UNSAFE roster still contributes path literals + // as hints — `silent` while it held entries, `undetermined` once it emptied + // and the stripped hint set is bare. That ledger is ⛔ SHRINK-ONLY and + // reaching zero is its GOAL, so the day it emptied this case went red over + // a gate that had not changed at all. Either verdict proves the subtree + // hint is the load-bearing half, so both are accepted — spelled as an + // explicit pair rather than `!== 'matched'`, so a NEW verdict value added + // later cannot slip through here as a pass. const undeclared = entry ? { ...entry, hints: entry.hints.filter((h) => !h.includes('/*')) } : null; + const residual = undeclared ? classifyEntry(undeclared, [unwrittenScript]).verdict : null; t( - `…and it is the subtree declaration doing it: strip it and ${gate} goes back to silent`, + `…and it is the subtree declaration doing it: strip it and ${gate} goes back to NOT MATCHED`, // The length check is what stops this passing VACUOUSLY. With no subtree - // hint to remove, `undeclared` is the entry itself and `silent === silent` - // reads as a pass — measured, on the ablation run that removed both - // declarations: this case stayed green while the two above went red. + // hint to remove, `undeclared` is the entry itself and a not-matched + // verdict compared against itself reads as a pass — measured, on the + // ablation run that removed both declarations: this case stayed green + // while the two above went red. Boolean(undeclared) && undeclared.hints.length < entry.hints.length && - classifyEntry(undeclared, [unwrittenScript]).verdict === 'silent', - JSON.stringify({ before: entry?.hints?.length, after: undeclared?.hints?.length }), + ['silent', 'undetermined'].includes(residual), + JSON.stringify({ before: entry?.hints?.length, after: undeclared?.hints?.length, residual }), ); }