fix(verify): suppress W006 for phases with unchecked ROADMAP summary checkbox (#2009) (#2027)

W006 (Phase in ROADMAP.md but no directory on disk) fired for every phase
listed in ROADMAP.md that lacked a phase directory, including future phases
that haven't been started yet. This produced false DEGRADED health status
on any project with more than one phase planned.

Fix: before emitting W006, check the ROADMAP summary list for a
'- [ ] **Phase N:**' unchecked checkbox. Phases explicitly marked as not
yet started are intentionally absent from disk -- skip W006 for them.
Phases with a checked checkbox ([x]) or with no summary entry still
trigger W006 as before.

Adds two regression tests: one verifying W006 is suppressed for unchecked
phases, and one verifying W006 still fires for checked phases with no disk
directory.
This commit is contained in:
Tom Boucher
2026-04-10 11:03:10 -04:00
committed by GitHub
parent 0f77681df4
commit 14b8add69e
2 changed files with 84 additions and 0 deletions

View File

@@ -743,10 +743,24 @@ function cmdValidateHealth(cwd, options, raw) {
}
} catch { /* intentionally empty */ }
// Build a set of phases explicitly marked not-yet-started in the ROADMAP
// summary list (- [ ] **Phase N:**). These phases are intentionally absent
// from disk -- W006 must not fire for them (#2009).
const notStartedPhases = new Set();
const uncheckedPattern = /-\s*\[\s\]\s*\*{0,2}Phase\s+(\d+[A-Z]?(?:\.\d+)*)[:\s*]/gi;
let um;
while ((um = uncheckedPattern.exec(roadmapContent)) !== null) {
notStartedPhases.add(um[1]);
// Also add zero-padded variant so 1 and 01 both match
notStartedPhases.add(String(parseInt(um[1], 10)).padStart(2, '0'));
}
// Phases in ROADMAP but not on disk
for (const p of roadmapPhases) {
const padded = String(parseInt(p, 10)).padStart(2, '0');
if (!diskPhases.has(p) && !diskPhases.has(padded)) {
// Skip phases explicitly flagged as not-yet-started in the summary list
if (notStartedPhases.has(p) || notStartedPhases.has(padded)) continue;
addIssue('warning', 'W006', `Phase ${p} in ROADMAP.md but no directory on disk`, 'Create phase directory or remove from roadmap');
}
}

View File

@@ -418,6 +418,76 @@ describe('validate health command', () => {
);
});
// ─── Check 8b: W006 false-positives for not-yet-started phases (#2009) ──────
test('does not emit W006 for phases listed in ROADMAP summary as unchecked (not started)', () => {
// A ROADMAP with Phase 1 started (has disk dir) and Phase 2 listed but
// unchecked (- [ ]) — phase 2 has no directory because it hasn't started.
// W006 must NOT fire for phase 2.
writeMinimalProjectMd(tmpDir);
fs.writeFileSync(
path.join(tmpDir, '.planning', 'ROADMAP.md'),
[
'# Roadmap',
'',
'## v1.0.0',
'',
'- [x] **Phase 1: Setup** - First phase',
'- [ ] **Phase 2: Build** - Not yet started',
'',
'### Phase 1: Setup',
'',
'### Phase 2: Build',
'',
].join('\n')
);
writeMinimalStateMd(tmpDir, '# Session State\n\nPhase 1 in progress.\n');
writeValidConfigJson(tmpDir);
// Only phase 1 dir exists; phase 2 dir does not (not started yet)
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '01-setup'), { recursive: true });
const result = runGsdTools('validate health', tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const output = JSON.parse(result.output);
const w006s = output.warnings.filter(w => w.code === 'W006');
assert.ok(
w006s.length === 0,
'W006 must not fire for phases with an unchecked summary checkbox (not yet started), got: ' +
JSON.stringify(w006s)
);
});
test('still emits W006 for a phase that was started (checked) but has no directory', () => {
// Phase 1 is marked complete ([x]) in ROADMAP summary but has no directory
// on disk — that IS a genuine inconsistency and should still trigger W006.
writeMinimalProjectMd(tmpDir);
fs.writeFileSync(
path.join(tmpDir, '.planning', 'ROADMAP.md'),
[
'# Roadmap',
'',
'- [x] **Phase 1: Setup** - Completed',
'',
'### Phase 1: Setup',
'',
].join('\n')
);
writeMinimalStateMd(tmpDir, '# Session State\n\nPhase 1 done.\n');
writeValidConfigJson(tmpDir);
// No phase 1 directory — even though roadmap says it's complete
const result = runGsdTools('validate health', tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const output = JSON.parse(result.output);
assert.ok(
output.warnings.some(w => w.code === 'W006'),
'W006 must still fire when a completed phase has no directory, warnings: ' +
JSON.stringify(output.warnings)
);
});
// ─── Check 7b: Nyquist VALIDATION.md consistency (W009) ──────────────────
test('detects W009 when RESEARCH.md has Validation Architecture but no VALIDATION.md', () => {