refactor(query): manifest-backed routing seam + family adapters (#2908)

Merging validated command-seam foundation.
This commit is contained in:
Tom Boucher
2026-04-30 14:04:50 -04:00
committed by GitHub
parent 6dce1de4a7
commit 444db1714b
29 changed files with 1478 additions and 411 deletions

View File

@@ -10,6 +10,20 @@ This document records contracts for the typed query layer consumed by `gsd-sdk q
- CJS `**summary-extract**` → SDK `**summary.extract**` / `**summary extract**` / `**history-digest**` (see `index.ts`).
- CJS top-level `**scaffold <type> ...**` → SDK `**phase.scaffold**` / `**phase scaffold**` with the scaffold type as the first argument (no separate `scaffold` alias on the registry).
### Manifest-backed family ownership
These families are sourced from `command-manifest.*.ts` files and expanded into generated alias artifacts (`command-aliases.generated.ts` + CJS mirror):
- `state.*` → `command-manifest.state.ts`
- `verify.*` → `command-manifest.verify.ts`
- `init.*` → `command-manifest.init.ts`
- `phase.*` → `command-manifest.phase.ts`
- `phases.*` → `command-manifest.phases.ts`
- `validate.*` → `command-manifest.validate.ts`
- `roadmap.*` → `command-manifest.roadmap.ts`
CJS routing seams mirror these families with thin adapters (`state/verify/init/phase/phases/validate/roadmap-command-router.cjs`) so `gsd-tools.cjs` stays orchestration-only.
## `gsd-sdk query` routing
1. **`normalizeQueryCommand()`** (`normalize-query-command.ts`) — maps the first argv tokens to the same **command + subcommand** patterns as `gsd-tools` `runCommand()` where needed (e.g. `state json` → `state.json`, `init execute-phase 9` → `init.execute-phase` with args `['9']`, `scaffold …` → `phase.scaffold`). Re-exported from **`@gsd-build/sdk`** and **`createRegistry`’s module** (`sdk/src/query/index.ts`) so programmatic callers can mirror CLI tokenization without importing a deep path.

View File

@@ -0,0 +1,122 @@
/**
* GENERATED FILE — command alias expansion for state.*, verify.*, init.*, phase.*, phases.*, validate.*, and roadmap.* pilots.
* Source: sdk/src/query/command-manifest.{state,verify,init,phase,phases,validate,roadmap}.ts
*/
export interface FamilyCommandAlias {
canonical: string;
aliases: string[];
subcommand: string;
mutation: boolean;
}
export const STATE_COMMAND_ALIASES: readonly FamilyCommandAlias[] = [
{ canonical: 'state.load', aliases: [], subcommand: 'load', mutation: false },
{ canonical: 'state.json', aliases: ['state json'], subcommand: 'json', mutation: false },
{ canonical: 'state.get', aliases: ['state get'], subcommand: 'get', mutation: false },
{ canonical: 'state.update', aliases: ['state update'], subcommand: 'update', mutation: true },
{ canonical: 'state.patch', aliases: ['state patch'], subcommand: 'patch', mutation: true },
{ canonical: 'state.begin-phase', aliases: ['state begin-phase'], subcommand: 'begin-phase', mutation: true },
{ canonical: 'state.advance-plan', aliases: ['state advance-plan'], subcommand: 'advance-plan', mutation: true },
{ canonical: 'state.record-metric', aliases: ['state record-metric'], subcommand: 'record-metric', mutation: true },
{ canonical: 'state.update-progress', aliases: ['state update-progress'], subcommand: 'update-progress', mutation: true },
{ canonical: 'state.add-decision', aliases: ['state add-decision'], subcommand: 'add-decision', mutation: true },
{ canonical: 'state.add-blocker', aliases: ['state add-blocker'], subcommand: 'add-blocker', mutation: true },
{ canonical: 'state.resolve-blocker', aliases: ['state resolve-blocker'], subcommand: 'resolve-blocker', mutation: true },
{ canonical: 'state.record-session', aliases: ['state record-session'], subcommand: 'record-session', mutation: true },
{ canonical: 'state.signal-waiting', aliases: ['state signal-waiting'], subcommand: 'signal-waiting', mutation: true },
{ canonical: 'state.signal-resume', aliases: ['state signal-resume'], subcommand: 'signal-resume', mutation: true },
{ canonical: 'state.planned-phase', aliases: ['state planned-phase'], subcommand: 'planned-phase', mutation: true },
{ canonical: 'state.validate', aliases: ['state validate'], subcommand: 'validate', mutation: false },
{ canonical: 'state.sync', aliases: ['state sync'], subcommand: 'sync', mutation: true },
{ canonical: 'state.prune', aliases: ['state prune'], subcommand: 'prune', mutation: true },
{ canonical: 'state.milestone-switch', aliases: ['state milestone-switch'], subcommand: 'milestone-switch', mutation: true },
{ canonical: 'state.add-roadmap-evolution', aliases: ['state add-roadmap-evolution'], subcommand: 'add-roadmap-evolution', mutation: true },
] as const;
export const VERIFY_COMMAND_ALIASES: readonly FamilyCommandAlias[] = [
{ canonical: 'verify.plan-structure', aliases: ['verify plan-structure'], subcommand: 'plan-structure', mutation: false },
{ canonical: 'verify.phase-completeness', aliases: ['verify phase-completeness'], subcommand: 'phase-completeness', mutation: false },
{ canonical: 'verify.references', aliases: ['verify references'], subcommand: 'references', mutation: false },
{ canonical: 'verify.commits', aliases: ['verify commits'], subcommand: 'commits', mutation: false },
{ canonical: 'verify.artifacts', aliases: ['verify artifacts'], subcommand: 'artifacts', mutation: false },
{ canonical: 'verify.key-links', aliases: ['verify key-links'], subcommand: 'key-links', mutation: false },
{ canonical: 'verify.schema-drift', aliases: ['verify schema-drift'], subcommand: 'schema-drift', mutation: false },
{ canonical: 'verify.codebase-drift', aliases: ['verify codebase-drift'], subcommand: 'codebase-drift', mutation: false },
] as const;
export const INIT_COMMAND_ALIASES: readonly FamilyCommandAlias[] = [
{ canonical: 'init.execute-phase', aliases: ['init execute-phase'], subcommand: 'execute-phase', mutation: false },
{ canonical: 'init.plan-phase', aliases: ['init plan-phase'], subcommand: 'plan-phase', mutation: false },
{ canonical: 'init.new-project', aliases: ['init new-project'], subcommand: 'new-project', mutation: false },
{ canonical: 'init.new-milestone', aliases: ['init new-milestone'], subcommand: 'new-milestone', mutation: false },
{ canonical: 'init.quick', aliases: ['init quick'], subcommand: 'quick', mutation: false },
{ canonical: 'init.ingest-docs', aliases: ['init ingest-docs'], subcommand: 'ingest-docs', mutation: false },
{ canonical: 'init.resume', aliases: ['init resume'], subcommand: 'resume', mutation: false },
{ canonical: 'init.verify-work', aliases: ['init verify-work'], subcommand: 'verify-work', mutation: false },
{ canonical: 'init.phase-op', aliases: ['init phase-op'], subcommand: 'phase-op', mutation: false },
{ canonical: 'init.todos', aliases: ['init todos'], subcommand: 'todos', mutation: false },
{ canonical: 'init.milestone-op', aliases: ['init milestone-op'], subcommand: 'milestone-op', mutation: false },
{ canonical: 'init.map-codebase', aliases: ['init map-codebase'], subcommand: 'map-codebase', mutation: false },
{ canonical: 'init.progress', aliases: ['init progress'], subcommand: 'progress', mutation: false },
{ canonical: 'init.manager', aliases: ['init manager'], subcommand: 'manager', mutation: false },
{ canonical: 'init.new-workspace', aliases: ['init new-workspace'], subcommand: 'new-workspace', mutation: false },
{ canonical: 'init.list-workspaces', aliases: ['init list-workspaces'], subcommand: 'list-workspaces', mutation: false },
{ canonical: 'init.remove-workspace', aliases: ['init remove-workspace'], subcommand: 'remove-workspace', mutation: false },
] as const;
export const PHASE_COMMAND_ALIASES: readonly FamilyCommandAlias[] = [
{ canonical: 'phase.list-plans', aliases: ['phase list-plans'], subcommand: 'list-plans', mutation: false },
{ canonical: 'phase.list-artifacts', aliases: ['phase list-artifacts'], subcommand: 'list-artifacts', mutation: false },
{ canonical: 'phase.next-decimal', aliases: ['phase next-decimal'], subcommand: 'next-decimal', mutation: false },
{ canonical: 'phase.add', aliases: ['phase add'], subcommand: 'add', mutation: true },
{ canonical: 'phase.add-batch', aliases: ['phase add-batch'], subcommand: 'add-batch', mutation: true },
{ canonical: 'phase.insert', aliases: ['phase insert'], subcommand: 'insert', mutation: true },
{ canonical: 'phase.remove', aliases: ['phase remove'], subcommand: 'remove', mutation: true },
{ canonical: 'phase.complete', aliases: ['phase complete'], subcommand: 'complete', mutation: true },
{ canonical: 'phase.scaffold', aliases: ['phase scaffold'], subcommand: 'scaffold', mutation: true },
] as const;
export const PHASES_COMMAND_ALIASES: readonly FamilyCommandAlias[] = [
{ canonical: 'phases.list', aliases: ['phases list'], subcommand: 'list', mutation: false },
{ canonical: 'phases.clear', aliases: ['phases clear'], subcommand: 'clear', mutation: true },
{ canonical: 'phases.archive', aliases: ['phases archive'], subcommand: 'archive', mutation: true },
] as const;
export const VALIDATE_COMMAND_ALIASES: readonly FamilyCommandAlias[] = [
{ canonical: 'validate.consistency', aliases: ['validate consistency'], subcommand: 'consistency', mutation: false },
{ canonical: 'validate.health', aliases: ['validate health'], subcommand: 'health', mutation: false },
{ canonical: 'validate.agents', aliases: ['validate agents'], subcommand: 'agents', mutation: false },
{ canonical: 'validate.context', aliases: ['validate context'], subcommand: 'context', mutation: false },
] as const;
export const ROADMAP_COMMAND_ALIASES: readonly FamilyCommandAlias[] = [
{ canonical: 'roadmap.analyze', aliases: ['roadmap analyze'], subcommand: 'analyze', mutation: false },
{ canonical: 'roadmap.get-phase', aliases: ['roadmap get-phase'], subcommand: 'get-phase', mutation: false },
{ canonical: 'roadmap.update-plan-progress', aliases: ['roadmap update-plan-progress'], subcommand: 'update-plan-progress', mutation: true },
{ canonical: 'roadmap.annotate-dependencies', aliases: ['roadmap annotate-dependencies'], subcommand: 'annotate-dependencies', mutation: true },
] as const;
export const STATE_SUBCOMMANDS = new Set<string>(STATE_COMMAND_ALIASES.map((entry) => entry.subcommand));
export const VERIFY_SUBCOMMANDS = new Set<string>(VERIFY_COMMAND_ALIASES.map((entry) => entry.subcommand));
export const INIT_SUBCOMMANDS = new Set<string>(INIT_COMMAND_ALIASES.map((entry) => entry.subcommand));
export const PHASE_SUBCOMMANDS = new Set<string>(PHASE_COMMAND_ALIASES.map((entry) => entry.subcommand));
export const PHASES_SUBCOMMANDS = new Set<string>(PHASES_COMMAND_ALIASES.map((entry) => entry.subcommand));
export const VALIDATE_SUBCOMMANDS = new Set<string>(VALIDATE_COMMAND_ALIASES.map((entry) => entry.subcommand));
export const ROADMAP_SUBCOMMANDS = new Set<string>(ROADMAP_COMMAND_ALIASES.map((entry) => entry.subcommand));
export const STATE_MUTATION_COMMANDS: readonly string[] = STATE_COMMAND_ALIASES
.filter((entry) => entry.mutation)
.flatMap((entry) => [entry.canonical, ...entry.aliases]);
export const PHASE_MUTATION_COMMANDS: readonly string[] = PHASE_COMMAND_ALIASES
.filter((entry) => entry.mutation)
.flatMap((entry) => [entry.canonical, ...entry.aliases]);
export const PHASES_MUTATION_COMMANDS: readonly string[] = PHASES_COMMAND_ALIASES
.filter((entry) => entry.mutation)
.flatMap((entry) => [entry.canonical, ...entry.aliases]);
export const ROADMAP_MUTATION_COMMANDS: readonly string[] = ROADMAP_COMMAND_ALIASES
.filter((entry) => entry.mutation)
.flatMap((entry) => [entry.canonical, ...entry.aliases]);

View File

@@ -0,0 +1,24 @@
import type { CommandManifestEntry } from './command-manifest.types.js';
/**
* Canonical init.* command manifest.
*/
export const INIT_COMMAND_MANIFEST: readonly CommandManifestEntry[] = [
{ family: 'init', canonical: 'init.execute-phase', aliases: ['init execute-phase'], mutation: false, outputMode: 'json' },
{ family: 'init', canonical: 'init.plan-phase', aliases: ['init plan-phase'], mutation: false, outputMode: 'json' },
{ family: 'init', canonical: 'init.new-project', aliases: ['init new-project'], mutation: false, outputMode: 'json' },
{ family: 'init', canonical: 'init.new-milestone', aliases: ['init new-milestone'], mutation: false, outputMode: 'json' },
{ family: 'init', canonical: 'init.quick', aliases: ['init quick'], mutation: false, outputMode: 'json' },
{ family: 'init', canonical: 'init.ingest-docs', aliases: ['init ingest-docs'], mutation: false, outputMode: 'json' },
{ family: 'init', canonical: 'init.resume', aliases: ['init resume'], mutation: false, outputMode: 'json' },
{ family: 'init', canonical: 'init.verify-work', aliases: ['init verify-work'], mutation: false, outputMode: 'json' },
{ family: 'init', canonical: 'init.phase-op', aliases: ['init phase-op'], mutation: false, outputMode: 'json' },
{ family: 'init', canonical: 'init.todos', aliases: ['init todos'], mutation: false, outputMode: 'json' },
{ family: 'init', canonical: 'init.milestone-op', aliases: ['init milestone-op'], mutation: false, outputMode: 'json' },
{ family: 'init', canonical: 'init.map-codebase', aliases: ['init map-codebase'], mutation: false, outputMode: 'json' },
{ family: 'init', canonical: 'init.progress', aliases: ['init progress'], mutation: false, outputMode: 'json' },
{ family: 'init', canonical: 'init.manager', aliases: ['init manager'], mutation: false, outputMode: 'json' },
{ family: 'init', canonical: 'init.new-workspace', aliases: ['init new-workspace'], mutation: false, outputMode: 'json' },
{ family: 'init', canonical: 'init.list-workspaces', aliases: ['init list-workspaces'], mutation: false, outputMode: 'json' },
{ family: 'init', canonical: 'init.remove-workspace', aliases: ['init remove-workspace'], mutation: false, outputMode: 'json' },
] as const;

View File

@@ -0,0 +1,16 @@
import type { CommandManifestEntry } from './command-manifest.types.js';
/**
* Canonical phase.* command manifest.
*/
export const PHASE_COMMAND_MANIFEST: readonly CommandManifestEntry[] = [
{ family: 'phase', canonical: 'phase.list-plans', aliases: ['phase list-plans'], mutation: false, outputMode: 'json' },
{ family: 'phase', canonical: 'phase.list-artifacts', aliases: ['phase list-artifacts'], mutation: false, outputMode: 'json' },
{ family: 'phase', canonical: 'phase.next-decimal', aliases: ['phase next-decimal'], mutation: false, outputMode: 'json' },
{ family: 'phase', canonical: 'phase.add', aliases: ['phase add'], mutation: true, outputMode: 'json' },
{ family: 'phase', canonical: 'phase.add-batch', aliases: ['phase add-batch'], mutation: true, outputMode: 'json' },
{ family: 'phase', canonical: 'phase.insert', aliases: ['phase insert'], mutation: true, outputMode: 'json' },
{ family: 'phase', canonical: 'phase.remove', aliases: ['phase remove'], mutation: true, outputMode: 'json' },
{ family: 'phase', canonical: 'phase.complete', aliases: ['phase complete'], mutation: true, outputMode: 'json' },
{ family: 'phase', canonical: 'phase.scaffold', aliases: ['phase scaffold'], mutation: true, outputMode: 'json' },
] as const;

View File

@@ -0,0 +1,11 @@
import type { CommandManifestEntry } from './command-manifest.types.js';
/**
* Canonical phases.* command manifest.
* Note: `phases.archive` is SDK-only; CJS `gsd-tools phases` currently supports list/clear.
*/
export const PHASES_COMMAND_MANIFEST: readonly CommandManifestEntry[] = [
{ family: 'phases', canonical: 'phases.list', aliases: ['phases list'], mutation: false, outputMode: 'json' },
{ family: 'phases', canonical: 'phases.clear', aliases: ['phases clear'], mutation: true, outputMode: 'json' },
{ family: 'phases', canonical: 'phases.archive', aliases: ['phases archive'], mutation: true, outputMode: 'json' },
] as const;

View File

@@ -0,0 +1,11 @@
import type { CommandManifestEntry } from './command-manifest.types.js';
/**
* Canonical roadmap.* command manifest.
*/
export const ROADMAP_COMMAND_MANIFEST: readonly CommandManifestEntry[] = [
{ family: 'roadmap', canonical: 'roadmap.analyze', aliases: ['roadmap analyze'], mutation: false, outputMode: 'json' },
{ family: 'roadmap', canonical: 'roadmap.get-phase', aliases: ['roadmap get-phase'], mutation: false, outputMode: 'json' },
{ family: 'roadmap', canonical: 'roadmap.update-plan-progress', aliases: ['roadmap update-plan-progress'], mutation: true, outputMode: 'json' },
{ family: 'roadmap', canonical: 'roadmap.annotate-dependencies', aliases: ['roadmap annotate-dependencies'], mutation: true, outputMode: 'json' },
] as const;

View File

@@ -0,0 +1,31 @@
import type { CommandManifestEntry } from './command-manifest.types.js';
/**
* Canonical state.* command manifest.
*
* Source of truth for the state family seam. Adapters derive registry aliases,
* mutation classification, and CJS subcommand routing metadata from this list.
*/
export const STATE_COMMAND_MANIFEST: readonly CommandManifestEntry[] = [
{ family: 'state', canonical: 'state.load', aliases: [], mutation: false, outputMode: 'raw' },
{ family: 'state', canonical: 'state.json', aliases: ['state json'], mutation: false, outputMode: 'json' },
{ family: 'state', canonical: 'state.get', aliases: ['state get'], mutation: false, outputMode: 'json' },
{ family: 'state', canonical: 'state.update', aliases: ['state update'], mutation: true, outputMode: 'json' },
{ family: 'state', canonical: 'state.patch', aliases: ['state patch'], mutation: true, outputMode: 'json' },
{ family: 'state', canonical: 'state.begin-phase', aliases: ['state begin-phase'], mutation: true, outputMode: 'json' },
{ family: 'state', canonical: 'state.advance-plan', aliases: ['state advance-plan'], mutation: true, outputMode: 'json' },
{ family: 'state', canonical: 'state.record-metric', aliases: ['state record-metric'], mutation: true, outputMode: 'json' },
{ family: 'state', canonical: 'state.update-progress', aliases: ['state update-progress'], mutation: true, outputMode: 'json' },
{ family: 'state', canonical: 'state.add-decision', aliases: ['state add-decision'], mutation: true, outputMode: 'json' },
{ family: 'state', canonical: 'state.add-blocker', aliases: ['state add-blocker'], mutation: true, outputMode: 'json' },
{ family: 'state', canonical: 'state.resolve-blocker', aliases: ['state resolve-blocker'], mutation: true, outputMode: 'json' },
{ family: 'state', canonical: 'state.record-session', aliases: ['state record-session'], mutation: true, outputMode: 'json' },
{ family: 'state', canonical: 'state.signal-waiting', aliases: ['state signal-waiting'], mutation: true, outputMode: 'json' },
{ family: 'state', canonical: 'state.signal-resume', aliases: ['state signal-resume'], mutation: true, outputMode: 'json' },
{ family: 'state', canonical: 'state.planned-phase', aliases: ['state planned-phase'], mutation: true, outputMode: 'json' },
{ family: 'state', canonical: 'state.validate', aliases: ['state validate'], mutation: false, outputMode: 'json' },
{ family: 'state', canonical: 'state.sync', aliases: ['state sync'], mutation: true, outputMode: 'json' },
{ family: 'state', canonical: 'state.prune', aliases: ['state prune'], mutation: true, outputMode: 'json' },
{ family: 'state', canonical: 'state.milestone-switch', aliases: ['state milestone-switch'], mutation: true, outputMode: 'json' },
{ family: 'state', canonical: 'state.add-roadmap-evolution', aliases: ['state add-roadmap-evolution'], mutation: true, outputMode: 'json' },
] as const;

View File

@@ -0,0 +1,17 @@
import { STATE_COMMAND_MANIFEST } from './command-manifest.state.js';
import { VERIFY_COMMAND_MANIFEST } from './command-manifest.verify.js';
import { INIT_COMMAND_MANIFEST } from './command-manifest.init.js';
import { PHASE_COMMAND_MANIFEST } from './command-manifest.phase.js';
import { PHASES_COMMAND_MANIFEST } from './command-manifest.phases.js';
import { VALIDATE_COMMAND_MANIFEST } from './command-manifest.validate.js';
import { ROADMAP_COMMAND_MANIFEST } from './command-manifest.roadmap.js';
export const COMMAND_MANIFEST = [
...STATE_COMMAND_MANIFEST,
...VERIFY_COMMAND_MANIFEST,
...INIT_COMMAND_MANIFEST,
...PHASE_COMMAND_MANIFEST,
...PHASES_COMMAND_MANIFEST,
...VALIDATE_COMMAND_MANIFEST,
...ROADMAP_COMMAND_MANIFEST,
] as const;

View File

@@ -0,0 +1,11 @@
export type CommandFamily = 'state' | 'verify' | 'init' | 'phase' | 'phases' | 'validate' | 'roadmap';
export type OutputMode = 'json' | 'raw';
export interface CommandManifestEntry {
family: CommandFamily;
canonical: string;
aliases: string[];
mutation: boolean;
outputMode: OutputMode;
}

View File

@@ -0,0 +1,11 @@
import type { CommandManifestEntry } from './command-manifest.types.js';
/**
* Canonical validate.* command manifest.
*/
export const VALIDATE_COMMAND_MANIFEST: readonly CommandManifestEntry[] = [
{ family: 'validate', canonical: 'validate.consistency', aliases: ['validate consistency'], mutation: false, outputMode: 'json' },
{ family: 'validate', canonical: 'validate.health', aliases: ['validate health'], mutation: false, outputMode: 'json' },
{ family: 'validate', canonical: 'validate.agents', aliases: ['validate agents'], mutation: false, outputMode: 'json' },
{ family: 'validate', canonical: 'validate.context', aliases: ['validate context'], mutation: false, outputMode: 'json' },
] as const;

View File

@@ -0,0 +1,15 @@
import type { CommandManifestEntry } from './command-manifest.types.js';
/**
* Canonical verify.* command manifest.
*/
export const VERIFY_COMMAND_MANIFEST: readonly CommandManifestEntry[] = [
{ family: 'verify', canonical: 'verify.plan-structure', aliases: ['verify plan-structure'], mutation: false, outputMode: 'json' },
{ family: 'verify', canonical: 'verify.phase-completeness', aliases: ['verify phase-completeness'], mutation: false, outputMode: 'json' },
{ family: 'verify', canonical: 'verify.references', aliases: ['verify references'], mutation: false, outputMode: 'json' },
{ family: 'verify', canonical: 'verify.commits', aliases: ['verify commits'], mutation: false, outputMode: 'json' },
{ family: 'verify', canonical: 'verify.artifacts', aliases: ['verify artifacts'], mutation: false, outputMode: 'json' },
{ family: 'verify', canonical: 'verify.key-links', aliases: ['verify key-links'], mutation: false, outputMode: 'json' },
{ family: 'verify', canonical: 'verify.schema-drift', aliases: ['verify schema-drift'], mutation: false, outputMode: 'json' },
{ family: 'verify', canonical: 'verify.codebase-drift', aliases: ['verify codebase-drift'], mutation: false, outputMode: 'json' },
] as const;

View File

@@ -0,0 +1,118 @@
import { describe, it, expect } from 'vitest';
import { createRequire } from 'node:module';
import { createRegistry } from './index.js';
import { STATE_COMMAND_MANIFEST } from './command-manifest.state.js';
import { VERIFY_COMMAND_MANIFEST } from './command-manifest.verify.js';
import { INIT_COMMAND_MANIFEST } from './command-manifest.init.js';
import { PHASE_COMMAND_MANIFEST } from './command-manifest.phase.js';
import { PHASES_COMMAND_MANIFEST } from './command-manifest.phases.js';
import { VALIDATE_COMMAND_MANIFEST } from './command-manifest.validate.js';
import { ROADMAP_COMMAND_MANIFEST } from './command-manifest.roadmap.js';
import {
STATE_COMMAND_ALIASES,
VERIFY_COMMAND_ALIASES,
INIT_COMMAND_ALIASES,
PHASE_COMMAND_ALIASES,
PHASES_COMMAND_ALIASES,
VALIDATE_COMMAND_ALIASES,
ROADMAP_COMMAND_ALIASES,
} from './command-aliases.generated.js';
function subcommandFor(canonical: string, family: 'state' | 'verify' | 'init' | 'phase' | 'phases' | 'validate' | 'roadmap'): string {
return canonical.slice(`${family}.`.length);
}
describe('command seam coverage (manifest -> generated -> adapters)', () => {
it('state/verify/init/phase/phases/validate/roadmap manifest canonicals are present in generated alias artifacts', () => {
const generated = new Map<string, { aliases: string[]; subcommand: string; mutation: boolean }>();
for (const entry of [...STATE_COMMAND_ALIASES, ...VERIFY_COMMAND_ALIASES, ...INIT_COMMAND_ALIASES, ...PHASE_COMMAND_ALIASES, ...PHASES_COMMAND_ALIASES, ...VALIDATE_COMMAND_ALIASES, ...ROADMAP_COMMAND_ALIASES]) {
generated.set(entry.canonical, { aliases: [...entry.aliases], subcommand: entry.subcommand, mutation: !!entry.mutation });
}
for (const entry of STATE_COMMAND_MANIFEST) {
const g = generated.get(entry.canonical);
expect(g, `missing generated canonical ${entry.canonical}`).toBeTruthy();
expect(g?.subcommand).toBe(subcommandFor(entry.canonical, 'state'));
expect(g?.aliases ?? []).toEqual(entry.aliases);
expect(g?.mutation).toBe(entry.mutation);
}
for (const entry of VERIFY_COMMAND_MANIFEST) {
const g = generated.get(entry.canonical);
expect(g, `missing generated canonical ${entry.canonical}`).toBeTruthy();
expect(g?.subcommand).toBe(subcommandFor(entry.canonical, 'verify'));
expect(g?.aliases ?? []).toEqual(entry.aliases);
expect(g?.mutation).toBe(entry.mutation);
}
for (const entry of INIT_COMMAND_MANIFEST) {
const g = generated.get(entry.canonical);
expect(g, `missing generated canonical ${entry.canonical}`).toBeTruthy();
expect(g?.subcommand).toBe(subcommandFor(entry.canonical, 'init'));
expect(g?.aliases ?? []).toEqual(entry.aliases);
expect(g?.mutation).toBe(entry.mutation);
}
for (const entry of PHASE_COMMAND_MANIFEST) {
const g = generated.get(entry.canonical);
expect(g, `missing generated canonical ${entry.canonical}`).toBeTruthy();
expect(g?.subcommand).toBe(subcommandFor(entry.canonical, 'phase'));
expect(g?.aliases ?? []).toEqual(entry.aliases);
expect(g?.mutation).toBe(entry.mutation);
}
for (const entry of PHASES_COMMAND_MANIFEST) {
const g = generated.get(entry.canonical);
expect(g, `missing generated canonical ${entry.canonical}`).toBeTruthy();
expect(g?.subcommand).toBe(subcommandFor(entry.canonical, 'phases'));
expect(g?.aliases ?? []).toEqual(entry.aliases);
expect(g?.mutation).toBe(entry.mutation);
}
for (const entry of VALIDATE_COMMAND_MANIFEST) {
const g = generated.get(entry.canonical);
expect(g, `missing generated canonical ${entry.canonical}`).toBeTruthy();
expect(g?.subcommand).toBe(subcommandFor(entry.canonical, 'validate'));
expect(g?.aliases ?? []).toEqual(entry.aliases);
expect(g?.mutation).toBe(entry.mutation);
}
for (const entry of ROADMAP_COMMAND_MANIFEST) {
const g = generated.get(entry.canonical);
expect(g, `missing generated canonical ${entry.canonical}`).toBeTruthy();
expect(g?.subcommand).toBe(subcommandFor(entry.canonical, 'roadmap'));
expect(g?.aliases ?? []).toEqual(entry.aliases);
expect(g?.mutation).toBe(entry.mutation);
}
});
it('registry has every canonical + alias for migrated families', () => {
const registry = createRegistry();
for (const entry of [...STATE_COMMAND_ALIASES, ...VERIFY_COMMAND_ALIASES, ...INIT_COMMAND_ALIASES, ...PHASE_COMMAND_ALIASES, ...PHASES_COMMAND_ALIASES, ...VALIDATE_COMMAND_ALIASES, ...ROADMAP_COMMAND_ALIASES]) {
expect(registry.has(entry.canonical), `missing registry canonical ${entry.canonical}`).toBe(true);
for (const alias of entry.aliases) {
expect(registry.has(alias), `missing registry alias ${alias}`).toBe(true);
}
}
});
it('CJS seam adapters export expected router functions', () => {
const require = createRequire(import.meta.url);
const stateRouter = require('../../../get-shit-done/bin/lib/state-command-router.cjs');
const verifyRouter = require('../../../get-shit-done/bin/lib/verify-command-router.cjs');
const initRouter = require('../../../get-shit-done/bin/lib/init-command-router.cjs');
const phaseRouter = require('../../../get-shit-done/bin/lib/phase-command-router.cjs');
const phasesRouter = require('../../../get-shit-done/bin/lib/phases-command-router.cjs');
const validateRouter = require('../../../get-shit-done/bin/lib/validate-command-router.cjs');
const roadmapRouter = require('../../../get-shit-done/bin/lib/roadmap-command-router.cjs');
expect(typeof stateRouter.routeStateCommand).toBe('function');
expect(typeof verifyRouter.routeVerifyCommand).toBe('function');
expect(typeof initRouter.routeInitCommand).toBe('function');
expect(typeof phaseRouter.routePhaseCommand).toBe('function');
expect(typeof phasesRouter.routePhasesCommand).toBe('function');
expect(typeof validateRouter.routeValidateCommand).toBe('function');
expect(typeof roadmapRouter.routeRoadmapCommand).toBe('function');
});
});

View File

@@ -20,6 +20,19 @@ import { frontmatterGet } from './frontmatter.js';
import { configGet, configPath, resolveModel } from './config-query.js';
import { stateJson, stateGet, stateSnapshot } from './state.js';
import { stateProjectLoad } from './state-project-load.js';
import {
STATE_COMMAND_ALIASES,
STATE_MUTATION_COMMANDS,
VERIFY_COMMAND_ALIASES,
INIT_COMMAND_ALIASES,
PHASE_COMMAND_ALIASES,
PHASE_MUTATION_COMMANDS,
PHASES_COMMAND_ALIASES,
PHASES_MUTATION_COMMANDS,
VALIDATE_COMMAND_ALIASES,
ROADMAP_COMMAND_ALIASES,
ROADMAP_MUTATION_COMMANDS,
} from './command-aliases.generated.js';
import { findPhase, phasePlanIndex } from './phase.js';
import { phaseListPlans, phaseListArtifacts } from './phase-list-queries.js';
import { planTaskStructure } from './plan-task-structure.js';
@@ -126,28 +139,14 @@ export { normalizeQueryCommand } from './normalize-query-command.js';
* (they emit JSON for workflows; agents perform writes).
*/
export const QUERY_MUTATION_COMMANDS = new Set<string>([
'state.update', 'state.patch', 'state.begin-phase', 'state.advance-plan',
'state.record-metric', 'state.update-progress', 'state.add-decision',
'state.add-blocker', 'state.resolve-blocker', 'state.record-session',
'state.planned-phase', 'state planned-phase',
'state.signal-waiting', 'state signal-waiting',
'state.signal-resume', 'state signal-resume',
'state.sync', 'state sync',
'state.prune', 'state prune',
'state.milestone-switch', 'state milestone-switch',
'state.add-roadmap-evolution', 'state add-roadmap-evolution',
...STATE_MUTATION_COMMANDS,
'frontmatter.set', 'frontmatter.merge', 'frontmatter.validate', 'frontmatter validate',
'config-set', 'config-set-model-profile', 'config-new-project', 'config-ensure-section',
'commit', 'check-commit', 'commit-to-subrepo',
'template.fill', 'template.select', 'template select',
'validate.health', 'validate health',
'validate.context', 'validate context',
'phase.add', 'phase.add-batch', 'phase.insert', 'phase.remove', 'phase.complete',
'phase.scaffold', 'phases.clear', 'phases.archive',
'phase add', 'phase add-batch', 'phase insert', 'phase remove', 'phase complete',
'phase scaffold', 'phases clear', 'phases archive',
'roadmap.update-plan-progress', 'roadmap update-plan-progress',
'roadmap.annotate-dependencies', 'roadmap annotate-dependencies',
...PHASE_MUTATION_COMMANDS,
...PHASES_MUTATION_COMMANDS,
...ROADMAP_MUTATION_COMMANDS,
'requirements.mark-complete', 'requirements mark-complete',
'todo.complete', 'todo complete',
'milestone.complete', 'milestone complete',
@@ -284,22 +283,62 @@ export function createRegistry(
registry.register('config-get', configGet);
registry.register('config-path', configPath);
registry.register('resolve-model', resolveModel);
registry.register('state.load', stateProjectLoad);
registry.register('state.json', stateJson);
registry.register('state.get', stateGet);
const stateHandlers: Record<string, QueryHandler> = {
'state.load': stateProjectLoad,
'state.json': stateJson,
'state.get': stateGet,
'state.update': stateUpdate,
'state.patch': statePatch,
'state.begin-phase': stateBeginPhase,
'state.advance-plan': stateAdvancePlan,
'state.record-metric': stateRecordMetric,
'state.update-progress': stateUpdateProgress,
'state.add-decision': stateAddDecision,
'state.add-blocker': stateAddBlocker,
'state.resolve-blocker': stateResolveBlocker,
'state.record-session': stateRecordSession,
'state.signal-waiting': stateSignalWaiting,
'state.signal-resume': stateSignalResume,
'state.planned-phase': statePlannedPhase,
'state.validate': stateValidate,
'state.sync': stateSync,
'state.prune': statePrune,
'state.milestone-switch': stateMilestoneSwitch,
'state.add-roadmap-evolution': stateAddRoadmapEvolution,
};
for (const entry of STATE_COMMAND_ALIASES) {
const handler = stateHandlers[entry.canonical];
if (!handler) continue;
registry.register(entry.canonical, handler);
for (const alias of entry.aliases) {
registry.register(alias, handler);
}
}
registry.register('state-snapshot', stateSnapshot);
registry.register('find-phase', findPhase);
registry.register('phase-plan-index', phasePlanIndex);
registry.register('phase.list-plans', phaseListPlans);
registry.register('phase list-plans', phaseListPlans);
registry.register('phase.list-artifacts', phaseListArtifacts);
registry.register('phase list-artifacts', phaseListArtifacts);
registry.register('plan.task-structure', planTaskStructure);
registry.register('plan task-structure', planTaskStructure);
registry.register('requirements.extract-from-plans', requirementsExtractFromPlans);
registry.register('requirements extract-from-plans', requirementsExtractFromPlans);
registry.register('roadmap.analyze', roadmapAnalyze);
registry.register('roadmap.get-phase', roadmapGetPhase);
const roadmapHandlers: Record<string, QueryHandler> = {
'roadmap.analyze': roadmapAnalyze,
'roadmap.get-phase': roadmapGetPhase,
'roadmap.update-plan-progress': roadmapUpdatePlanProgress,
'roadmap.annotate-dependencies': roadmapAnnotateDependencies,
};
for (const entry of ROADMAP_COMMAND_ALIASES) {
const handler = roadmapHandlers[entry.canonical];
if (!handler) continue;
registry.register(entry.canonical, handler);
for (const alias of entry.aliases) {
registry.register(alias, handler);
}
}
registry.register('progress', progressJson);
registry.register('progress.json', progressJson);
@@ -309,32 +348,6 @@ export function createRegistry(
registry.register('frontmatter.validate', frontmatterValidate);
registry.register('frontmatter validate', frontmatterValidate);
// State mutation handlers
registry.register('state.update', stateUpdate);
registry.register('state.patch', statePatch);
registry.register('state.begin-phase', stateBeginPhase);
registry.register('state.advance-plan', stateAdvancePlan);
registry.register('state.record-metric', stateRecordMetric);
registry.register('state.update-progress', stateUpdateProgress);
registry.register('state.add-decision', stateAddDecision);
registry.register('state.add-blocker', stateAddBlocker);
registry.register('state.resolve-blocker', stateResolveBlocker);
registry.register('state.record-session', stateRecordSession);
registry.register('state.signal-waiting', stateSignalWaiting);
registry.register('state.signal-resume', stateSignalResume);
registry.register('state.validate', stateValidate);
registry.register('state.sync', stateSync);
registry.register('state.prune', statePrune);
registry.register('state.milestone-switch', stateMilestoneSwitch);
registry.register('state.add-roadmap-evolution', stateAddRoadmapEvolution);
registry.register('state milestone-switch', stateMilestoneSwitch);
registry.register('state add-roadmap-evolution', stateAddRoadmapEvolution);
registry.register('state signal-waiting', stateSignalWaiting);
registry.register('state signal-resume', stateSignalResume);
registry.register('state validate', stateValidate);
registry.register('state sync', stateSync);
registry.register('state prune', statePrune);
// Config mutation handlers
registry.register('config-set', configSet);
registry.register('config-set-model-profile', configSetModelProfile);
@@ -350,19 +363,26 @@ export function createRegistry(
registry.register('template.select', templateSelect);
registry.register('template select', templateSelect);
// Verification handlers
registry.register('verify.plan-structure', verifyPlanStructure);
registry.register('verify plan-structure', verifyPlanStructure);
registry.register('verify.phase-completeness', verifyPhaseCompleteness);
registry.register('verify phase-completeness', verifyPhaseCompleteness);
registry.register('verify.artifacts', verifyArtifacts);
registry.register('verify artifacts', verifyArtifacts);
registry.register('verify.key-links', verifyKeyLinks);
registry.register('verify key-links', verifyKeyLinks);
registry.register('verify.commits', verifyCommits);
registry.register('verify commits', verifyCommits);
registry.register('verify.references', verifyReferences);
registry.register('verify references', verifyReferences);
const verifyHandlers: Record<string, QueryHandler> = {
'verify.plan-structure': verifyPlanStructure,
'verify.phase-completeness': verifyPhaseCompleteness,
'verify.references': verifyReferences,
'verify.commits': verifyCommits,
'verify.artifacts': verifyArtifacts,
'verify.key-links': verifyKeyLinks,
'verify.schema-drift': verifySchemaDrift,
'verify.codebase-drift': verifyCodebaseDrift,
};
for (const entry of VERIFY_COMMAND_ALIASES) {
const handler = verifyHandlers[entry.canonical];
if (!handler) continue;
registry.register(entry.canonical, handler);
for (const alias of entry.aliases) {
registry.register(alias, handler);
}
}
registry.register('verify-summary', verifySummary);
registry.register('verify.summary', verifySummary);
registry.register('verify summary', verifySummary);
@@ -377,14 +397,21 @@ export function createRegistry(
registry.register('check decision-coverage-plan', checkDecisionCoveragePlan);
registry.register('check.decision-coverage-verify', checkDecisionCoverageVerify);
registry.register('check decision-coverage-verify', checkDecisionCoverageVerify);
registry.register('validate.consistency', validateConsistency);
registry.register('validate consistency', validateConsistency);
registry.register('validate.health', validateHealth);
registry.register('validate health', validateHealth);
registry.register('validate.agents', validateAgents);
registry.register('validate agents', validateAgents);
registry.register('validate.context', validateContext);
registry.register('validate context', validateContext);
const validateHandlers: Record<string, QueryHandler> = {
'validate.consistency': validateConsistency,
'validate.health': validateHealth,
'validate.agents': validateAgents,
'validate.context': validateContext,
};
for (const entry of VALIDATE_COMMAND_ALIASES) {
const handler = validateHandlers[entry.canonical];
if (!handler) continue;
registry.register(entry.canonical, handler);
for (const alias of entry.aliases) {
registry.register(alias, handler);
}
}
// Decision routing (SDK-only — no `gsd-tools.cjs` mirror yet; see QUERY-HANDLERS.md)
registry.register('check.config-gates', checkConfigGates);
@@ -406,82 +433,75 @@ export function createRegistry(
registry.register('check.ship-ready', checkShipReady);
registry.register('check ship-ready', checkShipReady);
// Phase lifecycle handlers
registry.register('phase.add', phaseAdd);
registry.register('phase.add-batch', phaseAddBatch);
registry.register('phase.insert', phaseInsert);
registry.register('phase.remove', phaseRemove);
registry.register('phase.complete', phaseComplete);
registry.register('phase.scaffold', phaseScaffold);
registry.register('phases.clear', phasesClear);
registry.register('phases.archive', phasesArchive);
registry.register('phases.list', phasesList);
registry.register('phase.next-decimal', phaseNextDecimal);
// Space-delimited aliases for CJS compatibility
registry.register('phase add', phaseAdd);
registry.register('phase add-batch', phaseAddBatch);
registry.register('phase insert', phaseInsert);
registry.register('phase remove', phaseRemove);
registry.register('phase complete', phaseComplete);
registry.register('phase scaffold', phaseScaffold);
registry.register('phases clear', phasesClear);
registry.register('phases archive', phasesArchive);
registry.register('phases list', phasesList);
registry.register('phase next-decimal', phaseNextDecimal);
const phaseHandlers: Record<string, QueryHandler> = {
'phase.list-plans': phaseListPlans,
'phase.list-artifacts': phaseListArtifacts,
'phase.add': phaseAdd,
'phase.add-batch': phaseAddBatch,
'phase.insert': phaseInsert,
'phase.remove': phaseRemove,
'phase.complete': phaseComplete,
'phase.scaffold': phaseScaffold,
'phase.next-decimal': phaseNextDecimal,
};
// Init composition handlers
registry.register('init.execute-phase', initExecutePhase);
registry.register('init.plan-phase', initPlanPhase);
registry.register('init.new-milestone', initNewMilestone);
registry.register('init.quick', initQuick);
registry.register('init.resume', initResume);
registry.register('init.verify-work', initVerifyWork);
registry.register('init.phase-op', initPhaseOp);
registry.register('init.todos', initTodos);
registry.register('init.milestone-op', initMilestoneOp);
registry.register('init.map-codebase', initMapCodebase);
registry.register('init.new-workspace', initNewWorkspace);
registry.register('init.list-workspaces', initListWorkspaces);
registry.register('init.remove-workspace', initRemoveWorkspace);
registry.register('init.ingest-docs', initIngestDocs);
// Space-delimited aliases for CJS compatibility
registry.register('init execute-phase', initExecutePhase);
registry.register('init plan-phase', initPlanPhase);
registry.register('init new-milestone', initNewMilestone);
registry.register('init quick', initQuick);
registry.register('init resume', initResume);
registry.register('init verify-work', initVerifyWork);
registry.register('init phase-op', initPhaseOp);
registry.register('init todos', initTodos);
registry.register('init milestone-op', initMilestoneOp);
registry.register('init map-codebase', initMapCodebase);
registry.register('init new-workspace', initNewWorkspace);
registry.register('init list-workspaces', initListWorkspaces);
registry.register('init remove-workspace', initRemoveWorkspace);
registry.register('init ingest-docs', initIngestDocs);
for (const entry of PHASE_COMMAND_ALIASES) {
const handler = phaseHandlers[entry.canonical];
if (!handler) continue;
registry.register(entry.canonical, handler);
for (const alias of entry.aliases) {
registry.register(alias, handler);
}
}
// Complex init handlers
registry.register('init.new-project', initNewProject);
registry.register('init.progress', initProgress);
registry.register('init.manager', initManager);
registry.register('init new-project', initNewProject);
registry.register('init progress', initProgress);
registry.register('init manager', initManager);
const phasesHandlers: Record<string, QueryHandler> = {
'phases.list': phasesList,
'phases.clear': phasesClear,
'phases.archive': phasesArchive,
};
for (const entry of PHASES_COMMAND_ALIASES) {
const handler = phasesHandlers[entry.canonical];
if (!handler) continue;
registry.register(entry.canonical, handler);
for (const alias of entry.aliases) {
registry.register(alias, handler);
}
}
const initHandlers: Record<string, QueryHandler> = {
'init.execute-phase': initExecutePhase,
'init.plan-phase': initPlanPhase,
'init.new-project': initNewProject,
'init.new-milestone': initNewMilestone,
'init.quick': initQuick,
'init.ingest-docs': initIngestDocs,
'init.resume': initResume,
'init.verify-work': initVerifyWork,
'init.phase-op': initPhaseOp,
'init.todos': initTodos,
'init.milestone-op': initMilestoneOp,
'init.map-codebase': initMapCodebase,
'init.progress': initProgress,
'init.manager': initManager,
'init.new-workspace': initNewWorkspace,
'init.list-workspaces': initListWorkspaces,
'init.remove-workspace': initRemoveWorkspace,
};
for (const entry of INIT_COMMAND_ALIASES) {
const handler = initHandlers[entry.canonical];
if (!handler) continue;
registry.register(entry.canonical, handler);
for (const alias of entry.aliases) {
registry.register(alias, handler);
}
}
// Domain-specific handlers (fully implemented)
registry.register('agent-skills', agentSkills);
registry.register('roadmap.update-plan-progress', roadmapUpdatePlanProgress);
registry.register('roadmap update-plan-progress', roadmapUpdatePlanProgress);
registry.register('roadmap.annotate-dependencies', roadmapAnnotateDependencies);
registry.register('roadmap annotate-dependencies', roadmapAnnotateDependencies);
registry.register('requirements.mark-complete', requirementsMarkComplete);
registry.register('requirements mark-complete', requirementsMarkComplete);
registry.register('state.planned-phase', statePlannedPhase);
registry.register('state planned-phase', statePlannedPhase);
registry.register('verify.schema-drift', verifySchemaDrift);
registry.register('verify schema-drift', verifySchemaDrift);
registry.register('verify.codebase-drift', verifyCodebaseDrift);
registry.register('verify codebase-drift', verifyCodebaseDrift);
registry.register('todo.match-phase', todoMatchPhase);
registry.register('todo match-phase', todoMatchPhase);
registry.register('list-todos', listTodos);

View File

@@ -7,15 +7,28 @@ describe('normalizeQueryCommand', () => {
expect(normalizeQueryCommand('state', ['validate'])).toEqual(['state.validate', []]);
});
it('merges verify known subcommands only', () => {
expect(normalizeQueryCommand('verify', ['plan-structure', 'x.md'])).toEqual(['verify.plan-structure', ['x.md']]);
expect(normalizeQueryCommand('verify', ['unknown-op'])).toEqual(['verify', ['unknown-op']]);
});
it('maps bare state to state.load', () => {
expect(normalizeQueryCommand('state', [])).toEqual(['state.load', []]);
});
it('does not merge unknown state subcommands', () => {
expect(normalizeQueryCommand('state', ['not-a-subcommand'])).toEqual(['state', ['not-a-subcommand']]);
});
it('merges init workflows', () => {
expect(normalizeQueryCommand('init', ['execute-phase', '9'])).toEqual(['init.execute-phase', ['9']]);
expect(normalizeQueryCommand('init', ['new-project'])).toEqual(['init.new-project', []]);
});
it('does not merge unknown init subcommands', () => {
expect(normalizeQueryCommand('init', ['made-up-init-op', 'x'])).toEqual(['init', ['made-up-init-op', 'x']]);
});
it('maps scaffold to phase.scaffold', () => {
expect(normalizeQueryCommand('scaffold', ['phase-dir', '--phase', '1'])).toEqual([
'phase.scaffold',
@@ -33,7 +46,7 @@ describe('normalizeQueryCommand', () => {
expect(normalizeQueryCommand('generate-slug', ['Hello'])).toEqual(['generate-slug', ['Hello']]);
});
it('merges phase add-batch for future handler', () => {
it('merges check/route helper commands', () => {
expect(normalizeQueryCommand('check', ['config-gates', 'plan-phase'])).toEqual([
'check.config-gates',
['plan-phase'],
@@ -41,10 +54,49 @@ describe('normalizeQueryCommand', () => {
expect(normalizeQueryCommand('check', ['phase-ready', '3'])).toEqual(['check.phase-ready', ['3']]);
expect(normalizeQueryCommand('check', ['auto-mode'])).toEqual(['check.auto-mode', []]);
expect(normalizeQueryCommand('route', ['next-action'])).toEqual(['route.next-action', []]);
});
it('merges known phase subcommands and preserves unknown ones', () => {
expect(normalizeQueryCommand('phase', ['add-batch', '--descriptions', '[]'])).toEqual([
'phase.add-batch',
['--descriptions', '[]'],
]);
expect(normalizeQueryCommand('phase', ['made-up-phase-op', 'x'])).toEqual([
'phase',
['made-up-phase-op', 'x'],
]);
});
it('merges known phases subcommands and preserves unknown ones', () => {
expect(normalizeQueryCommand('phases', ['clear', 'v1.0'])).toEqual([
'phases.clear',
['v1.0'],
]);
expect(normalizeQueryCommand('phases', ['made-up-phases-op', 'x'])).toEqual([
'phases',
['made-up-phases-op', 'x'],
]);
});
it('merges known validate subcommands and preserves unknown ones', () => {
expect(normalizeQueryCommand('validate', ['consistency'])).toEqual([
'validate.consistency',
[],
]);
expect(normalizeQueryCommand('validate', ['made-up-validate-op', 'x'])).toEqual([
'validate',
['made-up-validate-op', 'x'],
]);
});
it('merges known roadmap subcommands and preserves unknown ones', () => {
expect(normalizeQueryCommand('roadmap', ['analyze'])).toEqual([
'roadmap.analyze',
[],
]);
expect(normalizeQueryCommand('roadmap', ['made-up-roadmap-op', 'x'])).toEqual([
'roadmap',
['made-up-roadmap-op', 'x'],
]);
});
});

View File

@@ -7,16 +7,23 @@
* under `runCommand()` so two-token (and longer) invocations resolve to dotted registry names.
*/
import {
STATE_SUBCOMMANDS,
VERIFY_SUBCOMMANDS,
INIT_SUBCOMMANDS,
PHASE_SUBCOMMANDS,
PHASES_SUBCOMMANDS,
VALIDATE_SUBCOMMANDS,
ROADMAP_SUBCOMMANDS,
} from './command-aliases.generated.js';
const MERGE_FIRST_WITH_SUBCOMMAND = new Set<string>([
'state',
'template',
'frontmatter',
'verify',
'phase',
'phases',
'roadmap',
'requirements',
'validate',
'init',
'workstream',
'intel',
@@ -43,6 +50,62 @@ export function normalizeQueryCommand(command: string, args: string[]): [string,
return ['state.load', []];
}
if (command === 'state' && args.length > 0) {
const sub = args[0];
if (STATE_SUBCOMMANDS.has(sub)) {
return [`state.${sub}`, args.slice(1)];
}
return [command, args];
}
if (command === 'verify' && args.length > 0) {
const sub = args[0];
if (VERIFY_SUBCOMMANDS.has(sub)) {
return [`verify.${sub}`, args.slice(1)];
}
return [command, args];
}
if (command === 'init' && args.length > 0) {
const sub = args[0];
if (INIT_SUBCOMMANDS.has(sub)) {
return [`init.${sub}`, args.slice(1)];
}
return [command, args];
}
if (command === 'phase' && args.length > 0) {
const sub = args[0];
if (PHASE_SUBCOMMANDS.has(sub)) {
return [`phase.${sub}`, args.slice(1)];
}
return [command, args];
}
if (command === 'phases' && args.length > 0) {
const sub = args[0];
if (PHASES_SUBCOMMANDS.has(sub)) {
return [`phases.${sub}`, args.slice(1)];
}
return [command, args];
}
if (command === 'validate' && args.length > 0) {
const sub = args[0];
if (VALIDATE_SUBCOMMANDS.has(sub)) {
return [`validate.${sub}`, args.slice(1)];
}
return [command, args];
}
if (command === 'roadmap' && args.length > 0) {
const sub = args[0];
if (ROADMAP_SUBCOMMANDS.has(sub)) {
return [`roadmap.${sub}`, args.slice(1)];
}
return [command, args];
}
if (MERGE_FIRST_WITH_SUBCOMMAND.has(command) && args.length > 0) {
const sub = args[0];
return [`${command}.${sub}`, args.slice(1)];