* 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.
215 lines
6.9 KiB
TypeScript
215 lines
6.9 KiB
TypeScript
import {
|
|
STATE_SUBCOMMANDS,
|
|
VERIFY_SUBCOMMANDS,
|
|
INIT_SUBCOMMANDS,
|
|
PHASE_SUBCOMMANDS,
|
|
PHASES_SUBCOMMANDS,
|
|
VALIDATE_SUBCOMMANDS,
|
|
ROADMAP_SUBCOMMANDS,
|
|
STATE_MUTATION_COMMANDS,
|
|
PHASE_MUTATION_COMMANDS,
|
|
PHASES_MUTATION_COMMANDS,
|
|
ROADMAP_MUTATION_COMMANDS,
|
|
} from './command-aliases.generated.js';
|
|
|
|
export interface QueryCommandRegistryLike {
|
|
has(command: string): boolean;
|
|
}
|
|
|
|
export type QueryMatchMode = 'dotted' | 'spaced';
|
|
export type QueryResolutionSource = 'normalized' | 'expanded';
|
|
|
|
export interface QueryCommandResolution {
|
|
cmd: string;
|
|
args: string[];
|
|
matchedBy: QueryMatchMode;
|
|
expanded: boolean;
|
|
source: QueryResolutionSource;
|
|
}
|
|
|
|
export interface QueryCommandNoMatch {
|
|
normalized: { command: string; args: string[]; tokens: string[] };
|
|
attempted: { dotted: string[]; spaced: string[]; expandedTokens: string[] | null };
|
|
}
|
|
|
|
const MERGE_FIRST_WITH_SUBCOMMAND = new Set<string>([
|
|
'state',
|
|
'template',
|
|
'frontmatter',
|
|
'verify',
|
|
'phase',
|
|
'requirements',
|
|
'init',
|
|
'workstream',
|
|
'intel',
|
|
'learnings',
|
|
'uat',
|
|
'todo',
|
|
'milestone',
|
|
'check',
|
|
'detect',
|
|
'route',
|
|
]);
|
|
|
|
export const QUERY_MUTATION_COMMAND_LIST: readonly string[] = [
|
|
...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',
|
|
...PHASE_MUTATION_COMMANDS,
|
|
...PHASES_MUTATION_COMMANDS,
|
|
...ROADMAP_MUTATION_COMMANDS,
|
|
'requirements.mark-complete', 'requirements mark-complete',
|
|
'todo.complete', 'todo complete',
|
|
'milestone.complete', 'milestone complete',
|
|
'workstream.create', 'workstream.set', 'workstream.complete', 'workstream.progress',
|
|
'workstream create', 'workstream set', 'workstream complete', 'workstream progress',
|
|
'docs-init',
|
|
'learnings.copy', 'learnings copy',
|
|
'learnings.prune', 'learnings prune',
|
|
'learnings.delete', 'learnings delete',
|
|
'intel.snapshot', 'intel.patch-meta', 'intel snapshot', 'intel patch-meta',
|
|
'write-profile', 'generate-claude-profile', 'generate-dev-preferences', 'generate-claude-md',
|
|
] as const;
|
|
|
|
export const TRANSPORT_RAW_COMMANDS: readonly string[] = [
|
|
'commit',
|
|
'config-set',
|
|
'verify-summary',
|
|
'verify.summary',
|
|
'verify summary',
|
|
] as const;
|
|
|
|
const QUERY_MUTATION_COMMAND_SET = new Set(QUERY_MUTATION_COMMAND_LIST);
|
|
|
|
export function isQueryMutationCommand(command: string): boolean {
|
|
return QUERY_MUTATION_COMMAND_SET.has(command);
|
|
}
|
|
|
|
export function normalizeQueryCommand(command: string, args: string[]): [string, string[]] {
|
|
if (command === 'scaffold') return ['phase.scaffold', args];
|
|
if (command === 'state' && args.length === 0) 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) {
|
|
return [`${command}.${args[0]}`, args.slice(1)];
|
|
}
|
|
if ((command === 'progress' || command === 'stats') && args.length > 0 && !args[0].startsWith('-')) {
|
|
return [`${command}.${args[0]}`, args.slice(1)];
|
|
}
|
|
return [command, args];
|
|
}
|
|
|
|
function expandFirstDottedToken(tokens: string[]): string[] {
|
|
if (tokens.length === 0) return tokens;
|
|
const first = tokens[0];
|
|
if (first.startsWith('--') || !first.includes('.')) return tokens;
|
|
return [...first.split('.'), ...tokens.slice(1)];
|
|
}
|
|
|
|
function matchRegisteredPrefix(
|
|
tokens: string[],
|
|
registry: QueryCommandRegistryLike,
|
|
track?: { dotted: string[]; spaced: string[] },
|
|
): { cmd: string; args: string[]; matchedBy: QueryMatchMode } | null {
|
|
for (let i = tokens.length; i >= 1; i--) {
|
|
const head = tokens.slice(0, i);
|
|
const dotted = head.join('.');
|
|
const spaced = head.join(' ');
|
|
track?.dotted.push(dotted);
|
|
track?.spaced.push(spaced);
|
|
if (registry.has(dotted)) return { cmd: dotted, args: tokens.slice(i), matchedBy: 'dotted' };
|
|
if (registry.has(spaced)) return { cmd: spaced, args: tokens.slice(i), matchedBy: 'spaced' };
|
|
}
|
|
return null;
|
|
}
|
|
|
|
export function resolveQueryTokens(
|
|
tokens: string[],
|
|
registry: QueryCommandRegistryLike,
|
|
): QueryCommandResolution | null {
|
|
const direct = matchRegisteredPrefix(tokens, registry);
|
|
if (direct) return { ...direct, expanded: false, source: 'normalized' };
|
|
|
|
const expanded = expandFirstDottedToken(tokens);
|
|
if (expanded !== tokens) {
|
|
const afterExpand = matchRegisteredPrefix(expanded, registry);
|
|
if (afterExpand) return { ...afterExpand, expanded: true, source: 'expanded' };
|
|
}
|
|
return null;
|
|
}
|
|
|
|
export function resolveQueryCommand(
|
|
command: string,
|
|
args: string[],
|
|
registry: QueryCommandRegistryLike,
|
|
): QueryCommandResolution | null {
|
|
const [normCmd, normArgs] = normalizeQueryCommand(command, args);
|
|
return resolveQueryTokens([normCmd, ...normArgs], registry);
|
|
}
|
|
|
|
export function explainQueryCommandNoMatch(
|
|
command: string,
|
|
args: string[],
|
|
registry: QueryCommandRegistryLike,
|
|
): QueryCommandNoMatch {
|
|
const [normalizedCommand, normalizedArgs] = normalizeQueryCommand(command, args);
|
|
const normalizedTokens = [normalizedCommand, ...normalizedArgs];
|
|
const attempted = { dotted: [] as string[], spaced: [] as string[] };
|
|
matchRegisteredPrefix(normalizedTokens, registry, attempted);
|
|
|
|
const expandedTokens = expandFirstDottedToken(normalizedTokens);
|
|
if (expandedTokens !== normalizedTokens) {
|
|
matchRegisteredPrefix(expandedTokens, registry, attempted);
|
|
}
|
|
|
|
return {
|
|
normalized: {
|
|
command: normalizedCommand,
|
|
args: normalizedArgs,
|
|
tokens: normalizedTokens,
|
|
},
|
|
attempted: {
|
|
dotted: attempted.dotted,
|
|
spaced: attempted.spaced,
|
|
expandedTokens: expandedTokens !== normalizedTokens ? expandedTokens : null,
|
|
},
|
|
};
|
|
}
|