fix: zero-padded phase numbers bypass archived-phase guard; stale current_milestone (#2458)
* fix(sdk): extractCurrentMilestone Backlog leak + state.begin-phase flag parsing Closes #2422 Closes #2420 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(sdk): skip stateVersion early-return for shipped milestones When STATE.md has a stale `milestone: v1.0` entry but v1.0 is already shipped (heading contains ✅ in ROADMAP.md), the stateVersion early-return path in getMilestoneInfo was returning v1.0 instead of detecting the new active milestone. Two-part fix: 1. In the stateVersion block: skip the early-return when the matched heading line includes ✅ (shipped marker). Fall through to normal detection instead. 2. In the heading-format fallback regex: add a negative lookahead `(?!.*✅)` so the regex never matches a ✅ heading regardless of whether stateVersion was present. This handles the no-STATE.md case and ensures fallthrough from part 1 actually finds the next milestone. Adds two regression tests covering both ✅-suffix (`## v1.0 ✅ Name`) and ✅-prefix (`## ✅ v1.0 Name`) heading formats. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(core): allow padded-and-unpadded phase headings in getRoadmapPhaseInternal The zero-strip normalization (01→1) fixed the archived-phase guard but broke lookup against ROADMAP headings that still use zero-padded numbers like "Phase 01:". Change the regex to use 0*<normalized> so both formats match, making the fix robust regardless of ROADMAP heading style. 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:
@@ -422,6 +422,66 @@ describe('init commands ignore archived phases from prior milestones sharing a n
|
||||
});
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Bug #2391: zero-padded phase numbers must not bypass archived-phase guard
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
describe('init plan-phase zero-padded phase number (bug #2391)', () => {
|
||||
let tmpDir;
|
||||
|
||||
beforeEach(() => {
|
||||
tmpDir = createTempProject();
|
||||
// Current milestone ROADMAP has Phase 3 (unpadded heading)
|
||||
fs.writeFileSync(
|
||||
path.join(tmpDir, '.planning', 'ROADMAP.md'),
|
||||
'# v2.0 Roadmap\n\n### Phase 3: Rotation Engine + Availability\n**Goal**: Rotation\n**Requirements**: ROTA-01, ROTA-02\n**Plans:** TBD\n'
|
||||
);
|
||||
// Prior milestone archive has a shipped Phase 3 with different content
|
||||
const archivedDir = path.join(tmpDir, '.planning', 'milestones', 'v1.0-phases', '03-plant-collection-and-rooms');
|
||||
fs.mkdirSync(archivedDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(archivedDir, '03-CONTEXT.md'), '# OLD v1.0 Phase 3 context');
|
||||
fs.writeFileSync(path.join(archivedDir, '03-RESEARCH.md'), '# OLD v1.0 Phase 3 research');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup(tmpDir);
|
||||
});
|
||||
|
||||
test('zero-padded "03" returns current ROADMAP phase, not archived v1.0 phase', () => {
|
||||
const result = runGsdTools('init plan-phase 03', tmpDir);
|
||||
assert.ok(result.success, `Command failed: ${result.error}`);
|
||||
|
||||
const output = JSON.parse(result.output);
|
||||
assert.strictEqual(output.phase_found, true);
|
||||
assert.strictEqual(output.phase_name, 'Rotation Engine + Availability',
|
||||
'phase_name must come from current ROADMAP.md, not the archived v1.0 phase');
|
||||
assert.strictEqual(output.phase_dir, null,
|
||||
'phase_dir must be null — current milestone has no directory yet');
|
||||
assert.strictEqual(output.has_context, false,
|
||||
'has_context must not inherit archived v1.0 artifacts');
|
||||
assert.strictEqual(output.has_research, false,
|
||||
'has_research must not inherit archived v1.0 artifacts');
|
||||
assert.ok(!output.context_path || !output.context_path.includes('v1.0'),
|
||||
'context_path must not point at archived v1.0 file');
|
||||
assert.strictEqual(output.phase_req_ids, 'ROTA-01, ROTA-02');
|
||||
});
|
||||
|
||||
test('unpadded "3" and zero-padded "03" return identical phase identity', () => {
|
||||
const result3 = runGsdTools('init plan-phase 3', tmpDir);
|
||||
const result03 = runGsdTools('init plan-phase 03', tmpDir);
|
||||
assert.ok(result3.success && result03.success, 'both commands must succeed');
|
||||
|
||||
const out3 = JSON.parse(result3.output);
|
||||
const out03 = JSON.parse(result03.output);
|
||||
assert.strictEqual(out03.phase_name, out3.phase_name,
|
||||
'phase_name must be identical regardless of padding');
|
||||
assert.strictEqual(out03.phase_slug, out3.phase_slug,
|
||||
'phase_slug must be identical regardless of padding');
|
||||
assert.strictEqual(out03.phase_req_ids, out3.phase_req_ids,
|
||||
'phase_req_ids must be identical regardless of padding');
|
||||
});
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// cmdInitTodos (INIT-01)
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user