Files
msd-core/sdk/src/query/registry-assembly.ts
Tom Boucher 5975f06b6a refactor(query): extract command catalog seam for registry wiring (#3060)
* refactor(sdk): extract gsdtools transport seam with per-command policy

* refactor(query): centralize registry command catalog wiring

* refactor(query): unify command resolution seam across sdk callers

* fix(sdk): address CodeRabbit transport policy and timeout findings

* refactor(query): extract mutation event mapper seam

* refactor(query): converge mutation and transport policy data

* refactor(query): share fallback orchestration across cli and sdk

* refactor(query): split static registry catalog by domain clusters

* refactor(query): extract mutation event emission decorator seam

* refactor(query): extract alias-family handler catalog module

* refactor(query): extract cjs fallback execution adapter

* refactor(query): deepen command semantics seam

* refactor(query): extract deep dispatch seam

* refactor(query): deepen cjs fallback execution seam

* refactor(query): merge routing plan into dispatch seam

* fix(query): address CodeRabbit review findings on PR #3060

Critical: prevent double-execution race by checking timeout errors
before subprocess fallback (gsd-transport.ts).

Major: fix execRaw() to respect transport policy outputMode instead
of hardcoding 'raw' (gsd-tools.ts).

Major: add explicit 30s timeout to subprocess fallback execution
(query-fallback-executor.ts).

Major: remove raw args from stderr banner to prevent secret leakage
(query-fallback-executor.ts).

Minor: ensure native text output has trailing newline for CLI parity
(query-dispatch.ts).

Update gsd-tools.test.ts to match new execRaw() behavior.

* fix(tests): update CLI integration tests for catalog-based registration

The refactoring moved handler registration from inline registry.register()
calls to catalog-based registration (registerStaticCatalog/registerAliasCatalog).

- gsd-sdk-query-registry-integration.test.cjs: collectRegisteredNames() now
  also scans catalog files for handler names registered via the new system.
- bug-2492-context-coverage-gate.test.cjs: checks for catalog-based
  registration (DECISION_ROUTING_STATIC_CATALOG) instead of inline strings.
- bug-2524-sdk-query-ws-flag.test.cjs: checks for dispatchNative callback
  pattern instead of direct registry.dispatch() call.

* fix(query): address remaining CodeRabbit review findings

- query-command-semantics.ts: guard stats/progress rewrite so option
  tokens (e.g. --pick) are not turned into subcommands, preserving the
  top-level handler dispatch.

- query-dispatch.ts: formatOutput now skips --pick for text-format
  responses (matching CJS fallback behavior) and surfaces a proper error
  when extractField returns undefined instead of silently producing
  'undefined'.

- query-dispatch.ts: fix backwards error message — 'registered' is the
  restrictive policy that disables fallback, not enables it.

- tests/bug-2492-context-coverage-gate.test.cjs: check
  VERIFY_DECISION_STATIC_CATALOG (the correct catalog for plan-gate
  handlers) instead of DECISION_ROUTING_STATIC_CATALOG.

- tests/gsd-sdk-query-registry-integration.test.cjs: resolve catalog
  variable before loading entries so the drift guard checks each
  referenced catalog individually.

* refactor(query): deepen registry assembly module with strict invariants

- extract registry assembly into dedicated module
- split build vs mutation decoration internals
- add strict assembly invariants:
  1) no duplicate keys
  2) alias canonicals must have handlers
  3) mutation commands must be registered
  4) raw-output policy commands must be registered
- slim query index to thin re-export seam
- add focused registry assembly tests
- update drift-guard tests to target new seam

* test(query): add thin-seam coverage for query index re-exports

* fix(query): return structured native dispatch errors + tighten decisions.parse guard

- runQueryDispatch native path now catches adapter errors and returns
  QueryDispatchResult.error instead of throwing.
- preserve legacy CLI exit contract by using code=1 for native dispatch
  failures.
- strengthen bug-2492 guard: decisions.parse assertion now checks
  VERIFY_DECISION_STATIC_CATALOG OR explicit command token.
2026-05-03 13:57:32 -04:00

121 lines
5.3 KiB
TypeScript

import { QueryRegistry } from './registry.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';
import { GSDEventStream } from '../event-stream.js';
import type { QueryHandler } from './utils.js';
import { registerAliasCatalog, registerStaticCatalog } from './command-catalog.js';
import {
FOUNDATION_STATIC_CATALOG,
STATE_SUPPORT_STATIC_CATALOG,
MUTATION_SURFACES_STATIC_CATALOG,
VERIFY_DECISION_STATIC_CATALOG,
DECISION_ROUTING_STATIC_CATALOG,
} from './command-static-catalog-foundation.js';
import { DOMAIN_STATIC_CATALOG } from './command-static-catalog-domain.js';
import { QUERY_MUTATION_COMMAND_LIST, TRANSPORT_RAW_COMMANDS } from './policy-convergence.js';
import { decorateMutationsWithEvents } from './mutation-event-decorator.js';
import { FAMILY_HANDLERS } from './command-family-handlers.js';
import {
assertAliasCanonicalsHaveHandlers,
assertMutationCommandsRegistered,
assertNoDuplicateRegisteredCommands,
assertRawOutputPolicyCommandsRegistered,
type RegistryAssemblyAliasGroup,
type RegistryAssemblyStaticGroup,
} from './registry-assembly-invariants.js';
/**
* Command names that perform durable writes (disk, git, or global profile store).
*/
export const QUERY_MUTATION_COMMANDS = new Set<string>(QUERY_MUTATION_COMMAND_LIST);
const STATIC_CATALOG_GROUPS: readonly RegistryAssemblyStaticGroup[] = [
{ name: 'FOUNDATION_STATIC_CATALOG', entries: FOUNDATION_STATIC_CATALOG },
{ name: 'STATE_SUPPORT_STATIC_CATALOG', entries: STATE_SUPPORT_STATIC_CATALOG },
{ name: 'MUTATION_SURFACES_STATIC_CATALOG', entries: MUTATION_SURFACES_STATIC_CATALOG },
{ name: 'VERIFY_DECISION_STATIC_CATALOG', entries: VERIFY_DECISION_STATIC_CATALOG },
{ name: 'DECISION_ROUTING_STATIC_CATALOG', entries: DECISION_ROUTING_STATIC_CATALOG },
{ name: 'DOMAIN_STATIC_CATALOG', entries: DOMAIN_STATIC_CATALOG },
] as const;
const ALIAS_GROUPS: readonly RegistryAssemblyAliasGroup[] = [
{ family: 'state', aliases: STATE_COMMAND_ALIASES, handlers: FAMILY_HANDLERS.state as Record<string, QueryHandler> },
{ family: 'roadmap', aliases: ROADMAP_COMMAND_ALIASES, handlers: FAMILY_HANDLERS.roadmap as Record<string, QueryHandler> },
{ family: 'verify', aliases: VERIFY_COMMAND_ALIASES, handlers: FAMILY_HANDLERS.verify as Record<string, QueryHandler> },
{ family: 'validate', aliases: VALIDATE_COMMAND_ALIASES, handlers: FAMILY_HANDLERS.validate as Record<string, QueryHandler> },
{ family: 'phase', aliases: PHASE_COMMAND_ALIASES, handlers: FAMILY_HANDLERS.phase as Record<string, QueryHandler> },
{ family: 'phases', aliases: PHASES_COMMAND_ALIASES, handlers: FAMILY_HANDLERS.phases as Record<string, QueryHandler> },
{ family: 'init', aliases: INIT_COMMAND_ALIASES, handlers: FAMILY_HANDLERS.init as Record<string, QueryHandler> },
] as const;
export function buildRegistry(): QueryRegistry {
assertAliasCanonicalsHaveHandlers({
staticGroups: STATIC_CATALOG_GROUPS,
aliasGroups: ALIAS_GROUPS,
mutationCommands: QUERY_MUTATION_COMMANDS,
rawOutputPolicyCommands: TRANSPORT_RAW_COMMANDS,
});
assertNoDuplicateRegisteredCommands({
staticGroups: STATIC_CATALOG_GROUPS,
aliasGroups: ALIAS_GROUPS,
mutationCommands: QUERY_MUTATION_COMMANDS,
rawOutputPolicyCommands: TRANSPORT_RAW_COMMANDS,
});
const registry = new QueryRegistry();
registerStaticCatalog(registry, FOUNDATION_STATIC_CATALOG);
registerAliasCatalog(registry, STATE_COMMAND_ALIASES, FAMILY_HANDLERS.state as Record<string, QueryHandler>);
registerStaticCatalog(registry, STATE_SUPPORT_STATIC_CATALOG);
registerAliasCatalog(registry, ROADMAP_COMMAND_ALIASES, FAMILY_HANDLERS.roadmap as Record<string, QueryHandler>);
registerStaticCatalog(registry, MUTATION_SURFACES_STATIC_CATALOG);
registerAliasCatalog(registry, VERIFY_COMMAND_ALIASES, FAMILY_HANDLERS.verify as Record<string, QueryHandler>);
registerStaticCatalog(registry, VERIFY_DECISION_STATIC_CATALOG);
registerAliasCatalog(registry, VALIDATE_COMMAND_ALIASES, FAMILY_HANDLERS.validate as Record<string, QueryHandler>);
registerStaticCatalog(registry, DECISION_ROUTING_STATIC_CATALOG);
registerAliasCatalog(registry, PHASE_COMMAND_ALIASES, FAMILY_HANDLERS.phase as Record<string, QueryHandler>);
registerAliasCatalog(registry, PHASES_COMMAND_ALIASES, FAMILY_HANDLERS.phases as Record<string, QueryHandler>);
registerAliasCatalog(registry, INIT_COMMAND_ALIASES, FAMILY_HANDLERS.init as Record<string, QueryHandler>);
registerStaticCatalog(registry, DOMAIN_STATIC_CATALOG);
assertMutationCommandsRegistered(registry, QUERY_MUTATION_COMMANDS);
assertRawOutputPolicyCommandsRegistered(registry, TRANSPORT_RAW_COMMANDS);
return registry;
}
export function decorateRegistryMutations(
registry: QueryRegistry,
eventStream?: GSDEventStream,
correlationSessionId?: string,
): void {
if (!eventStream) return;
const mutationSessionId = correlationSessionId ?? '';
decorateMutationsWithEvents(registry, QUERY_MUTATION_COMMANDS, eventStream, mutationSessionId);
}
export function createRegistry(
eventStream?: GSDEventStream,
correlationSessionId?: string,
): QueryRegistry {
const registry = buildRegistry();
decorateRegistryMutations(registry, eventStream, correlationSessionId);
return registry;
}