Mechanical rename produced by scripts/msd-rename.cjs: gsd/Gsd/GSD -> msd/Msd/MSD across contents and paths, upstream package/repo coordinates -> @golem15/msd-core and golem15com/msd-core. Deep links into upstream history, sibling upstream packages, the GSD-2 import feature, CHANGELOG.md and .changeset/ are kept as-is. Hand edits on top: MSD block-letter banner and logos, LICENSE copyright line, package/plugin identity, regenerated lockfile, install-tree fixtures, derived registries and benchmark baseline; migration checksum baseline re-locked (MSD keeps its own install state, so no install had applied the old sums); sort-order and regex-escaped expectations in tests adjusted.
3.4 KiB
3.4 KiB
id, title, group
| id | title | group |
|---|---|---|
| 3910 | The Raw Terminator Is Banned by Construction | v1.7.0 Features |
Purpose: Make a bare process.exit(...) a lint error everywhere it matters, so the
"nothing fails with success" defect class ADR-3889 exists to close cannot silently reopen
through a new call site.
What changed (ADR-3889 Phase 6, #3910):
- New rule
local/require-registered-exit(eslint-rules/require-registered-exit.cjs) flags anyCallExpressionshaped exactly likeprocess.exit(...). It does not flagprocess.exitCode = N— that assignment is the correct drain-then-exit patternrunMainitself uses, and the two are structurally distinct (an assignment target is never aCallExpression). - Registered on four globs:
src/**/*.cts,scripts/**/*.cjs,hooks/**/*.js,msd-core/bin/**/*.cjs(eslint.config.mjs:420-426,545-547,574-576,601). Registering onsrc/**/*.cts— not only the emittedmsd-core/bin/lib/*.cjsmirrors, which are globally eslint-ignored (ADR-457) — is load-bearing: a rule registered only on the emitted surface is blind to the real sources, the same wayn/no-process-exitwent invisible (#3496). - The dead
n/no-process-exit: 'off'carve-out forhooks/**is deleted: Phase 7 (#3911) migrated every enforcement hook ontoterminateNow, so it protected nothing. - Exactly two allowlist entries, repo-wide:
- The body of
terminateNowinsrc/cli-exit.cts— detected structurally (anyprocess.exit()lexically nested inside a function namedterminateNow, and the file's basename iscli-exit.cts), not by path+line, so it does not rot when the function moves. msd-core/bin/msd-tools.cjs'sensureRuntimeBuildbootstrap-failure path, via an inline// eslint-disable-next-line local/require-registered-exitwith a stated reason — it runs before./lib/cli-exit.cjsis even required, so the registered-exit seam does not exist yet at that point in the process's lifetime.
- The body of
- The last raw terminators in
src/**/*.ctswere migrated onto the seam, most notablysrc/io.cts'serror(): it changed from an uncatchableprocess.exit(1)to a catchablethrow new ExitError(1)(stderr output is byte-identical;runMainprojects the exit code).terminateNowcould not serve this site — ADR-3889 §1 makes exit codes 0 and 1 unallocatable, sonameForExitCode(1)throws. That control-flow change required three interceptor fixes so anExitErrorreachesrunMain:command-routing-hub'sdispatch()now rethrows it, and the profile-pipeline router's detached.catch()no longer callserror()— it writes stderr and setsexitCodein place. - Known limits (documented and test-pinned, not endorsed): the rule matches the literal
process.exit(...)shape only, with no scope/flow analysis. It does not catchprocess['exit'](0)(computed member access),const e = process.exit; e(1)(aliasing to a local binding before calling), orprocess.exit.call(...)/.apply(...)(indirect invocation). Catching these needs binding/scope-aware analysis, out of scope for this issue; pinning tests intests/eslint-rules.test.cjsassert today's non-detection so a future widening is a visible choice, not a silent one.
See Resolve a raw-terminator finding for what to do when this rule fires, and ADR-3889 for the exit-code registry the seam is layered over.