Files
msd-core/get-shit-done/bin/lib/command-routing-hub.cjs
Tom Boucher 21e3ce53c0 feat(hub): drop mode/sdkLoader/SdkDispatchFailed (#175) (#220)
* feat(hub): drop mode/sdkLoader/SdkDispatchFailed (#175)

The Command Routing Hub no longer carries dual-runtime selection.
Removes `mode` and `sdkLoader` constructor parameters and the
`SdkDispatchFailed` and `SdkLoadFailed` errorKind values. The Hub
now routes exclusively through the CJS registry / handler resolution
path. ERROR_KINDS enum shrinks from 6 to 4 values.

phase-command-router.cjs updated to construct the Hub without the
removed params (removes tryLoadSdk, getExecuteForCjs, sdkLoader fn,
mode variable, and the post-dispatch SDK output branch). The 7
remaining family routers (init, phases, roadmap, state, validate,
verify, cjs-command-router-adapter) do not use createHub directly
and require no changes.

The CJS↔SDK bridge (bin/lib/cjs-sdk-bridge.cjs) is unchanged and
remains separately invokable; its removal is tracked in Phase 4 (#190).

ADR-0012 is no longer amended in this PR — the decision is captured
in ADR-0174 (which supersedes ADR-0012 entirely as part of the
SDK-retirement migration). Amending a superseded ADR would be
redundant noise.

Tests:
- Added assertions that Hub rejects/ignores `mode` and `sdkLoader`
- Removed obsolete mode-selection branching tests
- 57/57 local tests pass

Closes #175.
Part of #174 (ADR-0174).

* chore(changeset): add docs-exempt marker (#175)

P1.1's CommandRoutingHub work has no docs/ touchpoints — the
architectural decision is captured in ADR-0174 (merged via PR #198).
Per-phase ADR amendments would create noise; the SDK-retirement
migration's docs land in Phase 6 PRs (#193-#196) once the relevant
state is removed.

Adds the standard <!-- docs-exempt: <reason> --> marker inside the
changeset fragment so lint:docs accepts the PR without forcing a
docs/ touch that would be redundant.

Refs #175. Part of #174 (ADR-0174).
2026-05-24 13:04:11 -04:00

164 lines
5.1 KiB
JavaScript

'use strict';
/**
* Command Routing Hub — issue #3788, simplified in #175.
*
* A pure-result dispatch hub that centralizes CJS routing,
* the error taxonomy, and the no-throw contract that all command-family routers
* currently duplicate independently.
*
* Design:
* createHub({ cjsRegistry, manifest }) -> hub
* hub.dispatch({ family, subcommand, args, cwd, raw }) -> Result
*
* Result = { ok: true, data }
* | { ok: false, errorKind, message, details? }
*
* Invariants:
* - Hub always routes through CJS handlers. There is no SDK path (#175).
* - Hub never prints to stdout/stderr, never calls process.exit.
* - Hub never throws — all internal throws are caught and converted to
* { ok: false, errorKind: 'HandlerFailure', message, details }.
* - The errorKind taxonomy is closed. Callers switch on ERROR_KINDS values.
*/
/**
* Closed errorKind enum. Export as a frozen object so callers can switch on
* ERROR_KINDS.UnknownCommand etc. without relying on bare string literals.
*
* #175: SdkLoadFailed and SdkDispatchFailed removed — Hub is CJS-only.
*
* @readonly
*/
const ERROR_KINDS = Object.freeze({
/** The requested family/subcommand combination is not present in the manifest. */
UnknownCommand: 'UnknownCommand',
/** The handler rejected the supplied arguments before executing. */
InvalidArgs: 'InvalidArgs',
/** A CJS handler returned an explicit refusal (e.g. unsupported subcommand). */
HandlerRefusal: 'HandlerRefusal',
/** A handler threw an unexpected exception. */
HandlerFailure: 'HandlerFailure',
});
/**
* @typedef {{ ok: true, data: unknown }} OkResult
* @typedef {{ ok: false, errorKind: string, message: string, details?: unknown }} ErrResult
* @typedef {OkResult | ErrResult} HubResult
*/
/**
* @typedef {object} HubOptions
* @property {Record<string, Record<string, (ctx: object) => HubResult>>} [cjsRegistry] -
* Nested map of family -> subcommand -> handler.
* @property {Record<string, string[]>} [manifest] - Map of family -> known subcommands.
* Used for UnknownCommand detection.
*/
/**
* Construct a CommandRoutingHub.
*
* @param {HubOptions} options
* @returns {{ dispatch: (req: object) => HubResult }}
*/
function createHub({ cjsRegistry, manifest } = {}) {
const _cjsRegistry = cjsRegistry;
const _manifest = manifest;
/**
* Dispatch a command through the hub.
*
* @param {{ family: string, subcommand: string, args?: unknown[], cwd?: string, raw?: boolean }} req
* @returns {HubResult}
*/
function dispatch(req) {
try {
return _dispatch(req);
} catch (err) {
return {
ok: false,
errorKind: ERROR_KINDS.HandlerFailure,
message: err instanceof Error ? err.message : String(err),
details: { originalError: err },
};
}
}
function _dispatch(req) {
const { family, subcommand, args = [], cwd, raw } = req;
// ── manifest check ────────────────────────────────────────────────────────
if (_manifest) {
const knownSubcommands = _manifest[family];
if (!knownSubcommands) {
return {
ok: false,
errorKind: ERROR_KINDS.UnknownCommand,
message: `Unknown command family: ${family}`,
};
}
if (subcommand && !knownSubcommands.includes(subcommand)) {
return {
ok: false,
errorKind: ERROR_KINDS.UnknownCommand,
message: `Unknown subcommand: ${family} ${subcommand}`,
};
}
}
return _dispatchCjs({ family, subcommand, args, cwd, raw });
}
function _dispatchCjs({ family, subcommand, args, cwd, raw }) {
if (!_cjsRegistry) {
return {
ok: false,
errorKind: ERROR_KINDS.UnknownCommand,
message: `No CJS registry provided for family: ${family}`,
};
}
const familyHandlers = _cjsRegistry[family];
if (!familyHandlers) {
return {
ok: false,
errorKind: ERROR_KINDS.UnknownCommand,
message: `Unknown command family: ${family}`,
};
}
const handler = subcommand ? familyHandlers[subcommand] : familyHandlers[''];
if (typeof handler !== 'function') {
return {
ok: false,
errorKind: ERROR_KINDS.UnknownCommand,
message: `Unknown subcommand: ${family} ${subcommand}`,
};
}
// Invoke the handler. It must return a HubResult or throw.
// If it throws, the outer try/catch in dispatch() catches it.
const result = handler({ family, subcommand, args, cwd, raw });
// If the handler returned a well-formed HubResult, pass it through.
if (result && typeof result === 'object' && 'ok' in result) {
return result;
}
// If the handler returned nothing (undefined), treat as success with no data.
if (result === undefined || result === null) {
return { ok: true, data: null };
}
// Any other return value is treated as the data payload.
return { ok: true, data: result };
}
return { dispatch };
}
module.exports = {
createHub,
ERROR_KINDS,
};