refactor(query): deepen command/dispatch seams and resolve coderabbit findings
* refactor(query): deepen command definition seam and fold fallback mapping cleanup * refactor(query): add shared dispatch formatting module seam * fix(query): restore QueryResult type import in dispatch deps * test/query: align raw-output policy and definition normalization contracts * refactor(query): deepen diagnosis, invariant report, and error taxonomy seams * refactor(query): deepen dispatch plan, fallback bridge, policy snapshot, and hints seams * refactor(query): deepen validation, fallback policy, capability, and result builder seams * refactor(query): deepen resolution strategy, output classifier, observability, and policy-capability seams * refactor(query): finalize deep strategy/classifier/observability/capability seams * test/query: address coderabbit inline and out-of-diff dispatch nits * fix(query): address remaining coderabbit input-validation and bridge stderr threads * fix(query): address remaining coderabbit dispatch and strategy/output nits
This commit is contained in:
@@ -1,11 +1,15 @@
|
||||
import type { QueryRegistry } from './registry.js';
|
||||
import { extractField } from './registry.js';
|
||||
import { normalizeQueryCommand } from './normalize-query-command.js';
|
||||
import { explainQueryCommandNoMatch, resolveQueryCommand, type QueryCommandResolution } from './command-resolution.js';
|
||||
import { runCjsFallbackDispatch } from './query-fallback-executor.js';
|
||||
import type { QueryDispatchResult } from './query-dispatch-contract.js';
|
||||
import type { QueryResult } from './utils.js';
|
||||
import type { QueryDispatchResult, QueryDispatchErrorKind } from './query-dispatch-contract.js';
|
||||
import { mapNativeDispatchError, toDispatchFailure } from './query-dispatch-error-mapper.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';
|
||||
import { unknownCommandError, validationError } from './query-error-taxonomy.js';
|
||||
import { planQueryDispatch } from './query-dispatch-plan.js';
|
||||
import { validateQueryDispatchInput } from './query-dispatch-input-validation.js';
|
||||
import { dispatchSuccess } from './query-dispatch-result-builder.js';
|
||||
import { canUseCjsFallback } from './query-fallback-policy.js';
|
||||
|
||||
export interface QueryDispatchDeps {
|
||||
registry: QueryRegistry;
|
||||
@@ -16,125 +20,62 @@ export interface QueryDispatchDeps {
|
||||
dispatchNative: (cmd: string, args: string[]) => Promise<QueryResult>;
|
||||
}
|
||||
|
||||
type DispatchMode = 'native' | 'cjs' | 'error';
|
||||
|
||||
interface DispatchPlan {
|
||||
mode: DispatchMode;
|
||||
normalized: { command: string; args: string[]; tokens: string[] };
|
||||
matched: QueryCommandResolution | null;
|
||||
function fail(error: ReturnType<typeof validationError> | ReturnType<typeof unknownCommandError>, stderr: string[] = []): QueryDispatchResult {
|
||||
return toDispatchFailure(error, stderr);
|
||||
}
|
||||
|
||||
function fail(
|
||||
kind: QueryDispatchErrorKind,
|
||||
code: number,
|
||||
message: string,
|
||||
details?: Record<string, unknown>,
|
||||
stderr: string[] = [],
|
||||
): QueryDispatchResult {
|
||||
return toDispatchFailure({ kind, code, message, details }, stderr);
|
||||
}
|
||||
|
||||
function success(stdout: string, stderr: string[] = []): QueryDispatchResult {
|
||||
return { ok: true, stdout, stderr, exit_code: 0 };
|
||||
}
|
||||
|
||||
function planQueryDispatch(queryArgv: string[], registry: QueryRegistry, cjsFallbackEnabled: boolean): DispatchPlan {
|
||||
const queryCommand = queryArgv[0];
|
||||
if (!queryCommand) {
|
||||
return { mode: 'error', normalized: { command: '', args: [], tokens: [] }, matched: null };
|
||||
}
|
||||
|
||||
const [normCmd, normArgs] = normalizeQueryCommand(queryCommand, queryArgv.slice(1));
|
||||
const normalizedTokens = [normCmd, ...normArgs];
|
||||
const matched = resolveQueryCommand(queryCommand, queryArgv.slice(1), registry);
|
||||
if (matched) {
|
||||
return { mode: 'native', normalized: { command: normCmd, args: normArgs, tokens: normalizedTokens }, matched };
|
||||
}
|
||||
if (cjsFallbackEnabled) {
|
||||
return { mode: 'cjs', normalized: { command: normCmd, args: normArgs, tokens: normalizedTokens }, matched: null };
|
||||
}
|
||||
return { mode: 'error', normalized: { command: normCmd, args: normArgs, tokens: normalizedTokens }, matched: null };
|
||||
}
|
||||
|
||||
function extractPick(queryArgv: string[]): { queryArgs: string[]; pickField?: string; error?: QueryDispatchResult } {
|
||||
const queryArgs = [...queryArgv];
|
||||
const pickIdx = queryArgs.indexOf('--pick');
|
||||
if (pickIdx === -1) return { queryArgs };
|
||||
if (pickIdx + 1 >= queryArgs.length) {
|
||||
return {
|
||||
queryArgs,
|
||||
error: fail('validation_error', 10, 'Error: --pick requires a field name', { field: '--pick', reason: 'missing_value' }),
|
||||
};
|
||||
}
|
||||
const pickField = queryArgs[pickIdx + 1];
|
||||
queryArgs.splice(pickIdx, 2);
|
||||
return { queryArgs, pickField };
|
||||
}
|
||||
|
||||
function formatOutput(data: unknown, format: QueryResult['format'], pickField?: string): string {
|
||||
// Text-format responses ignore --pick to match CJS fallback behavior.
|
||||
if (format === 'text' && typeof data === 'string') {
|
||||
return data.endsWith('\n') ? data : `${data}\n`;
|
||||
}
|
||||
let output: unknown = data;
|
||||
if (pickField) output = extractField(output, pickField);
|
||||
return `${JSON.stringify(output, null, 2)}\n`;
|
||||
}
|
||||
|
||||
export async function runQueryDispatch(deps: QueryDispatchDeps, queryArgv: string[]): Promise<QueryDispatchResult> {
|
||||
const picked = extractPick(queryArgv);
|
||||
if (picked.error) return picked.error;
|
||||
const validated = validateQueryDispatchInput(queryArgv);
|
||||
if (validated.error) return validated.error;
|
||||
|
||||
const { queryArgs, pickField } = picked;
|
||||
if (queryArgs.length === 0 || !queryArgs[0]) {
|
||||
return fail('validation_error', 10, 'Error: "gsd-sdk query" requires a command', { reason: 'missing_command' });
|
||||
}
|
||||
const { queryArgs, pickField } = validated;
|
||||
|
||||
const plan = planQueryDispatch(queryArgs, deps.registry, deps.cjsFallbackEnabled);
|
||||
const normCmd = plan.normalized.command;
|
||||
const normArgs = plan.normalized.args;
|
||||
|
||||
if (!normCmd || !String(normCmd).trim()) {
|
||||
return fail('validation_error', 10, 'Error: "gsd-sdk query" requires a command', { reason: 'empty_normalized_command' });
|
||||
return fail(validationError({ message: 'Error: "gsd-sdk query" requires a command', details: { reason: 'empty_normalized_command' } }));
|
||||
}
|
||||
|
||||
if (plan.mode === 'error') {
|
||||
const noMatch = queryArgs[0]
|
||||
? explainQueryCommandNoMatch(queryArgs[0], queryArgs.slice(1), deps.registry)
|
||||
: null;
|
||||
return fail(
|
||||
'unknown_command',
|
||||
10,
|
||||
`Error: Unknown command: "${[normCmd, ...normArgs].join(' ')}". Use a registered \`gsd-sdk query\` subcommand (see sdk/src/query/QUERY-HANDLERS.md) or invoke \`node …/gsd-tools.cjs\` for CJS-only operations. CJS fallback is disabled (GSD_QUERY_FALLBACK=registered). To enable fallback, unset GSD_QUERY_FALLBACK or set it to a non-restricted value.${noMatch ? ` Attempted dotted: ${noMatch.attempted.dotted.slice(0, 2).join(' | ')}.` : ''}`,
|
||||
{ normalized: [normCmd, ...normArgs].join(' '), attempted: noMatch?.attempted.dotted.slice(0, 2) ?? [] },
|
||||
);
|
||||
const diagnosis = diagnoseUnknownCommand(queryArgs[0] ?? normCmd, queryArgs.slice(1), deps.registry, !deps.cjsFallbackEnabled);
|
||||
return fail(unknownCommandError({
|
||||
message: diagnosis.message,
|
||||
normalized: diagnosis.normalized,
|
||||
attempted: diagnosis.attempted,
|
||||
hints: diagnosis.hints,
|
||||
}));
|
||||
}
|
||||
|
||||
if (plan.mode === 'cjs') {
|
||||
try {
|
||||
const gsdPath = deps.resolveGsdToolsPath(deps.projectDir);
|
||||
return await runCjsFallbackDispatch({
|
||||
projectDir: deps.projectDir,
|
||||
gsdToolsPath: gsdPath,
|
||||
normCmd,
|
||||
normArgs,
|
||||
ws: deps.ws,
|
||||
pickField,
|
||||
});
|
||||
} catch (e) {
|
||||
const msg = e instanceof Error ? e.message : String(e);
|
||||
return fail('fallback_failure', 1, `Error: gsd-tools.cjs fallback failed: ${msg}`, {
|
||||
command: normCmd,
|
||||
args: normArgs,
|
||||
backend: 'cjs',
|
||||
});
|
||||
if (canUseCjsFallback({ cjsFallbackEnabled: deps.cjsFallbackEnabled })) {
|
||||
try {
|
||||
const gsdPath = deps.resolveGsdToolsPath(deps.projectDir);
|
||||
return await runCjsFallbackDispatch({
|
||||
projectDir: deps.projectDir,
|
||||
gsdToolsPath: gsdPath,
|
||||
normCmd,
|
||||
normArgs,
|
||||
ws: deps.ws,
|
||||
pickField,
|
||||
});
|
||||
} catch (e) {
|
||||
return toDispatchFailure(mapFallbackDispatchError(e, normCmd, normArgs));
|
||||
}
|
||||
}
|
||||
return toDispatchFailure(mapFallbackDispatchError(new Error('CJS fallback denied by policy'), normCmd, normArgs));
|
||||
}
|
||||
|
||||
const matched = plan.matched!;
|
||||
const matched = plan.matched;
|
||||
if (!matched) {
|
||||
return toDispatchFailure(mapFallbackDispatchError(new Error('No native match in dispatch plan'), normCmd, normArgs));
|
||||
}
|
||||
try {
|
||||
const result = await deps.dispatchNative(matched.cmd, matched.args);
|
||||
return success(formatOutput(result.data, result.format, pickField));
|
||||
return dispatchSuccess(formatSuccess(result.data, result.format, pickField));
|
||||
} catch (e) {
|
||||
return toDispatchFailure(mapNativeDispatchError(e, matched.cmd, matched.args));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user