From e3bc614eb7947ea36de99d8a5f13433e93e01829 Mon Sep 17 00:00:00 2001 From: j2h4u <39818683+j2h4u@users.noreply.github.com> Date: Wed, 4 Mar 2026 03:14:44 +0500 Subject: [PATCH] fix(roadmap): handle 5-column progress tables with Milestone column The regex-based table parser captured a fixed number of columns, so 5-column tables (Phase | Milestone | Plans | Status | Completed) had the Milestone column eaten and Status/Date written to wrong cells. Replaced regex with cell-based `split('|')` parsing that detects column count (4 or 5) and updates the correct cells by index. Affects both `cmdRoadmapUpdatePlanProgress` and `cmdPhaseComplete`. Co-Authored-By: Claude Opus 4.6 --- get-shit-done/bin/lib/phase.cjs | 25 ++++++++++++------- get-shit-done/bin/lib/roadmap.cjs | 27 ++++++++++++++------- tests/phase.test.cjs | 40 +++++++++++++++++++++++++++++++ tests/roadmap.test.cjs | 32 +++++++++++++++++++++++++ 4 files changed, 108 insertions(+), 16 deletions(-) diff --git a/get-shit-done/bin/lib/phase.cjs b/get-shit-done/bin/lib/phase.cjs index d3b7d98b7..097175e15 100644 --- a/get-shit-done/bin/lib/phase.cjs +++ b/get-shit-done/bin/lib/phase.cjs @@ -767,16 +767,25 @@ function cmdPhaseComplete(cwd, phaseNum, raw) { ); roadmapContent = replaceInCurrentMilestone(roadmapContent, checkboxPattern, `$1x$2 (completed ${today})`); - // Progress table: update Status to Complete, add date + // Progress table: update Status to Complete, add date (handles 4 or 5 column tables) const phaseEscaped = escapeRegex(phaseNum); - const tablePattern = new RegExp( - `(\\|\\s*${phaseEscaped}\\.?\\s[^|]*\\|[^|]*\\|)\\s*[^|]*(\\|)\\s*[^|]*(\\|)`, - 'i' - ); - roadmapContent = replaceInCurrentMilestone( - roadmapContent, tablePattern, - `$1 Complete $2 ${today} $3` + const tableRowPattern = new RegExp( + `^(\\|\\s*${phaseEscaped}\\.?\\s[^|]*(?:\\|[^\\n]*))$`, + 'im' ); + roadmapContent = roadmapContent.replace(tableRowPattern, (fullRow) => { + const cells = fullRow.split('|').slice(1, -1); + if (cells.length === 5) { + // 5-col: Phase | Milestone | Plans | Status | Completed + cells[3] = ' Complete '; + cells[4] = ` ${today} `; + } else if (cells.length === 4) { + // 4-col: Phase | Plans | Status | Completed + cells[2] = ' Complete '; + cells[3] = ` ${today} `; + } + return '|' + cells.join('|') + '|'; + }); // Update plan count in phase section const planCountPattern = new RegExp( diff --git a/get-shit-done/bin/lib/roadmap.cjs b/get-shit-done/bin/lib/roadmap.cjs index fc4c7010c..693baf4f5 100644 --- a/get-shit-done/bin/lib/roadmap.cjs +++ b/get-shit-done/bin/lib/roadmap.cjs @@ -257,16 +257,27 @@ function cmdRoadmapUpdatePlanProgress(cwd, phaseNum, raw) { let roadmapContent = fs.readFileSync(roadmapPath, 'utf-8'); const phaseEscaped = escapeRegex(phaseNum); - // Progress table row: update Plans column (summaries/plans) and Status column - const tablePattern = new RegExp( - `(\\|\\s*${phaseEscaped}\\.?\\s[^|]*\\|)[^|]*(\\|)\\s*[^|]*(\\|)\\s*[^|]*(\\|)`, - 'i' + // Progress table row: update Plans/Status/Date columns (handles 4 or 5 column tables) + const tableRowPattern = new RegExp( + `^(\\|\\s*${phaseEscaped}\\.?\\s[^|]*(?:\\|[^\\n]*))$`, + 'im' ); const dateField = isComplete ? ` ${today} ` : ' '; - roadmapContent = replaceInCurrentMilestone( - roadmapContent, tablePattern, - `$1 ${summaryCount}/${planCount} $2 ${status.padEnd(11)}$3${dateField}$4` - ); + roadmapContent = roadmapContent.replace(tableRowPattern, (fullRow) => { + const cells = fullRow.split('|').slice(1, -1); // drop leading/trailing empty from split + if (cells.length === 5) { + // 5-col: Phase | Milestone | Plans | Status | Completed + cells[2] = ` ${summaryCount}/${planCount} `; + cells[3] = ` ${status.padEnd(11)}`; + cells[4] = dateField; + } else if (cells.length === 4) { + // 4-col: Phase | Plans | Status | Completed + cells[1] = ` ${summaryCount}/${planCount} `; + cells[2] = ` ${status.padEnd(11)}`; + cells[3] = dateField; + } + return '|' + cells.join('|') + '|'; + }); // Update plan count in phase detail section const planCountPattern = new RegExp( diff --git a/tests/phase.test.cjs b/tests/phase.test.cjs index 76a21a68e..a4df0743c 100644 --- a/tests/phase.test.cjs +++ b/tests/phase.test.cjs @@ -1468,6 +1468,46 @@ describe('phase complete command', () => { const req = fs.readFileSync(path.join(tmpDir, '.planning', 'REQUIREMENTS.md'), 'utf-8'); assert.ok(req.includes('- [ ] **AMT-01**'), 'AMT-01 should remain unchanged'); }); + + test('preserves Milestone column in 5-column progress table', () => { + fs.writeFileSync( + path.join(tmpDir, '.planning', 'ROADMAP.md'), + `# Roadmap + +- [ ] Phase 1: Foundation + +### Phase 1: Foundation +**Goal:** Setup +**Plans:** 1 plans + +## Progress + +| Phase | Milestone | Plans Complete | Status | Completed | +|-------|-----------|----------------|--------|-----------| +| 1. Foundation | v1.0 | 0/1 | Planned | | +` + ); + fs.writeFileSync( + path.join(tmpDir, '.planning', 'STATE.md'), + `# State\n\n**Current Phase:** 01\n**Status:** In progress\n**Current Plan:** 01-01\n**Last Activity:** 2025-01-01\n**Last Activity Description:** Working\n` + ); + + const p1 = path.join(tmpDir, '.planning', 'phases', '01-foundation'); + fs.mkdirSync(p1, { recursive: true }); + fs.writeFileSync(path.join(p1, '01-01-PLAN.md'), '# Plan'); + fs.writeFileSync(path.join(p1, '01-01-SUMMARY.md'), '# Summary'); + + const result = runGsdTools('phase complete 1', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const roadmap = fs.readFileSync(path.join(tmpDir, '.planning', 'ROADMAP.md'), 'utf-8'); + const rowMatch = roadmap.match(/^\|[^\n]*1\. Foundation[^\n]*$/m); + assert.ok(rowMatch, 'table row should exist'); + const cells = rowMatch[0].split('|').slice(1, -1).map(c => c.trim()); + assert.strictEqual(cells.length, 5, 'should have 5 columns'); + assert.strictEqual(cells[1], 'v1.0', 'Milestone column should be preserved'); + assert.ok(cells[3].includes('Complete'), 'Status column should be Complete'); + }); }); // ───────────────────────────────────────────────────────────────────────────── diff --git a/tests/roadmap.test.cjs b/tests/roadmap.test.cjs index 7f41055a3..f043e40f9 100644 --- a/tests/roadmap.test.cjs +++ b/tests/roadmap.test.cjs @@ -792,6 +792,38 @@ describe('roadmap update-plan-progress command', () => { assert.ok(roadmap.includes('[ ] 50-02-PLAN.md') || roadmap.includes('[ ] 50-02'), 'incomplete plan checkbox should remain unchecked'); }); + + test('preserves Milestone column in 5-column progress table', () => { + const roadmapContent = `# Roadmap + +### Phase 50: Build +**Goal:** Build stuff +**Plans:** 1 plans + +## Progress + +| Phase | Milestone | Plans Complete | Status | Completed | +|-------|-----------|----------------|--------|-----------| +| 50. Build | v2.0 | 0/1 | Planned | | +`; + fs.writeFileSync(path.join(tmpDir, '.planning', 'ROADMAP.md'), roadmapContent); + + const p50 = path.join(tmpDir, '.planning', 'phases', '50-build'); + fs.mkdirSync(p50, { recursive: true }); + fs.writeFileSync(path.join(p50, '50-01-PLAN.md'), '# Plan'); + fs.writeFileSync(path.join(p50, '50-01-SUMMARY.md'), '# Summary'); + + const result = runGsdTools('roadmap update-plan-progress 50', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const roadmap = fs.readFileSync(path.join(tmpDir, '.planning', 'ROADMAP.md'), 'utf-8'); + const rowMatch = roadmap.match(/^\|[^\n]*50\. Build[^\n]*$/m); + assert.ok(rowMatch, 'table row should exist'); + const cells = rowMatch[0].split('|').slice(1, -1).map(c => c.trim()); + assert.strictEqual(cells.length, 5, 'should have 5 columns'); + assert.strictEqual(cells[1], 'v2.0', 'Milestone column should be preserved'); + assert.ok(cells[3].includes('Complete'), 'Status column should show Complete'); + }); }); // ─────────────────────────────────────────────────────────────────────────────