From 3cf112b3de3f51e351ea13658c7bad71181583cb Mon Sep 17 00:00:00 2001 From: Joe <44273333+jslitzkerttcu@users.noreply.github.com> Date: Tue, 2 Jun 2026 22:32:10 -0500 Subject: [PATCH] refactor(#596): drop dead projection metadata in state-command-router (#633) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit cjsFallbackHandler was a no-op pass-through returning only its last argument, and state-command-router was its sole consumer — every handler wrapped a thunk in a 5-arg projection tuple whose first four args (registryCommand, registryArgs, legacyArgs, rawFormatter) were constructed and immediately discarded (rawFormatter null at 100% of sites). The SDK-dispatch path the metadata was nominally for is a retired shim. - Collapse all 20 state handlers to plain () => state.cmd*(...) thunks, matching the post-#298 inline convention used by the verify/roadmap/ phases routers (complete-phase already used this form). - Remove the local fallback() helper and Handler type from state-command-router.cts. - Drop cjsFallbackHandler from cjs-command-router-adapter.cts exports — no remaining consumers. Behavior-preserving: each thunk body is unchanged. No test referenced cjsFallbackHandler. Adapter test (8/8), state-routing tests (36/36), and the full unit suite (3435 pass / 0 fail) stay green. Source-only diff; gsd-core/bin/lib/*.cjs are built-at-publish per ADR-457. Co-authored-by: Claude Opus 4.8 (1M context) Co-authored-by: Tom Boucher --- src/cjs-command-router-adapter.cts | 12 -- src/state-command-router.cts | 316 +++++++++-------------------- 2 files changed, 94 insertions(+), 234 deletions(-) diff --git a/src/cjs-command-router-adapter.cts b/src/cjs-command-router-adapter.cts index a007056b9..f3f2f242c 100644 --- a/src/cjs-command-router-adapter.cts +++ b/src/cjs-command-router-adapter.cts @@ -135,19 +135,7 @@ function routeHubCommandFamily({ error((result as { message: string }).message); } -/** - * Projection helper for family routers that still declare SDK registry metadata - * but execute the CJS fallback path at this seam. - * - * Accepts variable argument shapes so routers can pass legacy projection tuples - * (`registryCommand`, `registryArgs`, `legacyArgs`, optional `rawFormatter`, `cjsFallback`). - */ -function cjsFallbackHandler(...projectionArgs: unknown[]): unknown { - return projectionArgs[projectionArgs.length - 1]; -} - export = { routeCjsCommandFamily, routeHubCommandFamily, - cjsFallbackHandler, }; diff --git a/src/state-command-router.cts b/src/state-command-router.cts index 5e89456a8..5aaea8e63 100644 --- a/src/state-command-router.cts +++ b/src/state-command-router.cts @@ -17,19 +17,11 @@ import { STATE_SUBCOMMANDS } from './command-aliases.cjs'; // eslint-disable-next-line @typescript-eslint/no-require-imports import cjsCommandRouterAdapter = require('./cjs-command-router-adapter.cjs'); -const { routeHubCommandFamily, cjsFallbackHandler } = cjsCommandRouterAdapter; +const { routeHubCommandFamily } = cjsCommandRouterAdapter; import { parseNamedArgs } from './command-arg-projection.cjs'; // ─── Types ──────────────────────────────────────────────────────────────────── -// Handler type matching cjs-command-router-adapter's internal Handler type. -type Handler = () => unknown; - -// Helper: cast cjsFallbackHandler result (always the last arg, a Handler) to Handler. -function fallback(...projectionArgs: unknown[]): Handler { - return cjsFallbackHandler(...projectionArgs) as Handler; -} - // Helper: extract string-only named arg value (value flags never return boolean). function strArg(opts: Record, key: string): string | null | undefined { const v = opts[key]; @@ -94,226 +86,106 @@ function routeStateCommand({ state, args, cwd, raw, error }: RouteStateCommandOp raw, unknownMessage: (subcommand: string, available: string[]) => `Unknown state subcommand: "${subcommand}". Available: ${available.join(', ')}`, handlers: { - load: fallback( - 'state.load', - [], - args.slice(1), - null, - () => state.cmdStateLoad(cwd, raw), - ), - json: fallback( - 'state.json', - [], - args.slice(1), - null, - () => state.cmdStateJson(cwd, raw), - ), - get: fallback( - 'state.get', - args.slice(2), - args.slice(1), - null, - () => state.cmdStateGet(cwd, args[2], raw), - ), - update: fallback( - 'state.update', - args.slice(2), - args.slice(1), - null, - () => state.cmdStateUpdate(cwd, args[2], args[3]), - ), - patch: fallback( - 'state.patch', - args.slice(2), - args.slice(1), - null, - () => { - const patches: Record = {}; - if (args.length === 3 && typeof args[2] === 'string' && args[2].trim().startsWith('{')) { - let parsed: unknown; - try { - parsed = JSON.parse(args[2]); - } catch (err) { - error(`state patch: invalid JSON object: ${(err as Error).message}`); - return; - } - if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) { - error('state patch: JSON input must be an object of field/value pairs.'); - return; - } - for (const [key, value] of Object.entries(parsed as Record)) { - if (key && value !== undefined) { - // eslint-disable-next-line @typescript-eslint/no-base-to-string - patches[key] = String(value); - } - } - } else { - for (let i = 2; i < args.length; i += 2) { - const key = args[i].replace(/^--/, ''); - const value = args[i + 1]; - if (key && value !== undefined) { - patches[key] = value; - } + load: () => state.cmdStateLoad(cwd, raw), + json: () => state.cmdStateJson(cwd, raw), + get: () => state.cmdStateGet(cwd, args[2], raw), + update: () => state.cmdStateUpdate(cwd, args[2], args[3]), + patch: () => { + const patches: Record = {}; + if (args.length === 3 && typeof args[2] === 'string' && args[2].trim().startsWith('{')) { + let parsed: unknown; + try { + parsed = JSON.parse(args[2]); + } catch (err) { + error(`state patch: invalid JSON object: ${(err as Error).message}`); + return; + } + if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) { + error('state patch: JSON input must be an object of field/value pairs.'); + return; + } + for (const [key, value] of Object.entries(parsed as Record)) { + if (key && value !== undefined) { + // eslint-disable-next-line @typescript-eslint/no-base-to-string + patches[key] = String(value); } } - state.cmdStatePatch(cwd, patches, raw); - }, - ), - 'advance-plan': fallback( - 'state.advance-plan', - [], - args.slice(1), - null, - () => state.cmdStateAdvancePlan(cwd, raw), - ), - 'record-metric': fallback( - 'state.record-metric', - args.slice(2), - args.slice(1), - null, - () => { - const a = parseNamedArgs(args, ['phase', 'plan', 'duration', 'tasks', 'files']); - state.cmdStateRecordMetric(cwd, { - phase: strArg(a, 'phase'), - plan: strArg(a, 'plan'), - duration: strArg(a, 'duration'), - tasks: strArg(a, 'tasks'), - files: strArg(a, 'files'), - }, raw); - }, - ), - 'update-progress': fallback( - 'state.update-progress', - [], - args.slice(1), - null, - () => state.cmdStateUpdateProgress(cwd, raw), - ), - 'add-decision': fallback( - 'state.add-decision', - args.slice(2), - args.slice(1), - null, - () => { - const a = parseNamedArgs(args, ['phase', 'summary', 'summary-file', 'rationale', 'rationale-file']); - state.cmdStateAddDecision(cwd, { - phase: strArg(a, 'phase'), - summary: strArg(a, 'summary'), - summary_file: strArg(a, 'summary-file'), - rationale: strArg(a, 'rationale') || '', - rationale_file: strArg(a, 'rationale-file'), - }, raw); - }, - ), - 'add-blocker': fallback( - 'state.add-blocker', - args.slice(2), - args.slice(1), - null, - () => { - const a = parseNamedArgs(args, ['text', 'text-file']); - state.cmdStateAddBlocker(cwd, { text: strArg(a, 'text'), text_file: strArg(a, 'text-file') }, raw); - }, - ), - 'resolve-blocker': fallback( - 'state.resolve-blocker', - args.slice(2), - args.slice(1), - null, - () => state.cmdStateResolveBlocker(cwd, strArg(parseNamedArgs(args, ['text']), 'text'), raw), - ), - 'record-session': fallback( - 'state.record-session', - args.slice(2), - args.slice(1), - null, - () => { - const a = parseNamedArgs(args, ['stopped-at', 'resume-file']); - // Pass resume_file as-is (undefined when --resume-file was not provided) so - // cmdStateRecordSession can distinguish "caller explicitly passed a value" from - // "option was not supplied" and apply the template-default-only replacement guard. - state.cmdStateRecordSession(cwd, { stopped_at: strArg(a, 'stopped-at'), resume_file: strArg(a, 'resume-file') }, raw); - }, - ), - 'begin-phase': fallback( - 'state.begin-phase', - args.slice(2), - args.slice(1), - null, - () => { - const a = parseNamedArgs(args, ['phase', 'name', 'plans']); - state.cmdStateBeginPhase(cwd, strArg(a, 'phase'), strArg(a, 'name'), parsePlans(strArg(a, 'plans')), raw); - }, - ), - 'signal-waiting': fallback( - 'state.signal-waiting', - args.slice(2), - args.slice(1), - null, - () => { - const a = parseNamedArgs(args, ['type', 'question', 'options', 'phase']); - state.cmdSignalWaiting(cwd, strArg(a, 'type'), strArg(a, 'question'), strArg(a, 'options'), strArg(a, 'phase'), raw); - }, - ), - 'signal-resume': fallback( - 'state.signal-resume', - [], - args.slice(1), - null, - () => state.cmdSignalResume(cwd, raw), - ), - 'planned-phase': fallback( - 'state.planned-phase', - args.slice(2), - args.slice(1), - null, - () => { - const a = parseNamedArgs(args, ['phase', 'name', 'plans']); - state.cmdStatePlannedPhase(cwd, strArg(a, 'phase'), parsePlans(strArg(a, 'plans')), raw); - }, - ), - validate: fallback( - 'state.validate', - [], - args.slice(1), - null, - () => state.cmdStateValidate(cwd, raw), - ), - sync: fallback( - 'state.sync', - args.slice(2), - args.slice(1), - null, - () => { - const a = parseNamedArgs(args, [], ['verify']); - state.cmdStateSync(cwd, { verify: a['verify'] }, raw); - }, - ), - prune: fallback( - 'state.prune', - args.slice(2), - args.slice(1), - null, - () => { - const a = parseNamedArgs(args, ['keep-recent'], ['dry-run']); - state.cmdStatePrune(cwd, { keepRecent: strArg(a, 'keep-recent') || '3', dryRun: a['dry-run'] === true }, raw); - }, - ), + } else { + for (let i = 2; i < args.length; i += 2) { + const key = args[i].replace(/^--/, ''); + const value = args[i + 1]; + if (key && value !== undefined) { + patches[key] = value; + } + } + } + state.cmdStatePatch(cwd, patches, raw); + }, + 'advance-plan': () => state.cmdStateAdvancePlan(cwd, raw), + 'record-metric': () => { + const a = parseNamedArgs(args, ['phase', 'plan', 'duration', 'tasks', 'files']); + state.cmdStateRecordMetric(cwd, { + phase: strArg(a, 'phase'), + plan: strArg(a, 'plan'), + duration: strArg(a, 'duration'), + tasks: strArg(a, 'tasks'), + files: strArg(a, 'files'), + }, raw); + }, + 'update-progress': () => state.cmdStateUpdateProgress(cwd, raw), + 'add-decision': () => { + const a = parseNamedArgs(args, ['phase', 'summary', 'summary-file', 'rationale', 'rationale-file']); + state.cmdStateAddDecision(cwd, { + phase: strArg(a, 'phase'), + summary: strArg(a, 'summary'), + summary_file: strArg(a, 'summary-file'), + rationale: strArg(a, 'rationale') || '', + rationale_file: strArg(a, 'rationale-file'), + }, raw); + }, + 'add-blocker': () => { + const a = parseNamedArgs(args, ['text', 'text-file']); + state.cmdStateAddBlocker(cwd, { text: strArg(a, 'text'), text_file: strArg(a, 'text-file') }, raw); + }, + 'resolve-blocker': () => state.cmdStateResolveBlocker(cwd, strArg(parseNamedArgs(args, ['text']), 'text'), raw), + 'record-session': () => { + const a = parseNamedArgs(args, ['stopped-at', 'resume-file']); + // Pass resume_file as-is (undefined when --resume-file was not provided) so + // cmdStateRecordSession can distinguish "caller explicitly passed a value" from + // "option was not supplied" and apply the template-default-only replacement guard. + state.cmdStateRecordSession(cwd, { stopped_at: strArg(a, 'stopped-at'), resume_file: strArg(a, 'resume-file') }, raw); + }, + 'begin-phase': () => { + const a = parseNamedArgs(args, ['phase', 'name', 'plans']); + state.cmdStateBeginPhase(cwd, strArg(a, 'phase'), strArg(a, 'name'), parsePlans(strArg(a, 'plans')), raw); + }, + 'signal-waiting': () => { + const a = parseNamedArgs(args, ['type', 'question', 'options', 'phase']); + state.cmdSignalWaiting(cwd, strArg(a, 'type'), strArg(a, 'question'), strArg(a, 'options'), strArg(a, 'phase'), raw); + }, + 'signal-resume': () => state.cmdSignalResume(cwd, raw), + 'planned-phase': () => { + const a = parseNamedArgs(args, ['phase', 'name', 'plans']); + state.cmdStatePlannedPhase(cwd, strArg(a, 'phase'), parsePlans(strArg(a, 'plans')), raw); + }, + validate: () => state.cmdStateValidate(cwd, raw), + sync: () => { + const a = parseNamedArgs(args, [], ['verify']); + state.cmdStateSync(cwd, { verify: a['verify'] }, raw); + }, + prune: () => { + const a = parseNamedArgs(args, ['keep-recent'], ['dry-run']); + state.cmdStatePrune(cwd, { keepRecent: strArg(a, 'keep-recent') || '3', dryRun: a['dry-run'] === true }, raw); + }, // complete-phase: CJS-only — no SDK counterpart. 'complete-phase': () => { const a = parseNamedArgs(args, ['phase']); state.cmdStateCompletePhase(cwd, raw, strArg(a, 'phase') || args[2]); }, - 'milestone-switch': fallback( - 'state.milestone-switch', - args.slice(2), - args.slice(1), - null, - () => { - const a = parseNamedArgs(args, ['milestone', 'name']); - state.cmdStateMilestoneSwitch(cwd, strArg(a, 'milestone'), strArg(a, 'name'), raw); - }, - ), + 'milestone-switch': () => { + const a = parseNamedArgs(args, ['milestone', 'name']); + state.cmdStateMilestoneSwitch(cwd, strArg(a, 'milestone'), strArg(a, 'name'), raw); + }, }, }); }