fix(2137): skip worktree isolation when .gitmodules detected (#2144)
* feat(sdk): add typed query foundation and gsd-sdk query (Phase 1) Add sdk/src/query registry and handlers with tests, GSDQueryError, CLI query wiring, and supporting type/tool-scoping hooks. Update CHANGELOG. Vitest 4 constructor mock fixes in milestone-runner tests. Made-with: Cursor * fix(2137): skip worktree isolation when .gitmodules detected When a project contains git submodules, worktree isolation cannot correctly handle submodule commits — three separate gaps exist in worktree setup, executor commit protocol, and merge-back. Rather than patch each gap individually, detect .gitmodules at phase start and fall back to sequential execution, which handles submodules transparently (Option B). Affected workflows: execute-phase.md, quick.md --------- Co-authored-by: David Sienkowski <dave@sienkowski.com>
This commit is contained in:
121
sdk/src/query/registry.ts
Normal file
121
sdk/src/query/registry.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
/**
|
||||
* Query command registry — routes commands to native SDK handlers.
|
||||
*
|
||||
* The registry is a flat `Map<string, QueryHandler>` that maps command names
|
||||
* to handler functions. Unknown commands throw GSDError — the gsd-tools.cjs
|
||||
* fallback was removed in v3.0 when all commands were migrated to native handlers.
|
||||
*
|
||||
* Also exports `extractField` — a TypeScript port of the `--pick` field
|
||||
* extraction logic from gsd-tools.cjs (lines 365-382).
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { QueryRegistry, extractField } from './registry.js';
|
||||
*
|
||||
* const registry = new QueryRegistry();
|
||||
* registry.register('generate-slug', generateSlug);
|
||||
* const result = await registry.dispatch('generate-slug', ['My Phase'], '/project');
|
||||
* const slug = extractField(result.data, 'slug'); // 'my-phase'
|
||||
* ```
|
||||
*/
|
||||
|
||||
import type { QueryResult, QueryHandler } from './utils.js';
|
||||
import { GSDError, ErrorClassification } from '../errors.js';
|
||||
|
||||
// ─── extractField ──────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Extract a nested field from an object using dot-notation and bracket syntax.
|
||||
*
|
||||
* Direct port of `extractField()` from gsd-tools.cjs (lines 365-382).
|
||||
* Supports `a.b.c` dot paths, `items[0]` array indexing, and `items[-1]`
|
||||
* negative indexing.
|
||||
*
|
||||
* @param obj - The object to extract from
|
||||
* @param fieldPath - Dot-separated path with optional bracket notation
|
||||
* @returns The extracted value, or undefined if the path doesn't resolve
|
||||
*/
|
||||
export function extractField(obj: unknown, fieldPath: string): unknown {
|
||||
const parts = fieldPath.split('.');
|
||||
let current: unknown = obj;
|
||||
for (const part of parts) {
|
||||
if (current === null || current === undefined) return undefined;
|
||||
const bracketMatch = part.match(/^(.+?)\[(-?\d+)]$/);
|
||||
if (bracketMatch) {
|
||||
const key = bracketMatch[1];
|
||||
const index = parseInt(bracketMatch[2], 10);
|
||||
current = (current as Record<string, unknown>)[key];
|
||||
if (!Array.isArray(current)) return undefined;
|
||||
current = index < 0 ? current[current.length + index] : current[index];
|
||||
} else {
|
||||
current = (current as Record<string, unknown>)[part];
|
||||
}
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
// ─── QueryRegistry ─────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Flat command registry that routes query commands to native handlers.
|
||||
*
|
||||
* Unknown commands throw `GSDError` from `dispatch()` — there is no fallback
|
||||
* to gsd-tools.cjs (bridge removed in v3.0). All supported commands must be
|
||||
* registered via `register()`.
|
||||
*/
|
||||
export class QueryRegistry {
|
||||
private handlers = new Map<string, QueryHandler>();
|
||||
|
||||
/**
|
||||
* Register a native handler for a command name.
|
||||
*
|
||||
* @param command - The command name (e.g., 'generate-slug', 'state.load')
|
||||
* @param handler - The handler function to invoke
|
||||
*/
|
||||
register(command: string, handler: QueryHandler): void {
|
||||
this.handlers.set(command, handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a command has a registered native handler.
|
||||
*
|
||||
* @param command - The command name to check
|
||||
* @returns True if the command has a native handler
|
||||
*/
|
||||
has(command: string): boolean {
|
||||
return this.handlers.has(command);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the handler for a command without dispatching.
|
||||
*
|
||||
* @param command - The command name to look up
|
||||
* @returns The handler function, or undefined if not registered
|
||||
*/
|
||||
getHandler(command: string): QueryHandler | undefined {
|
||||
return this.handlers.get(command);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatch a command to its registered native handler.
|
||||
*
|
||||
* Throws GSDError for unknown commands — the gsd-tools.cjs fallback was
|
||||
* removed in v3.0. All commands must be registered as native handlers (T-14-13).
|
||||
*
|
||||
* @param command - The command name to dispatch
|
||||
* @param args - Arguments to pass to the handler
|
||||
* @param projectDir - The project directory for context
|
||||
* @returns The query result from the handler
|
||||
* @throws GSDError if no handler is registered for the command
|
||||
*/
|
||||
async dispatch(command: string, args: string[], projectDir: string): Promise<QueryResult> {
|
||||
const handler = this.handlers.get(command);
|
||||
if (!handler) {
|
||||
throw new GSDError(
|
||||
`Unknown command: "${command}". No native handler registered.`,
|
||||
ErrorClassification.Validation,
|
||||
);
|
||||
}
|
||||
return handler(args, projectDir);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user