* 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
190 lines
5.5 KiB
TypeScript
190 lines
5.5 KiB
TypeScript
/**
|
|
* Schema drift detection — ports `get-shit-done/bin/lib/schema-detect.cjs`.
|
|
* Used by `verify.schema-drift` to match gsd-tools.cjs JSON output.
|
|
*/
|
|
|
|
// ─── ORM patterns ─────────────────────────────────────────────────────────
|
|
|
|
const SCHEMA_PATTERNS: Array<{ pattern: RegExp; orm: string }> = [
|
|
{ pattern: /^src\/collections\/.*\.ts$/, orm: 'payload' },
|
|
{ pattern: /^src\/globals\/.*\.ts$/, orm: 'payload' },
|
|
{ pattern: /^prisma\/schema\.prisma$/, orm: 'prisma' },
|
|
{ pattern: /^prisma\/schema\/.*\.prisma$/, orm: 'prisma' },
|
|
{ pattern: /^drizzle\/schema\.ts$/, orm: 'drizzle' },
|
|
{ pattern: /^src\/db\/schema\.ts$/, orm: 'drizzle' },
|
|
{ pattern: /^drizzle\/.*\.ts$/, orm: 'drizzle' },
|
|
{ pattern: /^supabase\/migrations\/.*\.sql$/, orm: 'supabase' },
|
|
{ pattern: /^src\/entities\/.*\.ts$/, orm: 'typeorm' },
|
|
{ pattern: /^src\/migrations\/.*\.ts$/, orm: 'typeorm' },
|
|
];
|
|
|
|
const ORM_INFO: Record<
|
|
string,
|
|
{
|
|
pushCommand: string;
|
|
envHint: string | null;
|
|
interactiveWarning: string | null;
|
|
evidencePatterns: RegExp[];
|
|
}
|
|
> = {
|
|
payload: {
|
|
pushCommand: 'npx payload migrate',
|
|
envHint: 'CI=true PAYLOAD_MIGRATING=true npx payload migrate',
|
|
interactiveWarning:
|
|
'Payload migrate may require interactive prompts — use CI=true PAYLOAD_MIGRATING=true to suppress',
|
|
evidencePatterns: [/payload\s+migrate/i, /PAYLOAD_MIGRATING/],
|
|
},
|
|
prisma: {
|
|
pushCommand: 'npx prisma db push',
|
|
envHint: 'npx prisma db push --accept-data-loss (if destructive changes are intended)',
|
|
interactiveWarning:
|
|
'Prisma db push may prompt for confirmation on destructive changes — use --accept-data-loss to bypass',
|
|
evidencePatterns: [/prisma\s+db\s+push/i, /prisma\s+migrate\s+deploy/i, /prisma\s+migrate\s+dev/i],
|
|
},
|
|
drizzle: {
|
|
pushCommand: 'npx drizzle-kit push',
|
|
envHint: 'npx drizzle-kit push',
|
|
interactiveWarning: null,
|
|
evidencePatterns: [/drizzle-kit\s+push/i, /drizzle-kit\s+migrate/i],
|
|
},
|
|
supabase: {
|
|
pushCommand: 'supabase db push',
|
|
envHint: 'supabase db push',
|
|
interactiveWarning:
|
|
'Supabase db push may require authentication — ensure SUPABASE_ACCESS_TOKEN is set',
|
|
evidencePatterns: [/supabase\s+db\s+push/i, /supabase\s+migration\s+up/i],
|
|
},
|
|
typeorm: {
|
|
pushCommand: 'npx typeorm migration:run',
|
|
envHint: 'npx typeorm migration:run -d src/data-source.ts',
|
|
interactiveWarning: null,
|
|
evidencePatterns: [/typeorm\s+migration:run/i, /typeorm\s+schema:sync/i],
|
|
},
|
|
};
|
|
|
|
// ─── Public API ───────────────────────────────────────────────────────────
|
|
|
|
export function detectSchemaFiles(files: string[]): {
|
|
detected: boolean;
|
|
matches: string[];
|
|
orms: string[];
|
|
} {
|
|
const matches: string[] = [];
|
|
const orms = new Set<string>();
|
|
|
|
for (const rawFile of files) {
|
|
const file = rawFile.replace(/\\/g, '/');
|
|
for (const { pattern, orm } of SCHEMA_PATTERNS) {
|
|
if (pattern.test(file)) {
|
|
matches.push(rawFile);
|
|
orms.add(orm);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
return {
|
|
detected: matches.length > 0,
|
|
matches,
|
|
orms: [...orms],
|
|
};
|
|
}
|
|
|
|
export function checkSchemaDrift(
|
|
changedFiles: string[],
|
|
executionLog: string,
|
|
options: { skipCheck?: boolean } = {},
|
|
): {
|
|
driftDetected: boolean;
|
|
blocking: boolean;
|
|
schemaFiles: string[];
|
|
orms: string[];
|
|
unpushedOrms: string[];
|
|
message: string;
|
|
skipped?: boolean;
|
|
} {
|
|
const { skipCheck = false } = options;
|
|
|
|
const detection = detectSchemaFiles(changedFiles);
|
|
|
|
if (!detection.detected) {
|
|
return {
|
|
driftDetected: false,
|
|
blocking: false,
|
|
schemaFiles: [],
|
|
orms: [],
|
|
unpushedOrms: [],
|
|
message: '',
|
|
};
|
|
}
|
|
|
|
const pushedOrms = new Set<string>();
|
|
const unpushedOrms: string[] = [];
|
|
|
|
for (const orm of detection.orms) {
|
|
const info = ORM_INFO[orm];
|
|
if (!info) continue;
|
|
|
|
const hasPushEvidence = info.evidencePatterns.some(p => p.test(executionLog));
|
|
if (hasPushEvidence) {
|
|
pushedOrms.add(orm);
|
|
} else {
|
|
unpushedOrms.push(orm);
|
|
}
|
|
}
|
|
|
|
const driftDetected = unpushedOrms.length > 0;
|
|
|
|
if (!driftDetected) {
|
|
return {
|
|
driftDetected: false,
|
|
blocking: false,
|
|
schemaFiles: detection.matches,
|
|
orms: detection.orms,
|
|
unpushedOrms: [],
|
|
message: '',
|
|
};
|
|
}
|
|
|
|
const pushCommands = unpushedOrms
|
|
.map(orm => {
|
|
const info = ORM_INFO[orm];
|
|
return info ? ` ${orm}: ${info.envHint || info.pushCommand}` : null;
|
|
})
|
|
.filter(Boolean)
|
|
.join('\n');
|
|
|
|
const message = [
|
|
'Schema drift detected: schema-relevant files changed but no database push was executed.',
|
|
'',
|
|
`Schema files changed: ${detection.matches.join(', ')}`,
|
|
`ORMs requiring push: ${unpushedOrms.join(', ')}`,
|
|
'',
|
|
'Required push commands:',
|
|
pushCommands,
|
|
'',
|
|
'Run the appropriate push command, or set GSD_SKIP_SCHEMA_CHECK=true to bypass this gate.',
|
|
].join('\n');
|
|
|
|
if (skipCheck) {
|
|
return {
|
|
driftDetected: true,
|
|
blocking: false,
|
|
skipped: true,
|
|
schemaFiles: detection.matches,
|
|
orms: detection.orms,
|
|
unpushedOrms,
|
|
message: 'Schema drift detected but check was skipped (GSD_SKIP_SCHEMA_CHECK=true).',
|
|
};
|
|
}
|
|
|
|
return {
|
|
driftDetected: true,
|
|
blocking: true,
|
|
schemaFiles: detection.matches,
|
|
orms: detection.orms,
|
|
unpushedOrms,
|
|
message,
|
|
};
|
|
}
|