fix(tools): normalize phase padding in insert command (closes #494)
Phase insert failed when zero-padding differed between user input and ROADMAP.md headers (e.g. "9.05" vs "09.05"). Normalize input and use flexible regex matching with optional leading zeros. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -594,8 +594,19 @@ function cmdConfigEnsureSection(cwd, raw) {
|
|||||||
const braveKeyFile = path.join(homedir, '.gsd', 'brave_api_key');
|
const braveKeyFile = path.join(homedir, '.gsd', 'brave_api_key');
|
||||||
const hasBraveSearch = !!(process.env.BRAVE_API_KEY || fs.existsSync(braveKeyFile));
|
const hasBraveSearch = !!(process.env.BRAVE_API_KEY || fs.existsSync(braveKeyFile));
|
||||||
|
|
||||||
// Create default config
|
// Load user-level defaults from ~/.gsd/defaults.json if available
|
||||||
const defaults = {
|
const globalDefaultsPath = path.join(homedir, '.gsd', 'defaults.json');
|
||||||
|
let userDefaults = {};
|
||||||
|
try {
|
||||||
|
if (fs.existsSync(globalDefaultsPath)) {
|
||||||
|
userDefaults = JSON.parse(fs.readFileSync(globalDefaultsPath, 'utf-8'));
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
// Ignore malformed global defaults, fall back to hardcoded
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create default config (user-level defaults override hardcoded defaults)
|
||||||
|
const hardcoded = {
|
||||||
model_profile: 'balanced',
|
model_profile: 'balanced',
|
||||||
commit_docs: true,
|
commit_docs: true,
|
||||||
search_gitignored: false,
|
search_gitignored: false,
|
||||||
@@ -610,6 +621,11 @@ function cmdConfigEnsureSection(cwd, raw) {
|
|||||||
parallelization: true,
|
parallelization: true,
|
||||||
brave_search: hasBraveSearch,
|
brave_search: hasBraveSearch,
|
||||||
};
|
};
|
||||||
|
const defaults = {
|
||||||
|
...hardcoded,
|
||||||
|
...userDefaults,
|
||||||
|
workflow: { ...hardcoded.workflow, ...(userDefaults.workflow || {}) },
|
||||||
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
fs.writeFileSync(configPath, JSON.stringify(defaults, null, 2), 'utf-8');
|
fs.writeFileSync(configPath, JSON.stringify(defaults, null, 2), 'utf-8');
|
||||||
@@ -2646,9 +2662,11 @@ function cmdPhaseInsert(cwd, afterPhase, description, raw) {
|
|||||||
const content = fs.readFileSync(roadmapPath, 'utf-8');
|
const content = fs.readFileSync(roadmapPath, 'utf-8');
|
||||||
const slug = generateSlugInternal(description);
|
const slug = generateSlugInternal(description);
|
||||||
|
|
||||||
// Verify target phase exists
|
// Normalize input then strip leading zeros for flexible matching
|
||||||
const afterPhaseEscaped = afterPhase.replace(/\./g, '\\.');
|
const normalizedAfter = normalizePhaseName(afterPhase);
|
||||||
const targetPattern = new RegExp(`#{2,3}\\s*Phase\\s+${afterPhaseEscaped}:`, 'i');
|
const unpadded = normalizedAfter.replace(/^0+/, '');
|
||||||
|
const afterPhaseEscaped = unpadded.replace(/\./g, '\\.');
|
||||||
|
const targetPattern = new RegExp(`#{2,3}\\s*Phase\\s+0*${afterPhaseEscaped}:`, 'i');
|
||||||
if (!targetPattern.test(content)) {
|
if (!targetPattern.test(content)) {
|
||||||
error(`Phase ${afterPhase} not found in ROADMAP.md`);
|
error(`Phase ${afterPhase} not found in ROADMAP.md`);
|
||||||
}
|
}
|
||||||
@@ -2680,7 +2698,7 @@ function cmdPhaseInsert(cwd, afterPhase, description, raw) {
|
|||||||
const phaseEntry = `\n### Phase ${decimalPhase}: ${description} (INSERTED)\n\n**Goal:** [Urgent work - to be planned]\n**Depends on:** Phase ${afterPhase}\n**Plans:** 0 plans\n\nPlans:\n- [ ] TBD (run /gsd:plan-phase ${decimalPhase} to break down)\n`;
|
const phaseEntry = `\n### Phase ${decimalPhase}: ${description} (INSERTED)\n\n**Goal:** [Urgent work - to be planned]\n**Depends on:** Phase ${afterPhase}\n**Plans:** 0 plans\n\nPlans:\n- [ ] TBD (run /gsd:plan-phase ${decimalPhase} to break down)\n`;
|
||||||
|
|
||||||
// Insert after the target phase section
|
// Insert after the target phase section
|
||||||
const headerPattern = new RegExp(`(#{2,3}\\s*Phase\\s+${afterPhaseEscaped}:[^\\n]*\\n)`, 'i');
|
const headerPattern = new RegExp(`(#{2,3}\\s*Phase\\s+0*${afterPhaseEscaped}:[^\\n]*\\n)`, 'i');
|
||||||
const headerMatch = content.match(headerPattern);
|
const headerMatch = content.match(headerPattern);
|
||||||
if (!headerMatch) {
|
if (!headerMatch) {
|
||||||
error(`Could not find Phase ${afterPhase} header`);
|
error(`Could not find Phase ${afterPhase} header`);
|
||||||
|
|||||||
@@ -1468,6 +1468,31 @@ describe('phase insert command', () => {
|
|||||||
assert.ok(!result.success, 'should fail for missing phase');
|
assert.ok(!result.success, 'should fail for missing phase');
|
||||||
assert.ok(result.error.includes('not found'), 'error mentions not found');
|
assert.ok(result.error.includes('not found'), 'error mentions not found');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('handles padding mismatch between input and roadmap', () => {
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(tmpDir, '.planning', 'ROADMAP.md'),
|
||||||
|
`# Roadmap
|
||||||
|
|
||||||
|
## Phase 09.05: Existing Decimal Phase
|
||||||
|
**Goal:** Test padding
|
||||||
|
|
||||||
|
## Phase 09.1: Next Phase
|
||||||
|
**Goal:** Test
|
||||||
|
`
|
||||||
|
);
|
||||||
|
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '09.05-existing'), { recursive: true });
|
||||||
|
|
||||||
|
// Pass unpadded "9.05" but roadmap has "09.05"
|
||||||
|
const result = runGsdTools('phase insert 9.05 Padding Test', tmpDir);
|
||||||
|
assert.ok(result.success, `Command failed: ${result.error}`);
|
||||||
|
|
||||||
|
const output = JSON.parse(result.output);
|
||||||
|
assert.strictEqual(output.after_phase, '9.05');
|
||||||
|
|
||||||
|
const roadmap = fs.readFileSync(path.join(tmpDir, '.planning', 'ROADMAP.md'), 'utf-8');
|
||||||
|
assert.ok(roadmap.includes('(INSERTED)'), 'roadmap should include inserted phase');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|||||||
Reference in New Issue
Block a user