* 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:
@@ -447,6 +447,51 @@ describe('initMilestoneOp', () => {
|
||||
expect(data.completed_phases).toBeGreaterThanOrEqual(0);
|
||||
expect(data.project_root).toBe(tmpDir);
|
||||
});
|
||||
|
||||
// Regression: #2633 — ROADMAP.md is the authority for current-milestone
|
||||
// phase count, not on-disk phase directories. After `phases clear` a new
|
||||
// milestone's roadmap may list phases 3/4/5 while only 03 and 04 exist on
|
||||
// disk yet. Deriving phase_count from disk yields 2 and falsely flags
|
||||
// all_phases_complete=true once both on-disk phases have summaries.
|
||||
it('derives phase_count from ROADMAP current milestone, not on-disk dirs (#2633)', async () => {
|
||||
// Custom fixture overriding the shared beforeEach: simulate post-cleanup
|
||||
// start of v1.1 where roadmap declares phases 3, 4, 5 but only 03 and 04
|
||||
// have been materialized on disk (both with summaries).
|
||||
const fresh = await mkdtemp(join(tmpdir(), 'gsd-init-2633-'));
|
||||
try {
|
||||
await mkdir(join(fresh, '.planning', 'phases', '03-alpha'), { recursive: true });
|
||||
await mkdir(join(fresh, '.planning', 'phases', '04-beta'), { recursive: true });
|
||||
await writeFile(join(fresh, '.planning', 'config.json'), JSON.stringify({
|
||||
model_profile: 'balanced',
|
||||
workflow: { nyquist_validation: true },
|
||||
}));
|
||||
await writeFile(join(fresh, '.planning', 'STATE.md'), [
|
||||
'---', 'milestone: v1.1', 'milestone_name: Next', 'status: executing', '---', '',
|
||||
].join('\n'));
|
||||
await writeFile(join(fresh, '.planning', 'ROADMAP.md'), [
|
||||
'# Roadmap', '',
|
||||
'## v1.1: Next',
|
||||
'',
|
||||
'### Phase 3: Alpha', '**Goal:** A', '',
|
||||
'### Phase 4: Beta', '**Goal:** B', '',
|
||||
'### Phase 5: Gamma', '**Goal:** C', '',
|
||||
].join('\n'));
|
||||
// Both on-disk phases have summaries (completed).
|
||||
await writeFile(join(fresh, '.planning', 'phases', '03-alpha', '03-01-SUMMARY.md'), '# S');
|
||||
await writeFile(join(fresh, '.planning', 'phases', '04-beta', '04-01-SUMMARY.md'), '# S');
|
||||
|
||||
const result = await initMilestoneOp([], fresh);
|
||||
const data = result.data as Record<string, unknown>;
|
||||
// Roadmap declares 3 phases for the current milestone.
|
||||
expect(data.phase_count).toBe(3);
|
||||
// Only 2 are materialized + summarized on disk.
|
||||
expect(data.completed_phases).toBe(2);
|
||||
// Therefore milestone is NOT complete — phase 5 is still outstanding.
|
||||
expect(data.all_phases_complete).toBe(false);
|
||||
} finally {
|
||||
await rm(fresh, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('initMapCodebase', () => {
|
||||
|
||||
Reference in New Issue
Block a user