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:
@@ -25,7 +25,10 @@ interface RouteCjsCommandFamilyOptions {
|
||||
defaultSubcommand?: string;
|
||||
unsupported?: Record<string, string>;
|
||||
unknownMessage: (subcommand: string, available: string[]) => string;
|
||||
error: (message: string) => void;
|
||||
// Amendment #1642 (#1644 Phase 1): widened to accept optional ERROR_REASON
|
||||
// enum value as second arg. io.cts's error() already accepts (msg, reason?);
|
||||
// the prior one-arg signature was narrower than the runtime contract.
|
||||
error: (message: string, reason?: string) => void;
|
||||
cwd?: string;
|
||||
raw?: boolean;
|
||||
}
|
||||
@@ -38,7 +41,7 @@ interface RouteHubCommandFamilyOptions {
|
||||
defaultSubcommand?: string;
|
||||
unsupported?: Record<string, string>;
|
||||
unknownMessage: (subcommand: string, available: string[]) => string;
|
||||
error: (message: string) => void;
|
||||
error: (message: string, reason?: string) => void;
|
||||
cwd?: string;
|
||||
raw?: boolean;
|
||||
}
|
||||
@@ -100,6 +103,18 @@ function routeHubCommandFamily({
|
||||
const registryHandlers = Object.fromEntries(
|
||||
Object.entries(handlers).map(([name, handler]) => [
|
||||
name,
|
||||
// Honestified via amendment #1642 (#1644 Phase 1): the runtime check
|
||||
// `'ok' in result` already passes any `{ok:*}` object through, so the
|
||||
// historical `{ok:true, data}` return type was a lie whenever the
|
||||
// handler returned an err Result. The lying cast below is preserved
|
||||
// because the Hub's `export =` syntax doesn't expose `HubResult` for
|
||||
// import; the Hub's `_validateErrResult` runtime-validates the actual
|
||||
// shape, so structural compatibility is sufficient. The wrapper's
|
||||
// 0-arg signature is assignable to the Hub's `(ctx) => HubResult`
|
||||
// Handler type via TypeScript parameter bivariance; the Hub's per-call
|
||||
// ctx is intentionally ignored (host-router handlers don't use it;
|
||||
// capability-router handlers in Phase 2 will return HubResults that
|
||||
// already carry context).
|
||||
(): { ok: true; data: unknown } => {
|
||||
const result = handler();
|
||||
if (result && typeof result === 'object' && Object.prototype.hasOwnProperty.call(result, 'ok')) {
|
||||
@@ -128,7 +143,21 @@ function routeHubCommandFamily({
|
||||
error(unknownMessage(subcommand ?? '', available));
|
||||
return;
|
||||
}
|
||||
if (result.kind === ERROR_KINDS.InvalidArgs || result.kind === ERROR_KINDS.HandlerRefusal) {
|
||||
if (result.kind === ERROR_KINDS.InvalidArgs) {
|
||||
// Amendment #1642 (#1644): when the handler provided exitReason, pass it
|
||||
// as the second arg to error() so the JSON-error envelope
|
||||
// (GSD_JSON_ERRORS=1) preserves the typed ERROR_REASON value for
|
||||
// downstream consumers. When exitReason is absent, call error(msg) with
|
||||
// exactly one arg — byte-identical with prior behavior.
|
||||
const invalidArgs = result as { reason: string; exitReason?: string };
|
||||
if (invalidArgs.exitReason) {
|
||||
error(invalidArgs.reason, invalidArgs.exitReason);
|
||||
} else {
|
||||
error(invalidArgs.reason);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (result.kind === ERROR_KINDS.HandlerRefusal) {
|
||||
error((result as { reason: string }).reason);
|
||||
return;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user