* feat(#3905): the exit-code registry — one number, one meaning, enforced at build A generated registry replaces locally-invented exit codes. Every entry records code, name, meaning, owning module and the decision that authorized it. The generator refuses to build a table where two entries claim one code, two claim one name, a code falls in a range Node or the shell reserves, 2 is claimed by anything but the hook adapter, or an allocation carries no justification. exitCodeFor is pure and total: it throws rather than returning undefined, including for prototype-chain names. Inert by design — nothing emits a registered code until #3906. Every registered code is non-zero, asserted over the whole table, so a caller testing for failure behaves identically for pass and trips for everything else. * feat(#3905): make the registry generator's failures machine-readable Adds a --json mode carrying {ok, reason, context, detail}, where context is a typed payload naming the specifics the prose embedded - which code collided and under which names, which band rejected a code, which field was missing. The tests now assert on that structure instead of regex-matching the generator's stderr, which CONTRIBUTING prohibits, and the CONTEXT.md glossary gains the entry the issue's scope requires. * test(#3905): refresh the install-tree fixtures for the new declaration The registry declaration ships in the install tree, so all 19 golden fixtures needed regenerating. Caught by the remote matrix, not by lint:ci - the install-tree goldens are verified by a test rather than a lint, so a newly shipped file clears every local gate and fails only under the suite. * chore(#3905): backfill changeset pr number --------- Co-authored-by: sim <sim@local>
This commit is contained in:
87
gsd-core/bin/lib/exit-code-registry.cjs
Normal file
87
gsd-core/bin/lib/exit-code-registry.cjs
Normal file
@@ -0,0 +1,87 @@
|
||||
'use strict';
|
||||
|
||||
// GENERATED FILE — DO NOT EDIT BY HAND.
|
||||
// Source of truth: gsd-core/bin/shared/exit-codes.json. Regenerate with:
|
||||
// node scripts/gen-exit-code-registry.cjs --write
|
||||
// Byte-compared by `npm run lint:generated-sync` (#3905, ADR-3889 Phase 1).
|
||||
//
|
||||
// exitCodeFor(name) / nameForExitCode(code) are pure and total over this
|
||||
// closed table — each throws for anything not registered here.
|
||||
|
||||
const EXIT_CODES = Object.freeze([
|
||||
Object.freeze({
|
||||
code: 2,
|
||||
name: "HOOK_DENY",
|
||||
meaning: "Claude Code hook protocol — deny the tool call",
|
||||
owner: "hook-adapter",
|
||||
authorizedBy: "ADR-3889",
|
||||
}),
|
||||
Object.freeze({
|
||||
code: 64,
|
||||
name: "USAGE",
|
||||
meaning: "Caller error — bad argv, unknown subcommand, missing argument",
|
||||
owner: "generic",
|
||||
authorizedBy: "ADR-3889",
|
||||
}),
|
||||
Object.freeze({
|
||||
code: 66,
|
||||
name: "NO_INPUT",
|
||||
meaning: "Ran; zero units were in scope, and that emptiness is known to be genuine",
|
||||
owner: "generic",
|
||||
authorizedBy: "ADR-3889",
|
||||
}),
|
||||
Object.freeze({
|
||||
code: 69,
|
||||
name: "UNAVAILABLE",
|
||||
meaning: "Could not run — prerequisite absent, input unreadable, scope unestablished",
|
||||
owner: "generic",
|
||||
authorizedBy: "ADR-3889",
|
||||
}),
|
||||
Object.freeze({
|
||||
code: 70,
|
||||
name: "INTERNAL",
|
||||
meaning: "Self-failure — crash, timeout, killed subprocess",
|
||||
owner: "generic",
|
||||
authorizedBy: "ADR-3889",
|
||||
})
|
||||
]);
|
||||
|
||||
const NAME_TO_CODE = new Map(EXIT_CODES.map((entry) => [entry.name, entry.code]));
|
||||
const CODE_TO_NAME = new Map(EXIT_CODES.map((entry) => [entry.code, entry.name]));
|
||||
|
||||
/**
|
||||
* Resolve the registered exit code for a symbolic name. Pure, total: throws
|
||||
* for anything not an exact, registered, exact-case key — including
|
||||
* non-strings, the empty string, untrimmed strings, wrong case, and
|
||||
* prototype-chain names like `__proto__`/`constructor`/`toString` (a Map
|
||||
* lookup never touches the prototype chain, so these are indistinguishable
|
||||
* from any other unregistered name).
|
||||
*
|
||||
* @param {string} name
|
||||
* @returns {number}
|
||||
*/
|
||||
function exitCodeFor(name) {
|
||||
if (typeof name !== 'string' || name.length === 0) {
|
||||
throw new Error(`exitCodeFor: name must be a non-empty string, received ${JSON.stringify(name)}`);
|
||||
}
|
||||
if (!NAME_TO_CODE.has(name)) {
|
||||
throw new Error(`exitCodeFor: unregistered exit code name: ${JSON.stringify(name)}`);
|
||||
}
|
||||
return NAME_TO_CODE.get(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse of exitCodeFor: resolve the symbolic name for a registered exit
|
||||
* code. Pure, total: throws for anything not an exact, registered code.
|
||||
*
|
||||
* @param {number} code
|
||||
* @returns {string}
|
||||
*/
|
||||
function nameForExitCode(code) {
|
||||
if (!CODE_TO_NAME.has(code)) {
|
||||
throw new Error(`nameForExitCode: unregistered exit code: ${JSON.stringify(code)}`);
|
||||
}
|
||||
return CODE_TO_NAME.get(code);
|
||||
}
|
||||
|
||||
module.exports = { EXIT_CODES, exitCodeFor, nameForExitCode };
|
||||
Reference in New Issue
Block a user