From 0d6349a6c11061f46ad0c3f4b8b4c3fbd615f235 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Wed, 22 Apr 2026 12:03:56 -0400 Subject: [PATCH] fix(#2554): preserve leading zero in getMilestonePhaseFilter (#2585) The normalization `replace(/^0+/, '')` over-stripped decimal phase IDs: `"00.1"` collapsed to `".1"`, while the disk-side extractor yielded `"0.1"` from `"00.1-"`. Set membership failed and inserted decimal phases were silently excluded from every disk scan inside `buildStateFrontmatter`, causing `state update` to rewind progress counters. Strip leading zeros only when followed by a digit (`replace(/^0+(?=\d)/, '')`), preserving the zero before the decimal point while keeping existing behavior for zero-padded integer IDs. Closes #2554 --- get-shit-done/bin/lib/core.cjs | 2 +- tests/bug-2554-decimal-phase-filter.test.cjs | 78 ++++++++++++++++++++ 2 files changed, 79 insertions(+), 1 deletion(-) create mode 100644 tests/bug-2554-decimal-phase-filter.test.cjs diff --git a/get-shit-done/bin/lib/core.cjs b/get-shit-done/bin/lib/core.cjs index a426525aa..2c7aeb3d3 100644 --- a/get-shit-done/bin/lib/core.cjs +++ b/get-shit-done/bin/lib/core.cjs @@ -1624,7 +1624,7 @@ function getMilestonePhaseFilter(cwd) { } const normalized = new Set( - [...milestonePhaseNums].map(n => (n.replace(/^0+/, '') || '0').toLowerCase()) + [...milestonePhaseNums].map(n => (n.replace(/^0+(?=\d)/, '') || '0').toLowerCase()) ); function isDirInMilestone(dirName) { diff --git a/tests/bug-2554-decimal-phase-filter.test.cjs b/tests/bug-2554-decimal-phase-filter.test.cjs new file mode 100644 index 000000000..d0be44a19 --- /dev/null +++ b/tests/bug-2554-decimal-phase-filter.test.cjs @@ -0,0 +1,78 @@ +/** + * Regression test for bug #2554: + * state disk-scan excludes decimal phase dirs (e.g. "00.1") from progress counts. + * + * Root cause: getMilestonePhaseFilter normalized phase IDs with `replace(/^0+/, '')`, + * which over-strips on decimals: "00.1" → ".1", while the disk-side extractor + * applied to "00.1-" yields "0.1" — so the dir is excluded from the milestone. + * + * Fix: strip leading zeros only when followed by a digit (`replace(/^0+(?=\d)/, '')`), + * preserving the zero before the decimal point. + */ + +const { test, describe, beforeEach, afterEach } = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('fs'); +const path = require('path'); +const { createTempProject, cleanup } = require('./helpers.cjs'); +const { getMilestonePhaseFilter } = require('../get-shit-done/bin/lib/core.cjs'); + +describe('bug #2554 — getMilestonePhaseFilter decimal phase dirs', () => { + let tmpDir; + + beforeEach(() => { + tmpDir = createTempProject(); + }); + + afterEach(() => { + cleanup(tmpDir); + }); + + test('matches decimal phase directory like "00.1-" against ROADMAP phase "00.1"', () => { + fs.writeFileSync( + path.join(tmpDir, '.planning', 'ROADMAP.md'), + [ + '## Roadmap v1.0: Current', + '', + '### Phase 0: Foundation', + '**Goal:** foundation', + '', + '### Phase 00.1: Inserted urgent work', + '**Goal:** inserted', + '', + '### Phase 1: Feature', + '**Goal:** feature', + ].join('\n') + ); + + const filter = getMilestonePhaseFilter(tmpDir); + + // Phase 00.1 inserted between Phase 0 and Phase 1 must match its on-disk dir. + assert.strictEqual( + filter('00.1-app-namespace-rename'), + true, + 'decimal phase dir "00.1-" must be counted in the milestone' + ); + + // Neighbours should still match (no regression). + assert.strictEqual(filter('0-foundation'), true); + assert.strictEqual(filter('1-feature'), true); + }); + + test('preserves existing behavior for zero-padded integer phases', () => { + fs.writeFileSync( + path.join(tmpDir, '.planning', 'ROADMAP.md'), + [ + '### Phase 01: One', + '**Goal:** g', + '', + '### Phase 10: Ten', + '**Goal:** g', + ].join('\n') + ); + + const filter = getMilestonePhaseFilter(tmpDir); + assert.strictEqual(filter('01-one'), true); + assert.strictEqual(filter('10-ten'), true); + }); +});