feat(sdk): Phase 1 typed query foundation (gsd-sdk query) (#2118)

* 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

* chore: gitignore .cursor for local-only Cursor assets

Made-with: Cursor

* fix(sdk): harden query layer for PR review (paths, locks, CLI, ReDoS)

- resolvePathUnderProject: realpath + relative containment for frontmatter and key_links

- commitToSubrepo: path checks + sanitizeCommitMessage

- statePlannedPhase: readModifyWriteStateMd (lock); MUTATION_COMMANDS + events

- key_links: regexForKeyLinkPattern length/ReDoS guard; phase dirs: reject .. and separators

- gsd-sdk: strip --pick before parseArgs; strict parser; QueryRegistry.commands()

- progress: static GSDError import; tests updated

Made-with: Cursor

* feat(sdk): query follow-up — tests, QUERY-HANDLERS, registry, locks, intel depth

Made-with: Cursor

* docs(sdk): use ASCII punctuation in QUERY-HANDLERS.md

Made-with: Cursor
This commit is contained in:
Rezolv
2026-04-12 18:15:04 -04:00
committed by GitHub
parent 66a5f939b0
commit 6f79b1dd5e
36 changed files with 848 additions and 142 deletions

View File

@@ -18,9 +18,9 @@
*/
import { readFile } from 'node:fs/promises';
import { join } from 'node:path';
import { spawnSync } from 'node:child_process';
import { planningPaths } from './helpers.js';
import { GSDError } from '../errors.js';
import { planningPaths, resolvePathUnderProject } from './helpers.js';
import type { QueryHandler } from './utils.js';
// ─── execGit ──────────────────────────────────────────────────────────────
@@ -227,11 +227,20 @@ export const commitToSubrepo: QueryHandler = async (args, projectDir) => {
return { data: { committed: false, reason: 'commit message required' } };
}
const sanitized = sanitizeCommitMessage(message);
if (!sanitized && message) {
return { data: { committed: false, reason: 'commit message empty after sanitization' } };
}
try {
for (const file of files) {
const resolved = join(projectDir, file);
if (!resolved.startsWith(projectDir)) {
return { data: { committed: false, reason: `file path escapes project: ${file}` } };
try {
await resolvePathUnderProject(projectDir, file);
} catch (err) {
if (err instanceof GSDError) {
return { data: { committed: false, reason: `${err.message}: ${file}` } };
}
throw err;
}
}
@@ -239,7 +248,7 @@ export const commitToSubrepo: QueryHandler = async (args, projectDir) => {
spawnSync('git', ['-C', projectDir, 'add', ...fileArgs], { stdio: 'pipe' });
const commitResult = spawnSync(
'git', ['-C', projectDir, 'commit', '-m', message],
'git', ['-C', projectDir, 'commit', '-m', sanitized],
{ stdio: 'pipe', encoding: 'utf-8' },
);
if (commitResult.status !== 0) {
@@ -251,7 +260,7 @@ export const commitToSubrepo: QueryHandler = async (args, projectDir) => {
{ encoding: 'utf-8' },
);
const hash = hashResult.stdout.trim();
return { data: { committed: true, hash, message } };
return { data: { committed: true, hash, message: sanitized } };
} catch (err) {
return { data: { committed: false, reason: String(err) } };
}