Merge pull request #3112 from gsd-build/fix/3101-plan-summary-matcher-in-core-cjs-reports
fix: canonicalize plan-summary matching for suffixless summaries
This commit is contained in:
5
.changeset/pr-3112-release-note.md
Normal file
5
.changeset/pr-3112-release-note.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
type: Fixed
|
||||
pr: 3112
|
||||
---
|
||||
Fixes for issue #3112 were applied to keep command/workflow behavior and SDK parity aligned with current documented usage.
|
||||
@@ -921,6 +921,17 @@ function phaseTokenMatches(dirName, normalized) {
|
||||
return false;
|
||||
}
|
||||
|
||||
function extractCanonicalPlanId(filename) {
|
||||
const base = filename.replace(/-PLAN\.md$/i, '').replace(/-SUMMARY\.md$/i, '').replace(/\.md$/i, '');
|
||||
const parts = base.split('-').filter(Boolean);
|
||||
const tokenRe = /^\d+[A-Z]?(?:\.\d+)*$/i;
|
||||
const phaseIdx = parts.findIndex(p => tokenRe.test(p));
|
||||
if (phaseIdx >= 0 && phaseIdx + 1 < parts.length && tokenRe.test(parts[phaseIdx + 1])) {
|
||||
return `${parts[phaseIdx]}-${parts[phaseIdx + 1]}`;
|
||||
}
|
||||
return base;
|
||||
}
|
||||
|
||||
function searchPhaseInDir(baseDir, relBase, normalized) {
|
||||
try {
|
||||
const dirs = readSubdirectories(baseDir, true);
|
||||
@@ -941,11 +952,16 @@ function searchPhaseInDir(baseDir, relBase, normalized) {
|
||||
const summaries = unsortedSummaries.sort();
|
||||
|
||||
const completedPlanIds = new Set(
|
||||
summaries.map(s => s.replace('-SUMMARY.md', '').replace('SUMMARY.md', ''))
|
||||
summaries.flatMap(s => {
|
||||
const exact = s.replace('-SUMMARY.md', '').replace('SUMMARY.md', '');
|
||||
const canonical = extractCanonicalPlanId(s);
|
||||
return canonical === exact ? [exact] : [exact, canonical];
|
||||
})
|
||||
);
|
||||
const incompletePlans = plans.filter(p => {
|
||||
const planId = p.replace('-PLAN.md', '').replace('PLAN.md', '');
|
||||
return !completedPlanIds.has(planId);
|
||||
const canonical = extractCanonicalPlanId(p);
|
||||
return !completedPlanIds.has(planId) && !completedPlanIds.has(canonical);
|
||||
});
|
||||
|
||||
return {
|
||||
|
||||
@@ -50,6 +50,17 @@ function describeNonCanonicalPlans(dirFiles, matchedFiles) {
|
||||
);
|
||||
}
|
||||
|
||||
function extractCanonicalPlanId(filename) {
|
||||
const base = filename.replace(/-PLAN\.md$/i, '').replace(/-SUMMARY\.md$/i, '').replace(/\.md$/i, '');
|
||||
const parts = base.split('-').filter(Boolean);
|
||||
const tokenRe = /^\d+[A-Z]?(?:\.\d+)*$/i;
|
||||
const phaseIdx = parts.findIndex(p => tokenRe.test(p));
|
||||
if (phaseIdx >= 0 && phaseIdx + 1 < parts.length && tokenRe.test(parts[phaseIdx + 1])) {
|
||||
return `${parts[phaseIdx]}-${parts[phaseIdx + 1]}`;
|
||||
}
|
||||
return base;
|
||||
}
|
||||
|
||||
function cmdPhasesList(cwd, options, raw) {
|
||||
const phasesDir = path.join(planningDir(cwd), 'phases');
|
||||
const { type, phase, includeArchived } = options;
|
||||
@@ -288,7 +299,11 @@ function cmdPhasePlanIndex(cwd, phase, raw) {
|
||||
|
||||
// Build set of plan IDs with summaries
|
||||
const completedPlanIds = new Set(
|
||||
summaryFiles.map(s => s.replace('-SUMMARY.md', '').replace('SUMMARY.md', ''))
|
||||
summaryFiles.flatMap(s => {
|
||||
const exact = s.replace('-SUMMARY.md', '').replace('SUMMARY.md', '');
|
||||
const canonical = extractCanonicalPlanId(s);
|
||||
return canonical === exact ? [exact] : [exact, canonical];
|
||||
})
|
||||
);
|
||||
|
||||
const plans = [];
|
||||
@@ -327,7 +342,7 @@ function cmdPhasePlanIndex(cwd, phase, raw) {
|
||||
filesModified = Array.isArray(fmFiles) ? fmFiles : [fmFiles];
|
||||
}
|
||||
|
||||
const hasSummary = completedPlanIds.has(planId);
|
||||
const hasSummary = completedPlanIds.has(planId) || completedPlanIds.has(extractCanonicalPlanId(planFile));
|
||||
if (!hasSummary) {
|
||||
incomplete.push(planId);
|
||||
}
|
||||
|
||||
@@ -80,6 +80,17 @@ async function getPhaseFileStats(phaseDir: string): Promise<{
|
||||
*
|
||||
* Port of searchPhaseInDir from core.cjs lines 956-1000.
|
||||
*/
|
||||
function extractCanonicalPlanId(filename: string): string {
|
||||
const base = filename.replace(/-PLAN\.md$/i, '').replace(/-SUMMARY\.md$/i, '').replace(/\.md$/i, '');
|
||||
const parts = base.split('-').filter(Boolean);
|
||||
const tokenRe = /^\d+[A-Z]?(?:\.\d+)*$/i;
|
||||
const phaseIdx = parts.findIndex((p) => tokenRe.test(p));
|
||||
if (phaseIdx >= 0 && phaseIdx + 1 < parts.length && tokenRe.test(parts[phaseIdx + 1])) {
|
||||
return `${parts[phaseIdx]}-${parts[phaseIdx + 1]}`;
|
||||
}
|
||||
return base;
|
||||
}
|
||||
|
||||
async function searchPhaseInDir(baseDir: string, relBase: string, normalized: string): Promise<PhaseInfo | null> {
|
||||
try {
|
||||
const entries = await readdir(baseDir, { withFileTypes: true });
|
||||
@@ -105,11 +116,16 @@ async function searchPhaseInDir(baseDir: string, relBase: string, normalized: st
|
||||
const summaries = unsortedSummaries.sort();
|
||||
|
||||
const completedPlanIds = new Set(
|
||||
summaries.map(s => s.replace('-SUMMARY.md', '').replace('SUMMARY.md', ''))
|
||||
summaries.flatMap((s) => {
|
||||
const exact = s.replace('-SUMMARY.md', '').replace('SUMMARY.md', '');
|
||||
const canonical = extractCanonicalPlanId(s);
|
||||
return canonical === exact ? [exact] : [exact, canonical];
|
||||
})
|
||||
);
|
||||
const incompletePlans = plans.filter(p => {
|
||||
const incompletePlans = plans.filter((p) => {
|
||||
const planId = p.replace('-PLAN.md', '').replace('PLAN.md', '');
|
||||
return !completedPlanIds.has(planId);
|
||||
const canonical = extractCanonicalPlanId(p);
|
||||
return !completedPlanIds.has(planId) && !completedPlanIds.has(canonical);
|
||||
});
|
||||
|
||||
return {
|
||||
@@ -265,7 +281,11 @@ export const phasePlanIndex: QueryHandler = async (args, projectDir, workstream)
|
||||
|
||||
// Build set of plan IDs with summaries — match the planId derivation logic
|
||||
const completedPlanIds = new Set(
|
||||
summaryFiles.map(s => s === 'SUMMARY.md' ? 'PLAN' : s.replace('-SUMMARY.md', ''))
|
||||
summaryFiles.flatMap((s) => {
|
||||
const exact = s === 'SUMMARY.md' ? 'PLAN' : s.replace('-SUMMARY.md', '');
|
||||
const canonical = extractCanonicalPlanId(s);
|
||||
return canonical === exact ? [exact] : [exact, canonical];
|
||||
})
|
||||
);
|
||||
|
||||
const plans: Array<Record<string, unknown>> = [];
|
||||
@@ -306,7 +326,7 @@ export const phasePlanIndex: QueryHandler = async (args, projectDir, workstream)
|
||||
filesModified = Array.isArray(fmFiles) ? fmFiles : [fmFiles];
|
||||
}
|
||||
|
||||
const hasSummary = completedPlanIds.has(planId);
|
||||
const hasSummary = completedPlanIds.has(planId) || completedPlanIds.has(extractCanonicalPlanId(planFile));
|
||||
if (!hasSummary) {
|
||||
incomplete.push(planId);
|
||||
}
|
||||
|
||||
@@ -869,6 +869,16 @@ describe('searchPhaseInDir', () => {
|
||||
assert.ok(result.incomplete_plans.includes('01-02-PLAN.md'));
|
||||
});
|
||||
|
||||
test('treats prefix summary as complete for descriptive plan filename (#3101)', () => {
|
||||
const phaseDir = path.join(phasesDir, '01-foundation');
|
||||
fs.mkdirSync(phaseDir);
|
||||
fs.writeFileSync(path.join(phaseDir, '01-01-auth-hardening-PLAN.md'), '# Plan 1');
|
||||
fs.writeFileSync(path.join(phaseDir, '01-01-SUMMARY.md'), '# Summary 1');
|
||||
|
||||
const result = searchPhaseInDir(phasesDir, '.planning/phases', '01');
|
||||
assert.strictEqual(result.incomplete_plans.length, 0);
|
||||
});
|
||||
|
||||
test('detects research and context files', () => {
|
||||
const phaseDir = path.join(phasesDir, '01-foundation');
|
||||
fs.mkdirSync(phaseDir);
|
||||
|
||||
@@ -462,6 +462,21 @@ objective: API routes
|
||||
assert.deepStrictEqual(output.incomplete, ['03-02'], 'incomplete list correct');
|
||||
});
|
||||
|
||||
test('phase-plan-index matches descriptive plan with prefix summary (#3101)', () => {
|
||||
const phaseDir = path.join(tmpDir, '.planning', 'phases', '03-api');
|
||||
fs.mkdirSync(phaseDir, { recursive: true });
|
||||
|
||||
fs.writeFileSync(path.join(phaseDir, '03-01-auth-hardening-PLAN.md'), `---\nwave: 1\n---\n## Task 1`);
|
||||
fs.writeFileSync(path.join(phaseDir, '03-01-SUMMARY.md'), `# Summary`);
|
||||
|
||||
const result = runGsdTools('phase-plan-index 03', tmpDir);
|
||||
assert.ok(result.success, `Command failed: ${result.error}`);
|
||||
|
||||
const output = JSON.parse(result.output);
|
||||
assert.strictEqual(output.plans[0].has_summary, true, 'descriptive plan should match prefix summary');
|
||||
assert.deepStrictEqual(output.incomplete, [], 'plan should not be marked incomplete');
|
||||
});
|
||||
|
||||
test('detects checkpoints (autonomous: false)', () => {
|
||||
const phaseDir = path.join(tmpDir, '.planning', 'phases', '03-api');
|
||||
fs.mkdirSync(phaseDir, { recursive: true });
|
||||
|
||||
Reference in New Issue
Block a user