* feat(sdk): golden parity harness and query handler CJS alignment (#2302 Track A) Golden/read-only parity tests and registry alignment, query handler fixes (check-completion, state-mutation, commit, validate, summary, etc.), and WAITING.json dual-write for .gsd/.planning readers. Refs gsd-build/get-shit-done#2341 * fix(sdk): getMilestoneInfo matches GSD ROADMAP (🟡, last bold, STATE fallback) - Recognize in-flight 🟡 milestone bullets like 🚧. - Derive from last **vX.Y Title** before ## Phases when emoji absent. - Fall back to STATE.md milestone when ROADMAP is missing; use last bare vX.Y in cleaned text instead of first (avoids v1.0 from shipped list). - Fixes init.execute-phase milestone_version and buildStateFrontmatter after state.begin-phase (syncStateFrontmatter). * feat(sdk): phase list, plan task structure, requirements extract handlers - Register phase.list-plans, phase.list-artifacts, plan.task-structure, requirements.extract-from-plans (SDK-only; golden-policy exceptions). - Add unit tests; document in QUERY-HANDLERS.md. - writeProfile: honor --output, render dimensions, return profile_path and dimensions_scored. * feat(sdk): centralize getGsdAgentsDir in query helpers Extract agent directory resolution to helpers (GSD_AGENTS_DIR, primary ~/.claude/agents, legacy path). Use from init and docs-init init bundles. docs(15): add 15-CONTEXT for autonomous phase-15 run. * feat(sdk): query CLI CJS fallback and session correlation - createRegistry(eventStream, sessionId) threads correlation into mutation events - gsd-sdk query falls back to gsd-tools.cjs when no native handler matches (disable with GSD_QUERY_FALLBACK=off); stderr bridge warnings - Export createRegistry from @gsd-build/sdk; add sdk/README.md - Update QUERY-HANDLERS.md and registry module docs for fallback + sessionId - Agents: prefer node dist/cli.js query over cat/grep for STATE and plans * fix(sdk): init phase_found parity, docs-init agents path, state field extract - Normalize findPhase not-found to null before roadmap fallback (matches findPhaseInternal) - docs-init: use detectRuntime + resolveAgentsDir for checkAgentsInstalled - state.cjs stateExtractField: horizontal whitespace only after colon (YAML progress guard) - Tests: commit_docs default true; config-get golden uses temp config; golden integration green Refs: #2302 * refactor(sdk): share SessionJsonlRecord in profile-extract-messages CodeRabbit nit: dedupe JSONL record shape for isGenuineUserMessage and streamExtractMessages. * fix(sdk): address CodeRabbit major threads (paths, gates, audit, verify) - Resolve @file: and CLI JSON indirection relative to projectDir; guard empty normalized query command - plan.task-structure + intel extract/patch-meta: resolvePathUnderProject containment - check.config-gates: safe string booleans; plan_checker alias precedence over plan_check default - state.validate/sync: phaseTokenMatches + comparePhaseNum ordering - verify.schema-drift: token match phase dirs; files_modified from parsed frontmatter - audit-open: has_scan_errors, unreadable rows, human report when scans fail - requirements PLANNED key PLAN for root PLAN.md; gsd-tools timeout note - ingest-docs: repo-root path containment; classifier output slug-hash Golden parity test strips has_scan_errors until CJS adds field. * fix: Resolve CodeRabbit security and quality findings - Secure intel.ts and cli.ts against path traversal - Catch and validate git add status in commit.ts - Expand roadmap milestone marker extraction - Fix parsing array-of-objects in frontmatter YAML - Fix unhandled config evaluations - Improve coverage test parity mapping * test: raise planner character extraction limit to 48K * fix(sdk): resolve TS build error in docs-init passing config
This commit is contained in:
@@ -18,10 +18,10 @@
|
||||
*/
|
||||
|
||||
import { existsSync, readdirSync, readFileSync, writeFileSync, mkdirSync, statSync } from 'node:fs';
|
||||
import { join, resolve } from 'node:path';
|
||||
import { join } from 'node:path';
|
||||
import { createHash } from 'node:crypto';
|
||||
|
||||
import { planningPaths } from './helpers.js';
|
||||
import { planningPaths, resolvePathUnderProject } from './helpers.js';
|
||||
import type { QueryHandler } from './utils.js';
|
||||
|
||||
// ─── Constants ───────────────────────────────────────────────────────────
|
||||
@@ -116,9 +116,11 @@ function searchArchMd(filePath: string, term: string): string[] {
|
||||
|
||||
// ─── Handlers ────────────────────────────────────────────────────────────
|
||||
|
||||
const INTEL_DISABLED_MSG = 'Intel system disabled. Set intel.enabled=true in config.json to activate.';
|
||||
|
||||
export const intelStatus: QueryHandler = async (_args, projectDir) => {
|
||||
if (!isIntelEnabled(projectDir)) {
|
||||
return { data: { disabled: true, message: 'Intel system disabled. Set intel.enabled=true in config.json to activate.' } };
|
||||
return { data: { disabled: true, message: INTEL_DISABLED_MSG } };
|
||||
}
|
||||
const now = Date.now();
|
||||
const files: Record<string, unknown> = {};
|
||||
@@ -149,7 +151,7 @@ export const intelStatus: QueryHandler = async (_args, projectDir) => {
|
||||
|
||||
export const intelDiff: QueryHandler = async (_args, projectDir) => {
|
||||
if (!isIntelEnabled(projectDir)) {
|
||||
return { data: { disabled: true, message: 'Intel system disabled.' } };
|
||||
return { data: { disabled: true, message: INTEL_DISABLED_MSG } };
|
||||
}
|
||||
const snapshotPath = intelFilePath(projectDir, '.last-refresh.json');
|
||||
const snapshot = safeReadJson(snapshotPath) as Record<string, unknown> | null;
|
||||
@@ -172,7 +174,7 @@ export const intelDiff: QueryHandler = async (_args, projectDir) => {
|
||||
|
||||
export const intelSnapshot: QueryHandler = async (_args, projectDir) => {
|
||||
if (!isIntelEnabled(projectDir)) {
|
||||
return { data: { disabled: true, message: 'Intel system disabled.' } };
|
||||
return { data: { disabled: true, message: INTEL_DISABLED_MSG } };
|
||||
}
|
||||
const dir = intelDir(projectDir);
|
||||
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
||||
@@ -192,7 +194,7 @@ export const intelSnapshot: QueryHandler = async (_args, projectDir) => {
|
||||
|
||||
export const intelValidate: QueryHandler = async (_args, projectDir) => {
|
||||
if (!isIntelEnabled(projectDir)) {
|
||||
return { data: { disabled: true, message: 'Intel system disabled.' } };
|
||||
return { data: { disabled: true, message: INTEL_DISABLED_MSG } };
|
||||
}
|
||||
const errors: string[] = [];
|
||||
const warnings: string[] = [];
|
||||
@@ -220,7 +222,7 @@ export const intelValidate: QueryHandler = async (_args, projectDir) => {
|
||||
export const intelQuery: QueryHandler = async (args, projectDir) => {
|
||||
const term = args[0] || '';
|
||||
if (!isIntelEnabled(projectDir)) {
|
||||
return { data: { disabled: true, message: 'Intel system disabled.' } };
|
||||
return { data: { disabled: true, message: INTEL_DISABLED_MSG } };
|
||||
}
|
||||
const matches: unknown[] = [];
|
||||
let total = 0;
|
||||
@@ -241,9 +243,22 @@ export const intelQuery: QueryHandler = async (args, projectDir) => {
|
||||
return { data: { matches, term, total } };
|
||||
};
|
||||
|
||||
/**
|
||||
* Extract exports from a JS/CJS/ESM file — port of `intelExtractExports` in `intel.cjs` (lines 502–614).
|
||||
* Returns `{ file, exports, method }` with `file` as a resolved absolute path (matches `gsd-tools.cjs`).
|
||||
*/
|
||||
export const intelExtractExports: QueryHandler = async (args, projectDir) => {
|
||||
const filePath = args[0] ? resolve(projectDir, args[0]) : '';
|
||||
if (!filePath || !existsSync(filePath)) {
|
||||
const raw = args[0];
|
||||
if (!raw) {
|
||||
return { data: { file: '', exports: [], method: 'none' } };
|
||||
}
|
||||
let filePath: string;
|
||||
try {
|
||||
filePath = await resolvePathUnderProject(projectDir, raw);
|
||||
} catch {
|
||||
return { data: { file: raw, exports: [], method: 'none' } };
|
||||
}
|
||||
if (!existsSync(filePath)) {
|
||||
return { data: { file: filePath, exports: [], method: 'none' } };
|
||||
}
|
||||
|
||||
@@ -253,9 +268,10 @@ export const intelExtractExports: QueryHandler = async (args, projectDir) => {
|
||||
|
||||
const allMatches = [...content.matchAll(/module\.exports\s*=\s*\{/g)];
|
||||
if (allMatches.length > 0) {
|
||||
const lastMatch = allMatches[allMatches.length - 1];
|
||||
const lastMatch = allMatches[allMatches.length - 1]!;
|
||||
const startIdx = lastMatch.index! + lastMatch[0].length;
|
||||
let depth = 1; let endIdx = startIdx;
|
||||
let depth = 1;
|
||||
let endIdx = startIdx;
|
||||
while (endIdx < content.length && depth > 0) {
|
||||
if (content[endIdx] === '{') depth++;
|
||||
else if (content[endIdx] === '}') depth--;
|
||||
@@ -264,40 +280,90 @@ export const intelExtractExports: QueryHandler = async (args, projectDir) => {
|
||||
const block = content.substring(startIdx, endIdx);
|
||||
method = 'module.exports';
|
||||
for (const line of block.split('\n')) {
|
||||
const t = line.trim();
|
||||
if (!t || t.startsWith('//') || t.startsWith('*')) continue;
|
||||
const k = t.match(/^(\w+)\s*[,}:]/) || t.match(/^(\w+)$/);
|
||||
if (k) exports.push(k[1]);
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed || trimmed.startsWith('//') || trimmed.startsWith('*')) continue;
|
||||
const keyMatch = trimmed.match(/^(\w+)\s*[,}:]/) || trimmed.match(/^(\w+)$/);
|
||||
if (keyMatch) exports.push(keyMatch[1]!);
|
||||
}
|
||||
}
|
||||
for (const m of content.matchAll(/^exports\.(\w+)\s*=/gm)) {
|
||||
if (!exports.includes(m[1])) { exports.push(m[1]); if (method === 'none') method = 'exports.X'; }
|
||||
|
||||
const individualPattern = /^exports\.(\w+)\s*=/gm;
|
||||
let im: RegExpExecArray | null;
|
||||
while ((im = individualPattern.exec(content)) !== null) {
|
||||
if (!exports.includes(im[1]!)) {
|
||||
exports.push(im[1]!);
|
||||
if (method === 'none') method = 'exports.X';
|
||||
}
|
||||
}
|
||||
|
||||
const hadCjs = exports.length > 0;
|
||||
|
||||
const esmExports: string[] = [];
|
||||
for (const m of content.matchAll(/^export\s+(?:default\s+)?(?:async\s+)?(?:function|class)\s+(\w+)/gm)) {
|
||||
if (!esmExports.includes(m[1])) esmExports.push(m[1]);
|
||||
|
||||
const defaultNamedPattern = /^export\s+default\s+(?:function|class)\s+(\w+)/gm;
|
||||
let em: RegExpExecArray | null;
|
||||
while ((em = defaultNamedPattern.exec(content)) !== null) {
|
||||
if (!esmExports.includes(em[1]!)) esmExports.push(em[1]!);
|
||||
}
|
||||
for (const m of content.matchAll(/^export\s+(?:const|let|var)\s+(\w+)\s*=/gm)) {
|
||||
if (!esmExports.includes(m[1])) esmExports.push(m[1]);
|
||||
|
||||
const defaultAnonPattern = /^export\s+default\s+(?!function\s|class\s)/gm;
|
||||
if (defaultAnonPattern.test(content) && esmExports.length === 0) {
|
||||
if (!esmExports.includes('default')) esmExports.push('default');
|
||||
}
|
||||
for (const m of content.matchAll(/^export\s*\{([^}]+)\}/gm)) {
|
||||
for (const item of m[1].split(',')) {
|
||||
const name = item.trim().split(/\s+as\s+/)[0].trim();
|
||||
|
||||
const exportFnPattern = /^export\s+(?:async\s+)?function\s+(\w+)\s*\(/gm;
|
||||
while ((em = exportFnPattern.exec(content)) !== null) {
|
||||
if (!esmExports.includes(em[1]!)) esmExports.push(em[1]!);
|
||||
}
|
||||
|
||||
const exportVarPattern = /^export\s+(?:const|let|var)\s+(\w+)\s*=/gm;
|
||||
while ((em = exportVarPattern.exec(content)) !== null) {
|
||||
if (!esmExports.includes(em[1]!)) esmExports.push(em[1]!);
|
||||
}
|
||||
|
||||
const exportClassPattern = /^export\s+class\s+(\w+)/gm;
|
||||
while ((em = exportClassPattern.exec(content)) !== null) {
|
||||
if (!esmExports.includes(em[1]!)) esmExports.push(em[1]!);
|
||||
}
|
||||
|
||||
const exportBlockPattern = /^export\s*\{([^}]+)\}/gm;
|
||||
while ((em = exportBlockPattern.exec(content)) !== null) {
|
||||
const items = em[1]!.split(',');
|
||||
for (const item of items) {
|
||||
const trimmed = item.trim();
|
||||
if (!trimmed) continue;
|
||||
const name = trimmed.split(/\s+as\s+/)[0]!.trim();
|
||||
if (name && !esmExports.includes(name)) esmExports.push(name);
|
||||
}
|
||||
}
|
||||
|
||||
for (const e of esmExports) {
|
||||
if (!exports.includes(e)) exports.push(e);
|
||||
}
|
||||
if (esmExports.length > 0 && exports.length > esmExports.length) method = 'mixed';
|
||||
else if (esmExports.length > 0 && method === 'none') method = 'esm';
|
||||
|
||||
return { data: { file: args[0], exports, method } };
|
||||
const hadEsm = esmExports.length > 0;
|
||||
if (hadCjs && hadEsm) {
|
||||
method = 'mixed';
|
||||
} else if (hadEsm && !hadCjs) {
|
||||
method = 'esm';
|
||||
}
|
||||
|
||||
return { data: { file: filePath, exports, method } };
|
||||
};
|
||||
|
||||
export const intelPatchMeta: QueryHandler = async (args, projectDir) => {
|
||||
const filePath = args[0] ? resolve(projectDir, args[0]) : '';
|
||||
if (!filePath || !existsSync(filePath)) {
|
||||
const raw = args[0];
|
||||
if (!raw) {
|
||||
return { data: { patched: false, error: 'File not found' } };
|
||||
}
|
||||
let filePath: string;
|
||||
try {
|
||||
filePath = await resolvePathUnderProject(projectDir, raw);
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
return { data: { patched: false, error: msg } };
|
||||
}
|
||||
if (!existsSync(filePath)) {
|
||||
return { data: { patched: false, error: `File not found: ${filePath}` } };
|
||||
}
|
||||
try {
|
||||
@@ -309,8 +375,30 @@ export const intelPatchMeta: QueryHandler = async (args, projectDir) => {
|
||||
meta.updated_at = timestamp;
|
||||
meta.version = ((meta.version as number) || 0) + 1;
|
||||
writeFileSync(filePath, JSON.stringify(data, null, 2) + '\n', 'utf-8');
|
||||
return { data: { patched: true, file: args[0], timestamp } };
|
||||
return { data: { patched: true, file: filePath, timestamp } };
|
||||
} catch (err) {
|
||||
return { data: { patched: false, error: String(err) } };
|
||||
}
|
||||
};
|
||||
|
||||
// ─── intelUpdate ───────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* `gsd-tools intel update` entry point: returns the same JSON as `intel.cjs` `intelUpdate`.
|
||||
* Does not run the full graph refresh in-process — that work is done by the
|
||||
* **gsd-intel-updater** agent after spawn. When `.planning/intel/` is disabled in config,
|
||||
* returns `{ disabled: true, message }` so SDK output matches the CJS CLI.
|
||||
*
|
||||
* Port of `intelUpdate` from `intel.cjs` lines 314–321.
|
||||
*/
|
||||
export const intelUpdate: QueryHandler = async (_args, projectDir) => {
|
||||
if (!isIntelEnabled(projectDir)) {
|
||||
return { data: { disabled: true, message: INTEL_DISABLED_MSG } };
|
||||
}
|
||||
return {
|
||||
data: {
|
||||
action: 'spawn_agent',
|
||||
message: 'Run gsd-tools intel update or spawn gsd-intel-updater agent for full refresh',
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user