fix(#2012): scope phase.complete Progress-row regex to ## Progress section (#2032)

* fix(#2012): scope Progress-row regex to ## Progress section (was binding to earlier table)

The Progress-row writer used a non-global regex that matched ANY table row
starting with the phase number. When an earlier table (e.g. Requirements
coverage | Phase | Requirements | Count |) preceded ## Progress, the regex
bound to the wrong row (3-column), no-op'd, and never reached the real
Progress row. roadmap_updated stayed true (it's existsSync), masking the
failure.

- src/phase.cts: scope the tableRowPattern regex to the ## Progress section
  (indexOf + slice) so it only matches Progress-table rows.
- tests/phase.test.cjs: regression test — ROADMAP with a phase-numbered
  Requirements table before ## Progress → Progress row updated, Requirements
  row untouched.

Closes #2012

* docs(#2012): backfill changeset pr 2032
This commit is contained in:
Tom Boucher
2026-07-05 17:39:11 -04:00
committed by GitHub
parent 460956bfed
commit f77179248d
3 changed files with 65 additions and 2 deletions

View File

@@ -0,0 +1,5 @@
---
type: Fixed
pr: 2032
---
**`phase.complete` now updates the `## Progress` rollup row even when an earlier phase-numbered table precedes it** — the Progress-row writer used a non-global regex that matched *any* table row starting with the phase number, so it bound to the first such row (e.g. a `| Phase | Requirements | Count |` coverage table), no-op'd on the wrong 3-column row, and never reached the real Progress row. The regex is now scoped to the `## Progress` section so it binds to the correct table. The command still returned `roadmap_updated: true` (that field is `fs.existsSync(ROADMAP.md)`), masking the silent failure. (#2012)

View File

@@ -1460,7 +1460,10 @@ function cmdPhaseComplete(cwd: string, phaseNum: string, raw: boolean): void {
`^(\\|\\s*${phaseEscaped}\\.?\\s[^|]*(?:\\|[^\\n]*))$`,
'im',
);
roadmapContent = roadmapContent.replace(tableRowPattern, (fullRow) => {
// Scope the Progress-row search to the ## Progress section so the regex
// doesn't bind to an earlier table (e.g. | Phase | Requirements | Count |)
// whose rows also start with the phase number. (#2012)
const updateProgressRow = (fullRow: string): string => {
const cells = fullRow.split('|').slice(1, -1);
const dateShape = /^\d{4}-\d{2}-\d{2}$/;
if (cells.length === 5) {
@@ -1477,7 +1480,15 @@ function cmdPhaseComplete(cwd: string, phaseNum: string, raw: boolean): void {
cells[3] = dateShape.test(existingDate4) ? cells[3] : ` ${today} `;
}
return '|' + cells.join('|') + '|';
});
};
const progressIdx = roadmapContent.indexOf('## Progress');
if (progressIdx >= 0) {
const beforeProgress = roadmapContent.slice(0, progressIdx);
const progressSection = roadmapContent.slice(progressIdx);
roadmapContent = beforeProgress + progressSection.replace(tableRowPattern, updateProgressRow);
} else {
roadmapContent = roadmapContent.replace(tableRowPattern, updateProgressRow);
}
const planCountPattern = new RegExp(
`(#{2,4}\\s*Phase\\s+${phaseEscaped}[\\s\\S]*?\\*\\*Plans:\\*\\*\\s*)[^\\n]+`,

View File

@@ -2530,6 +2530,53 @@ describe('phase complete command', () => {
assert.ok(roadmap.includes('completed'), 'completion date should be added');
});
test('#2012 — Progress row updated even when an earlier phase-numbered table precedes ## Progress', () => {
fs.writeFileSync(
path.join(tmpDir, '.planning', 'ROADMAP.md'),
`# Roadmap
- [ ] Phase 3: Build
### Phase 3: Build
**Goal:** Build stuff
## Requirements Coverage
| Phase | Requirements | Count |
|-------|-------------|-------|
| 3. Build | R-01 | 5 |
## Progress
| Phase | Milestone | Plans Complete | Status | Completed |
|-------|-----------|----------------|--------|-----------|
| 3. Build | v1.0 | 0/1 | Planned | - |
`
);
fs.writeFileSync(
path.join(tmpDir, '.planning', 'STATE.md'),
`# State\n\n**Current Phase:** 03\n**Status:** In progress\n**Last Activity:** 2025-01-01\n`
);
const p3 = path.join(tmpDir, '.planning', 'phases', '03-build');
fs.mkdirSync(p3, { recursive: true });
fs.writeFileSync(path.join(p3, '03-01-PLAN.md'), '# Plan');
fs.writeFileSync(path.join(p3, '03-01-SUMMARY.md'), '# Summary');
const result = runVerifiedPhaseComplete('phase complete 3', tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const roadmap = fs.readFileSync(path.join(tmpDir, '.planning', 'ROADMAP.md'), 'utf-8');
// The Requirements coverage table row must be UNCHANGED (3 columns, not a Progress row).
const reqRow = roadmap.match(/^\| 3\. Build \| R-01 \| 5 \|$/m);
assert.ok(reqRow, 'Requirements coverage row must be untouched');
// The Progress row must be updated to Complete with a date.
const progressRow = roadmap.match(/^\| 3\. Build \| v1\.0 \| 1\/1 \| Complete\s+\| \d{4}-\d{2}-\d{2} \|/m);
assert.ok(progressRow, 'Progress row must be updated to Complete with a date');
});
test('detects last phase in milestone', () => {
fs.writeFileSync(
path.join(tmpDir, '.planning', 'ROADMAP.md'),