fix roadmap progress padded phase matching
This commit is contained in:
@@ -51,6 +51,17 @@ function countPhasePlansAndSummaries(phaseDir) {
|
||||
};
|
||||
}
|
||||
|
||||
function phaseMarkdownRegexSource(phaseNum) {
|
||||
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}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Search for a phase header (and its section) within the given content string.
|
||||
* Returns a result object if found (either a full match or a malformed_roadmap
|
||||
@@ -341,11 +352,11 @@ function cmdRoadmapUpdatePlanProgress(cwd, phaseNum, raw) {
|
||||
// Wrap entire read-modify-write in lock to prevent concurrent corruption
|
||||
withPlanningLock(cwd, () => {
|
||||
let roadmapContent = fs.readFileSync(roadmapPath, 'utf-8');
|
||||
const phaseEscaped = escapeRegex(phaseNum);
|
||||
const phasePattern = phaseMarkdownRegexSource(phaseNum);
|
||||
|
||||
// Progress table row: update Plans/Status/Date columns (handles 4 or 5 column tables)
|
||||
const tableRowPattern = new RegExp(
|
||||
`^(\\|\\s*${phaseEscaped}\\.?\\s[^|]*(?:\\|[^\\n]*))$`,
|
||||
`^(\\|\\s*${phasePattern}\\.?\\s[^|]*(?:\\|[^\\n]*))$`,
|
||||
'im'
|
||||
);
|
||||
const dateField = isComplete ? ` ${today} ` : ' ';
|
||||
@@ -367,7 +378,7 @@ function cmdRoadmapUpdatePlanProgress(cwd, phaseNum, raw) {
|
||||
|
||||
// Update plan count in phase detail section
|
||||
const planCountPattern = new RegExp(
|
||||
`(#{2,4}\\s*Phase\\s+${phaseEscaped}[\\s\\S]*?\\*\\*Plans:\\*\\*\\s*)[^\\n]+`,
|
||||
`(#{2,4}\\s*Phase\\s+${phasePattern}(?=[:\\s])[\\s\\S]*?\\*\\*Plans:\\*\\*\\s*)[^\\n]+`,
|
||||
'i'
|
||||
);
|
||||
const planCountText = isComplete
|
||||
@@ -378,7 +389,7 @@ function cmdRoadmapUpdatePlanProgress(cwd, phaseNum, raw) {
|
||||
// If complete: check checkbox
|
||||
if (isComplete) {
|
||||
const checkboxPattern = new RegExp(
|
||||
`(-\\s*\\[)[ ](\\]\\s*.*Phase\\s+${phaseEscaped}[:\\s][^\\n]*)`,
|
||||
`(-\\s*\\[)[ ](\\]\\s*.*Phase\\s+${phasePattern}[:\\s][^\\n]*)`,
|
||||
'i'
|
||||
);
|
||||
roadmapContent = replaceInCurrentMilestone(roadmapContent, checkboxPattern, `$1x$2 (completed ${today})`);
|
||||
|
||||
@@ -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');
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -771,6 +771,41 @@ describe('roadmap update-plan-progress command', () => {
|
||||
assert.ok(roadmapContent.includes('1/1'), 'roadmap should contain updated plan count');
|
||||
});
|
||||
|
||||
test('updates unpadded ROADMAP phase entries when called with padded phase argument', () => {
|
||||
fs.writeFileSync(
|
||||
path.join(tmpDir, '.planning', 'ROADMAP.md'),
|
||||
`# Roadmap
|
||||
|
||||
- [ ] **Phase 3: Build** - description
|
||||
|
||||
### Phase 3: Build
|
||||
**Goal:** Test goal
|
||||
**Plans:** 0 plans
|
||||
- [ ] 03-01-PLAN.md
|
||||
|
||||
## Progress
|
||||
|
||||
| Phase | Plans Complete | Status | Completed |
|
||||
|-------|----------------|--------|-----------|
|
||||
| 3. Build | 0/1 | Planned | |
|
||||
`
|
||||
);
|
||||
|
||||
const p3 = path.join(tmpDir, '.planning', 'phases', '03-build');
|
||||
fs.mkdirSync(p3, { recursive: true });
|
||||
fs.writeFileSync(path.join(p3, '03-01-PLAN.md'), '# Plan 1');
|
||||
fs.writeFileSync(path.join(p3, '03-01-SUMMARY.md'), '# Summary 1');
|
||||
|
||||
const result = runGsdTools('roadmap update-plan-progress 03', tmpDir);
|
||||
assert.ok(result.success, `Command failed: ${result.error}`);
|
||||
|
||||
const roadmapContent = fs.readFileSync(path.join(tmpDir, '.planning', 'ROADMAP.md'), 'utf-8');
|
||||
assert.match(roadmapContent, /- \[x\] \*\*Phase 3: Build\*\* - description \(completed \d{4}-\d{2}-\d{2}\)/);
|
||||
assert.ok(roadmapContent.includes('**Plans:** 1/1 plans complete'), 'phase detail plan count should be updated');
|
||||
assert.match(roadmapContent, /\| 3\. Build \| 1\/1 \| Complete\s+\| \d{4}-\d{2}-\d{2} \|/);
|
||||
assert.ok(roadmapContent.includes('- [x] 03-01-PLAN.md'), 'completed plan checkbox should still be marked');
|
||||
});
|
||||
|
||||
test('missing ROADMAP.md returns updated false', () => {
|
||||
// Create phase dir with plans and summaries but NO ROADMAP.md
|
||||
const p1 = path.join(tmpDir, '.planning', 'phases', '01-test');
|
||||
@@ -858,4 +893,3 @@ describe('roadmap update-plan-progress command', () => {
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// phase add command
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
Reference in New Issue
Block a user