fix(sdk): extractCurrentMilestone Backlog leak + state.begin-phase flag parsing (#2455)

* 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: patch-version semver in milestone boundary regex + flag-parser validation

Two follow-on correctness issues identified in code review:

1. roadmap.ts: currentVersionMatch and nextMilestoneRegex only captured
   major.minor (v(\d+\.\d+)), collapsing v2.0.1 to "2.0". A sub-heading
   "## v2.0.2 Phase Details" would match the same prefix and be incorrectly
   skipped. Both patterns updated to v(\d+(?:\.\d+)+) to capture full semver.

2. state-mutation.ts: pair-wise flag parsing loop advanced i by 2 unconditionally,
   so a missing flag value caused the next flag token to be assigned as the value
   (e.g. flags['phase'] = '--name'). Fix: iterate with i++ and validate that the
   candidate value exists and does not start with '--' before assigning; throw
   GSDError('missing value for --<key>') on invalid input. Added regression test.

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:14 -04:00
committed by GitHub
parent 990c3e648d
commit 59cfbbba6a
4 changed files with 145 additions and 22 deletions

View File

@@ -144,6 +144,57 @@ describe('extractCurrentMilestone', () => {
const result = await extractCurrentMilestone(content, tmpDir);
expect(result).toBe('current content');
});
// ─── Bug #2422: preamble Backlog leak ─────────────────────────────────
it('bug-2422: does not include ## Backlog section before the current milestone', async () => {
const roadmapWithBacklog = `# ROADMAP
## Backlog
### Phase 999.1: Parking lot item A
### Phase 999.2: Parking lot item B
### 🚧 v2.0 My Milestone (In Progress)
- [ ] **Phase 100: Real work**
## v2.0 Phase Details
### Phase 100: Real work
**Goal**: Do stuff.
`;
const state = `---\nmilestone: v2.0\n---\n# State\n`;
await writeFile(join(tmpDir, '.planning', 'STATE.md'), state);
await writeFile(join(tmpDir, '.planning', 'ROADMAP.md'), roadmapWithBacklog);
const result = await extractCurrentMilestone(roadmapWithBacklog, tmpDir);
// Must NOT include backlog phases
expect(result).not.toContain('Phase 999.1');
expect(result).not.toContain('Phase 999.2');
expect(result).not.toContain('Parking lot');
// Must include the actual v2.0 content
expect(result).toContain('Phase 100');
});
// ─── Bug #2422: same-version sub-heading truncation ───────────────────
it('bug-2422: does not truncate at same-version sub-heading (## v2.0 Phase Details)', async () => {
const roadmapWithDetails = `# ROADMAP
### 🚧 v2.0 My Milestone (In Progress)
- [ ] **Phase 100: Real work**
## v2.0 Phase Details
### Phase 100: Real work
**Goal**: Do stuff.
`;
const state = `---\nmilestone: v2.0\n---\n# State\n`;
await writeFile(join(tmpDir, '.planning', 'STATE.md'), state);
await writeFile(join(tmpDir, '.planning', 'ROADMAP.md'), roadmapWithDetails);
const result = await extractCurrentMilestone(roadmapWithDetails, tmpDir);
// The detail section must survive — not be cut off
expect(result).toContain('Phase 100');
expect(result).toContain('Phase Details');
});
});
// ─── roadmapGetPhase ──────────────────────────────────────────────────────