* 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.
74 lines
2.6 KiB
TypeScript
74 lines
2.6 KiB
TypeScript
import type { QueryResult } from './query/utils.js';
|
|
import type { QueryRegistry } from './query/registry.js';
|
|
import type { TransportMode } from './gsd-transport-policy.js';
|
|
|
|
export interface TransportRequest {
|
|
legacyCommand: string;
|
|
legacyArgs: string[];
|
|
registryCommand: string;
|
|
registryArgs: string[];
|
|
mode: TransportMode;
|
|
projectDir: string;
|
|
workstream?: string;
|
|
}
|
|
|
|
export interface TransportAdapters {
|
|
dispatchNative: (request: TransportRequest) => Promise<QueryResult>;
|
|
execSubprocessJson: (legacyCommand: string, legacyArgs: string[]) => Promise<unknown>;
|
|
execSubprocessRaw: (legacyCommand: string, legacyArgs: string[]) => Promise<string>;
|
|
formatNativeRaw?: (registryCommand: string, data: unknown) => string;
|
|
}
|
|
|
|
export interface TransportPolicyLike {
|
|
preferNative: boolean;
|
|
allowFallbackToSubprocess: boolean;
|
|
}
|
|
|
|
function isTimeoutLikeError(error: unknown): boolean {
|
|
if (!(error instanceof Error)) return false;
|
|
if (error.name === 'TimeoutError' || error.name === 'AbortError') return true;
|
|
return error.message.includes('timed out after');
|
|
}
|
|
|
|
export class GSDTransport {
|
|
constructor(
|
|
private readonly registry: QueryRegistry,
|
|
private readonly adapters: TransportAdapters,
|
|
) {}
|
|
|
|
async run(request: TransportRequest, policy: TransportPolicyLike): Promise<unknown> {
|
|
const forceSubprocess = Boolean(request.workstream);
|
|
|
|
if (!forceSubprocess && policy.preferNative && this.registry.has(request.registryCommand)) {
|
|
try {
|
|
const native = await this.adapters.dispatchNative(request);
|
|
if (request.mode === 'raw') {
|
|
if (this.adapters.formatNativeRaw) {
|
|
return this.adapters.formatNativeRaw(request.registryCommand, native.data).trim();
|
|
}
|
|
return this.toRaw(native.data);
|
|
}
|
|
return native.data;
|
|
} catch (error) {
|
|
if (!policy.allowFallbackToSubprocess) throw error;
|
|
// Do not subprocess-fallback after a timed-out native dispatch:
|
|
// the timeout does not cancel the native handler, so falling through
|
|
// would run the same command twice (double-execution race).
|
|
if (isTimeoutLikeError(error)) throw error;
|
|
}
|
|
}
|
|
|
|
if (request.mode === 'raw') {
|
|
return this.adapters.execSubprocessRaw(request.legacyCommand, request.legacyArgs);
|
|
}
|
|
return this.adapters.execSubprocessJson(request.legacyCommand, request.legacyArgs);
|
|
}
|
|
|
|
private toRaw(data: unknown): string {
|
|
if (typeof data === 'string') return data.trim();
|
|
const json = JSON.stringify(data, null, 2);
|
|
if (json == null) return '';
|
|
return json.trim();
|
|
}
|
|
}
|