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:
@@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
Reference in New Issue
Block a user