From 7b0a8b623722fa4ae3d13cccba30e8f6d7883fd1 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Wed, 15 Apr 2026 14:59:35 -0400 Subject: [PATCH] fix: normalize phase numbers in stats Map to prevent duplicate rows (#2220) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When ROADMAP.md uses unpadded phase numbers (e.g. "Phase 1:") and the phases/ directory uses zero-padded names (e.g. "01-auth"), the phasesByNumber Map held two separate entries — one keyed "1" from the ROADMAP heading scan and one keyed "01" from the directory scan — doubling phases_total in /gsd-stats output. Apply normalizePhaseName() to all Map keys in both the ROADMAP heading scan and the directory scan so the two code paths always produce the same canonical key and merge into a single entry. Closes #2195 Co-authored-by: Claude Sonnet 4.6 --- get-shit-done/bin/lib/commands.cjs | 12 +++++++----- tests/commands.test.cjs | 30 ++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 5 deletions(-) diff --git a/get-shit-done/bin/lib/commands.cjs b/get-shit-done/bin/lib/commands.cjs index 630b4703..a7743664 100644 --- a/get-shit-done/bin/lib/commands.cjs +++ b/get-shit-done/bin/lib/commands.cjs @@ -831,8 +831,9 @@ function cmdStats(cwd, format, raw) { const headingPattern = /#{2,4}\s*Phase\s+(\d+[A-Z]?(?:\.\d+)*)\s*:\s*([^\n]+)/gi; let match; while ((match = headingPattern.exec(roadmapContent)) !== null) { - phasesByNumber.set(match[1], { - number: match[1], + const key = normalizePhaseName(match[1]); + phasesByNumber.set(key, { + number: key, name: match[2].replace(/\(INSERTED\)/i, '').trim(), plans: 0, summaries: 0, @@ -862,9 +863,10 @@ function cmdStats(cwd, format, raw) { const status = determinePhaseStatus(plans, summaries, path.join(phasesDir, dir), 'Not Started'); - const existing = phasesByNumber.get(phaseNum); - phasesByNumber.set(phaseNum, { - number: phaseNum, + const normalizedNum = normalizePhaseName(phaseNum); + const existing = phasesByNumber.get(normalizedNum); + phasesByNumber.set(normalizedNum, { + number: normalizedNum, name: existing?.name || phaseName, plans: (existing?.plans || 0) + plans, summaries: (existing?.summaries || 0) + summaries, diff --git a/tests/commands.test.cjs b/tests/commands.test.cjs index a899dcfa..0c3f88dc 100644 --- a/tests/commands.test.cjs +++ b/tests/commands.test.cjs @@ -1693,6 +1693,36 @@ describe('stats command', () => { const output = JSON.parse(result.output); assert.strictEqual(output.phases[0].status, 'Executed', 'progress should show Executed without verification'); }); + + test('does not duplicate phases when ROADMAP uses unpadded numbers and dirs use padded numbers', () => { + // ROADMAP.md uses "Phase 1:" (unpadded) but directory is "01-auth" (padded). + // Without normalization, the Map holds two entries: "1" and "01", doubling phases_total. + const p1 = path.join(tmpDir, '.planning', 'phases', '01-auth'); + 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'); + fs.writeFileSync(path.join(p1, 'VERIFICATION.md'), '---\nstatus: passed\n---\n# Verified'); + + fs.writeFileSync( + path.join(tmpDir, '.planning', 'ROADMAP.md'), + [ + '# Roadmap', + '', + '## Milestone v1', + '', + '### Phase 1: Auth', + '**Goal:** Authentication', + ].join('\n') + ); + + const result = runGsdTools('stats', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const stats = JSON.parse(result.output); + assert.strictEqual(stats.phases_total, 1, 'unpadded ROADMAP heading and padded dir should merge into one phase'); + assert.strictEqual(stats.phases_completed, 1); + assert.strictEqual(stats.phases.length, 1); + }); }); // ─────────────────────────────────────────────────────────────────────────────