refactor(#189): retire generated cjs artifacts from bin/lib
This commit is contained in:
@@ -17,7 +17,7 @@ const {
|
||||
VALID_CONFIG_KEYS,
|
||||
RUNTIME_STATE_KEYS,
|
||||
DYNAMIC_KEY_PATTERNS,
|
||||
} = require('./configuration.generated.cjs');
|
||||
} = require('./configuration.cjs');
|
||||
|
||||
/**
|
||||
* Returns true if keyPath is a valid config key (exact, dynamic pattern, or runtime state).
|
||||
|
||||
@@ -671,7 +671,7 @@ function cmdConfigPath(cwd) {
|
||||
* summary when --raw is set. Exits 0 in all cases (including no-op).
|
||||
*/
|
||||
async function cmdMigrateConfig(cwd, raw) {
|
||||
const { migrateOnDisk } = require('./configuration.generated.cjs');
|
||||
const { migrateOnDisk } = require('./configuration.cjs');
|
||||
const ws = process.env.GSD_WORKSTREAM || null;
|
||||
const report = await migrateOnDisk(cwd, ws || undefined);
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ const {
|
||||
setActiveWorkstream,
|
||||
findContextMdIn,
|
||||
} = require('./planning-workspace.cjs');
|
||||
const { findProjectRoot } = require('./project-root.generated.cjs');
|
||||
const { findProjectRoot } = require('./project-root.cjs');
|
||||
|
||||
// ─── Configuration Module (generated CJS mirror) ────────────────────────────
|
||||
// Cycle 4: import canonical defaults + normalization primitives from the
|
||||
@@ -36,7 +36,7 @@ const { findProjectRoot } = require('./project-root.generated.cjs');
|
||||
const {
|
||||
CONFIG_DEFAULTS: CANONICAL_CONFIG_DEFAULTS,
|
||||
normalizeLegacyKeys,
|
||||
} = require('./configuration.generated.cjs');
|
||||
} = require('./configuration.cjs');
|
||||
|
||||
// ─── Path helpers ────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@@ -1,19 +1,121 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Decisions Module — CJS adapter.
|
||||
*
|
||||
* The implementation is generated from sdk/src/query/decisions.ts and
|
||||
* lives in decisions.generated.cjs. This file is a thin re-export so
|
||||
* that existing call sites (gap-checker.cjs, tests) can continue to
|
||||
* require('./decisions') unchanged.
|
||||
*
|
||||
* Exports (from generated file):
|
||||
* - parseDecisions(content) — parse <decisions> blocks, returns {id, text, category, tags, trackable}[]
|
||||
* CJS callers using only {id, text} safely ignore the extra fields.
|
||||
* Accepts both numeric (D-42) and alphanumeric (D-INFRA-01) IDs.
|
||||
* GENERATED FILE — DO NOT EDIT.
|
||||
*
|
||||
* Source: sdk/src/query/decisions.ts
|
||||
* Regenerate: cd sdk && npm run gen:decisions
|
||||
*
|
||||
* Shared parser for CONTEXT.md <decisions> blocks.
|
||||
* Accepts both numeric (D-42) and alphanumeric (D-INFRA-01) IDs.
|
||||
* Returns {id, text, category, tags, trackable} per decision.
|
||||
* CJS callers that only use {id, text} safely ignore the extra fields.
|
||||
*/
|
||||
|
||||
module.exports = require('./decisions.generated.cjs');
|
||||
const DISCRETION_HEADINGS = new Set([
|
||||
"claude's discretion",
|
||||
'claudes discretion',
|
||||
'claude discretion',
|
||||
]);
|
||||
const NON_TRACKABLE_TAGS = new Set(['informational', 'folded', 'deferred']);
|
||||
/**
|
||||
* Strip fenced code blocks from `content` so example `<decisions>` snippets
|
||||
* inside ```` ``` ```` do not pollute the parser (review F11).
|
||||
*/
|
||||
function stripFencedCode(content) {
|
||||
return content.replace(/```[\s\S]*?```/g, ' ').replace(/~~~[\s\S]*?~~~/g, ' ');
|
||||
}
|
||||
/**
|
||||
* Extract the inner text of EVERY `<decisions>...</decisions>` block in
|
||||
* order, concatenated by `\n\n`. Returns null when no block is present.
|
||||
*
|
||||
* CONTEXT.md may legitimately contain more than one block (for example, a
|
||||
* "current decisions" block plus a "carry-over from prior phase" block);
|
||||
* dropping all-but-the-first silently lost the second batch (review F13).
|
||||
*/
|
||||
function extractDecisionsBlock(content) {
|
||||
const cleaned = stripFencedCode(content);
|
||||
const matches = [...cleaned.matchAll(/<decisions>([\s\S]*?)<\/decisions>/g)];
|
||||
if (matches.length === 0)
|
||||
return null;
|
||||
return matches.map((m) => m[1]).join('\n\n');
|
||||
}
|
||||
/**
|
||||
* Parse trackable decisions from CONTEXT.md content.
|
||||
*
|
||||
* Returns ALL D-NN decisions found inside `<decisions>` (including
|
||||
* non-trackable ones, with `trackable: false`). Callers that only want the
|
||||
* gate-enforced decisions should filter `.filter(d => d.trackable)`.
|
||||
*/
|
||||
function parseDecisions(content) {
|
||||
if (!content || typeof content !== 'string')
|
||||
return [];
|
||||
const block = extractDecisionsBlock(content);
|
||||
if (block === null)
|
||||
return [];
|
||||
const lines = block.split(/\r?\n/);
|
||||
const out = [];
|
||||
let category = '';
|
||||
let inDiscretion = false;
|
||||
// Bullet line: `- **D-NN[ [tags]]:** text`
|
||||
// Phase 6 (#3575): aligned to CJS regex — accepts alphanumeric IDs (D-01, D-INFRA-01, D-FOO_BAR)
|
||||
// in addition to numeric-only IDs (D-42). The first character after `D-` must
|
||||
// be alphanumeric, so malformed shapes like `D--foo` or `D-_bar` are rejected.
|
||||
// CJS callers consume {id, text} and ignore the optional extras.
|
||||
const bulletRe = /^\s*-\s+\*\*D-([A-Za-z0-9][A-Za-z0-9_-]*)(?:\s*\[([^\]]+)\])?\s*:\*\*\s*(.*)$/;
|
||||
let current = null;
|
||||
const flush = () => {
|
||||
if (current) {
|
||||
current.text = current.text.trim();
|
||||
out.push(current);
|
||||
current = null;
|
||||
}
|
||||
};
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim();
|
||||
// Track category headings (`### Heading`)
|
||||
const headingMatch = trimmed.match(/^###\s+(.+?)\s*$/);
|
||||
if (headingMatch) {
|
||||
flush();
|
||||
category = headingMatch[1];
|
||||
// Strip the full unicode-quote family so any rendering of "Claude's
|
||||
// Discretion" (ASCII apostrophe, curly U+2019, U+2018, U+201A, U+201B,
|
||||
// double-quote variants U+201C/D/E/F, etc.) collapses to the same key
|
||||
// (review F20).
|
||||
const normalized = category
|
||||
.toLowerCase()
|
||||
.replace(/[\u2018\u2019\u201A\u201B\u201C\u201D\u201E\u201F'"`]/g, '')
|
||||
.trim();
|
||||
inDiscretion = DISCRETION_HEADINGS.has(normalized);
|
||||
continue;
|
||||
}
|
||||
const bulletMatch = line.match(bulletRe);
|
||||
if (bulletMatch) {
|
||||
flush();
|
||||
const id = `D-${bulletMatch[1]}`;
|
||||
const tags = bulletMatch[2]
|
||||
? bulletMatch[2]
|
||||
.split(',')
|
||||
.map((t) => t.trim().toLowerCase())
|
||||
.filter(Boolean)
|
||||
: [];
|
||||
const trackable = !inDiscretion && !tags.some((t) => NON_TRACKABLE_TAGS.has(t));
|
||||
current = { id, text: bulletMatch[3], category, tags, trackable };
|
||||
continue;
|
||||
}
|
||||
// Continuation line for current decision (indented with space OR tab,
|
||||
// non-bullet, non-empty) — tab indentation must work too (review F12).
|
||||
if (current && trimmed !== '' && !trimmed.startsWith('-') && /^[ \t]/.test(line)) {
|
||||
current.text += ' ' + trimmed;
|
||||
continue;
|
||||
}
|
||||
// Blank line or unrelated content terminates the current decision
|
||||
if (trimmed === '') {
|
||||
flush();
|
||||
}
|
||||
}
|
||||
flush();
|
||||
return out;
|
||||
}
|
||||
|
||||
module.exports = { parseDecisions };
|
||||
|
||||
@@ -1,121 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* GENERATED FILE — DO NOT EDIT.
|
||||
*
|
||||
* Source: sdk/src/query/decisions.ts
|
||||
* Regenerate: cd sdk && npm run gen:decisions
|
||||
*
|
||||
* Shared parser for CONTEXT.md <decisions> blocks.
|
||||
* Accepts both numeric (D-42) and alphanumeric (D-INFRA-01) IDs.
|
||||
* Returns {id, text, category, tags, trackable} per decision.
|
||||
* CJS callers that only use {id, text} safely ignore the extra fields.
|
||||
*/
|
||||
|
||||
const DISCRETION_HEADINGS = new Set([
|
||||
"claude's discretion",
|
||||
'claudes discretion',
|
||||
'claude discretion',
|
||||
]);
|
||||
const NON_TRACKABLE_TAGS = new Set(['informational', 'folded', 'deferred']);
|
||||
/**
|
||||
* Strip fenced code blocks from `content` so example `<decisions>` snippets
|
||||
* inside ```` ``` ```` do not pollute the parser (review F11).
|
||||
*/
|
||||
function stripFencedCode(content) {
|
||||
return content.replace(/```[\s\S]*?```/g, ' ').replace(/~~~[\s\S]*?~~~/g, ' ');
|
||||
}
|
||||
/**
|
||||
* Extract the inner text of EVERY `<decisions>...</decisions>` block in
|
||||
* order, concatenated by `\n\n`. Returns null when no block is present.
|
||||
*
|
||||
* CONTEXT.md may legitimately contain more than one block (for example, a
|
||||
* "current decisions" block plus a "carry-over from prior phase" block);
|
||||
* dropping all-but-the-first silently lost the second batch (review F13).
|
||||
*/
|
||||
function extractDecisionsBlock(content) {
|
||||
const cleaned = stripFencedCode(content);
|
||||
const matches = [...cleaned.matchAll(/<decisions>([\s\S]*?)<\/decisions>/g)];
|
||||
if (matches.length === 0)
|
||||
return null;
|
||||
return matches.map((m) => m[1]).join('\n\n');
|
||||
}
|
||||
/**
|
||||
* Parse trackable decisions from CONTEXT.md content.
|
||||
*
|
||||
* Returns ALL D-NN decisions found inside `<decisions>` (including
|
||||
* non-trackable ones, with `trackable: false`). Callers that only want the
|
||||
* gate-enforced decisions should filter `.filter(d => d.trackable)`.
|
||||
*/
|
||||
function parseDecisions(content) {
|
||||
if (!content || typeof content !== 'string')
|
||||
return [];
|
||||
const block = extractDecisionsBlock(content);
|
||||
if (block === null)
|
||||
return [];
|
||||
const lines = block.split(/\r?\n/);
|
||||
const out = [];
|
||||
let category = '';
|
||||
let inDiscretion = false;
|
||||
// Bullet line: `- **D-NN[ [tags]]:** text`
|
||||
// Phase 6 (#3575): aligned to CJS regex — accepts alphanumeric IDs (D-01, D-INFRA-01, D-FOO_BAR)
|
||||
// in addition to numeric-only IDs (D-42). The first character after `D-` must
|
||||
// be alphanumeric, so malformed shapes like `D--foo` or `D-_bar` are rejected.
|
||||
// CJS callers consume {id, text} and ignore the optional extras.
|
||||
const bulletRe = /^\s*-\s+\*\*D-([A-Za-z0-9][A-Za-z0-9_-]*)(?:\s*\[([^\]]+)\])?\s*:\*\*\s*(.*)$/;
|
||||
let current = null;
|
||||
const flush = () => {
|
||||
if (current) {
|
||||
current.text = current.text.trim();
|
||||
out.push(current);
|
||||
current = null;
|
||||
}
|
||||
};
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim();
|
||||
// Track category headings (`### Heading`)
|
||||
const headingMatch = trimmed.match(/^###\s+(.+?)\s*$/);
|
||||
if (headingMatch) {
|
||||
flush();
|
||||
category = headingMatch[1];
|
||||
// Strip the full unicode-quote family so any rendering of "Claude's
|
||||
// Discretion" (ASCII apostrophe, curly U+2019, U+2018, U+201A, U+201B,
|
||||
// double-quote variants U+201C/D/E/F, etc.) collapses to the same key
|
||||
// (review F20).
|
||||
const normalized = category
|
||||
.toLowerCase()
|
||||
.replace(/[\u2018\u2019\u201A\u201B\u201C\u201D\u201E\u201F'"`]/g, '')
|
||||
.trim();
|
||||
inDiscretion = DISCRETION_HEADINGS.has(normalized);
|
||||
continue;
|
||||
}
|
||||
const bulletMatch = line.match(bulletRe);
|
||||
if (bulletMatch) {
|
||||
flush();
|
||||
const id = `D-${bulletMatch[1]}`;
|
||||
const tags = bulletMatch[2]
|
||||
? bulletMatch[2]
|
||||
.split(',')
|
||||
.map((t) => t.trim().toLowerCase())
|
||||
.filter(Boolean)
|
||||
: [];
|
||||
const trackable = !inDiscretion && !tags.some((t) => NON_TRACKABLE_TAGS.has(t));
|
||||
current = { id, text: bulletMatch[3], category, tags, trackable };
|
||||
continue;
|
||||
}
|
||||
// Continuation line for current decision (indented with space OR tab,
|
||||
// non-bullet, non-empty) — tab indentation must work too (review F12).
|
||||
if (current && trimmed !== '' && !trimmed.startsWith('-') && /^[ \t]/.test(line)) {
|
||||
current.text += ' ' + trimmed;
|
||||
continue;
|
||||
}
|
||||
// Blank line or unrelated content terminates the current decision
|
||||
if (trimmed === '') {
|
||||
flush();
|
||||
}
|
||||
}
|
||||
flush();
|
||||
return out;
|
||||
}
|
||||
|
||||
module.exports = { parseDecisions };
|
||||
@@ -1,6 +1,6 @@
|
||||
'use strict';
|
||||
|
||||
const { INIT_SUBCOMMANDS } = require('./command-aliases.generated.cjs');
|
||||
const { INIT_SUBCOMMANDS } = require('./command-aliases.cjs');
|
||||
const { routeCjsCommandFamily } = require('./cjs-command-router-adapter.cjs');
|
||||
const { output } = require('./core.cjs');
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use strict';
|
||||
|
||||
const { PHASE_SUBCOMMANDS } = require('./command-aliases.generated.cjs');
|
||||
const { PHASE_SUBCOMMANDS } = require('./command-aliases.cjs');
|
||||
|
||||
// ─── CommandRoutingHub (issue #3788, simplified in #175, typed in #176) ───────
|
||||
const { createHub, ERROR_KINDS, makeInvalidArgs } = require('./command-routing-hub.cjs');
|
||||
|
||||
@@ -1,202 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* GENERATED FILE — DO NOT EDIT.
|
||||
*
|
||||
* Source: sdk/src/query/phase-lifecycle-policy.ts
|
||||
* Regenerate: cd sdk && npm run gen:phase-lifecycle-policy
|
||||
*
|
||||
* Phase Lifecycle Policy — pure computation helpers for phase directory naming,
|
||||
* roadmap entry generation, decimal-phase management, and ID computation.
|
||||
* No I/O. No async. No filesystem operations.
|
||||
*
|
||||
* I/O adapter pattern (ADR-3524 §4): pure transforms extracted from the SDK;
|
||||
* GSDError is replaced with plain throws that CJS callers can catch.
|
||||
*
|
||||
* References:
|
||||
* - ADR-3524 (docs/adr/3524-cjs-sdk-hard-seam.md)
|
||||
* - Issue #4 (open-gsd/get-shit-done-redux)
|
||||
*/
|
||||
|
||||
// Lightweight stub replacing sdk/src/errors.js GSDError.
|
||||
// CJS callers that need to translate to process.exit(1) should catch these.
|
||||
class GSDError extends Error {
|
||||
constructor(message, classification) {
|
||||
super(message);
|
||||
this.name = 'GSDError';
|
||||
this.classification = classification;
|
||||
}
|
||||
}
|
||||
// ErrorClassification values used by policy functions
|
||||
const ErrorClassification = { Validation: 'Validation', Internal: 'Internal' };
|
||||
|
||||
// escapeRegex — inlined from sdk/dist/query/helpers.js
|
||||
function escapeRegex(value) {
|
||||
return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
}
|
||||
|
||||
function assertNoNullBytes(value, label) {
|
||||
if (value.includes('\0')) {
|
||||
throw new GSDError(`${label} contains null byte`, ErrorClassification.Validation);
|
||||
}
|
||||
}
|
||||
|
||||
function assertSafePhaseDirName(dirName, label = 'phase directory') {
|
||||
if (/[/\\]|\.\./.test(dirName)) {
|
||||
throw new GSDError(`${label} contains invalid path segments`, ErrorClassification.Validation);
|
||||
}
|
||||
}
|
||||
|
||||
function assertSafeProjectCode(code) {
|
||||
if (code && /[/\\]|\.\./.test(code)) {
|
||||
throw new GSDError('project_code contains invalid characters', ErrorClassification.Validation);
|
||||
}
|
||||
}
|
||||
|
||||
function generatePhaseSlug(text) {
|
||||
return text
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, '-')
|
||||
.replace(/^-+|-+$/g, '')
|
||||
.substring(0, 60);
|
||||
}
|
||||
|
||||
function parseMultiwordArg(args, flag) {
|
||||
const idx = args.indexOf(`--${flag}`);
|
||||
if (idx === -1)
|
||||
return null;
|
||||
const tokens = [];
|
||||
for (let i = idx + 1; i < args.length; i++) {
|
||||
if (args[i].startsWith('--'))
|
||||
break;
|
||||
tokens.push(args[i]);
|
||||
}
|
||||
return tokens.length > 0 ? tokens.join(' ') : null;
|
||||
}
|
||||
|
||||
function extractOneLinerFromBody(content) {
|
||||
if (!content)
|
||||
return null;
|
||||
const body = content.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n*/, '');
|
||||
// Find the first heading of any level (GFM section ATX headings)
|
||||
const headingMatch = body.match(/^(#{1,6}\s[^\n]+\n)/m);
|
||||
if (!headingMatch || headingMatch.index === undefined)
|
||||
return null;
|
||||
const afterHeading = body.slice(headingMatch.index + headingMatch[0].length);
|
||||
// Bound to the first section: truncate at the next heading of any level
|
||||
const nextHeadingMatch = afterHeading.match(/^#{1,6}\s/m);
|
||||
const sectionScope = nextHeadingMatch && nextHeadingMatch.index !== undefined
|
||||
? afterHeading.slice(0, nextHeadingMatch.index)
|
||||
: afterHeading;
|
||||
const boldMatch = sectionScope.match(/\*\*([^*]+)\*\*/);
|
||||
return boldMatch ? boldMatch[1].trim() : null;
|
||||
}
|
||||
|
||||
function scanSequentialMaxPhaseFromMilestone(milestoneContent) {
|
||||
const phasePattern = /(?:^|\n)\s*(?:[-*]\s*(?:\[[x ]\]\s*)?|#{2,4}\s*|\*{1,2}\s*)Phase\s+(\d+)[A-Z]?(?:\.\d+)*:/gi;
|
||||
let maxPhase = 0;
|
||||
let m;
|
||||
while ((m = phasePattern.exec(milestoneContent)) !== null) {
|
||||
const num = parseInt(m[1], 10);
|
||||
if (num === 999)
|
||||
continue;
|
||||
if (num > maxPhase)
|
||||
maxPhase = num;
|
||||
}
|
||||
return maxPhase;
|
||||
}
|
||||
|
||||
function scanSequentialMaxPhaseFromDirs(dirNames) {
|
||||
let maxPhase = 0;
|
||||
const dirNumPattern = /^(?:[A-Z][A-Z0-9]*-)?(\d+)[A-Z]?(?:\.\d+)*-/i;
|
||||
for (const dirName of dirNames) {
|
||||
const match = dirNumPattern.exec(dirName);
|
||||
if (!match)
|
||||
continue;
|
||||
const num = parseInt(match[1], 10);
|
||||
if (num === 999)
|
||||
continue;
|
||||
if (num > maxPhase)
|
||||
maxPhase = num;
|
||||
}
|
||||
return maxPhase;
|
||||
}
|
||||
|
||||
function computeNextSequentialPhaseId(milestoneContent, dirNames) {
|
||||
return Math.max(scanSequentialMaxPhaseFromMilestone(milestoneContent), scanSequentialMaxPhaseFromDirs(dirNames)) + 1;
|
||||
}
|
||||
|
||||
function computePhaseDirectory(namingMode, descriptionSlug, prefix, nextSequentialPhaseId, customId) {
|
||||
if (customId || namingMode === 'custom') {
|
||||
const phaseId = customId || descriptionSlug.toUpperCase().replace(/-/g, '_');
|
||||
if (!phaseId) {
|
||||
throw new GSDError('--id required when phase_naming is "custom"', ErrorClassification.Validation);
|
||||
}
|
||||
assertSafePhaseDirName(String(phaseId), 'custom phase id');
|
||||
const dirName = `${prefix}${phaseId}-${descriptionSlug}`;
|
||||
assertSafePhaseDirName(dirName);
|
||||
return { phaseId, dirName };
|
||||
}
|
||||
const phaseId = nextSequentialPhaseId;
|
||||
const paddedNum = String(phaseId).padStart(2, '0');
|
||||
const dirName = `${prefix}${paddedNum}-${descriptionSlug}`;
|
||||
assertSafePhaseDirName(dirName);
|
||||
return { phaseId, dirName };
|
||||
}
|
||||
|
||||
function buildPhaseRoadmapEntry(phaseId, description, namingMode) {
|
||||
const prevPhase = typeof phaseId === 'number' ? phaseId - 1 : null;
|
||||
const dependsOn = namingMode === 'custom' || prevPhase === null || prevPhase < 1
|
||||
? ''
|
||||
: `\n**Depends on:** Phase ${prevPhase}`;
|
||||
return `\n### Phase ${phaseId}: ${description}\n\n**Goal:** [To be planned]\n**Requirements**: TBD${dependsOn}\n**Plans:** 0 plans\n\nPlans:\n- [ ] TBD (run /gsd-plan-phase ${phaseId} to break down)\n`;
|
||||
}
|
||||
|
||||
function collectDecimalSuffixesFromDirNames(basePhase, dirNames) {
|
||||
const decimalSet = new Set();
|
||||
const decimalPattern = new RegExp(`^(?:[A-Z][A-Z0-9]*-)?${escapeRegex(basePhase)}\\.(\\d+)`, 'i');
|
||||
for (const dir of dirNames) {
|
||||
const match = dir.match(decimalPattern);
|
||||
if (match)
|
||||
decimalSet.add(parseInt(match[1], 10));
|
||||
}
|
||||
return decimalSet;
|
||||
}
|
||||
|
||||
function collectDecimalSuffixesFromRoadmap(basePhase, roadmapContent) {
|
||||
const decimalSet = new Set();
|
||||
const phasePattern = new RegExp(`#{2,4}\\s*Phase\\s+0*${escapeRegex(basePhase)}\\.(\\d+)\\s*:`, 'gi');
|
||||
let match;
|
||||
while ((match = phasePattern.exec(roadmapContent)) !== null) {
|
||||
decimalSet.add(parseInt(match[1], 10));
|
||||
}
|
||||
return decimalSet;
|
||||
}
|
||||
|
||||
function computeNextDecimalPhase(basePhase, decimalSet) {
|
||||
const existing = Array.from(decimalSet)
|
||||
.sort((a, b) => a - b)
|
||||
.map((n) => `${basePhase}.${n}`);
|
||||
const next = decimalSet.size === 0
|
||||
? `${basePhase}.1`
|
||||
: `${basePhase}.${Math.max(...decimalSet) + 1}`;
|
||||
return { next, existing };
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
GSDError,
|
||||
assertNoNullBytes,
|
||||
assertSafePhaseDirName,
|
||||
assertSafeProjectCode,
|
||||
generatePhaseSlug,
|
||||
parseMultiwordArg,
|
||||
extractOneLinerFromBody,
|
||||
scanSequentialMaxPhaseFromMilestone,
|
||||
scanSequentialMaxPhaseFromDirs,
|
||||
computeNextSequentialPhaseId,
|
||||
computePhaseDirectory,
|
||||
buildPhaseRoadmapEntry,
|
||||
collectDecimalSuffixesFromDirNames,
|
||||
collectDecimalSuffixesFromRoadmap,
|
||||
computeNextDecimalPhase,
|
||||
};
|
||||
@@ -22,7 +22,7 @@ const { writeStateMd, readModifyWriteStateMd, stateExtractField, stateReplaceFie
|
||||
const { formatGsdSlash, resolveRuntime } = require('./runtime-slash.cjs');
|
||||
// Generated pure-computation helpers for cmdPhaseComplete (issue #4 fix).
|
||||
// Source: sdk/src/query/phase-lifecycle.ts. Regenerate: node sdk/scripts/gen-phase-lifecycle.mjs
|
||||
const { deriveProgressFromRoadmap, clampPercent } = require('./phase-lifecycle.generated.cjs');
|
||||
const { deriveProgressFromRoadmap, clampPercent } = require('./phase-lifecycle.cjs');
|
||||
|
||||
// #2893 — strict canonical filter: `{padded_phase}-{NN}-PLAN.md` or `PLAN.md`.
|
||||
// Documented in agents/gsd-planner.md (write_phase_prompt step). The wider
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* GENERATED FILE — DO NOT EDIT.
|
||||
*
|
||||
* Source: sdk/src/query/phase.ts
|
||||
* Regenerate: cd sdk && npm run gen:phase
|
||||
*
|
||||
* Phase Query Module — pure helper functions shared between the CJS CLI and SDK.
|
||||
* No I/O. No async. No filesystem operations.
|
||||
*
|
||||
* Scope: isCanonicalPlanFile, describeNonCanonicalPlans.
|
||||
* Async query handlers (findPhase, phasePlanIndex) are I/O-bound and remain
|
||||
* per-side per ADR-3524 §4.
|
||||
*
|
||||
* References:
|
||||
* - ADR-3524 (docs/adr/3524-cjs-sdk-hard-seam.md)
|
||||
* - Issue #4 (open-gsd/get-shit-done-redux)
|
||||
*/
|
||||
|
||||
const isCanonicalPlanFile = (f) => f.endsWith('-PLAN.md') || f === 'PLAN.md';
|
||||
|
||||
// Regex constants closed over by describeNonCanonicalPlans (from phase.js module scope)
|
||||
const PLAN_OUTLINE_RE = /-PLAN-OUTLINE\.md$/i;
|
||||
const PLAN_PRE_BOUNCE_RE = /-PLAN.*\.pre-bounce\.md$/i;
|
||||
const looksLikePlanFile = (f) =>
|
||||
/\.md$/i.test(f)
|
||||
&& /PLAN/i.test(f)
|
||||
&& !PLAN_OUTLINE_RE.test(f)
|
||||
&& !PLAN_PRE_BOUNCE_RE.test(f);
|
||||
|
||||
function describeNonCanonicalPlans(dirFiles, matchedFiles) {
|
||||
const matched = new Set(matchedFiles);
|
||||
const offenders = dirFiles.filter((f) => looksLikePlanFile(f) && !matched.has(f));
|
||||
if (offenders.length === 0)
|
||||
return null;
|
||||
return (`Found ${offenders.length} plan-shaped file(s) in this phase that don't match the canonical `
|
||||
+ `naming convention "{padded_phase}-{NN}-PLAN.md" (or bare "PLAN.md") and were skipped: `
|
||||
+ offenders.map((f) => `"${f}"`).join(', ')
|
||||
+ `. Rename to the canonical form (e.g. "01-01-PLAN.md") so the executor can detect them. `
|
||||
+ `See agents/gsd-planner.md write_phase_prompt step for the full contract.`);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
isCanonicalPlanFile,
|
||||
describeNonCanonicalPlans,
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
'use strict';
|
||||
|
||||
const { PHASES_SUBCOMMANDS } = require('./command-aliases.generated.cjs');
|
||||
const { PHASES_SUBCOMMANDS } = require('./command-aliases.cjs');
|
||||
const { routeCjsCommandFamily } = require('./cjs-command-router-adapter.cjs');
|
||||
const { output } = require('./core.cjs');
|
||||
|
||||
|
||||
@@ -1,26 +1,97 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Plan Scan Module — CJS adapter.
|
||||
*
|
||||
* The implementation is generated from sdk/src/query/plan-scan.ts and
|
||||
* lives in plan-scan.generated.cjs. This file is a thin re-export so
|
||||
* that existing call sites (state.cjs, roadmap.cjs, init.cjs,
|
||||
* workstream-inventory.cjs, and tests) can continue to require('./plan-scan')
|
||||
* unchanged.
|
||||
*
|
||||
* Exports (from generated file):
|
||||
* - scanPhasePlans(phaseDir) — canonical phase-plan scanner
|
||||
* - isRootPlanFile(fileName) — extended filter including /PLAN/i slug layouts
|
||||
* - isNestedPlanFile(fileName) — nested plans/ subdir filter
|
||||
* - isRootSummaryFile(fileName) — flat summary file filter
|
||||
* - isNestedSummaryFile(fileName) — nested summary file filter
|
||||
*
|
||||
* The isRootPlanFile helper uses /PLAN/i to match the extended slug layout
|
||||
* (e.g. 5-PLAN-01-setup-database.md) in addition to bare and canonical forms.
|
||||
* This was the fix for bug #3128 (roadmap.cjs plan-count regression).
|
||||
* GENERATED FILE — DO NOT EDIT.
|
||||
*
|
||||
* Source: sdk/src/query/plan-scan.ts
|
||||
* Regenerate: cd sdk && npm run gen:plan-scan
|
||||
*
|
||||
* Plan Scan Module — detects plan and summary files in a phase directory.
|
||||
* Supports both flat (pre-#3139) and nested (post-#3139) layouts.
|
||||
*/
|
||||
|
||||
module.exports = require('./plan-scan.generated.cjs');
|
||||
const { existsSync, readdirSync } = require('node:fs');
|
||||
const { join } = require('node:path');
|
||||
|
||||
// Excluded derivative files
|
||||
const PLAN_OUTLINE_RE = /-OUTLINE\.md$/i;
|
||||
const PLAN_PRE_BOUNCE_RE = /\.pre-bounce\.md$/i;
|
||||
|
||||
function isRootPlanFile(fileName) {
|
||||
if (PLAN_OUTLINE_RE.test(fileName))
|
||||
return false;
|
||||
if (PLAN_PRE_BOUNCE_RE.test(fileName))
|
||||
return false;
|
||||
if (fileName.endsWith('-PLAN.md') || fileName === 'PLAN.md')
|
||||
return true;
|
||||
return /\.md$/i.test(fileName) && /PLAN/i.test(fileName);
|
||||
}
|
||||
|
||||
function isNestedPlanFile(fileName) {
|
||||
if (PLAN_OUTLINE_RE.test(fileName))
|
||||
return false;
|
||||
if (PLAN_PRE_BOUNCE_RE.test(fileName))
|
||||
return false;
|
||||
return /^PLAN-\d+.*\.md$/i.test(fileName) || /-PLAN-\d+.*\.md$/i.test(fileName);
|
||||
}
|
||||
|
||||
function isRootSummaryFile(fileName) {
|
||||
return fileName.endsWith('-SUMMARY.md') || fileName === 'SUMMARY.md';
|
||||
}
|
||||
|
||||
function isNestedSummaryFile(fileName) {
|
||||
return /^SUMMARY-\d+.*\.md$/i.test(fileName) || /-SUMMARY-\d+.*\.md$/i.test(fileName);
|
||||
}
|
||||
|
||||
function scanPhasePlans(phaseDir) {
|
||||
let rootFiles;
|
||||
try {
|
||||
rootFiles = readdirSync(phaseDir);
|
||||
}
|
||||
catch {
|
||||
return {
|
||||
planCount: 0,
|
||||
summaryCount: 0,
|
||||
completed: false,
|
||||
hasNestedPlans: false,
|
||||
planFiles: [],
|
||||
summaryFiles: [],
|
||||
};
|
||||
}
|
||||
const rootPlanFiles = rootFiles.filter(isRootPlanFile);
|
||||
const rootSummaryFiles = rootFiles.filter(isRootSummaryFile);
|
||||
let nestedPlanFiles = [];
|
||||
let nestedSummaryFiles = [];
|
||||
let hasNestedPlans = false;
|
||||
const nestedDir = join(phaseDir, 'plans');
|
||||
if (existsSync(nestedDir)) {
|
||||
try {
|
||||
const nestedFiles = readdirSync(nestedDir);
|
||||
nestedPlanFiles = nestedFiles.filter(isNestedPlanFile);
|
||||
nestedSummaryFiles = nestedFiles.filter(isNestedSummaryFile);
|
||||
hasNestedPlans = nestedPlanFiles.length > 0;
|
||||
}
|
||||
catch { /* ignore unreadable nested layout */ }
|
||||
}
|
||||
const planFiles = rootPlanFiles.concat(nestedPlanFiles);
|
||||
const summaryFiles = rootSummaryFiles.concat(nestedSummaryFiles);
|
||||
const planCount = planFiles.length;
|
||||
const summaryCount = summaryFiles.length;
|
||||
return {
|
||||
planCount,
|
||||
summaryCount,
|
||||
completed: planCount > 0 && summaryCount >= planCount,
|
||||
hasNestedPlans,
|
||||
planFiles,
|
||||
summaryFiles,
|
||||
};
|
||||
}
|
||||
|
||||
// CJS callers do: const scanPhasePlans = require('./plan-scan.cjs')
|
||||
// and also destructure named exports — support both call styles.
|
||||
module.exports = scanPhasePlans;
|
||||
module.exports.scanPhasePlans = scanPhasePlans;
|
||||
module.exports.isRootPlanFile = isRootPlanFile;
|
||||
module.exports.isNestedPlanFile = isNestedPlanFile;
|
||||
module.exports.isRootSummaryFile = isRootSummaryFile;
|
||||
module.exports.isNestedSummaryFile = isNestedSummaryFile;
|
||||
|
||||
@@ -1,97 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* GENERATED FILE — DO NOT EDIT.
|
||||
*
|
||||
* Source: sdk/src/query/plan-scan.ts
|
||||
* Regenerate: cd sdk && npm run gen:plan-scan
|
||||
*
|
||||
* Plan Scan Module — detects plan and summary files in a phase directory.
|
||||
* Supports both flat (pre-#3139) and nested (post-#3139) layouts.
|
||||
*/
|
||||
|
||||
const { existsSync, readdirSync } = require('node:fs');
|
||||
const { join } = require('node:path');
|
||||
|
||||
// Excluded derivative files
|
||||
const PLAN_OUTLINE_RE = /-OUTLINE\.md$/i;
|
||||
const PLAN_PRE_BOUNCE_RE = /\.pre-bounce\.md$/i;
|
||||
|
||||
function isRootPlanFile(fileName) {
|
||||
if (PLAN_OUTLINE_RE.test(fileName))
|
||||
return false;
|
||||
if (PLAN_PRE_BOUNCE_RE.test(fileName))
|
||||
return false;
|
||||
if (fileName.endsWith('-PLAN.md') || fileName === 'PLAN.md')
|
||||
return true;
|
||||
return /\.md$/i.test(fileName) && /PLAN/i.test(fileName);
|
||||
}
|
||||
|
||||
function isNestedPlanFile(fileName) {
|
||||
if (PLAN_OUTLINE_RE.test(fileName))
|
||||
return false;
|
||||
if (PLAN_PRE_BOUNCE_RE.test(fileName))
|
||||
return false;
|
||||
return /^PLAN-\d+.*\.md$/i.test(fileName) || /-PLAN-\d+.*\.md$/i.test(fileName);
|
||||
}
|
||||
|
||||
function isRootSummaryFile(fileName) {
|
||||
return fileName.endsWith('-SUMMARY.md') || fileName === 'SUMMARY.md';
|
||||
}
|
||||
|
||||
function isNestedSummaryFile(fileName) {
|
||||
return /^SUMMARY-\d+.*\.md$/i.test(fileName) || /-SUMMARY-\d+.*\.md$/i.test(fileName);
|
||||
}
|
||||
|
||||
function scanPhasePlans(phaseDir) {
|
||||
let rootFiles;
|
||||
try {
|
||||
rootFiles = readdirSync(phaseDir);
|
||||
}
|
||||
catch {
|
||||
return {
|
||||
planCount: 0,
|
||||
summaryCount: 0,
|
||||
completed: false,
|
||||
hasNestedPlans: false,
|
||||
planFiles: [],
|
||||
summaryFiles: [],
|
||||
};
|
||||
}
|
||||
const rootPlanFiles = rootFiles.filter(isRootPlanFile);
|
||||
const rootSummaryFiles = rootFiles.filter(isRootSummaryFile);
|
||||
let nestedPlanFiles = [];
|
||||
let nestedSummaryFiles = [];
|
||||
let hasNestedPlans = false;
|
||||
const nestedDir = join(phaseDir, 'plans');
|
||||
if (existsSync(nestedDir)) {
|
||||
try {
|
||||
const nestedFiles = readdirSync(nestedDir);
|
||||
nestedPlanFiles = nestedFiles.filter(isNestedPlanFile);
|
||||
nestedSummaryFiles = nestedFiles.filter(isNestedSummaryFile);
|
||||
hasNestedPlans = nestedPlanFiles.length > 0;
|
||||
}
|
||||
catch { /* ignore unreadable nested layout */ }
|
||||
}
|
||||
const planFiles = rootPlanFiles.concat(nestedPlanFiles);
|
||||
const summaryFiles = rootSummaryFiles.concat(nestedSummaryFiles);
|
||||
const planCount = planFiles.length;
|
||||
const summaryCount = summaryFiles.length;
|
||||
return {
|
||||
planCount,
|
||||
summaryCount,
|
||||
completed: planCount > 0 && summaryCount >= planCount,
|
||||
hasNestedPlans,
|
||||
planFiles,
|
||||
summaryFiles,
|
||||
};
|
||||
}
|
||||
|
||||
// CJS callers do: const scanPhasePlans = require('./plan-scan.cjs')
|
||||
// and also destructure named exports — support both call styles.
|
||||
module.exports = scanPhasePlans;
|
||||
module.exports.scanPhasePlans = scanPhasePlans;
|
||||
module.exports.isRootPlanFile = isRootPlanFile;
|
||||
module.exports.isNestedPlanFile = isNestedPlanFile;
|
||||
module.exports.isRootSummaryFile = isRootSummaryFile;
|
||||
module.exports.isNestedSummaryFile = isNestedSummaryFile;
|
||||
@@ -1,6 +1,6 @@
|
||||
'use strict';
|
||||
|
||||
const { ROADMAP_SUBCOMMANDS } = require('./command-aliases.generated.cjs');
|
||||
const { ROADMAP_SUBCOMMANDS } = require('./command-aliases.cjs');
|
||||
const { routeCjsCommandFamily } = require('./cjs-command-router-adapter.cjs');
|
||||
const { output } = require('./core.cjs');
|
||||
|
||||
|
||||
@@ -1,21 +1,170 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Schema Detect Module — CJS adapter.
|
||||
*
|
||||
* The implementation is generated from sdk/src/query/schema-detect.ts and
|
||||
* lives in schema-detect.generated.cjs. This file is a thin re-export so
|
||||
* that existing call sites (verify.cjs and tests) can continue to
|
||||
* require('./schema-detect') unchanged.
|
||||
*
|
||||
* Exports (from generated file):
|
||||
* - SCHEMA_PATTERNS — ORM file pattern list
|
||||
* - ORM_INFO — ORM push commands and evidence patterns
|
||||
* - detectSchemaFiles(files) — detect schema-relevant files
|
||||
* - detectSchemaOrm(ormName) — get ORM-specific push command info
|
||||
* - checkSchemaDrift(changedFiles, executionLog, options) — check for drift
|
||||
* GENERATED FILE — DO NOT EDIT.
|
||||
*
|
||||
* Source: sdk/src/query/schema-detect.ts
|
||||
* Regenerate: cd sdk && npm run gen:schema-detect
|
||||
*
|
||||
* Schema Drift Detection — detects schema-relevant file changes and verifies
|
||||
* that the appropriate database push command was executed during a phase.
|
||||
* This module does not read the filesystem directly.
|
||||
*/
|
||||
|
||||
module.exports = require('./schema-detect.generated.cjs');
|
||||
// ─── ORM Patterns ───────────────────────────────────────────────────────────
|
||||
const SCHEMA_PATTERNS = [
|
||||
{ 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' },
|
||||
];
|
||||
|
||||
// ─── Push Commands & Evidence Patterns ──────────────────────────────────────
|
||||
const ORM_INFO = {
|
||||
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 ──────────────────────────────────────────────────────────────
|
||||
function detectSchemaFiles(files) {
|
||||
const matches = [];
|
||||
const orms = new Set();
|
||||
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],
|
||||
};
|
||||
}
|
||||
|
||||
function detectSchemaOrm(ormName) {
|
||||
return ORM_INFO[ormName] || null;
|
||||
}
|
||||
|
||||
function checkSchemaDrift(changedFiles, executionLog, options = {}) {
|
||||
const { skipCheck = false } = options;
|
||||
const detection = detectSchemaFiles(changedFiles);
|
||||
if (!detection.detected) {
|
||||
return {
|
||||
driftDetected: false,
|
||||
blocking: false,
|
||||
schemaFiles: [],
|
||||
orms: [],
|
||||
unpushedOrms: [],
|
||||
message: '',
|
||||
};
|
||||
}
|
||||
const pushedOrms = new Set();
|
||||
const unpushedOrms = [];
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
SCHEMA_PATTERNS,
|
||||
ORM_INFO,
|
||||
detectSchemaFiles,
|
||||
detectSchemaOrm,
|
||||
checkSchemaDrift,
|
||||
};
|
||||
|
||||
@@ -1,170 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* GENERATED FILE — DO NOT EDIT.
|
||||
*
|
||||
* Source: sdk/src/query/schema-detect.ts
|
||||
* Regenerate: cd sdk && npm run gen:schema-detect
|
||||
*
|
||||
* Schema Drift Detection — detects schema-relevant file changes and verifies
|
||||
* that the appropriate database push command was executed during a phase.
|
||||
* This module does not read the filesystem directly.
|
||||
*/
|
||||
|
||||
// ─── ORM Patterns ───────────────────────────────────────────────────────────
|
||||
const SCHEMA_PATTERNS = [
|
||||
{ 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' },
|
||||
];
|
||||
|
||||
// ─── Push Commands & Evidence Patterns ──────────────────────────────────────
|
||||
const ORM_INFO = {
|
||||
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 ──────────────────────────────────────────────────────────────
|
||||
function detectSchemaFiles(files) {
|
||||
const matches = [];
|
||||
const orms = new Set();
|
||||
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],
|
||||
};
|
||||
}
|
||||
|
||||
function detectSchemaOrm(ormName) {
|
||||
return ORM_INFO[ormName] || null;
|
||||
}
|
||||
|
||||
function checkSchemaDrift(changedFiles, executionLog, options = {}) {
|
||||
const { skipCheck = false } = options;
|
||||
const detection = detectSchemaFiles(changedFiles);
|
||||
if (!detection.detected) {
|
||||
return {
|
||||
driftDetected: false,
|
||||
blocking: false,
|
||||
schemaFiles: [],
|
||||
orms: [],
|
||||
unpushedOrms: [],
|
||||
message: '',
|
||||
};
|
||||
}
|
||||
const pushedOrms = new Set();
|
||||
const unpushedOrms = [];
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
SCHEMA_PATTERNS,
|
||||
ORM_INFO,
|
||||
detectSchemaFiles,
|
||||
detectSchemaOrm,
|
||||
checkSchemaDrift,
|
||||
};
|
||||
@@ -1,20 +1,37 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Secrets Module — CJS adapter.
|
||||
*
|
||||
* The implementation is generated from sdk/src/query/secrets.ts and
|
||||
* lives in secrets.generated.cjs. This file is a thin re-export so
|
||||
* that existing call sites (config.cjs, init.cjs, and tests) can
|
||||
* continue to require('./secrets') unchanged.
|
||||
*
|
||||
* Exports (from generated file):
|
||||
* - SECRET_CONFIG_KEYS — Set of secret key paths
|
||||
* - isSecretKey(keyPath) — returns true if keyPath is a secret
|
||||
* - maskSecret(value) — masks a secret value
|
||||
* - maskIfSecret(keyPath, value) — masks value only if keyPath is secret
|
||||
* GENERATED FILE — DO NOT EDIT.
|
||||
*
|
||||
* Source: sdk/src/query/secrets.ts
|
||||
* Regenerate: cd sdk && npm run gen:secrets
|
||||
*
|
||||
* Secrets handling — masking convention for API keys and other
|
||||
* credentials managed via /gsd-settings-integrations.
|
||||
* This module does not read the filesystem.
|
||||
*/
|
||||
|
||||
module.exports = require('./secrets.generated.cjs');
|
||||
const SECRET_CONFIG_KEYS = new Set([
|
||||
'brave_search',
|
||||
'firecrawl',
|
||||
'exa_search',
|
||||
]);
|
||||
|
||||
function isSecretKey(keyPath) {
|
||||
return SECRET_CONFIG_KEYS.has(keyPath);
|
||||
}
|
||||
|
||||
function maskSecret(value) {
|
||||
if (value === null || value === undefined || value === '')
|
||||
return '(unset)';
|
||||
const s = String(value);
|
||||
if (s.length < 8)
|
||||
return '****';
|
||||
return '****' + s.slice(-4);
|
||||
}
|
||||
|
||||
function maskIfSecret(keyPath, value) {
|
||||
return isSecretKey(keyPath) ? maskSecret(value) : value;
|
||||
}
|
||||
|
||||
module.exports = { SECRET_CONFIG_KEYS, isSecretKey, maskSecret, maskIfSecret };
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* GENERATED FILE — DO NOT EDIT.
|
||||
*
|
||||
* Source: sdk/src/query/secrets.ts
|
||||
* Regenerate: cd sdk && npm run gen:secrets
|
||||
*
|
||||
* Secrets handling — masking convention for API keys and other
|
||||
* credentials managed via /gsd-settings-integrations.
|
||||
* This module does not read the filesystem.
|
||||
*/
|
||||
|
||||
const SECRET_CONFIG_KEYS = new Set([
|
||||
'brave_search',
|
||||
'firecrawl',
|
||||
'exa_search',
|
||||
]);
|
||||
|
||||
function isSecretKey(keyPath) {
|
||||
return SECRET_CONFIG_KEYS.has(keyPath);
|
||||
}
|
||||
|
||||
function maskSecret(value) {
|
||||
if (value === null || value === undefined || value === '')
|
||||
return '(unset)';
|
||||
const s = String(value);
|
||||
if (s.length < 8)
|
||||
return '****';
|
||||
return '****' + s.slice(-4);
|
||||
}
|
||||
|
||||
function maskIfSecret(keyPath, value) {
|
||||
return isSecretKey(keyPath) ? maskSecret(value) : value;
|
||||
}
|
||||
|
||||
module.exports = { SECRET_CONFIG_KEYS, isSecretKey, maskSecret, maskIfSecret };
|
||||
@@ -1,6 +1,6 @@
|
||||
'use strict';
|
||||
|
||||
const { STATE_SUBCOMMANDS } = require('./command-aliases.generated.cjs');
|
||||
const { STATE_SUBCOMMANDS } = require('./command-aliases.cjs');
|
||||
const { routeCjsCommandFamily } = require('./cjs-command-router-adapter.cjs');
|
||||
const { output } = require('./core.cjs');
|
||||
const {
|
||||
|
||||
@@ -1,12 +1,127 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* STATE.md Document Module — CJS adapter.
|
||||
* GENERATED FILE — DO NOT EDIT.
|
||||
*
|
||||
* The implementation is generated from sdk/src/state/index.ts and
|
||||
* lives in state-document.generated.cjs. This file is a thin re-export so
|
||||
* that existing call sites (state.cjs, workstream-inventory.cjs, init.cjs,
|
||||
* and tests) can continue to require('./state-document') unchanged.
|
||||
* Source: sdk/src/state/index.ts
|
||||
* Regenerate: cd sdk && npm run gen:state-document
|
||||
*
|
||||
* STATE.md Document Module — pure transforms for STATE.md text.
|
||||
* This module does not read the filesystem and does not own persistence or locking.
|
||||
*/
|
||||
|
||||
module.exports = require('./state-document.generated.cjs');
|
||||
// Internal helpers
|
||||
function escapeRegex(str) {
|
||||
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
}
|
||||
|
||||
function toFiniteNumber(value) {
|
||||
const number = Number(value);
|
||||
return Number.isFinite(number) ? number : null;
|
||||
}
|
||||
|
||||
function existingProgressExceedsDerived(existingProgress, derivedProgress, key) {
|
||||
const existing = toFiniteNumber(existingProgress[key]);
|
||||
const derived = toFiniteNumber(derivedProgress[key]);
|
||||
return existing !== null && derived !== null && existing > derived;
|
||||
}
|
||||
|
||||
function stateExtractField(content, fieldName) {
|
||||
const escaped = escapeRegex(fieldName);
|
||||
const boldPattern = new RegExp(`\\*\\*${escaped}:\\*\\*[ \\t]*(.+)`, 'i');
|
||||
const boldMatch = content.match(boldPattern);
|
||||
if (boldMatch)
|
||||
return boldMatch[1].trim();
|
||||
const plainPattern = new RegExp(`^${escaped}:[ \\t]*(.+)`, 'im');
|
||||
const plainMatch = content.match(plainPattern);
|
||||
return plainMatch ? plainMatch[1].trim() : null;
|
||||
}
|
||||
|
||||
function stateReplaceField(content, fieldName, newValue) {
|
||||
const escaped = escapeRegex(fieldName);
|
||||
const boldPattern = new RegExp(`(\\*\\*${escaped}:\\*\\*\\s*)(.*)`, 'i');
|
||||
if (boldPattern.test(content)) {
|
||||
return content.replace(boldPattern, (_match, prefix) => `${prefix}${newValue}`);
|
||||
}
|
||||
const plainPattern = new RegExp(`(^${escaped}:\\s*)(.*)`, 'im');
|
||||
if (plainPattern.test(content)) {
|
||||
return content.replace(plainPattern, (_match, prefix) => `${prefix}${newValue}`);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function stateReplaceFieldWithFallback(content, primary, fallback, value) {
|
||||
let result = stateReplaceField(content, primary, value);
|
||||
if (result)
|
||||
return result;
|
||||
if (fallback) {
|
||||
result = stateReplaceField(content, fallback, value);
|
||||
if (result)
|
||||
return result;
|
||||
}
|
||||
return content;
|
||||
}
|
||||
|
||||
function normalizeStateStatus(status, pausedAt) {
|
||||
let normalizedStatus = status || 'unknown';
|
||||
const statusLower = (status || '').toLowerCase();
|
||||
if (statusLower.includes('paused') || statusLower.includes('stopped') || pausedAt) {
|
||||
normalizedStatus = 'paused';
|
||||
}
|
||||
else if (statusLower.includes('executing') || statusLower.includes('in progress')) {
|
||||
normalizedStatus = 'executing';
|
||||
}
|
||||
else if (statusLower.includes('planning') || statusLower.includes('ready to plan')) {
|
||||
normalizedStatus = 'planning';
|
||||
}
|
||||
else if (statusLower.includes('discussing')) {
|
||||
normalizedStatus = 'discussing';
|
||||
}
|
||||
else if (statusLower.includes('verif')) {
|
||||
normalizedStatus = 'verifying';
|
||||
}
|
||||
else if (statusLower.includes('complete') || statusLower.includes('done')) {
|
||||
normalizedStatus = 'completed';
|
||||
}
|
||||
else if (statusLower.includes('ready to execute')) {
|
||||
normalizedStatus = 'executing';
|
||||
}
|
||||
return normalizedStatus;
|
||||
}
|
||||
|
||||
function computeProgressPercent(completedPlans, totalPlans, completedPhases, totalPhases) {
|
||||
const hasPlanData = totalPlans !== null && totalPlans > 0 && completedPlans !== null;
|
||||
const hasPhaseData = totalPhases !== null && totalPhases > 0 && completedPhases !== null;
|
||||
if (!hasPlanData && !hasPhaseData)
|
||||
return null;
|
||||
const planFraction = hasPlanData ? completedPlans / totalPlans : 1;
|
||||
const phaseFraction = hasPhaseData ? completedPhases / totalPhases : 1;
|
||||
return Math.min(100, Math.round(Math.min(planFraction, phaseFraction) * 100));
|
||||
}
|
||||
|
||||
function shouldPreserveExistingProgress(existingProgress, derivedProgress) {
|
||||
if (!existingProgress || typeof existingProgress !== 'object')
|
||||
return false;
|
||||
if (!derivedProgress || typeof derivedProgress !== 'object')
|
||||
return false;
|
||||
const existing = existingProgress;
|
||||
const derived = derivedProgress;
|
||||
return (existingProgressExceedsDerived(existing, derived, 'total_phases') ||
|
||||
existingProgressExceedsDerived(existing, derived, 'completed_phases') ||
|
||||
existingProgressExceedsDerived(existing, derived, 'total_plans') ||
|
||||
existingProgressExceedsDerived(existing, derived, 'completed_plans'));
|
||||
}
|
||||
|
||||
function normalizeProgressNumbers(progress) {
|
||||
if (!progress || typeof progress !== 'object')
|
||||
return progress;
|
||||
const normalized = { ...progress };
|
||||
for (const key of ['total_phases', 'completed_phases', 'total_plans', 'completed_plans', 'percent']) {
|
||||
const number = toFiniteNumber(normalized[key]);
|
||||
if (number !== null)
|
||||
normalized[key] = number;
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
module.exports = { stateExtractField, stateReplaceField, stateReplaceFieldWithFallback, normalizeStateStatus, computeProgressPercent, shouldPreserveExistingProgress, normalizeProgressNumbers };
|
||||
|
||||
@@ -1,127 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* GENERATED FILE — DO NOT EDIT.
|
||||
*
|
||||
* Source: sdk/src/state/index.ts
|
||||
* Regenerate: cd sdk && npm run gen:state-document
|
||||
*
|
||||
* STATE.md Document Module — pure transforms for STATE.md text.
|
||||
* This module does not read the filesystem and does not own persistence or locking.
|
||||
*/
|
||||
|
||||
// Internal helpers
|
||||
function escapeRegex(str) {
|
||||
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
}
|
||||
|
||||
function toFiniteNumber(value) {
|
||||
const number = Number(value);
|
||||
return Number.isFinite(number) ? number : null;
|
||||
}
|
||||
|
||||
function existingProgressExceedsDerived(existingProgress, derivedProgress, key) {
|
||||
const existing = toFiniteNumber(existingProgress[key]);
|
||||
const derived = toFiniteNumber(derivedProgress[key]);
|
||||
return existing !== null && derived !== null && existing > derived;
|
||||
}
|
||||
|
||||
function stateExtractField(content, fieldName) {
|
||||
const escaped = escapeRegex(fieldName);
|
||||
const boldPattern = new RegExp(`\\*\\*${escaped}:\\*\\*[ \\t]*(.+)`, 'i');
|
||||
const boldMatch = content.match(boldPattern);
|
||||
if (boldMatch)
|
||||
return boldMatch[1].trim();
|
||||
const plainPattern = new RegExp(`^${escaped}:[ \\t]*(.+)`, 'im');
|
||||
const plainMatch = content.match(plainPattern);
|
||||
return plainMatch ? plainMatch[1].trim() : null;
|
||||
}
|
||||
|
||||
function stateReplaceField(content, fieldName, newValue) {
|
||||
const escaped = escapeRegex(fieldName);
|
||||
const boldPattern = new RegExp(`(\\*\\*${escaped}:\\*\\*\\s*)(.*)`, 'i');
|
||||
if (boldPattern.test(content)) {
|
||||
return content.replace(boldPattern, (_match, prefix) => `${prefix}${newValue}`);
|
||||
}
|
||||
const plainPattern = new RegExp(`(^${escaped}:\\s*)(.*)`, 'im');
|
||||
if (plainPattern.test(content)) {
|
||||
return content.replace(plainPattern, (_match, prefix) => `${prefix}${newValue}`);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function stateReplaceFieldWithFallback(content, primary, fallback, value) {
|
||||
let result = stateReplaceField(content, primary, value);
|
||||
if (result)
|
||||
return result;
|
||||
if (fallback) {
|
||||
result = stateReplaceField(content, fallback, value);
|
||||
if (result)
|
||||
return result;
|
||||
}
|
||||
return content;
|
||||
}
|
||||
|
||||
function normalizeStateStatus(status, pausedAt) {
|
||||
let normalizedStatus = status || 'unknown';
|
||||
const statusLower = (status || '').toLowerCase();
|
||||
if (statusLower.includes('paused') || statusLower.includes('stopped') || pausedAt) {
|
||||
normalizedStatus = 'paused';
|
||||
}
|
||||
else if (statusLower.includes('executing') || statusLower.includes('in progress')) {
|
||||
normalizedStatus = 'executing';
|
||||
}
|
||||
else if (statusLower.includes('planning') || statusLower.includes('ready to plan')) {
|
||||
normalizedStatus = 'planning';
|
||||
}
|
||||
else if (statusLower.includes('discussing')) {
|
||||
normalizedStatus = 'discussing';
|
||||
}
|
||||
else if (statusLower.includes('verif')) {
|
||||
normalizedStatus = 'verifying';
|
||||
}
|
||||
else if (statusLower.includes('complete') || statusLower.includes('done')) {
|
||||
normalizedStatus = 'completed';
|
||||
}
|
||||
else if (statusLower.includes('ready to execute')) {
|
||||
normalizedStatus = 'executing';
|
||||
}
|
||||
return normalizedStatus;
|
||||
}
|
||||
|
||||
function computeProgressPercent(completedPlans, totalPlans, completedPhases, totalPhases) {
|
||||
const hasPlanData = totalPlans !== null && totalPlans > 0 && completedPlans !== null;
|
||||
const hasPhaseData = totalPhases !== null && totalPhases > 0 && completedPhases !== null;
|
||||
if (!hasPlanData && !hasPhaseData)
|
||||
return null;
|
||||
const planFraction = hasPlanData ? completedPlans / totalPlans : 1;
|
||||
const phaseFraction = hasPhaseData ? completedPhases / totalPhases : 1;
|
||||
return Math.min(100, Math.round(Math.min(planFraction, phaseFraction) * 100));
|
||||
}
|
||||
|
||||
function shouldPreserveExistingProgress(existingProgress, derivedProgress) {
|
||||
if (!existingProgress || typeof existingProgress !== 'object')
|
||||
return false;
|
||||
if (!derivedProgress || typeof derivedProgress !== 'object')
|
||||
return false;
|
||||
const existing = existingProgress;
|
||||
const derived = derivedProgress;
|
||||
return (existingProgressExceedsDerived(existing, derived, 'total_phases') ||
|
||||
existingProgressExceedsDerived(existing, derived, 'completed_phases') ||
|
||||
existingProgressExceedsDerived(existing, derived, 'total_plans') ||
|
||||
existingProgressExceedsDerived(existing, derived, 'completed_plans'));
|
||||
}
|
||||
|
||||
function normalizeProgressNumbers(progress) {
|
||||
if (!progress || typeof progress !== 'object')
|
||||
return progress;
|
||||
const normalized = { ...progress };
|
||||
for (const key of ['total_phases', 'completed_phases', 'total_plans', 'completed_plans', 'percent']) {
|
||||
const number = toFiniteNumber(normalized[key]);
|
||||
if (number !== null)
|
||||
normalized[key] = number;
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
module.exports = { stateExtractField, stateReplaceField, stateReplaceFieldWithFallback, normalizeStateStatus, computeProgressPercent, shouldPreserveExistingProgress, normalizeProgressNumbers };
|
||||
@@ -1,6 +1,6 @@
|
||||
'use strict';
|
||||
|
||||
const { VALIDATE_SUBCOMMANDS } = require('./command-aliases.generated.cjs');
|
||||
const { VALIDATE_SUBCOMMANDS } = require('./command-aliases.cjs');
|
||||
const { formatGsdSlash, resolveRuntime } = require('./runtime-slash.cjs');
|
||||
const { routeCjsCommandFamily } = require('./cjs-command-router-adapter.cjs');
|
||||
const { output } = require('./core.cjs');
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use strict';
|
||||
|
||||
const { VERIFY_SUBCOMMANDS } = require('./command-aliases.generated.cjs');
|
||||
const { VERIFY_SUBCOMMANDS } = require('./command-aliases.cjs');
|
||||
const { routeCjsCommandFamily } = require('./cjs-command-router-adapter.cjs');
|
||||
const { output } = require('./core.cjs');
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ const {
|
||||
phaseVariants, buildRoadmapPhaseVariants, buildNotStartedPhaseVariants,
|
||||
// Issue #26 exports (W005 regex, W006-archived regex constants, I001 helper)
|
||||
phaseDirNameRe, PHASE_TOKEN_FROM_DIR_RE, MILESTONE_ARCHIVE_DIR_RE, canonicalPlanStem,
|
||||
} = require('./validate.generated.cjs');
|
||||
} = require('./validate.cjs');
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
@@ -17,7 +17,7 @@ const { toPosixPath, readSubdirectories } = require('./core.cjs');
|
||||
const scanPhasePlans = require('./plan-scan.cjs');
|
||||
const { planningPaths, planningRoot, getActiveWorkstream } = require('./planning-workspace.cjs');
|
||||
const { stateExtractField } = require('./state-document.cjs');
|
||||
const { buildWorkstreamInventory, isCompletedInventory } = require('./workstream-inventory-builder.generated.cjs');
|
||||
const { buildWorkstreamInventory, isCompletedInventory } = require('./workstream-inventory-builder.cjs');
|
||||
|
||||
function workstreamsRoot(cwd) {
|
||||
return path.join(planningRoot(cwd), 'workstreams');
|
||||
|
||||
@@ -1,19 +1,61 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Workstream Name Policy Module — CJS adapter.
|
||||
*
|
||||
* The implementation is generated from sdk/src/workstream-name-policy.ts and
|
||||
* lives in workstream-name-policy.generated.cjs. This file is a thin re-export
|
||||
* so that existing call sites (active-workstream-store.cjs,
|
||||
* planning-workspace.cjs, workstream.cjs, and tests) can continue to
|
||||
* require('./workstream-name-policy') unchanged.
|
||||
*
|
||||
* Exports (from generated file):
|
||||
* - toWorkstreamSlug(name) — normalize to URL/filesystem slug
|
||||
* - hasInvalidPathSegment(name) — true if name has slashes or dot-dot
|
||||
* - isValidActiveWorkstreamName(name) — true if name passes all policy rules
|
||||
* - validateWorkstreamName(name) — SDK alias for isValidActiveWorkstreamName
|
||||
* GENERATED FILE — DO NOT EDIT.
|
||||
*
|
||||
* Source: sdk/src/workstream-name-policy.ts
|
||||
* Regenerate: cd sdk && npm run gen:workstream-name-policy
|
||||
*
|
||||
* Canonical workstream name validation and slug normalization.
|
||||
* Used by active-workstream-store.cjs, planning-workspace.cjs, workstream.cjs.
|
||||
*/
|
||||
|
||||
module.exports = require('./workstream-name-policy.generated.cjs');
|
||||
const ACTIVE_WORKSTREAM_RE = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/;
|
||||
/**
|
||||
* Validate a workstream name.
|
||||
* Allowed: alphanumeric, hyphens, underscores, dots.
|
||||
* Disallowed: empty, spaces, slashes, special chars, path traversal.
|
||||
*
|
||||
* Alias for isValidActiveWorkstreamName; provided for SDK-layer callers.
|
||||
*/
|
||||
function validateWorkstreamName(name) {
|
||||
return isValidActiveWorkstreamName(name);
|
||||
}
|
||||
/**
|
||||
* Convert a display name to a URL/filesystem-safe workstream slug.
|
||||
* Lowercases, collapses non-alphanumeric runs to hyphens, strips leading/trailing hyphens.
|
||||
*/
|
||||
function toWorkstreamSlug(name) {
|
||||
return String(name || '')
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, '-')
|
||||
.replace(/^-+|-+$/g, '');
|
||||
}
|
||||
/**
|
||||
* Returns true when `name` contains a path separator, a bare dot, or a
|
||||
* dot-dot sequence — any of which would make the name unsafe for use as a
|
||||
* filesystem path segment.
|
||||
*/
|
||||
function hasInvalidPathSegment(name) {
|
||||
const value = String(name || '');
|
||||
return /[/\\]/.test(value) || value === '.' || value === '..' || value.includes('..');
|
||||
}
|
||||
/**
|
||||
* Returns true when `name` is a valid active workstream name:
|
||||
* - Must start with alphanumeric
|
||||
* - May contain alphanumeric, dots, underscores, hyphens
|
||||
* - Must not contain path traversal sequences (..)
|
||||
*/
|
||||
function isValidActiveWorkstreamName(name) {
|
||||
const value = String(name || '');
|
||||
if (value === '..' || value.startsWith('../') || value.includes('..'))
|
||||
return false;
|
||||
return ACTIVE_WORKSTREAM_RE.test(value);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
validateWorkstreamName,
|
||||
toWorkstreamSlug,
|
||||
hasInvalidPathSegment,
|
||||
isValidActiveWorkstreamName,
|
||||
};
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* GENERATED FILE — DO NOT EDIT.
|
||||
*
|
||||
* Source: sdk/src/workstream-name-policy.ts
|
||||
* Regenerate: cd sdk && npm run gen:workstream-name-policy
|
||||
*
|
||||
* Canonical workstream name validation and slug normalization.
|
||||
* Used by active-workstream-store.cjs, planning-workspace.cjs, workstream.cjs.
|
||||
*/
|
||||
|
||||
const ACTIVE_WORKSTREAM_RE = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/;
|
||||
/**
|
||||
* Validate a workstream name.
|
||||
* Allowed: alphanumeric, hyphens, underscores, dots.
|
||||
* Disallowed: empty, spaces, slashes, special chars, path traversal.
|
||||
*
|
||||
* Alias for isValidActiveWorkstreamName; provided for SDK-layer callers.
|
||||
*/
|
||||
function validateWorkstreamName(name) {
|
||||
return isValidActiveWorkstreamName(name);
|
||||
}
|
||||
/**
|
||||
* Convert a display name to a URL/filesystem-safe workstream slug.
|
||||
* Lowercases, collapses non-alphanumeric runs to hyphens, strips leading/trailing hyphens.
|
||||
*/
|
||||
function toWorkstreamSlug(name) {
|
||||
return String(name || '')
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, '-')
|
||||
.replace(/^-+|-+$/g, '');
|
||||
}
|
||||
/**
|
||||
* Returns true when `name` contains a path separator, a bare dot, or a
|
||||
* dot-dot sequence — any of which would make the name unsafe for use as a
|
||||
* filesystem path segment.
|
||||
*/
|
||||
function hasInvalidPathSegment(name) {
|
||||
const value = String(name || '');
|
||||
return /[/\\]/.test(value) || value === '.' || value === '..' || value.includes('..');
|
||||
}
|
||||
/**
|
||||
* Returns true when `name` is a valid active workstream name:
|
||||
* - Must start with alphanumeric
|
||||
* - May contain alphanumeric, dots, underscores, hyphens
|
||||
* - Must not contain path traversal sequences (..)
|
||||
*/
|
||||
function isValidActiveWorkstreamName(name) {
|
||||
const value = String(name || '');
|
||||
if (value === '..' || value.startsWith('../') || value.includes('..'))
|
||||
return false;
|
||||
return ACTIVE_WORKSTREAM_RE.test(value);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
validateWorkstreamName,
|
||||
toWorkstreamSlug,
|
||||
hasInvalidPathSegment,
|
||||
isValidActiveWorkstreamName,
|
||||
};
|
||||
20
package-lock.json
generated
20
package-lock.json
generated
@@ -18,10 +18,12 @@
|
||||
"gsd-tools": "bin/gsd-sdk.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"c8": "^11.0.0"
|
||||
"c8": "^11.0.0",
|
||||
"typescript": "^5.9.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22.0.0"
|
||||
"node": ">=22.0.0",
|
||||
"npm": ">=10.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fallow": "^2.70.0"
|
||||
@@ -1970,6 +1972,20 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.9.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/unpipe": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
|
||||
|
||||
@@ -56,7 +56,8 @@
|
||||
"ws": "8.20.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"c8": "^11.0.0"
|
||||
"c8": "^11.0.0",
|
||||
"typescript": "^5.9.3"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fallow": "^2.70.0"
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
* I001 canonicalPlanStem.
|
||||
*
|
||||
* After the generator migration, all three helpers are sourced from
|
||||
* validate.generated.cjs. If they diverge from validate.ts, these
|
||||
* validate.cjs. If they diverge from validate.ts, these
|
||||
* tests go RED.
|
||||
*
|
||||
* References:
|
||||
@@ -62,7 +62,7 @@ function writeConfigJson(planningDir) {
|
||||
// node .claude/get-shit-done/bin/gsd-tools.cjs validate health
|
||||
// # Bug: emits W005 about 999.1-foo not following NN-name format
|
||||
//
|
||||
// verify.cjs must consume phaseDirNameRe from validate.generated.cjs so
|
||||
// verify.cjs must consume phaseDirNameRe from validate.cjs so
|
||||
// the regex /^\d{2,}(?:\.\d+)*-[\w-]+$/ is the single source of truth.
|
||||
|
||||
describe('Drift item W005 — phaseDirNameRe: 999.X-name dirs must not trigger W005', () => {
|
||||
@@ -95,10 +95,10 @@ describe('Drift item W005 — phaseDirNameRe: 999.X-name dirs must not trigger W
|
||||
`Expected zero W005 for 999.1-foo, got: ${JSON.stringify(w005)}`);
|
||||
});
|
||||
|
||||
test('phaseDirNameRe is exported from validate.generated.cjs', () => {
|
||||
const gen = require('../get-shit-done/bin/lib/validate.generated.cjs');
|
||||
test('phaseDirNameRe is exported from validate.cjs', () => {
|
||||
const gen = require('../get-shit-done/bin/lib/validate.cjs');
|
||||
assert.ok(gen.phaseDirNameRe instanceof RegExp,
|
||||
'validate.generated.cjs must export phaseDirNameRe as a RegExp');
|
||||
'validate.cjs must export phaseDirNameRe as a RegExp');
|
||||
const re = gen.phaseDirNameRe;
|
||||
assert.ok(re.test('01-setup'), 'should accept 01-setup');
|
||||
assert.ok(re.test('999-longphase'), 'should accept 999-longphase (3-digit prefix)');
|
||||
@@ -110,7 +110,7 @@ describe('Drift item W005 — phaseDirNameRe: 999.X-name dirs must not trigger W
|
||||
// ── Drift Item W006-archived: PHASE_TOKEN_FROM_DIR_RE / MILESTONE_ARCHIVE_DIR_RE ─
|
||||
//
|
||||
// forEachArchivedPhaseToken() in verify.cjs uses two inline regex constants.
|
||||
// After migration both are sourced from validate.generated.cjs:
|
||||
// After migration both are sourced from validate.cjs:
|
||||
// PHASE_TOKEN_FROM_DIR_RE — extracts token from dir name like "64-auth-service"
|
||||
// MILESTONE_ARCHIVE_DIR_RE — matches archive dirs like "v1.0-phases"
|
||||
//
|
||||
@@ -173,9 +173,9 @@ describe('Drift item W006-archived — MILESTONE_ARCHIVE_DIR_RE and PHASE_TOKEN_
|
||||
});
|
||||
|
||||
test('MILESTONE_ARCHIVE_DIR_RE is exported and matches vN.N-phases dirs', () => {
|
||||
const gen = require('../get-shit-done/bin/lib/validate.generated.cjs');
|
||||
const gen = require('../get-shit-done/bin/lib/validate.cjs');
|
||||
assert.ok(gen.MILESTONE_ARCHIVE_DIR_RE instanceof RegExp,
|
||||
'validate.generated.cjs must export MILESTONE_ARCHIVE_DIR_RE');
|
||||
'validate.cjs must export MILESTONE_ARCHIVE_DIR_RE');
|
||||
const re = gen.MILESTONE_ARCHIVE_DIR_RE;
|
||||
assert.ok(re.test('v1.0-phases'), 'should match v1.0-phases');
|
||||
assert.ok(re.test('v1.10-phases'), 'should match v1.10-phases');
|
||||
@@ -184,9 +184,9 @@ describe('Drift item W006-archived — MILESTONE_ARCHIVE_DIR_RE and PHASE_TOKEN_
|
||||
});
|
||||
|
||||
test('PHASE_TOKEN_FROM_DIR_RE is exported and extracts phase tokens correctly', () => {
|
||||
const gen = require('../get-shit-done/bin/lib/validate.generated.cjs');
|
||||
const gen = require('../get-shit-done/bin/lib/validate.cjs');
|
||||
assert.ok(gen.PHASE_TOKEN_FROM_DIR_RE instanceof RegExp,
|
||||
'validate.generated.cjs must export PHASE_TOKEN_FROM_DIR_RE');
|
||||
'validate.cjs must export PHASE_TOKEN_FROM_DIR_RE');
|
||||
const re = gen.PHASE_TOKEN_FROM_DIR_RE;
|
||||
assert.strictEqual(re.exec('64-auth-service')?.[1], '64');
|
||||
assert.strictEqual(re.exec('03B-feature')?.[1], '03B');
|
||||
@@ -199,7 +199,7 @@ describe('Drift item W006-archived — MILESTONE_ARCHIVE_DIR_RE and PHASE_TOKEN_
|
||||
//
|
||||
// validate.ts Check 7: canonicalPlanStem('68-01-scaffolding') → '68-01'
|
||||
// verify.cjs had an inline copy. After migration, canonicalPlanStem is
|
||||
// sourced from validate.generated.cjs.
|
||||
// sourced from validate.cjs.
|
||||
//
|
||||
// Test: "68-01-scaffolding-PLAN.md" + "68-01-SUMMARY.md" → no I001
|
||||
// Both stems canonicalize to "68-01" → match found → I001 suppressed.
|
||||
@@ -238,10 +238,10 @@ describe('Drift item I001 — canonicalPlanStem: long PLAN stem matches short SU
|
||||
`Expected zero I001, got: ${JSON.stringify(i001)}`);
|
||||
});
|
||||
|
||||
test('canonicalPlanStem is exported from validate.generated.cjs', () => {
|
||||
const gen = require('../get-shit-done/bin/lib/validate.generated.cjs');
|
||||
test('canonicalPlanStem is exported from validate.cjs', () => {
|
||||
const gen = require('../get-shit-done/bin/lib/validate.cjs');
|
||||
assert.strictEqual(typeof gen.canonicalPlanStem, 'function',
|
||||
'validate.generated.cjs must export canonicalPlanStem as a function');
|
||||
'validate.cjs must export canonicalPlanStem as a function');
|
||||
assert.strictEqual(gen.canonicalPlanStem('68-01-scaffolding'), '68-01');
|
||||
assert.strictEqual(gen.canonicalPlanStem('68-01'), '68-01');
|
||||
assert.strictEqual(gen.canonicalPlanStem('3A-01-feature'), '3A-01');
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Regression test for #3571: configuration.generated.cjs used the source
|
||||
* Regression test for #3571: configuration.cjs used the source
|
||||
* checkout sdk/shared path only, which breaks installed gsd-tools.cjs because
|
||||
* runtime installs copy get-shit-done/ but not sdk/.
|
||||
*/
|
||||
@@ -15,7 +15,7 @@ const os = require('node:os');
|
||||
const path = require('node:path');
|
||||
|
||||
const REPO_ROOT = path.join(__dirname, '..');
|
||||
const CONFIGURATION_CJS = path.join(REPO_ROOT, 'get-shit-done', 'bin', 'lib', 'configuration.generated.cjs');
|
||||
const CONFIGURATION_CJS = path.join(REPO_ROOT, 'get-shit-done', 'bin', 'lib', 'configuration.cjs');
|
||||
const SDK_SHARED_DIR = path.join(REPO_ROOT, 'sdk', 'shared');
|
||||
|
||||
const { install } = require('../bin/install.js');
|
||||
@@ -68,14 +68,14 @@ describe('bug #3571: configuration generated manifests resolve in install layout
|
||||
fs.rmSync(tmpRoot, { recursive: true, force: true, maxRetries: 10, retryDelay: 100 });
|
||||
});
|
||||
|
||||
test('co-located bin/shared manifests let configuration.generated.cjs load without sdk/shared', () => {
|
||||
test('co-located bin/shared manifests let configuration.cjs load without sdk/shared', () => {
|
||||
const gsdBinDir = path.join(tmpRoot, '.codex', 'get-shit-done', 'bin');
|
||||
const gsdLibDir = path.join(gsdBinDir, 'lib');
|
||||
const gsdSharedDir = path.join(gsdBinDir, 'shared');
|
||||
fs.mkdirSync(gsdLibDir, { recursive: true });
|
||||
fs.mkdirSync(gsdSharedDir, { recursive: true });
|
||||
|
||||
const installedCjs = path.join(gsdLibDir, 'configuration.generated.cjs');
|
||||
const installedCjs = path.join(gsdLibDir, 'configuration.cjs');
|
||||
fs.copyFileSync(CONFIGURATION_CJS, installedCjs);
|
||||
fs.copyFileSync(
|
||||
path.join(SDK_SHARED_DIR, 'config-defaults.manifest.json'),
|
||||
@@ -90,7 +90,7 @@ describe('bug #3571: configuration generated manifests resolve in install layout
|
||||
let mod;
|
||||
assert.doesNotThrow(() => {
|
||||
mod = require(installedCjs);
|
||||
}, 'installed configuration.generated.cjs must not require ~/.codex/sdk/shared');
|
||||
}, 'installed configuration.cjs must not require ~/.codex/sdk/shared');
|
||||
|
||||
assert.ok(mod.VALID_CONFIG_KEYS.has('workflow.plan_review_convergence'));
|
||||
});
|
||||
@@ -118,12 +118,12 @@ describe('bug #3571: configuration generated manifests resolve in install layout
|
||||
'get-shit-done',
|
||||
'bin',
|
||||
'lib',
|
||||
'configuration.generated.cjs'
|
||||
'configuration.cjs'
|
||||
);
|
||||
|
||||
delete require.cache[installedCjs];
|
||||
assert.doesNotThrow(() => {
|
||||
require(installedCjs);
|
||||
}, 'post-install configuration.generated.cjs must load from co-located manifests');
|
||||
}, 'post-install configuration.cjs must load from co-located manifests');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use strict';
|
||||
/**
|
||||
* Regression guard for issue #3251:
|
||||
* 14 commands used in workflows must be present in command-aliases.generated.cjs.
|
||||
* 14 commands used in workflows must be present in command-aliases.cjs.
|
||||
*
|
||||
* Asserts structurally by requiring the manifest and checking each canonical
|
||||
* command appears in either the family arrays or the non-family array.
|
||||
@@ -22,7 +22,7 @@ const COMMAND_ALIASES_FILE = path.join(
|
||||
'get-shit-done',
|
||||
'bin',
|
||||
'lib',
|
||||
'command-aliases.generated.cjs',
|
||||
'command-aliases.cjs',
|
||||
);
|
||||
const GSD_TOOLS = path.join(REPO_ROOT, 'get-shit-done', 'bin', 'gsd-tools.cjs');
|
||||
|
||||
@@ -43,7 +43,7 @@ const MISSING_14 = [
|
||||
'workstream.list',
|
||||
];
|
||||
|
||||
describe('feat-3251: command-aliases.generated.cjs manifest coverage', () => {
|
||||
describe('feat-3251: command-aliases.cjs manifest coverage', () => {
|
||||
let manifest;
|
||||
|
||||
test('manifest file can be required without error', () => {
|
||||
@@ -59,7 +59,7 @@ describe('feat-3251: command-aliases.generated.cjs manifest coverage', () => {
|
||||
manifest = manifest ?? require(COMMAND_ALIASES_FILE);
|
||||
assert.ok(
|
||||
Array.isArray(manifest.NON_FAMILY_COMMAND_ALIASES),
|
||||
'NON_FAMILY_COMMAND_ALIASES must be an exported array in command-aliases.generated.cjs',
|
||||
'NON_FAMILY_COMMAND_ALIASES must be an exported array in command-aliases.cjs',
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ const {
|
||||
|
||||
const {
|
||||
CONFIG_DEFAULTS: CANONICAL_CONFIG_DEFAULTS,
|
||||
} = require('../get-shit-done/bin/lib/configuration.generated.cjs');
|
||||
} = require('../get-shit-done/bin/lib/configuration.cjs');
|
||||
|
||||
const {
|
||||
makeStatusProject,
|
||||
|
||||
@@ -14,7 +14,7 @@ const path = require('path');
|
||||
|
||||
const REPO_ROOT = path.join(__dirname, '..');
|
||||
const REGISTRY_FILE = path.join(REPO_ROOT, 'sdk', 'src', 'query', 'registry-assembly.ts');
|
||||
const COMMAND_ALIASES_FILE = path.join(REPO_ROOT, 'get-shit-done', 'bin', 'lib', 'command-aliases.generated.cjs');
|
||||
const COMMAND_ALIASES_FILE = path.join(REPO_ROOT, 'get-shit-done', 'bin', 'lib', 'command-aliases.cjs');
|
||||
|
||||
// Prose tokens that repeatedly appear after `gsd-sdk query` in English
|
||||
// documentation but aren't real command names.
|
||||
|
||||
Reference in New Issue
Block a user