mirror of
https://github.com/glittercowboy/get-shit-done
synced 2026-04-25 17:25:23 +02:00
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:
@@ -625,7 +625,7 @@ function renameIntegerPhases(phasesDir, removedInt) {
|
||||
const m = dir.match(/^(\d+)([A-Z])?(?:\.(\d+))?-(.+)$/i);
|
||||
if (!m) return null;
|
||||
const dirInt = parseInt(m[1], 10);
|
||||
return dirInt > removedInt ? { dir, oldInt: dirInt, letter: m[2] ? m[2].toUpperCase() : '', decimal: m[3] ? parseInt(m[3], 10) : null, slug: m[4] } : null;
|
||||
return (dirInt > removedInt && dirInt < 999) ? { dir, oldInt: dirInt, letter: m[2] ? m[2].toUpperCase() : '', decimal: m[3] ? parseInt(m[3], 10) : null, slug: m[4] } : null;
|
||||
})
|
||||
.filter(Boolean)
|
||||
.sort((a, b) => a.oldInt !== b.oldInt ? b.oldInt - a.oldInt : (b.decimal || 0) - (a.decimal || 0));
|
||||
@@ -673,7 +673,7 @@ function updateRoadmapAfterPhaseRemoval(roadmapPath, targetPhase, isDecimal, rem
|
||||
const oldPad = oldStr.padStart(2, '0'), newPad = newStr.padStart(2, '0');
|
||||
content = content.replace(new RegExp(`(#{2,4}\\s*Phase\\s+)${oldStr}(\\s*:)`, 'gi'), `$1${newStr}$2`);
|
||||
content = content.replace(new RegExp(`(Phase\\s+)${oldStr}([:\\s])`, 'g'), `$1${newStr}$2`);
|
||||
content = content.replace(new RegExp(`${oldPad}-(\\d{2})`, 'g'), `${newPad}-$1`);
|
||||
content = content.replace(new RegExp(`(?<![0-9-])${oldPad}-(\\d{2})(?![0-9-])`, 'g'), `${newPad}-$1`);
|
||||
content = content.replace(new RegExp(`(\\|\\s*)${oldStr}\\.\\s`, 'g'), `$1${newStr}. `);
|
||||
content = content.replace(new RegExp(`(Depends on:\\*\\*\\s*Phase\\s+)${oldStr}\\b`, 'gi'), `$1${newStr}`);
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user