From a0c82f2bd800b21a914a703847599bb723b7f2e3 Mon Sep 17 00:00:00 2001 From: sim Date: Thu, 13 Aug 2026 04:30:40 -0400 Subject: [PATCH] fix(#3309): W002/W011/W026 state-consistency regressions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit gsd-test found three real regressions in the migrated STATE.md checks: - W002 didn't exempt phase refs whose only directory lives in an archived milestone (#3652) — now consults planning-snapshot.cts's archivedPhaseTokens field (added alongside this fix, shared with W006's identical need). - W011 (STATE/ROADMAP cross-validation) never fired: currentPhaseLabel only read the current template's bare "Phase:" field, silently missing legacy STATE.md fixtures that use the older bold "**Current Phase:**" field (mirrors state.cts's own resolveStatePhase fallback ladder, which the migration didn't carry over). - W026 (STATE milestone-complete vs. unstarted ROADMAP phases) had two independent defects: roadmapDeclaredPhases's milestone attribution can't see
/-shaped ROADMAP sections, and current- milestone resolution could go null — both silently emptied the "unstarted" set every time. Fixed by scoping ROADMAP.md to the current milestone via the same
-tolerant extractCurrentMilestone every other milestone-aware consumer uses, in a new dedicated planning-snapshot.cts field (currentMilestoneRoadmapPhaseIds) rather than reusing roadmapDeclaredPhases, which exists for a narrower,
-blind derivation (W021's own original logic) and would have regressed it if repurposed. Also fixes an unrelated drift-guard violation this same rule file introduced: its own phase-token regex was independently re-derived instead of built from the canonical PHASE_NUMBER_TOKEN_SOURCE. --- .../state-consistency.cts | 41 +++++++++++++------ 1 file changed, 28 insertions(+), 13 deletions(-) diff --git a/src/health-diagnostic-rules/state-consistency.cts b/src/health-diagnostic-rules/state-consistency.cts index b0a24e85c..1f88b43ff 100644 --- a/src/health-diagnostic-rules/state-consistency.cts +++ b/src/health-diagnostic-rules/state-consistency.cts @@ -55,7 +55,8 @@ type PlanningSnapshot = ReturnType { const valid = new Set(); @@ -120,6 +128,9 @@ function buildValidPhaseSet(snapshot: PlanningSnapshot): Set { for (const entry of snapshot.roadmapDeclaredPhases.value) { valid.add(entry.phaseId); } + for (const token of snapshot.archivedPhaseTokens.value) { + valid.add(token); + } return valid; } @@ -190,7 +201,7 @@ const RULE_W002: Rule = { */ function currentPhaseIdFromLabel(label: string | null): string | null { if (!label) return null; - const m = label.match(/^0*(\d+[A-Z]?(?:\.\d+)*)/); + const m = label.match(new RegExp(`^0*(${PHASE_NUMBER_TOKEN_SOURCE})`)); return m ? m[1] : null; } @@ -261,18 +272,22 @@ const RULE_W026: Rule = { const statusVal = (snapshot.stateStatus.value ?? '').trim().toLowerCase(); if (!/milestone complete|archived/.test(statusVal)) return []; - const currentMilestone = snapshot.milestone.value?.version ?? null; - if (currentMilestone === null) return []; - + // `currentMilestoneRoadmapPhaseIds` is already scoped to the current + // milestone (`extractCurrentMilestone(roadmapRaw, cwd)`, the same + // `
`/``-tolerant owner `verify.cts:2364` used) — no + // separate `currentMilestone` resolution/filter needed here (see the + // field's own doc comment on `PlanningSnapshot` for why + // `roadmapDeclaredPhases`'s `milestone` attribution is the wrong fit). const unstarted: string[] = []; - for (const entry of snapshot.roadmapDeclaredPhases.value) { - // Scoped to the current milestone only — mirrors the original's - // `extractCurrentMilestone(roadmapRaw, cwd)` narrowing before its - // phase-heading scan (`verify.cts:2363-2364`). - if (entry.milestone !== currentMilestone) continue; - const normalized = normalizePhaseName(entry.phaseId); - const hasDirectory = matchPhaseDirs(snapshot.phaseDirs.value, normalized).matches.length > 0; - if (!hasDirectory) unstarted.push(entry.phaseId); + for (const phaseId of snapshot.currentMilestoneRoadmapPhaseIds.value) { + const normalized = normalizePhaseName(phaseId); + // `allPhaseDirNames` — every directory under `phases/`, UNWINDOWED by + // ROADMAP-declaration membership — mirrors the original's own + // unwindowed `phaseDirNames2` (`verify.cts:2372-2382`, a direct + // `readdirSync` of the phases dir), not the current-milestone-windowed + // `phaseDirs`. + const hasDirectory = matchPhaseDirs(snapshot.allPhaseDirNames.value, normalized).matches.length > 0; + if (!hasDirectory) unstarted.push(phaseId); } if (unstarted.length === 0) return [];