* 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:
@@ -28,19 +28,21 @@ Complete the ADR-959 cutover and dissolve the switch entirely into a **two-layer
|
||||
`runCommand` collapses to a ~15-line dispatcher:
|
||||
|
||||
```
|
||||
try registry (dispatchCapabilityCommand) // families — ADR-959 mechanism, completed
|
||||
→ try leaf table (_dispatchNonFamily) // single-purpose verbs — fills the prepared seam
|
||||
try capability registry (dispatchCapabilityCommand) // toggleable FEATURE capabilities — ADR-959, unchanged
|
||||
→ try host dispatch table (dispatchHostCommand) // all non-capability commands — fills the prepared seam
|
||||
→ unknown-command error
|
||||
```
|
||||
|
||||
- **Families** (multi-subcommand, module-backed) route through the `commandFamilies` registry exactly as `graphify`/`audit`/`intel` already do.
|
||||
- **Leaf verbs** (single-purpose) live in a dispatch table that fills the prepared `_dispatchNonFamily` seam — single-purpose verbs are *not* perverted into fake capability families (a leaf like `generate-slug` has no feature bundle, no config gate, no tier).
|
||||
- **Layer 1 — capability registry (`commandFamilies`):** toggleable FEATURE capabilities only — `graphify`/`audit`/`intel` (+ genuine future features). Populated from `capability.json` `commands` arrays by `gen-capability-registry.cjs` per ADR-959. **Unchanged by this ADR.**
|
||||
- **Layer 2 — host dispatch table (`dispatchHostCommand` + `HOST_COMMAND_ROUTERS`, consulted in the `default` case):** ALL non-capability commands. This fills the seam ADR-959 named (`_dispatchNonFamily`). It holds **host routers** (multi-subcommand core commands like `state`/`phase`/`capability`) AND **leaf verbs** (single-purpose commands like `generate-slug`), dispatched by a `{ command → handler }` table.
|
||||
|
||||
### 2. Family/leaf classification rule
|
||||
**Host commands are NOT declared as capabilities.** They are core, non-toggleable, carry no `tier`/`activationKey`/install-profile membership, and cannot be tier-gated or turned off — so the capability registry (whose model is "toggleable feature bundle") is the wrong vehicle for them. The capability-vs-host boundary is the load-bearing distinction this ADR adds over ADR-959: a single-purpose leaf is never perverted into a fake capability, AND a core host command is never perverted into a toggleable feature.
|
||||
|
||||
> Promote a cluster to a **family** when it has **(a) ≥3 related subcommands**, **(b) a shared backing module**, and **(c) a shared parse/return shape**. Lone verbs or pairs stay **leaves** (two adapters over different modules ≠ one seam).
|
||||
### 2. Host-router vs leaf classification rule
|
||||
|
||||
Applied: 9 families result — `state`, `phase`, `init`, `roadmap`, `validate`/`verify`, `capability`, plus 4 promoted clusters (`config`, `research`, `resolve`, `git`). `worktree` + `workstream` stay leaves (2 verbs, different modules). ~40 remaining verbs rehome into ~4 themed leaf modules.
|
||||
> Organize a non-capability command as a **host router module** when its cluster has **(a) ≥3 related subcommands**, **(b) a shared backing module**, and **(c) a shared parse/return shape**. Lone verbs or pairs stay **leaves** (two adapters over different modules ≠ one seam). Both host routers and leaves dispatch through the Layer-2 host table — the distinction is code organization (a router module vs a themed leaf module), not dispatch routing.
|
||||
|
||||
Applied: 9 host-router clusters result — `state`, `phase`, `init`, `roadmap`, `validate`/`verify`, `capability`, plus 4 promoted clusters (`config`, `research`, `resolve`, `git`). `worktree` + `workstream` stay leaves (2 verbs, different modules). ~40 remaining verbs rehome into ~4 themed leaf modules. **None of these are capability declarations** — they are host routers/leaves in the Layer-2 table. (The capability registry's feature families — graphify/audit/intel — are unaffected.)
|
||||
|
||||
### 3. Shared `parseFamilyArgs`
|
||||
|
||||
|
||||
@@ -562,6 +562,57 @@ function dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error, load
|
||||
return true;
|
||||
}
|
||||
|
||||
// ─── ADR-2346 (epic #2345): host dispatch table ───────────────────────────────
|
||||
// Layer-2 of the two-layer dispatch. Core, non-capability host commands live
|
||||
// here — NOT in the capability registry (ADR-959's commandFamilies is reserved
|
||||
// for toggleable feature capabilities: graphify/audit/intel). A host command
|
||||
// like `state` is core, non-toggleable, carries no tier/activationKey, so it
|
||||
// cannot be a capability. Each entry maps a top-level command to its standard
|
||||
// `route*Command` router (the same routers the hardcoded `case` arms called).
|
||||
// Consulted in runCommand's `default` case, after capability + overlay
|
||||
// dispatch, before the unknown-command error. A migrated command's `case` arm
|
||||
// is removed at cutover so it reaches here; an unmigrated command still hits
|
||||
// its `case` (collision structurally impossible, same property as ADR-959).
|
||||
const HOST_COMMAND_ROUTERS = {
|
||||
// Each entry wraps its `route*Command` router so it receives the module-scope
|
||||
// lib the old `case` arm passed, plus the per-dispatch context
|
||||
// { args, cwd, raw, error }. Closes over module-scope libs (state/phase/…)
|
||||
// exactly as the old inline arms did — byte-identical dispatch.
|
||||
state: (ctx) => routeStateCommand({ state, ...ctx }),
|
||||
phase: (ctx) => routePhaseCommand({ phase, ...ctx }),
|
||||
roadmap: (ctx) => routeRoadmapCommand({ roadmap, ...ctx }),
|
||||
verify: (ctx) => routeVerifyCommand({ verify, ...ctx }),
|
||||
// validate additionally binds the module-scope `output` emitter.
|
||||
validate: (ctx) => routeValidateCommand({ verify, output, ...ctx }),
|
||||
// init preserves the #1688 stale-bake warning (best-effort, swallowed) that
|
||||
// ran before the router in the old `case 'init':` arm.
|
||||
init: (ctx) => {
|
||||
try { warnIfStaleBake(ctx.cwd); } catch { /* guard must never break init */ }
|
||||
routeInitCommand({ init, ...ctx });
|
||||
},
|
||||
};
|
||||
|
||||
// Returns true when consumed (suppress "Unknown command"), false to fall
|
||||
// through. Prototype-pollution-safe: own-property lookup rejects
|
||||
// `__proto__`/`constructor`/`prototype` command keys (same guard as
|
||||
// dispatchCapabilityCommand).
|
||||
function dispatchHostCommand({ command, args, cwd, raw, error }) {
|
||||
if (
|
||||
command === '__proto__' ||
|
||||
command === 'constructor' ||
|
||||
command === 'prototype'
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (!Object.prototype.hasOwnProperty.call(HOST_COMMAND_ROUTERS, command)) {
|
||||
return false;
|
||||
}
|
||||
const router = HOST_COMMAND_ROUTERS[command];
|
||||
if (typeof router !== 'function') return false;
|
||||
router({ args, cwd, raw, error });
|
||||
return true; // consumed — don't emit "Unknown command"
|
||||
}
|
||||
|
||||
// ─── Arg parsing helpers ──────────────────────────────────────────────────────
|
||||
|
||||
// ─── CLI Router ───────────────────────────────────────────────────────────────
|
||||
@@ -877,17 +928,6 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand
|
||||
break;
|
||||
}
|
||||
|
||||
case 'state': {
|
||||
routeStateCommand({
|
||||
state,
|
||||
args,
|
||||
cwd,
|
||||
raw,
|
||||
error,
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case 'resolve-model': {
|
||||
commands.cmdResolveModel(cwd, args[1], raw);
|
||||
break;
|
||||
@@ -1119,17 +1159,6 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand
|
||||
break;
|
||||
}
|
||||
|
||||
case 'verify': {
|
||||
routeVerifyCommand({
|
||||
verify,
|
||||
args,
|
||||
cwd,
|
||||
raw,
|
||||
error,
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case 'eval': {
|
||||
routeEvalCommand({ evalMod, args, cwd, raw, error });
|
||||
break;
|
||||
@@ -1475,17 +1504,6 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand
|
||||
break;
|
||||
}
|
||||
|
||||
case 'roadmap': {
|
||||
routeRoadmapCommand({
|
||||
roadmap,
|
||||
args,
|
||||
cwd,
|
||||
raw,
|
||||
error,
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case 'assumption-delta': {
|
||||
// #1561 — advisory architecture checkpoint. `scan <phase>` reads the
|
||||
// phase section via the same resolver as roadmap.get-phase and runs the
|
||||
@@ -1540,17 +1558,6 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand
|
||||
break;
|
||||
}
|
||||
|
||||
case 'phase': {
|
||||
routePhaseCommand({
|
||||
phase,
|
||||
args,
|
||||
cwd,
|
||||
raw,
|
||||
error,
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case 'milestone': {
|
||||
const subcommand = args[1];
|
||||
if (subcommand === 'complete') {
|
||||
@@ -1568,18 +1575,6 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand
|
||||
break;
|
||||
}
|
||||
|
||||
case 'validate': {
|
||||
routeValidateCommand({
|
||||
verify,
|
||||
args,
|
||||
cwd,
|
||||
raw,
|
||||
output: output,
|
||||
error,
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case 'progress': {
|
||||
const subcommand = args[1] || 'json';
|
||||
commands.cmdProgressRender(cwd, subcommand, raw);
|
||||
@@ -1630,21 +1625,6 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand
|
||||
break;
|
||||
}
|
||||
|
||||
case 'init': {
|
||||
// #1688: warn (at most once per process) if the user edited model_overrides
|
||||
// without re-running `gsd install <runtime>` on a static-frontmatter runtime.
|
||||
// Best-effort, stderr-only, swallowed errors — never blocks the command.
|
||||
try { warnIfStaleBake(cwd); } catch { /* guard must never break init */ }
|
||||
routeInitCommand({
|
||||
init,
|
||||
args,
|
||||
cwd,
|
||||
raw,
|
||||
error,
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case 'loop': {
|
||||
// loop render-hooks <point>
|
||||
const loopSubcommand = args[1];
|
||||
@@ -3232,6 +3212,12 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand
|
||||
// require()-ing its router FROM the capability's install root (confined to that root).
|
||||
if (dispatchOverlayCapabilityCommand({ command, args, cwd, raw, error })) break;
|
||||
|
||||
// ADR-2346 (epic #2345): host dispatch table — core, non-capability
|
||||
// commands (state, …) routed via their `route*Command` router instead of
|
||||
// a hardcoded `case` arm. Tried after capability/overlay dispatch and
|
||||
// before the unknown-command error.
|
||||
if (dispatchHostCommand({ command, args, cwd, raw, error })) break;
|
||||
|
||||
// #3243: if the caller passed a dotted form (e.g. "foo.bar"), the shim
|
||||
// above split it so `command` here is the head ("foo"). Use
|
||||
// originalCommand to reconstruct the original dotted form and suggest
|
||||
@@ -3263,4 +3249,4 @@ if (require.main === module) {
|
||||
// synthetic registry + requireModule injections.
|
||||
// ADR-1244 Phase 5: export dispatchOverlayCapabilityCommand + defaultRequireFromInstallRoot for
|
||||
// the third-party overlay dispatch + install-root confinement tests.
|
||||
module.exports = { dispatchCapabilityCommand, dispatchOverlayCapabilityCommand, defaultRequireFromInstallRoot };
|
||||
module.exports = { dispatchCapabilityCommand, dispatchOverlayCapabilityCommand, defaultRequireFromInstallRoot, dispatchHostCommand, HOST_COMMAND_ROUTERS };
|
||||
|
||||
1
node_modules
Symbolic link
1
node_modules
Symbolic link
@@ -0,0 +1 @@
|
||||
/Users/trekkie/projects/gsd-core/node_modules
|
||||
@@ -60,6 +60,7 @@
|
||||
"state": {
|
||||
"files": [
|
||||
"state-acquirestatelock-non-eexist.test.cjs",
|
||||
"state-command-cutover.test.cjs",
|
||||
"state-prune.test.cjs",
|
||||
"state-rebuild-cli.test.cjs",
|
||||
"state-rebuild.test.cjs",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
2
tests/fixtures/golden-install-parity/pi.json
vendored
2
tests/fixtures/golden-install-parity/pi.json
vendored
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
172
tests/state-command-cutover.test.cjs
Normal file
172
tests/state-command-cutover.test.cjs
Normal 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',
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user