From 9c92c32f6ec73a57ace2d9df74c18ca86f308c34 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Sun, 3 May 2026 16:31:48 -0400 Subject: [PATCH] refactor(query): deepen runtime context/native adapter/output seams (#3076) * refactor(query): deepen runtime context, native adapter, and cli output seams * chore(changeset): add fragment for query seam deepening continuation * refactor(query): converge internal command-resolution imports on canonical seam * refactor(query): remove dead seam wrappers and converge on canonical modules * docs(architecture): update context and adr for query seam completion * fix(query): preserve gsd-tools stderr in cli output and clarify static ws test scope * test(query): cover whitespace stderr and null exitCode fallback --- .changeset/bright-pumas-fold.md | 6 +++ CONTEXT.md | 12 +++++ docs/adr/0001-dispatch-policy-module.md | 20 +++++++ sdk/src/gsd-tools.ts | 28 +--------- sdk/src/gsd-transport-policy.ts | 2 +- sdk/src/query/QUERY-HANDLERS.md | 2 +- sdk/src/query/command-resolution.test.ts | 2 +- sdk/src/query/command-resolution.ts | 10 ---- sdk/src/query/index.ts | 2 +- sdk/src/query/normalize-query-command.test.ts | 2 +- sdk/src/query/normalize-query-command.ts | 1 - sdk/src/query/policy-convergence.test.ts | 2 +- sdk/src/query/policy-convergence.ts | 6 --- sdk/src/query/query-cli-adapter.test.ts | 4 +- sdk/src/query/query-cli-adapter.ts | 53 ++++--------------- sdk/src/query/query-cli-output.test.ts | 33 ++++++++++++ sdk/src/query/query-cli-output.ts | 35 ++++++++++++ sdk/src/query/query-dispatch-plan.ts | 7 ++- sdk/src/query/query-dispatch.ts | 15 +++++- .../query/query-native-dispatch-adapter.ts | 16 ++++++ sdk/src/query/query-policy-snapshot.test.ts | 2 +- sdk/src/query/query-policy-snapshot.ts | 6 --- .../query/query-registry-capability.test.ts | 2 +- sdk/src/query/query-registry-capability.ts | 4 -- sdk/src/query/query-runtime-context.ts | 29 ++++++++++ sdk/src/query/registry-assembly.ts | 2 +- sdk/src/query/registry.ts | 2 +- tests/bug-2524-sdk-query-ws-flag.test.cjs | 6 ++- 28 files changed, 196 insertions(+), 115 deletions(-) create mode 100644 .changeset/bright-pumas-fold.md delete mode 100644 sdk/src/query/command-resolution.ts delete mode 100644 sdk/src/query/normalize-query-command.ts delete mode 100644 sdk/src/query/policy-convergence.ts create mode 100644 sdk/src/query/query-cli-output.test.ts create mode 100644 sdk/src/query/query-cli-output.ts create mode 100644 sdk/src/query/query-native-dispatch-adapter.ts delete mode 100644 sdk/src/query/query-policy-snapshot.ts delete mode 100644 sdk/src/query/query-registry-capability.ts create mode 100644 sdk/src/query/query-runtime-context.ts diff --git a/.changeset/bright-pumas-fold.md b/.changeset/bright-pumas-fold.md new file mode 100644 index 000000000..d9ca770f0 --- /dev/null +++ b/.changeset/bright-pumas-fold.md @@ -0,0 +1,6 @@ +--- +type: Changed +pr: 3075 +--- + +**query architecture deepening pass** — extracted Query Runtime Context, Native Dispatch Adapter, and Query CLI Output Modules so dispatch policy, runtime context policy, and CLI projection logic each live behind focused seams with higher locality and leverage. \ No newline at end of file diff --git a/CONTEXT.md b/CONTEXT.md index bba6c800f..70597e151 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -15,3 +15,15 @@ Canonical error kind set: ### Command Definition Module Canonical command metadata Interface powering alias, catalog, and semantics generation. + +### Query Runtime Context Module +Module owning query-time context resolution for `projectDir` and `ws`, including precedence and validation policy used by query adapters. + +### Native Dispatch Adapter Module +Adapter Module that satisfies native query dispatch at the Dispatch Policy seam, so policy modules consume a focused dispatch Interface instead of closure-wired call sites. + +### Query CLI Output Module +Module owning projection from dispatch results/errors to CLI `{ exitCode, stdoutChunks, stderrLines }` output contract. + +### Query Command Resolution Module +Canonical command normalization and resolution Interface (`query-command-resolution-strategy`) used by internal query/transport paths after dead-wrapper convergence. diff --git a/docs/adr/0001-dispatch-policy-module.md b/docs/adr/0001-dispatch-policy-module.md index 93cca43a6..793c2f8d4 100644 --- a/docs/adr/0001-dispatch-policy-module.md +++ b/docs/adr/0001-dispatch-policy-module.md @@ -1,3 +1,23 @@ # Dispatch policy module as single seam for query execution outcomes We decided to centralize query dispatch outcomes in one Dispatch Policy Module that returns a structured union result (`ok` success or failure with typed `kind`, `details`, and final `exit_code`) instead of mixing throws and ad-hoc error mapping across CLI and SDK paths. This keeps fallback policy, timeout classification, and exit mapping in one place for better locality, prevents drift between native and fallback behavior, and makes callers thin adapters over a stable interface. + +## Amendment (2026-05-03): query seam deepening completion + +To complete the query architecture pass, we deepened adjacent seams around the Dispatch Policy Module: + +- Extracted **Query Runtime Context Module** to own `projectDir` + `ws` resolution policy. +- Extracted **Native Dispatch Adapter Module** so Dispatch Policy consumes a stable native dispatch Interface (not closure-wired call sites). +- Extracted **Query CLI Output Module** to own projection from dispatch results/errors to CLI output contract. +- Converged internal command-resolution and policy imports onto canonical modules and removed dead wrapper modules. + +### Dead-wrapper convergence + +Removed wrapper Modules after call-site convergence: +- `normalize-query-command.ts` +- `command-resolution.ts` +- `policy-convergence.ts` +- `query-policy-snapshot.ts` +- `query-registry-capability.ts` + +This amendment preserves the original ADR direction: keep policy depth high, adapters thin, and locality concentrated in explicit modules. diff --git a/sdk/src/gsd-tools.ts b/sdk/src/gsd-tools.ts index e362acdc9..b6880776f 100644 --- a/sdk/src/gsd-tools.ts +++ b/sdk/src/gsd-tools.ts @@ -20,7 +20,7 @@ import type { InitNewProjectInfo, PhaseOpInfo, PhasePlanIndex, RoadmapAnalysis } import type { GSDEventStream } from './event-stream.js'; import { GSDError, exitCodeFor } from './errors.js'; import { createRegistry } from './query/index.js'; -import { resolveQueryCommand, type QueryCommandResolution } from './query/command-resolution.js'; +import { resolveQueryCommand, type QueryCommandResolution } from './query/query-command-resolution-strategy.js'; import { formatStateLoadRawStdout } from './query/state-project-load.js'; import type { QueryResult } from './query/utils.js'; import { GSDTransport } from './gsd-transport.js'; @@ -556,32 +556,6 @@ export class GSDTools { } } -/** - * Run `gsd-sdk query` semantics in-process: normalize argv, resolve registry, dispatch. - * Returns handler JSON payload (same as stdout from the `gsd-sdk query` CLI without `--pick`). - */ -export async function runGsdToolsQuery(projectDir: string, queryArgv: string[]): Promise { - const { createRegistry } = await import('./query/index.js'); - const { normalizeQueryCommand } = await import('./query/normalize-query-command.js'); - const { resolveQueryCommand } = await import('./query/command-resolution.js'); - const { GSDError, ErrorClassification } = await import('./errors.js'); - - if (queryArgv.length === 0 || !queryArgv[0]) { - throw new GSDError('runGsdToolsQuery requires a command', ErrorClassification.Validation); - } - const registry = createRegistry(); - const [normCmd, normArgs] = normalizeQueryCommand(queryArgv[0], queryArgv.slice(1)); - const matched = resolveQueryCommand(queryArgv[0], queryArgv.slice(1), registry); - if (!matched) { - throw new GSDError( - `Unknown command: "${[normCmd, ...normArgs].join(' ')}". No native handler registered.`, - ErrorClassification.Validation, - ); - } - const result = await registry.dispatch(matched.cmd, matched.args, projectDir); - return result.data; -} - // ─── Path resolution ──────────────────────────────────────────────────────── /** diff --git a/sdk/src/gsd-transport-policy.ts b/sdk/src/gsd-transport-policy.ts index 3db7693a2..3cc6bff93 100644 --- a/sdk/src/gsd-transport-policy.ts +++ b/sdk/src/gsd-transport-policy.ts @@ -1,4 +1,4 @@ -import { TRANSPORT_RAW_COMMANDS } from './query/policy-convergence.js'; +import { TRANSPORT_RAW_COMMANDS } from './query/query-policy-capability.js'; export type TransportMode = 'json' | 'raw'; diff --git a/sdk/src/query/QUERY-HANDLERS.md b/sdk/src/query/QUERY-HANDLERS.md index 4bf224351..474844579 100644 --- a/sdk/src/query/QUERY-HANDLERS.md +++ b/sdk/src/query/QUERY-HANDLERS.md @@ -26,7 +26,7 @@ CJS routing seams mirror these families with thin adapters (`state/verify/init/p ## `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. +1. **`normalizeQueryCommand()`** (`query-command-resolution-strategy.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. 2. **`resolveQueryArgv()`** (`registry.ts`) — **longest-prefix match** on the normalized argv: tries joined keys `a.b.c` then `a b c` for each prefix length, longest first. Example: `state update status X` → handler `state.update` with args `[status, X]`. 3. **Dotted single token**: one token like `init.new-project` matches the registry; if the first pass finds no handler, a single dotted token is split and matching runs again. 4. **CJS fallback (CLI)**: if nothing matches a registered handler and `GSD_QUERY_FALLBACK` is not `off`/`never`/`false`/`0`, the CLI shells out to `gsd-tools.cjs` with argv derived from the normalized tokens (dotted commands are split into CJS-style segments). stderr receives a short bridge warning. Set `GSD_QUERY_FALLBACK=off` for strict mode (parity tests). CLI-only commands such as `graphify` rely on this path until native handlers exist. diff --git a/sdk/src/query/command-resolution.test.ts b/sdk/src/query/command-resolution.test.ts index df64f1f1b..9959c8042 100644 --- a/sdk/src/query/command-resolution.test.ts +++ b/sdk/src/query/command-resolution.test.ts @@ -1,6 +1,6 @@ import { describe, it, expect } from 'vitest'; import { createRegistry } from './index.js'; -import { explainQueryCommandNoMatch, resolveQueryCommand, resolveQueryTokens } from './command-resolution.js'; +import { explainQueryCommandNoMatch, resolveQueryCommand, resolveQueryTokens } from './query-command-resolution-strategy.js'; describe('command resolution', () => { it('resolves normalized tokens with metadata', () => { diff --git a/sdk/src/query/command-resolution.ts b/sdk/src/query/command-resolution.ts deleted file mode 100644 index a847ced8a..000000000 --- a/sdk/src/query/command-resolution.ts +++ /dev/null @@ -1,10 +0,0 @@ -export { - resolveQueryCommand, - resolveQueryTokens, - type QueryCommandRegistryLike, - type QueryCommandResolution, - type QueryMatchMode, - type QueryResolutionSource, - explainQueryCommandNoMatch, - type QueryCommandNoMatch, -} from './query-command-resolution-strategy.js'; diff --git a/sdk/src/query/index.ts b/sdk/src/query/index.ts index e0f09c29c..26e421297 100644 --- a/sdk/src/query/index.ts +++ b/sdk/src/query/index.ts @@ -4,4 +4,4 @@ export { createRegistry, buildRegistry, decorateRegistryMutations, QUERY_MUTATIO export type { QueryResult, QueryHandler } from './utils.js'; export { extractField } from './registry.js'; -export { normalizeQueryCommand } from './normalize-query-command.js'; +export { normalizeQueryCommand } from './query-command-resolution-strategy.js'; diff --git a/sdk/src/query/normalize-query-command.test.ts b/sdk/src/query/normalize-query-command.test.ts index 11da5abf0..685f0bb5a 100644 --- a/sdk/src/query/normalize-query-command.test.ts +++ b/sdk/src/query/normalize-query-command.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from 'vitest'; -import { normalizeQueryCommand } from './normalize-query-command.js'; +import { normalizeQueryCommand } from './query-command-resolution-strategy.js'; describe('normalizeQueryCommand', () => { it('merges nested gsd-tools-style state + subcommand', () => { diff --git a/sdk/src/query/normalize-query-command.ts b/sdk/src/query/normalize-query-command.ts deleted file mode 100644 index b34a8b97b..000000000 --- a/sdk/src/query/normalize-query-command.ts +++ /dev/null @@ -1 +0,0 @@ -export { normalizeQueryCommand } from './query-command-resolution-strategy.js'; diff --git a/sdk/src/query/policy-convergence.test.ts b/sdk/src/query/policy-convergence.test.ts index f937ce355..ed9201858 100644 --- a/sdk/src/query/policy-convergence.test.ts +++ b/sdk/src/query/policy-convergence.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from 'vitest'; -import { QUERY_MUTATION_COMMAND_LIST, TRANSPORT_RAW_COMMANDS, isQueryMutationCommand } from './policy-convergence.js'; +import { QUERY_MUTATION_COMMAND_LIST, TRANSPORT_RAW_COMMANDS, isQueryMutationCommand } from './query-policy-capability.js'; describe('policy convergence', () => { it('contains expected raw transport aliases', () => { diff --git a/sdk/src/query/policy-convergence.ts b/sdk/src/query/policy-convergence.ts deleted file mode 100644 index b7d559218..000000000 --- a/sdk/src/query/policy-convergence.ts +++ /dev/null @@ -1,6 +0,0 @@ -export { - QUERY_POLICY_SNAPSHOT, - QUERY_MUTATION_COMMAND_LIST, - TRANSPORT_RAW_COMMANDS, - isQueryMutationCommand, -} from './query-policy-snapshot.js'; diff --git a/sdk/src/query/query-cli-adapter.test.ts b/sdk/src/query/query-cli-adapter.test.ts index 8e4c5f2c9..54578930c 100644 --- a/sdk/src/query/query-cli-adapter.test.ts +++ b/sdk/src/query/query-cli-adapter.test.ts @@ -41,9 +41,9 @@ describe('query-cli-adapter', () => { expect(out.stderrLines.join('\n')).toContain('requires a command'); }); - it('forwards ws to registry.dispatch via dispatchNative', async () => { + it('forwards ws to registry.dispatch via native adapter', async () => { runQueryDispatchSpy.mockImplementationOnce(async (input: any) => { - await input.dispatchNative('state', ['show']); + await input.nativeAdapter.dispatch('state', ['show']); return { ok: true, exit_code: 0, stdout: '', stderr: [] }; }); diff --git a/sdk/src/query/query-cli-adapter.ts b/sdk/src/query/query-cli-adapter.ts index 5aa78950c..5af82d551 100644 --- a/sdk/src/query/query-cli-adapter.ts +++ b/sdk/src/query/query-cli-adapter.ts @@ -1,9 +1,9 @@ -import { findProjectRoot } from './helpers.js'; import { createRegistry } from './index.js'; import { runQueryDispatch } from './query-dispatch.js'; -import { resolveGsdToolsPath, GSDToolsError } from '../gsd-tools.js'; -import { GSDError, exitCodeFor } from '../errors.js'; -import { validateWorkstreamName } from '../workstream-utils.js'; +import { resolveGsdToolsPath } from '../gsd-tools.js'; +import { resolveQueryRuntimeContext } from './query-runtime-context.js'; +import { createQueryNativeDispatchAdapter } from './query-native-dispatch-adapter.js'; +import { buildQueryCliOutputFromDispatch, buildQueryCliOutputFromError, type QueryCliAdapterOutput } from './query-cli-output.js'; export interface QueryCliAdapterInput { projectDir: string; @@ -11,11 +11,6 @@ export interface QueryCliAdapterInput { queryArgv?: string[]; } -export interface QueryCliAdapterOutput { - exitCode: number; - stdoutChunks: string[]; - stderrLines: string[]; -} function queryFallbackToCjsEnabled(): boolean { const v = process.env.GSD_QUERY_FALLBACK?.toLowerCase(); @@ -23,49 +18,21 @@ function queryFallbackToCjsEnabled(): boolean { return true; } -function resolveQueryWorkstream(ws: string | undefined): string | undefined { - if (ws !== undefined) { - return validateWorkstreamName(ws) ? ws : undefined; - } - const envWs = process.env.GSD_WORKSTREAM; - if (!envWs) return undefined; - return validateWorkstreamName(envWs) ? envWs : undefined; -} - export async function runQueryCliCommand(input: QueryCliAdapterInput): Promise { - const stderrLines: string[] = []; - const stdoutChunks: string[] = []; - const ws = resolveQueryWorkstream(input.ws); - try { - const projectDir = findProjectRoot(input.projectDir); + const runtime = resolveQueryRuntimeContext({ projectDir: input.projectDir, ws: input.ws }); const registry = createRegistry(); const out = await runQueryDispatch({ registry, - projectDir, - ws, + projectDir: runtime.projectDir, + ws: runtime.ws, cjsFallbackEnabled: queryFallbackToCjsEnabled(), resolveGsdToolsPath, - dispatchNative: (cmd, argv) => registry.dispatch(cmd, argv, projectDir, ws), + nativeAdapter: createQueryNativeDispatchAdapter(registry, runtime.projectDir, runtime.ws), }, input.queryArgv ?? []); - stderrLines.push(...out.stderr); - if (!out.ok) { - stderrLines.push(out.error.message); - return { exitCode: out.exit_code, stdoutChunks, stderrLines }; - } - if (out.stdout) stdoutChunks.push(out.stdout); - return { exitCode: 0, stdoutChunks, stderrLines }; + return buildQueryCliOutputFromDispatch(out); } catch (err) { - if (err instanceof GSDError) { - stderrLines.push(`Error: ${err.message}`); - return { exitCode: exitCodeFor(err.classification), stdoutChunks, stderrLines }; - } - if (err instanceof GSDToolsError) { - stderrLines.push(`Error: ${err.message}`); - return { exitCode: err.exitCode ?? 1, stdoutChunks, stderrLines }; - } - stderrLines.push(`Error: ${err instanceof Error ? err.message : String(err)}`); - return { exitCode: 1, stdoutChunks, stderrLines }; + return buildQueryCliOutputFromError(err); } } diff --git a/sdk/src/query/query-cli-output.test.ts b/sdk/src/query/query-cli-output.test.ts new file mode 100644 index 000000000..894c2ac28 --- /dev/null +++ b/sdk/src/query/query-cli-output.test.ts @@ -0,0 +1,33 @@ +import { describe, expect, it } from 'vitest'; +import { GSDToolsError } from '../gsd-tools.js'; +import { buildQueryCliOutputFromError } from './query-cli-output.js'; + +describe('query-cli-output', () => { + it('prefers raw gsd-tools stderr when present', () => { + const err = new GSDToolsError('failed', 'list', ['json'], 2, 'line one\nline two\n'); + const out = buildQueryCliOutputFromError(err); + expect(out.exitCode).toBe(2); + expect(out.stderrLines).toEqual(['line one', 'line two']); + }); + + it('falls back to Error: message when gsd-tools stderr is empty', () => { + const err = new GSDToolsError('failed', 'list', ['json'], null, ''); + const out = buildQueryCliOutputFromError(err); + expect(out.exitCode).toBe(1); + expect(out.stderrLines).toEqual(['Error: failed']); + }); + + it('falls back to Error: message when gsd-tools stderr is whitespace-only', () => { + const err = new GSDToolsError('failed', 'build', ['json'], null, ' \n'); + const out = buildQueryCliOutputFromError(err); + expect(out.exitCode).toBe(1); + expect(out.stderrLines).toEqual(['Error: failed']); + }); + + it('uses exitCode 1 when gsd-tools exitCode is null and stderr is non-empty', () => { + const err = new GSDToolsError('failed', 'build', ['json'], null, 'line'); + const out = buildQueryCliOutputFromError(err); + expect(out.exitCode).toBe(1); + expect(out.stderrLines).toEqual(['line']); + }); +}); diff --git a/sdk/src/query/query-cli-output.ts b/sdk/src/query/query-cli-output.ts new file mode 100644 index 000000000..0a9c22ad6 --- /dev/null +++ b/sdk/src/query/query-cli-output.ts @@ -0,0 +1,35 @@ +import { GSDError, exitCodeFor } from '../errors.js'; +import { GSDToolsError } from '../gsd-tools.js'; +import type { QueryDispatchResult } from './query-dispatch-contract.js'; + +export interface QueryCliAdapterOutput { + exitCode: number; + stdoutChunks: string[]; + stderrLines: string[]; +} + +export function buildQueryCliOutputFromDispatch(out: QueryDispatchResult): QueryCliAdapterOutput { + const stderrLines = [...out.stderr]; + const stdoutChunks: string[] = []; + if (!out.ok) { + stderrLines.push(out.error.message); + return { exitCode: out.exit_code, stdoutChunks, stderrLines }; + } + if (out.stdout) stdoutChunks.push(out.stdout); + return { exitCode: 0, stdoutChunks, stderrLines }; +} + +export function buildQueryCliOutputFromError(err: unknown): QueryCliAdapterOutput { + const stdoutChunks: string[] = []; + if (err instanceof GSDError) { + return { stderrLines: [`Error: ${err.message}`], exitCode: exitCodeFor(err.classification), stdoutChunks }; + } + if (err instanceof GSDToolsError) { + // Prefer raw subprocess stderr when available so users see the original tool diagnostics. + const stderrLines = err.stderr && err.stderr.trim().length > 0 + ? err.stderr.split(/\r?\n/).filter(line => line.length > 0) + : [`Error: ${err.message}`]; + return { stderrLines, exitCode: err.exitCode ?? 1, stdoutChunks }; + } + return { stderrLines: [`Error: ${err instanceof Error ? err.message : String(err)}`], exitCode: 1, stdoutChunks }; +} diff --git a/sdk/src/query/query-dispatch-plan.ts b/sdk/src/query/query-dispatch-plan.ts index cca96ae67..aaa0e8ab4 100644 --- a/sdk/src/query/query-dispatch-plan.ts +++ b/sdk/src/query/query-dispatch-plan.ts @@ -1,6 +1,9 @@ import type { QueryRegistry } from './registry.js'; -import { normalizeQueryCommand } from './normalize-query-command.js'; -import { resolveQueryCommand, type QueryCommandResolution } from './command-resolution.js'; +import { + normalizeQueryCommand, + resolveQueryCommand, + type QueryCommandResolution, +} from './query-command-resolution-strategy.js'; export type DispatchMode = 'native' | 'cjs' | 'error'; diff --git a/sdk/src/query/query-dispatch.ts b/sdk/src/query/query-dispatch.ts index 44b338e68..b13d7809c 100644 --- a/sdk/src/query/query-dispatch.ts +++ b/sdk/src/query/query-dispatch.ts @@ -2,6 +2,7 @@ import type { QueryRegistry } from './registry.js'; import { runCjsFallbackDispatch } from './query-fallback-executor.js'; import type { QueryDispatchResult } from './query-dispatch-contract.js'; import type { QueryResult } from './utils.js'; +import type { QueryNativeDispatchAdapter } from './query-native-dispatch-adapter.js'; import { mapFallbackDispatchError, mapNativeDispatchError, toDispatchFailure } from './query-dispatch-error-mapper.js'; import { formatSuccess } from './query-dispatch-formatting.js'; import { diagnoseUnknownCommand } from './query-command-diagnosis.js'; @@ -17,7 +18,9 @@ export interface QueryDispatchDeps { ws?: string; cjsFallbackEnabled: boolean; resolveGsdToolsPath: (projectDir: string) => string; - dispatchNative: (cmd: string, args: string[]) => Promise; + /** @deprecated use nativeAdapter */ + dispatchNative?: (cmd: string, args: string[]) => Promise; + nativeAdapter?: QueryNativeDispatchAdapter; } @@ -73,8 +76,16 @@ export async function runQueryDispatch(deps: QueryDispatchDeps, queryArgv: strin if (!matched) { return toDispatchFailure(mapFallbackDispatchError(new Error('No native match in dispatch plan'), normCmd, normArgs)); } + const dispatchNative = deps.nativeAdapter + ? (cmd: string, args: string[]) => deps.nativeAdapter!.dispatch(cmd, args) + : deps.dispatchNative; + + if (!dispatchNative) { + return toDispatchFailure(mapNativeDispatchError(new Error('Missing native dispatch adapter'), matched.cmd, matched.args)); + } + try { - const result = await deps.dispatchNative(matched.cmd, matched.args); + const result = await dispatchNative(matched.cmd, matched.args); return dispatchSuccess(formatSuccess(result.data, result.format, pickField)); } catch (e) { return toDispatchFailure(mapNativeDispatchError(e, matched.cmd, matched.args)); diff --git a/sdk/src/query/query-native-dispatch-adapter.ts b/sdk/src/query/query-native-dispatch-adapter.ts new file mode 100644 index 000000000..c35ed076c --- /dev/null +++ b/sdk/src/query/query-native-dispatch-adapter.ts @@ -0,0 +1,16 @@ +import type { QueryRegistry } from './registry.js'; +import type { QueryResult } from './utils.js'; + +export interface QueryNativeDispatchAdapter { + dispatch(command: string, args: string[]): Promise; +} + +export function createQueryNativeDispatchAdapter( + registry: QueryRegistry, + projectDir: string, + ws?: string, +): QueryNativeDispatchAdapter { + return { + dispatch: (command, args) => registry.dispatch(command, args, projectDir, ws), + }; +} diff --git a/sdk/src/query/query-policy-snapshot.test.ts b/sdk/src/query/query-policy-snapshot.test.ts index 515eea103..f769467eb 100644 --- a/sdk/src/query/query-policy-snapshot.test.ts +++ b/sdk/src/query/query-policy-snapshot.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from 'vitest'; -import { QUERY_POLICY_SNAPSHOT, QUERY_MUTATION_COMMAND_LIST, TRANSPORT_RAW_COMMANDS } from './query-policy-snapshot.js'; +import { QUERY_POLICY_SNAPSHOT, QUERY_MUTATION_COMMAND_LIST, TRANSPORT_RAW_COMMANDS } from './query-policy-capability.js'; describe('query-policy-snapshot', () => { it('exposes policy constants through one snapshot interface', () => { diff --git a/sdk/src/query/query-policy-snapshot.ts b/sdk/src/query/query-policy-snapshot.ts deleted file mode 100644 index 7d0df6987..000000000 --- a/sdk/src/query/query-policy-snapshot.ts +++ /dev/null @@ -1,6 +0,0 @@ -export { - QUERY_POLICY_SNAPSHOT, - QUERY_MUTATION_COMMAND_LIST, - TRANSPORT_RAW_COMMANDS, - isQueryMutationCommand, -} from './query-policy-capability.js'; diff --git a/sdk/src/query/query-registry-capability.test.ts b/sdk/src/query/query-registry-capability.test.ts index bcb339b82..494630fcb 100644 --- a/sdk/src/query/query-registry-capability.test.ts +++ b/sdk/src/query/query-registry-capability.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from 'vitest'; -import { supportsMutationCommand, supportsRawOutputCommand } from './query-registry-capability.js'; +import { supportsMutationCommand, supportsRawOutputCommand } from './query-policy-capability.js'; describe('query-registry-capability', () => { it('reports mutation command capability', () => { diff --git a/sdk/src/query/query-registry-capability.ts b/sdk/src/query/query-registry-capability.ts deleted file mode 100644 index 7521221f6..000000000 --- a/sdk/src/query/query-registry-capability.ts +++ /dev/null @@ -1,4 +0,0 @@ -export { - supportsMutationCommand, - supportsRawOutputCommand, -} from './query-policy-capability.js'; diff --git a/sdk/src/query/query-runtime-context.ts b/sdk/src/query/query-runtime-context.ts new file mode 100644 index 000000000..037d348c6 --- /dev/null +++ b/sdk/src/query/query-runtime-context.ts @@ -0,0 +1,29 @@ +import { findProjectRoot } from './helpers.js'; +import { validateWorkstreamName } from '../workstream-utils.js'; + +export interface QueryRuntimeContextInput { + projectDir: string; + ws?: string; +} + +export interface QueryRuntimeContext { + projectDir: string; + ws?: string; +} + +export function resolveQueryRuntimeContext(input: QueryRuntimeContextInput): QueryRuntimeContext { + const projectDir = findProjectRoot(input.projectDir); + + if (input.ws !== undefined) { + return { + projectDir, + ws: validateWorkstreamName(input.ws) ? input.ws : undefined, + }; + } + + const envWs = process.env.GSD_WORKSTREAM; + return { + projectDir, + ws: envWs && validateWorkstreamName(envWs) ? envWs : undefined, + }; +} diff --git a/sdk/src/query/registry-assembly.ts b/sdk/src/query/registry-assembly.ts index d2d713a96..e84ae22b2 100644 --- a/sdk/src/query/registry-assembly.ts +++ b/sdk/src/query/registry-assembly.ts @@ -12,7 +12,7 @@ import { 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 { QUERY_MUTATION_COMMAND_LIST, TRANSPORT_RAW_COMMANDS } from './query-policy-capability.js'; import { COMMAND_DEFINITIONS_BY_FAMILY, type CommandDefinition } from './command-definition.js'; import { decorateMutationsWithEvents } from './mutation-event-decorator.js'; import { FAMILY_HANDLERS } from './command-family-handlers.js'; diff --git a/sdk/src/query/registry.ts b/sdk/src/query/registry.ts index 7218effcb..25491b724 100644 --- a/sdk/src/query/registry.ts +++ b/sdk/src/query/registry.ts @@ -22,7 +22,7 @@ import type { QueryResult, QueryHandler } from './utils.js'; import { GSDError, ErrorClassification } from '../errors.js'; -import { resolveQueryTokens } from './command-resolution.js'; +import { resolveQueryTokens } from './query-command-resolution-strategy.js'; // ─── extractField ────────────────────────────────────────────────────────── diff --git a/tests/bug-2524-sdk-query-ws-flag.test.cjs b/tests/bug-2524-sdk-query-ws-flag.test.cjs index 5567730dc..9e2bd50c0 100644 --- a/tests/bug-2524-sdk-query-ws-flag.test.cjs +++ b/tests/bug-2524-sdk-query-ws-flag.test.cjs @@ -6,8 +6,10 @@ /** * Bug #2524: gsd-sdk query --ws silently ignores the workstream flag. - * Tests that --ws is forwarded through the call chain: - * cli.ts -> runQueryCliCommand() -> registry.dispatch() -> planningPaths() + * + * This file is structural/static coverage only (source-file assertions). + * Runtime forwarding coverage for the query adapter path lives in: + * sdk/src/query/query-cli-adapter.test.ts * * Uses static source-file text assertions (no sdk/dist/ build required in CI). */