fix: normalize phase numbers in stats Map to prevent duplicate rows (#2220)

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 <noreply@anthropic.com>
This commit is contained in:
Tom Boucher
2026-04-15 14:59:35 -04:00
committed by GitHub
parent 899419ebec
commit 7b0a8b6237
2 changed files with 37 additions and 5 deletions

View File

@@ -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,

View File

@@ -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);
});
});
// ─────────────────────────────────────────────────────────────────────────────