fix(#2633): ROADMAP.md is the authority for current-milestone phase counts (#2665)

* fix(#2633): use ROADMAP.md as authority for current-milestone phase counts

initMilestoneOp (SDK + CJS) derives phase_count and completed_phases from
the current milestone section of ROADMAP.md instead of counting on-disk
`.planning/phases/` directories. After `phases clear` at the start of a new
milestone the on-disk set is a subset of the roadmap, causing premature
`all_phases_complete: true`.

validateHealth W002 now unions ROADMAP.md phase declarations (all milestones
— current, shipped, backlog) with on-disk dirs when checking STATE.md phase
refs. Eliminates false positives for future-phase refs in the current
milestone and history-phase refs from shipped milestones.

Falls back to legacy on-disk counting when ROADMAP.md is missing or
unparseable so no-roadmap fixtures still work.

Adds vitest regressions for both handlers; all 66 SDK + 118 CJS tests pass.

* fix(#2633): preserve full phase tokens in W002 + completion lookup

CodeRabbit flagged that the parseInt-based normalization collapses distinct
phase IDs (3, 3A, 3.1) into the same integer bucket, masking real
STATE/ROADMAP mismatches and miscounting completions in milestones with
inserted/sub-phases.

Index disk dirs and validate STATE.md refs by canonical full phase token —
strip leading zeros from the integer head only, preserve [A-Z] suffix and
dotted segments, and accept just the leading-zero variant of the integer
prefix as a tolerated alias. 3A and 3 never share a bucket.

Also widens the disk and STATE.md regexes to accept [A-Z]? suffix tokens.
This commit is contained in:
Tom Boucher
2026-04-24 18:11:12 -04:00
committed by GitHub
parent c8ae6b3b4f
commit 7b470f2625
6 changed files with 282 additions and 30 deletions

View File

@@ -550,6 +550,49 @@ describe('validateHealth', () => {
expect(w003!.repairable).toBe(true);
});
// Regression: #2633 — W002 must consult ROADMAP.md (current + shipped
// milestones) for valid phase numbers, not only on-disk phase dirs. After
// `phases clear` at the start of a new milestone, STATE.md can legitimately
// reference future phases (current milestone) and history phases (shipped
// milestones) that no longer have a corresponding disk directory.
it('does not emit W002 for roadmap-valid future or history phase refs (#2633)', async () => {
const planning = join(tmpDir, '.planning');
await mkdir(join(planning, 'phases', '03-alpha'), { recursive: true });
await mkdir(join(planning, 'phases', '04-beta'), { recursive: true });
await writeFile(join(planning, 'PROJECT.md'), '# Project\n\n## What This Is\n\nA project.\n\n## Core Value\n\nValue here.\n\n## Requirements\n\n- Req 1\n');
await writeFile(join(planning, 'ROADMAP.md'), [
'# Roadmap', '',
'## v1.0: Shipped ✅ SHIPPED', '',
'### Phase 1: Origin', '**Goal:** O', '',
'### Phase 2: Continuation', '**Goal:** C', '',
'## v1.1: Current', '',
'### Phase 3: Alpha', '**Goal:** A', '',
'### Phase 4: Beta', '**Goal:** B', '',
'### Phase 5: Gamma', '**Goal:** C', '',
].join('\n'));
await writeFile(join(planning, 'STATE.md'), [
'---', 'milestone: v1.1', 'milestone_name: Current', 'status: executing', '---', '',
'# State', '',
'**Current Phase:** 4',
'**Next:** Phase 5',
'',
'## Accumulated Context',
'- Decision from Phase 1',
'- Follow-up from Phase 2',
].join('\n'));
await writeFile(join(planning, 'config.json'), JSON.stringify({
model_profile: 'balanced',
workflow: { nyquist_validation: true },
}, null, 2));
const result = await validateHealth([], tmpDir);
const data = result.data as Record<string, unknown>;
const warnings = data.warnings as Array<Record<string, unknown>>;
const w002s = warnings.filter(w => w.code === 'W002');
expect(w002s).toEqual([]);
});
it('returns warning W005 for bad phase directory naming', async () => {
await createHealthyPlanning();
await mkdir(join(tmpDir, '.planning', 'phases', 'bad_name'), { recursive: true });