Phase 1 of parent #1641. Implements the contract documented in the Phase 0 ADR-0174 §5 amendment (#1642 / #1643). src/command-routing-hub.cts * InvalidArgsResult interface gains optional exitReason?: string (carries an ERROR_REASON enum value, separate from reason which is the explanation text). * makeInvalidArgs(arg, reason, exitReason?) factory conditionally adds the field only when the third arg is truthy — preserves the strict- keys invariant tested at command-routing-hub.test.cjs:444. * _VARIANT_SCHEMA.InvalidArgs.allowed Set extended to include 'exitReason' so the runtime validator does not coerce well-formed extended Results to HandlerFailure. src/cjs-command-router-adapter.cts * Honestified the wrapper comment: the runtime check ('ok' in result) already passes any {ok:*} object through, so the historical {ok:true, data} return type was a lie for err Results. The lying cast is preserved because the Hub's export = syntax doesn't expose HubResult for import; the Hub's _validateErrResult runtime-validates the actual shape. * Result→error() translation branched: when InvalidArgs carries exitReason, the adapter calls error(result.reason, result.exitReason) so the JSON-error envelope (GSD_JSON_ERRORS=1) preserves the typed ERROR_REASON value. When exitReason is absent, error(msg) is called with exactly one arg — byte-identical with prior behavior. * RouteCjsCommandFamilyOptions.error and RouteHubCommandFamilyOptions .error callback types widened from (message) to (message, reason?) to match io.cts's actual error() signature. CONTEXT.md * Command Routing Hub predicate updated to document the new field, factory signature, and dispatcher translation contract. Tests (TDD red→green) * tests/command-routing-hub.test.cjs: 8 new tests covering 2-arg (strict-keys), 3-arg (key present), undefined, empty string, frozen result, hub.dispatch propagation, and validator acceptance. * tests/cjs-command-router-adapter.test.cjs: 2 new tests covering exitReason passed as second arg + byte-identical prior behavior when absent. Verification * npm run test:unit: 2448 tests, 0 fail (no regressions) * gsd-test-summary on docker: outcome=passed, 0 failures (RULESET.PR-FLOW.docker-before-push) Memtrace blast radius: LOW (get_impact makeInvalidArgs → 3 nodes; the optional field is non-breaking for the 1 existing caller routePhaseCommand).
This commit is contained in:
@@ -76,6 +76,14 @@ interface InvalidArgsResult {
|
||||
kind: 'InvalidArgs';
|
||||
arg: string;
|
||||
reason: string;
|
||||
// Optional ERROR_REASON enum value (e.g. 'USAGE'), carried separately from
|
||||
// `reason` (the human-readable explanation). Added by amendment #1642 so
|
||||
// routers migrating from direct `error(msg, ERROR_REASON.USAGE)` calls to
|
||||
// `makeInvalidArgs(...)` Results can preserve ERROR_REASON granularity
|
||||
// through the Hub Result → `error(msg, exitReason)` translation. Omitted by
|
||||
// the factory when the third arg is absent, undefined, or empty string —
|
||||
// preserves the strict-keys invariant tested at command-routing-hub.test.cjs:444.
|
||||
exitReason?: string;
|
||||
}
|
||||
|
||||
interface HandlerRefusalResult {
|
||||
@@ -117,8 +125,14 @@ function makeUnknownCommand(command: string): Readonly<UnknownCommandResult> {
|
||||
return Object.freeze({ ok: false as const, kind: ERROR_KINDS.UnknownCommand, command });
|
||||
}
|
||||
|
||||
function makeInvalidArgs(arg: string, reason: string): Readonly<InvalidArgsResult> {
|
||||
return Object.freeze({ ok: false as const, kind: ERROR_KINDS.InvalidArgs, arg, reason });
|
||||
function makeInvalidArgs(arg: string, reason: string, exitReason?: string): Readonly<InvalidArgsResult> {
|
||||
const obj: InvalidArgsResult = { ok: false as const, kind: ERROR_KINDS.InvalidArgs, arg, reason };
|
||||
// Conditionally add exitReason only when truthy — preserves strict-keys
|
||||
// invariant (2-arg callers must continue to produce a 4-key frozen result).
|
||||
if (exitReason) {
|
||||
obj.exitReason = exitReason;
|
||||
}
|
||||
return Object.freeze(obj);
|
||||
}
|
||||
|
||||
function makeHandlerRefusal(reason: string): Readonly<HandlerRefusalResult> {
|
||||
@@ -163,10 +177,11 @@ const _VARIANT_SCHEMA: Record<string, { required: string[]; allowed: Set<string>
|
||||
required: ['command'],
|
||||
allowed: new Set(['ok', 'kind', 'command']),
|
||||
},
|
||||
InvalidArgs: {
|
||||
required: ['arg', 'reason'],
|
||||
allowed: new Set(['ok', 'kind', 'arg', 'reason']),
|
||||
},
|
||||
InvalidArgs: {
|
||||
required: ['arg', 'reason'],
|
||||
// Amendment #1642: exitReason? is allowed but not required.
|
||||
allowed: new Set(['ok', 'kind', 'arg', 'reason', 'exitReason']),
|
||||
},
|
||||
HandlerRefusal: {
|
||||
required: ['reason'],
|
||||
allowed: new Set(['ok', 'kind', 'reason']),
|
||||
|
||||
Reference in New Issue
Block a user