fix roadmap progress padded phase matching

This commit is contained in:
Jeremy McSpadden
2026-05-10 14:42:59 -05:00
parent 4cb5649e8a
commit 6ddbb97951
4 changed files with 106 additions and 9 deletions

View File

@@ -71,6 +71,47 @@ async function setupProject(opts: {
// ─── planCountPattern regression: **Plans:** on its own line ─────────────
describe('roadmapUpdatePlanProgress', () => {
it('updates unpadded ROADMAP phase entries when called with a padded phase argument', async () => {
const { roadmapUpdatePlanProgress } = await import('./roadmap-update-plan-progress.js');
const roadmap = [
'# Roadmap',
'',
'## Current Milestone: v3.0',
'',
'- [ ] **Phase 3: build** - build it',
'',
'### Phase 3: build',
'',
'**Goal:** Build it',
'**Plans:** 0 plans',
'- [ ] 03-01-PLAN.md',
'',
'## Progress',
'',
'| Phase | Plans Complete | Status | Completed |',
'|-------|----------------|--------|-----------|',
'| 3. build | 0/1 | Planned | |',
'',
].join('\n');
const { roadmapPath } = await setupProject({
roadmap,
phaseDir: '03-build',
plans: ['03-01-PLAN.md'],
summaries: ['03-01-SUMMARY.md'],
});
await roadmapUpdatePlanProgress(['03'], tmpDir, undefined);
const updated = await readFile(roadmapPath, 'utf-8');
expect(updated).toMatch(/- \[x\] \*\*Phase 3: build\*\* - build it \(completed \d{4}-\d{2}-\d{2}\)/);
expect(updated).toContain('**Plans:** 1/1 plans complete');
expect(updated).toMatch(/\| 3\. build \| 1\/1 \| Complete\s+\| \d{4}-\d{2}-\d{2} \|/);
expect(updated).toContain('- [x] 03-01-PLAN.md');
});
it('does not overwrite plan bullet list when **Plans:** is on its own line (regression #2728 propagation)', async () => {
const { roadmapUpdatePlanProgress } = await import('./roadmap-update-plan-progress.js');

View File

@@ -14,6 +14,17 @@ import { escapeRegex, planningPaths } from './helpers.js';
import { GSDError, ErrorClassification } from '../errors.js';
import type { QueryHandler } from './utils.js';
function phaseMarkdownRegexSource(phaseNum: string): string {
const stripped = String(phaseNum).replace(/^[A-Z]{1,6}-(?=\d)/i, '');
const match = stripped.match(/^0*(\d+)([A-Z])?((?:\.\d+)*)$/i);
if (!match) return escapeRegex(phaseNum);
const integer = match[1]!.replace(/^0+/, '') || '0';
const letter = match[2] ? escapeRegex(match[2]) : '';
const decimal = match[3] ? escapeRegex(match[3]) : '';
return `0*${escapeRegex(integer)}${letter}${decimal}`;
}
export const roadmapUpdatePlanProgress: QueryHandler = async (args, projectDir, workstream) => {
// Support --phase <N> flag form in addition to positional (fixes #2796).
// execute-phase.md:228 passes --phase so positional-only parsing silently
@@ -78,10 +89,10 @@ export const roadmapUpdatePlanProgress: QueryHandler = async (args, projectDir,
}
await readModifyWriteRoadmapMd(projectDir, (roadmapContent) => {
const phaseEscaped = escapeRegex(phaseNum);
const phasePattern = phaseMarkdownRegexSource(phaseNum);
const tableRowPattern = new RegExp(
`^(\\|\\s*${phaseEscaped}\\.?\\s[^|]*(?:\\|[^\\n]*))$`,
`^(\\|\\s*${phasePattern}\\.?\\s[^|]*(?:\\|[^\\n]*))$`,
'im',
);
const dateField = isComplete ? ` ${today} ` : ' ';
@@ -100,7 +111,7 @@ export const roadmapUpdatePlanProgress: QueryHandler = async (args, projectDir,
});
const planCountPattern = new RegExp(
`(#{2,4}\\s*Phase\\s+${phaseEscaped}(?:(?!\\n#{2,4})[\\s\\S])*?\\*\\*Plans:\\*\\*[ \\t]*)[^\\n]+`,
`(#{2,4}\\s*Phase\\s+${phasePattern}(?=[:\\s])(?:(?!\\n#{2,4})[\\s\\S])*?\\*\\*Plans:\\*\\*[ \\t]*)[^\\n]+`,
'i',
);
const planCountText = isComplete
@@ -110,7 +121,7 @@ export const roadmapUpdatePlanProgress: QueryHandler = async (args, projectDir,
if (isComplete) {
const checkboxPattern = new RegExp(
`(-\\s*\\[)[ ](\\]\\s*.*Phase\\s+${phaseEscaped}[:\\s][^\\n]*)`,
`(-\\s*\\[)[ ](\\]\\s*.*Phase\\s+${phasePattern}[:\\s][^\\n]*)`,
'i',
);
roadmapContent = replaceInCurrentMilestone(