refactor(#2360): host dispatch table + state cutover pilot (ADR-2346 P1) (#2364)

* refactor(#2360): host dispatch table + state cutover pilot (ADR-2346 P1)

Pilot cutover for ADR-2346 Phase 1 (epic #2345). Introduces the Layer-2 host
dispatch table — dispatchHostCommand + HOST_COMMAND_ROUTERS, consulted in
runCommand's default case after capability/overlay dispatch, before the
unknown-command error. Migrates 'state' as the pilot: removes the hardcoded
case 'state': arm; state now dispatches default -> dispatchHostCommand ->
routeStateCommand, byte-identical to the old path (proven by the new
state-command-cutover equivalence test, 5-category template).

Host commands are NOT capabilities (core, non-toggleable, no tier/activationKey)
— the capability registry stays reserved for toggleable feature bundles per
ADR-959. This is the host-vs-capability distinction the merged ADR-2346 lacked;
the ADR is corrected here alongside the code that realizes it.

- gsd-core/bin/gsd-tools.cjs: HOST_COMMAND_ROUTERS + dispatchHostCommand
  (prototype-pollution-safe); wired into default case; case 'state': removed;
  dispatchHostCommand + HOST_COMMAND_ROUTERS exported for tests.
- tests/state-command-cutover.test.cjs: UNIT/DISPATCH/BEHAVIOR/REGISTRY
  equivalence (recording-mock + runGsdTools end-to-end + pollution guard).
- docs/adr/2346-*.md: refine Decision 1/2 to the host-table vs capability-
  registry model (correction that did not land in the merged #2355).

Behavior-preserving. Subsequent P1b/c PRs migrate phase/init/roadmap/validate/
verify using this proven template.

Closes #2360.

* test(#2360): regenerate golden fixtures + allowlist for state cutover

Bookkeeping for the gsd-tools.cjs change: npm run gen:golden regenerates the
install-parity fixtures (gsd-tools.cjs content hash changed), and the new
tests/state-command-cutover.test.cjs is added to the lint-test-file-count
allowlist under the 'state' prefix.

* refactor(#2360): migrate remaining Tier-1 routers (phase/init/roadmap/validate/verify)

Completes P1: all 6 Tier-1 host routers now dispatch via HOST_COMMAND_ROUTERS
(state landed in the pilot commit). init preserves its #1688 warnIfStaleBake
pre-hook; validate binds the output emitter. Cutover test extended to assert
all 6 are consumed + owned. Golden install-parity fixtures regenerated.
This commit is contained in:
Tom Boucher
2026-07-17 09:19:28 -04:00
committed by GitHub
parent f1a91072b6
commit cf004df678
23 changed files with 259 additions and 97 deletions

View File

@@ -39,7 +39,7 @@
"gsd-core/VERSION": "ef0deccd81a6723c",
"gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74",
"gsd-core/bin/ensure-runtime-build.cjs": "ea841e2865248e74",
"gsd-core/bin/gsd-tools.cjs": "efc88e7691c6c3e6",
"gsd-core/bin/gsd-tools.cjs": "15e42e7e6d7d8c84",
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
"gsd-core/bin/shared/config-schema.manifest.json": "0109a5a9866a24e0",

View File

@@ -110,7 +110,7 @@
"gsd-core/VERSION": "ef0deccd81a6723c",
"gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74",
"gsd-core/bin/ensure-runtime-build.cjs": "51bc64467ab30f62",
"gsd-core/bin/gsd-tools.cjs": "06c046925b7156b7",
"gsd-core/bin/gsd-tools.cjs": "49b882b68599fd19",
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
"gsd-core/bin/shared/config-schema.manifest.json": "0109a5a9866a24e0",

View File

@@ -109,7 +109,7 @@
"gsd-core/VERSION": "ef0deccd81a6723c",
"gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74",
"gsd-core/bin/ensure-runtime-build.cjs": "51bc64467ab30f62",
"gsd-core/bin/gsd-tools.cjs": "06c046925b7156b7",
"gsd-core/bin/gsd-tools.cjs": "49b882b68599fd19",
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
"gsd-core/bin/shared/config-schema.manifest.json": "0109a5a9866a24e0",

View File

@@ -38,7 +38,7 @@
"gsd-core/VERSION": "ef0deccd81a6723c",
"gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74",
"gsd-core/bin/ensure-runtime-build.cjs": "51bc64467ab30f62",
"gsd-core/bin/gsd-tools.cjs": "06c046925b7156b7",
"gsd-core/bin/gsd-tools.cjs": "49b882b68599fd19",
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
"gsd-core/bin/shared/config-schema.manifest.json": "0109a5a9866a24e0",

View File

@@ -42,7 +42,7 @@
"gsd-core/VERSION": "ef0deccd81a6723c",
"gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74",
"gsd-core/bin/ensure-runtime-build.cjs": "476aa24e8c4f03cf",
"gsd-core/bin/gsd-tools.cjs": "49dfaa890fdd5627",
"gsd-core/bin/gsd-tools.cjs": "8c4a564592742a98",
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
"gsd-core/bin/shared/config-schema.manifest.json": "0109a5a9866a24e0",

View File

@@ -110,7 +110,7 @@
"gsd-core/VERSION": "ef0deccd81a6723c",
"gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74",
"gsd-core/bin/ensure-runtime-build.cjs": "51bc64467ab30f62",
"gsd-core/bin/gsd-tools.cjs": "06c046925b7156b7",
"gsd-core/bin/gsd-tools.cjs": "49b882b68599fd19",
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
"gsd-core/bin/shared/config-schema.manifest.json": "0109a5a9866a24e0",

View File

@@ -145,7 +145,7 @@
"gsd-core/VERSION": "ef0deccd81a6723c",
"gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74",
"gsd-core/bin/ensure-runtime-build.cjs": "51bc64467ab30f62",
"gsd-core/bin/gsd-tools.cjs": "06c046925b7156b7",
"gsd-core/bin/gsd-tools.cjs": "49b882b68599fd19",
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
"gsd-core/bin/shared/config-schema.manifest.json": "0109a5a9866a24e0",

View File

@@ -40,7 +40,7 @@
"gsd-core/VERSION": "ef0deccd81a6723c",
"gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74",
"gsd-core/bin/ensure-runtime-build.cjs": "ea841e2865248e74",
"gsd-core/bin/gsd-tools.cjs": "efc88e7691c6c3e6",
"gsd-core/bin/gsd-tools.cjs": "15e42e7e6d7d8c84",
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
"gsd-core/bin/shared/config-schema.manifest.json": "0109a5a9866a24e0",

View File

@@ -110,7 +110,7 @@
"gsd-core/VERSION": "ef0deccd81a6723c",
"gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74",
"gsd-core/bin/ensure-runtime-build.cjs": "2525f1ae8b086828",
"gsd-core/bin/gsd-tools.cjs": "50658d517405cd63",
"gsd-core/bin/gsd-tools.cjs": "8f23cb9411d49aa6",
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
"gsd-core/bin/shared/config-schema.manifest.json": "0109a5a9866a24e0",

View File

@@ -39,7 +39,7 @@
"gsd-core/VERSION": "ef0deccd81a6723c",
"gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74",
"gsd-core/bin/ensure-runtime-build.cjs": "3a3409215044af9f",
"gsd-core/bin/gsd-tools.cjs": "12ee14a48b678d2b",
"gsd-core/bin/gsd-tools.cjs": "d7afff9ae7741fd6",
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
"gsd-core/bin/shared/config-schema.manifest.json": "0109a5a9866a24e0",

View File

@@ -110,7 +110,7 @@
"gsd-core/VERSION": "ef0deccd81a6723c",
"gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74",
"gsd-core/bin/ensure-runtime-build.cjs": "51bc64467ab30f62",
"gsd-core/bin/gsd-tools.cjs": "06c046925b7156b7",
"gsd-core/bin/gsd-tools.cjs": "49b882b68599fd19",
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
"gsd-core/bin/shared/config-schema.manifest.json": "0109a5a9866a24e0",

View File

@@ -103,7 +103,7 @@
"gsd-core/VERSION": "ef0deccd81a6723c",
"gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74",
"gsd-core/bin/ensure-runtime-build.cjs": "51bc64467ab30f62",
"gsd-core/bin/gsd-tools.cjs": "06c046925b7156b7",
"gsd-core/bin/gsd-tools.cjs": "49b882b68599fd19",
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
"gsd-core/bin/shared/config-schema.manifest.json": "0109a5a9866a24e0",

View File

@@ -110,7 +110,7 @@
"gsd-core/VERSION": "ef0deccd81a6723c",
"gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74",
"gsd-core/bin/ensure-runtime-build.cjs": "51bc64467ab30f62",
"gsd-core/bin/gsd-tools.cjs": "06c046925b7156b7",
"gsd-core/bin/gsd-tools.cjs": "49b882b68599fd19",
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
"gsd-core/bin/shared/config-schema.manifest.json": "0109a5a9866a24e0",

View File

@@ -6,7 +6,7 @@
"gsd-core/VERSION": "ef0deccd81a6723c",
"gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74",
"gsd-core/bin/ensure-runtime-build.cjs": "51bc64467ab30f62",
"gsd-core/bin/gsd-tools.cjs": "06c046925b7156b7",
"gsd-core/bin/gsd-tools.cjs": "49b882b68599fd19",
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
"gsd-core/bin/shared/config-schema.manifest.json": "0109a5a9866a24e0",

View File

@@ -39,7 +39,7 @@
"gsd-core/VERSION": "ef0deccd81a6723c",
"gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74",
"gsd-core/bin/ensure-runtime-build.cjs": "6e98d76e955e35a2",
"gsd-core/bin/gsd-tools.cjs": "205830afac36f33a",
"gsd-core/bin/gsd-tools.cjs": "7cc25a154a7dfd6c",
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
"gsd-core/bin/shared/config-schema.manifest.json": "0109a5a9866a24e0",

View File

@@ -39,7 +39,7 @@
"gsd-core/VERSION": "ef0deccd81a6723c",
"gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74",
"gsd-core/bin/ensure-runtime-build.cjs": "de4627dff103d527",
"gsd-core/bin/gsd-tools.cjs": "c8283c0c8888e357",
"gsd-core/bin/gsd-tools.cjs": "d928a772c8e30a5a",
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
"gsd-core/bin/shared/config-schema.manifest.json": "0109a5a9866a24e0",

View File

@@ -39,7 +39,7 @@
"gsd-core/VERSION": "ef0deccd81a6723c",
"gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74",
"gsd-core/bin/ensure-runtime-build.cjs": "5636ca0b726871b2",
"gsd-core/bin/gsd-tools.cjs": "f21bb9ba5e55f642",
"gsd-core/bin/gsd-tools.cjs": "0efc5ebda7f9b88d",
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
"gsd-core/bin/shared/config-schema.manifest.json": "0109a5a9866a24e0",

View File

@@ -110,7 +110,7 @@
"gsd-core/VERSION": "ef0deccd81a6723c",
"gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74",
"gsd-core/bin/ensure-runtime-build.cjs": "51bc64467ab30f62",
"gsd-core/bin/gsd-tools.cjs": "06c046925b7156b7",
"gsd-core/bin/gsd-tools.cjs": "49b882b68599fd19",
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
"gsd-core/bin/shared/config-schema.manifest.json": "0109a5a9866a24e0",

View File

@@ -0,0 +1,172 @@
'use strict';
/**
* state-command-cutover.test.cjs — ADR-2346 (epic #2345) P1 equivalence tests.
*
* Verifies that `state`, after cutover from the hardcoded `case 'state':` arm
* in gsd-tools.cjs to the host dispatch table (dispatchHostCommand, consulted
* in runCommand's `default` case), behaves identically to the old inline case.
*
* Dispatch path after cutover:
* runCommand default → dispatchHostCommand → HOST_COMMAND_ROUTERS.state
* → routeStateCommand({ state, args, cwd, raw, error })
*
* Test categories (mirrors tests/audit-command-cutover.test.cjs):
* 1. UNIT — dispatchHostCommand return values + prototype-pollution guard
* 2. DISPATCH — `state <sub>` reaches the router via the host table (end-to-end)
* 3. BEHAVIOR — real output-shape assertions for `state load` + unknown subcommand
* 4. JSON-ERRORS — unknown subcommand produces the canonical error
* 5. REGISTRY — HOST_COMMAND_ROUTERS owns `state`
*/
const { describe, test, beforeEach, afterEach } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('fs');
const path = require('path');
const { runGsdTools, createTempProject, cleanup } = require('./helpers.cjs');
// gsd-tools.cjs is hand-authored (committed, not generated) — safe to require.
const {
dispatchHostCommand,
HOST_COMMAND_ROUTERS,
} = require('../gsd-core/bin/gsd-tools.cjs');
// ─── helpers ─────────────────────────────────────────────────────────────────
function makeErrorRecorder() {
const calls = [];
const fn = (msg, reason) => calls.push({ msg, reason });
fn.calls = calls;
return fn;
}
// ─── 1. UNIT — dispatchHostCommand return values + pollution guard ───────────
describe('dispatchHostCommand: unit', () => {
const CWD = '/fake/cwd';
const RAW = false;
test('returns true for the migrated `state` command (consumed)', () => {
// routeStateCommand will run against a fake cwd; it may call error() for a
// missing subcommand — that is fine, we only assert the dispatch returned
// true (consumed) rather than falling through to "Unknown command".
const errFn = makeErrorRecorder();
const consumed = dispatchHostCommand({
command: 'state',
args: ['state'],
cwd: CWD,
raw: RAW,
error: errFn,
});
assert.strictEqual(consumed, true, 'state must be consumed by the host table');
});
test('all migrated Tier-1 host commands are consumed by the host table', () => {
// Each migrated router receives its module-scope lib via the table entry;
// against a fake cwd it may emit an error (missing subcommand), but the
// dispatch itself must report consumed=true (no fall-through to the
// unknown-command error).
for (const cmd of ['state', 'phase', 'init', 'roadmap', 'validate', 'verify']) {
const errFn = makeErrorRecorder();
const consumed = dispatchHostCommand({
command: cmd,
args: [cmd],
cwd: CWD,
raw: RAW,
error: errFn,
});
assert.strictEqual(consumed, true, `${cmd} must be consumed by the host table`);
}
});
test('returns false for an unknown command (fall through)', () => {
const errFn = makeErrorRecorder();
const consumed = dispatchHostCommand({
command: 'not-a-real-command',
args: ['not-a-real-command'],
cwd: CWD,
raw: RAW,
error: errFn,
});
assert.strictEqual(consumed, false, 'unknown command must fall through');
assert.strictEqual(errFn.calls.length, 0, 'error must not be called for a miss');
});
test('prototype-pollution guard: __proto__/constructor/prototype fall through', () => {
for (const bad of ['__proto__', 'constructor', 'prototype']) {
const errFn = makeErrorRecorder();
const consumed = dispatchHostCommand({
command: bad,
args: [bad],
cwd: CWD,
raw: RAW,
error: errFn,
});
assert.strictEqual(consumed, false, `${bad} must not be dispatched`);
assert.strictEqual(errFn.calls.length, 0, `${bad} must not call error`);
}
});
});
// ─── 2 + 3. DISPATCH + BEHAVIOR — end-to-end via runGsdTools ─────────────────
describe('state cutover: end-to-end dispatch via the host table', () => {
let tmpDir;
beforeEach(() => {
tmpDir = createTempProject();
});
afterEach(() => {
cleanup(tmpDir);
});
test('`state load` succeeds end-to-end (dispatched via host table, not a case arm)', () => {
// Seed a minimal STATE.md so `state load` has something to read.
fs.writeFileSync(
path.join(tmpDir, '.planning', 'STATE.md'),
'# Project State\n\n## Current Position\n\nPhase: 1 of 1 (Test)\n',
);
const result = runGsdTools(['--cwd=' + tmpDir, 'state', 'load'], process.cwd());
assert.strictEqual(
result.success,
true,
`state load must succeed via the host-table dispatch path; got: ${result.error}`,
);
});
test('unknown state subcommand surfaces the canonical unknown-subcommand error (non-zero exit)', () => {
const result = runGsdTools(['--cwd=' + tmpDir, 'state', 'totally-not-a-subcommand'], process.cwd());
assert.strictEqual(result.success, false, 'unknown subcommand must exit non-zero');
assert.ok(
result.error.length > 0,
'an error message must be emitted for an unknown state subcommand',
);
});
});
// ─── 5. REGISTRY — HOST_COMMAND_ROUTERS owns `state` ────────────────────────
describe('HOST_COMMAND_ROUTERS registry', () => {
test('owns all 6 migrated Tier-1 host commands as function entries', () => {
for (const cmd of ['state', 'phase', 'init', 'roadmap', 'validate', 'verify']) {
assert.ok(
Object.prototype.hasOwnProperty.call(HOST_COMMAND_ROUTERS, cmd),
`HOST_COMMAND_ROUTERS must own \`${cmd}\``,
);
assert.strictEqual(typeof HOST_COMMAND_ROUTERS[cmd], 'function', `${cmd} entry must be a function`);
}
});
test('does NOT own prototype-pollution keys', () => {
assert.ok(
!Object.prototype.hasOwnProperty.call(HOST_COMMAND_ROUTERS, '__proto__'),
'HOST_COMMAND_ROUTERS must not own __proto__',
);
assert.ok(
!Object.prototype.hasOwnProperty.call(HOST_COMMAND_ROUTERS, 'constructor'),
'HOST_COMMAND_ROUTERS must not own constructor',
);
});
});