Measured while implementing #9640 (the swallowed export { maskComments };). Not repaired there: different defect class, and the repair needs a second file changed — see "Why it is its own card" below. Filed unassigned, severity for triage.
Facts
scripts/pm/dispatch-gates.mjs ends with an unguarded CLI dispatch at module top level (const argvPaths = process.argv.slice(2)... through the closing else branch). There is no entry guard, so importing the module runs the tool against the importer's argv and cwd, and on most of those paths calls process.exit(2) before the importer's own code runs.
Measured on main (2065e31), a bare dynamic import from a node -e:
dispatch-gates: this branch changes nothing against 'origin/main' (merge base 2065e31d5) — nothing to derive.
On the base branch already, or in the wrong checkout? Pass explicit paths to ask about a hypothetical surface.
...and the importer's own console.log never ran: exit 2 came from the gate, not from the consumer. To measure the module's export surface at all, #9640 had to pass a path argument so the CLI took its derive() branch and returned instead of exiting.
Why it matters
The module offers two re-export blocks, both with a stated rationale:
Both are the file's public surface by intent, and today none of it is reachable: any consumer that writes import { maskComments } from './dispatch-gates.mjs' gets the tool's output and an exit code instead of a function. #9640's card predicted this would fail "as undefined is not a function at call time, not at import" — the measurement says it fails harder and earlier than that, at import.
Precedent, same class, already ruled and landed: #6566 → PR #6695 added an entry guard to check-adr-0087-registration.mjs for exactly this ("一个不能被安全 import 的门,等于强迫每一个想与它保持一致的调用方自己起进程"). The sibling module this file re-exports FROM already carries the guard and the reason, three files away:
// Executed only as a CLI. Importing this module must have NO side effect: the// gates below it are the callers, and a shared module that exits on import is// a shared module nobody can share.
Why it is its own card, not a rider on #9640
The obvious repair — copy the guard — silently changes what pnpm check:pm-dispatch-gates measures. That gate is scripts/pm/check-dispatch-gates.mjs, and it runs the tool as a child process with stdio: 'inherit', then process.exit(result.status ?? 2). It holds the exit status only. A guard whose comparison does not match the invocation form turns the tool into a no-op that exits 0, and the gate reports that as a pass — the silent-success direction this tree treats as worse than no check. So whoever takes this should harden the gate to assert the self-test actually ran (its summary line, or a case count) in the same PR.
Second thing to settle while here: the tree spells this guard four different ways in four files — resolve(process.argv[1] ?? '') === resolve(fileURLToPath(import.meta.url)) (check-adr-0087-registration), import.meta.url === pathToFileURL(process.argv[1] ?? '').href (check-cross-repo-closer-outcome), the same with a process.argv[1] && prefix (check-kernel-hook-pairs), and new URL(\file://${process.argv[1]}`).href` (js-comment-mask, which breaks on a path needing percent-encoding). Picking one is cheap while the file is open.
Dedupe
search_issues for "dispatch-gates module executes its CLI on import, no main guard, import side effect" returns the ADR-0087 precedent (#6566, closed) and dispatch-gates cards about gate derivation (#9700, #9721, #9320, #9116) — none about this module's import semantics.
Generated by Claude Code
Measured while implementing #9640 (the swallowed
export { maskComments };). Not repaired there: different defect class, and the repair needs a second file changed — see "Why it is its own card" below. Filed unassigned, severity for triage.Facts
scripts/pm/dispatch-gates.mjsends with an unguarded CLI dispatch at module top level (const argvPaths = process.argv.slice(2)...through the closingelsebranch). There is no entry guard, so importing the module runs the tool against the importer's argv and cwd, and on most of those paths callsprocess.exit(2)before the importer's own code runs.Measured on
main(2065e31), a bare dynamic import from anode -e:...and the importer's own
console.lognever ran: exit 2 came from the gate, not from the consumer. To measure the module's export surface at all, #9640 had to pass a path argument so the CLI took itsderive()branch and returned instead of exiting.Why it matters
The module offers two re-export blocks, both with a stated rationale:
export { isExtractConfigPath, isMetadataFormModulePath };— "Re-exported so this tool's self-test drives the SAME predicates the gate runs, not copies of them."export { maskComments };— restored to live code by [finding] dispatch-gates'export { maskComments }is swallowed by an unterminated block comment — the docblock claims a re-export the module does not have #9640.Both are the file's public surface by intent, and today none of it is reachable: any consumer that writes
import { maskComments } from './dispatch-gates.mjs'gets the tool's output and an exit code instead of a function. #9640's card predicted this would fail "asundefined is not a functionat call time, not at import" — the measurement says it fails harder and earlier than that, at import.Precedent, same class, already ruled and landed: #6566 → PR #6695 added an entry guard to
check-adr-0087-registration.mjsfor exactly this ("一个不能被安全 import 的门,等于强迫每一个想与它保持一致的调用方自己起进程"). The sibling module this file re-exports FROM already carries the guard and the reason, three files away:Why it is its own card, not a rider on #9640
The obvious repair — copy the guard — silently changes what
pnpm check:pm-dispatch-gatesmeasures. That gate isscripts/pm/check-dispatch-gates.mjs, and it runs the tool as a child process withstdio: 'inherit', thenprocess.exit(result.status ?? 2). It holds the exit status only. A guard whose comparison does not match the invocation form turns the tool into a no-op that exits 0, and the gate reports that as a pass — the silent-success direction this tree treats as worse than no check. So whoever takes this should harden the gate to assert the self-test actually ran (its summary line, or a case count) in the same PR.Second thing to settle while here: the tree spells this guard four different ways in four files —
resolve(process.argv[1] ?? '') === resolve(fileURLToPath(import.meta.url))(check-adr-0087-registration),import.meta.url === pathToFileURL(process.argv[1] ?? '').href(check-cross-repo-closer-outcome), the same with aprocess.argv[1] &&prefix (check-kernel-hook-pairs), andnew URL(\file://${process.argv[1]}`).href` (js-comment-mask, which breaks on a path needing percent-encoding). Picking one is cheap while the file is open.Dedupe
search_issuesfor "dispatch-gates module executes its CLI on import, no main guard, import side effect" returns the ADR-0087 precedent (#6566, closed) and dispatch-gates cards about gate derivation (#9700, #9721, #9320, #9116) — none about this module's import semantics.Generated by Claude Code