fix(phase): guard backlog dirs and YYYY-MM dates in integer phase removal (#2466)

* fix(phase): guard backlog dirs and YYYY-MM dates in integer phase removal

Closes #2435
Closes #2434

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(phase): extend date-collision guard to hyphen-adjacent context

The lookbehind `(?<!\d)` in renameIntegerPhases only excluded
digit-prefixed matches; a YYYY-MM-DD date like 2026-05-14 has a hyphen
before the month digits, which passed the original guard and caused
date corruption when renumbering a phase whose zero-padded number
matched the month. Replace with `(?<![0-9-])` lookbehind and
`(?![0-9-])` lookahead to exclude both digit- and hyphen-adjacent
contexts. Adds a regression test for the hyphen-adjacent case.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Tom Boucher
2026-04-20 10:08:52 -04:00
committed by GitHub
parent d117c1045a
commit 4cd890b252
2 changed files with 138 additions and 2 deletions

View File

@@ -1268,6 +1268,142 @@ describe('phase remove command', () => {
const state = fs.readFileSync(path.join(tmpDir, '.planning', 'STATE.md'), 'utf-8');
assert.ok(state.includes('**Total Phases:** 1'), 'total phases should be decremented');
});
test('bug-2434: integer phase remove does not rename 999.x backlog directory', () => {
// Setup: an active integer phase 4 and a backlog phase 999.1
fs.writeFileSync(
path.join(tmpDir, '.planning', 'ROADMAP.md'),
`# Roadmap
### Phase 1: Foundation
**Goal:** Setup
### Phase 2: Auth
**Goal:** Authentication
### Phase 3: Features
**Goal:** Core features
### Phase 4: Extras
**Goal:** Extra stuff
### Phase 999.1: Backlog item
**Goal:** Parked backlog task
`
);
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '01-foundation'), { recursive: true });
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '02-auth'), { recursive: true });
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '03-features'), { recursive: true });
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '04-extras'), { recursive: true });
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '999.1-backlog-item'), { recursive: true });
const result = runGsdTools('phase remove 4', tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
// Backlog directory must remain at 999.1, not be decremented to 998.1
assert.ok(
fs.existsSync(path.join(tmpDir, '.planning', 'phases', '999.1-backlog-item')),
'backlog directory 999.1-backlog-item must not be renamed'
);
assert.ok(
!fs.existsSync(path.join(tmpDir, '.planning', 'phases', '998.1-backlog-item')),
'backlog directory must not be incorrectly renamed to 998.1'
);
});
test('bug-2435: integer phase remove does not corrupt YYYY-MM-DD dates in ROADMAP.md', () => {
// Setup: removing phase 4 from a roadmap containing 2026-04-14 date strings
fs.writeFileSync(
path.join(tmpDir, '.planning', 'ROADMAP.md'),
`# Roadmap
### Phase 1: Foundation
**Goal:** Setup
**Completed:** 2026-01-15
### Phase 2: Auth
**Goal:** Authentication
**Completed:** 2026-02-20
### Phase 3: Features
**Goal:** Core features
**Completed:** 2026-04-14
### Phase 4: Extras
**Goal:** Extra stuff
### Phase 5: Final
**Goal:** Final phase
`
);
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '01-foundation'), { recursive: true });
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '02-auth'), { recursive: true });
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '03-features'), { recursive: true });
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '04-extras'), { recursive: true });
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '05-final'), { recursive: true });
const result = runGsdTools('phase remove 4', tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const roadmap = fs.readFileSync(path.join(tmpDir, '.planning', 'ROADMAP.md'), 'utf-8');
// Dates must be preserved exactly
assert.ok(roadmap.includes('2026-01-15'), 'date 2026-01-15 must not be corrupted');
assert.ok(roadmap.includes('2026-02-20'), 'date 2026-02-20 must not be corrupted');
assert.ok(roadmap.includes('2026-04-14'), 'date 2026-04-14 must not be corrupted');
// Phase 5 should be renumbered to 4
assert.ok(roadmap.includes('Phase 4: Final'), 'Phase 5 should be renumbered to Phase 4');
});
test('bug-2435: integer phase remove does not corrupt date whose month matches removed phase number', () => {
// Setup: removing phase 4 from a roadmap containing 2026-05-14
// When renumbering phase 5→4, the regex must not replace "05-14" in the date "2026-05-14"
fs.writeFileSync(
path.join(tmpDir, '.planning', 'ROADMAP.md'),
`# Roadmap
### Phase 1: Foundation
**Goal:** Setup
**Completed:** 2026-01-15
### Phase 2: Auth
**Goal:** Authentication
**Completed:** 2026-02-20
### Phase 3: Features
**Goal:** Core features
**Completed:** 2026-03-10
### Phase 4: Extras
**Goal:** Extra stuff
### Phase 5: Final
**Goal:** Final phase
**Due:** 2026-05-14
`
);
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '01-foundation'), { recursive: true });
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '02-auth'), { recursive: true });
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '03-features'), { recursive: true });
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '04-extras'), { recursive: true });
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '05-final'), { recursive: true });
const result = runGsdTools('phase remove 4', tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const roadmap = fs.readFileSync(path.join(tmpDir, '.planning', 'ROADMAP.md'), 'utf-8');
// Date "2026-05-14" must not be corrupted to "2026-04-14" when phase 5 is renumbered to 4
assert.ok(roadmap.includes('2026-05-14'), 'date 2026-05-14 must not be corrupted when renumbering phase 5→4');
assert.ok(!roadmap.includes('2026-04-14'), 'date must not be incorrectly mutated to 2026-04-14');
// Phase 5 should be renumbered to 4
assert.ok(roadmap.includes('Phase 4: Final'), 'Phase 5 should be renumbered to Phase 4');
});
});
// ─────────────────────────────────────────────────────────────────────────────