fix(#892): parse checklist-style roadmap phases in validate/verify (#908)

buildRoadmapPhaseVariants() only matched heading-style phases (## Phase N:),
silently skipping the supported checklist format (- [x] **Phase N: name**).
This caused W007 false-positives for every on-disk phase dir when the project
uses a checklist ROADMAP. Fix adds a second regex pass (mirroring the existing
buildNotStartedPhaseVariants() approach). Also refactors the duplicate
inline heading-only regex in cmdValidateConsistency() to delegate to
buildRoadmapPhaseVariants() (DRY). Regression test in
tests/bug-892-validate-checklist-roadmap-phases.test.cjs covers both paths.

Closes #892

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Tom Boucher
2026-06-08 22:52:19 -04:00
committed by GitHub
parent 6edc39c4eb
commit 808df9110c
5 changed files with 316 additions and 22 deletions

View File

@@ -100,6 +100,15 @@ export function buildRoadmapPhaseVariants(roadmapContent: string): RoadmapPhaseV
roadmapPhases.add(m[1]);
for (const variant of phaseVariants(m[1])) roadmapPhaseVariants.add(variant);
}
// Also matches checklist-style entries (checked or unchecked):
// - [x] **Phase 01: name** - [X] **Phase 2-01: name** - [ ] **Phase 3: name**
// This is a supported ROADMAP format (parallel to buildNotStartedPhaseVariants).
const checklistPattern = /-\s*\[[ xX]\]\s*\*{0,2}Phase\s+([\w][\w.-]*)\s*:/gi;
let cm: RegExpExecArray | null;
while ((cm = checklistPattern.exec(roadmapContent)) !== null) {
roadmapPhases.add(cm[1]);
for (const variant of phaseVariants(cm[1])) roadmapPhaseVariants.add(variant);
}
return { roadmapPhases, roadmapPhaseVariants };
}

View File

@@ -641,21 +641,8 @@ function cmdValidateConsistency(cwd: string, raw: boolean): void {
const roadmapContentRaw = fs.readFileSync(roadmapPath, 'utf-8');
const roadmapContent = extractCurrentMilestone(roadmapContentRaw, cwd);
const roadmapPhases = new Set<string>();
const phasePattern =
/#{2,4}\s*(?:\[[^\]]+\]\s*)?Phase\s+([\w][\w.-]*)\s*:/gi;
let m: RegExpExecArray | null;
while ((m = phasePattern.exec(roadmapContent)) !== null) {
roadmapPhases.add(m[1]);
}
const fullRoadmapPhases = new Set<string>();
const fullPhasePattern =
/#{2,4}\s*(?:\[[^\]]+\]\s*)?Phase\s+([\w][\w.-]*)\s*:/gi;
let fm: RegExpExecArray | null;
while ((fm = fullPhasePattern.exec(roadmapContentRaw)) !== null) {
fullRoadmapPhases.add(fm[1]);
}
const { roadmapPhases } = buildRoadmapPhaseVariants(roadmapContent);
const { roadmapPhaseVariants: fullRoadmapPhaseVariants } = buildRoadmapPhaseVariants(roadmapContentRaw);
const diskPhases = collectDiskPhases(planBase);
@@ -666,13 +653,8 @@ function cmdValidateConsistency(cwd: string, raw: boolean): void {
}
for (const p of diskPhases) {
const normalized = normalizePhaseName(p);
const unpadded = String(parseInt(p, 10));
if (
!fullRoadmapPhases.has(p) &&
!fullRoadmapPhases.has(normalized) &&
!fullRoadmapPhases.has(unpadded)
) {
const variants = phaseVariants(p);
if (![...variants].some((v) => fullRoadmapPhaseVariants.has(v))) {
warnings.push(`Phase ${p} exists on disk but not in ROADMAP.md`);
}
}