Merge pull request #3565 from gsd-build/fix/3559-validate-health-w006-false-positive-for-
fix: avoid W006 for not-started future phases
This commit is contained in:
5
.changeset/zesty-eagles-fly.md
Normal file
5
.changeset/zesty-eagles-fly.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
type: Fixed
|
||||
pr: 3565
|
||||
---
|
||||
**validate.health no longer warns W006 for not-started phases** - unchecked roadmap phase entries are treated as planned/future and no longer require on-disk phase directories until work has started. Fixes #3559.
|
||||
@@ -643,6 +643,53 @@ describe('validateHealth', () => {
|
||||
expect(w006s.some(w => String(w.message).includes('Phase 7'))).toBe(false);
|
||||
});
|
||||
|
||||
it('does not emit W006 for unchecked future phases with no directory (#3559)', async () => {
|
||||
await createHealthyPlanning();
|
||||
await writeFile(join(tmpDir, '.planning', 'ROADMAP.md'), [
|
||||
'# Roadmap',
|
||||
'',
|
||||
'## v1.1: Current',
|
||||
'',
|
||||
'- [x] **Phase 21: Active**',
|
||||
'- [ ] **Phase 22: Future planned**',
|
||||
'',
|
||||
'### Phase 21: Active',
|
||||
'',
|
||||
'### Phase 22: Future planned',
|
||||
'',
|
||||
].join('\n'));
|
||||
await mkdir(join(tmpDir, '.planning', 'phases', '21-active'), { recursive: true });
|
||||
|
||||
const result = await validateHealth([], tmpDir);
|
||||
const data = result.data as Record<string, unknown>;
|
||||
const warnings = data.warnings as Array<Record<string, unknown>>;
|
||||
const w006s = warnings.filter(w => w.code === 'W006');
|
||||
expect(w006s.some(w => String(w.message).includes('Phase 22'))).toBe(false);
|
||||
});
|
||||
|
||||
it('does not alias 22A to 22 when suppressing W006 (#3565)', async () => {
|
||||
await createHealthyPlanning();
|
||||
await writeFile(join(tmpDir, '.planning', 'ROADMAP.md'), [
|
||||
'# Roadmap',
|
||||
'',
|
||||
'## v1.1: Current',
|
||||
'',
|
||||
'- [x] **Phase 22: Started phase**',
|
||||
'- [ ] **Phase 22A: Future variant**',
|
||||
'',
|
||||
'### Phase 22: Started phase',
|
||||
'',
|
||||
'### Phase 22A: Future variant',
|
||||
'',
|
||||
].join('\n'));
|
||||
|
||||
const result = await validateHealth([], tmpDir);
|
||||
const data = result.data as Record<string, unknown>;
|
||||
const warnings = data.warnings as Array<Record<string, unknown>>;
|
||||
const w006s = warnings.filter(w => w.code === 'W006');
|
||||
expect(w006s.some(w => String(w.message).includes('Phase 22 in ROADMAP.md'))).toBe(true);
|
||||
});
|
||||
|
||||
it('does not emit I001 when plan file has descriptor but summary uses canonical stem (#3473)', async () => {
|
||||
await createHealthyPlanning();
|
||||
await mkdir(join(tmpDir, '.planning', 'phases', '68-bug-surface'), { recursive: true });
|
||||
|
||||
@@ -594,9 +594,30 @@ export const validateHealth: QueryHandler = async (args, projectDir, workstream)
|
||||
const roadmapContent = await readFile(roadmapPath, 'utf-8');
|
||||
const roadmapPhases = new Set<string>();
|
||||
const phasePattern = /#{2,4}\s*Phase\s+(\d+[A-Z]?(?:\.\d+)*)\s*:/gi;
|
||||
const phaseVariants = (phase: string): Set<string> => {
|
||||
const variants = new Set<string>([phase]);
|
||||
const dotIdx = phase.indexOf('.');
|
||||
const head = dotIdx === -1 ? phase : phase.slice(0, dotIdx);
|
||||
const tail = dotIdx === -1 ? '' : phase.slice(dotIdx);
|
||||
const headMatch = head.match(/^(\d+)([A-Z]?)$/i);
|
||||
if (!headMatch) return variants;
|
||||
const numericHead = headMatch[1];
|
||||
const letterSuffix = headMatch[2] || '';
|
||||
variants.add(`${String(parseInt(numericHead, 10))}${letterSuffix}${tail}`);
|
||||
variants.add(`${numericHead.padStart(2, '0')}${letterSuffix}${tail}`);
|
||||
return variants;
|
||||
};
|
||||
const roadmapPhaseVariants = new Set<string>();
|
||||
let m: RegExpExecArray | null;
|
||||
while ((m = phasePattern.exec(roadmapContent)) !== null) {
|
||||
roadmapPhases.add(m[1]);
|
||||
for (const variant of phaseVariants(m[1])) roadmapPhaseVariants.add(variant);
|
||||
}
|
||||
const notStartedPhases = new Set<string>();
|
||||
const uncheckedPattern = /-\s*\[\s\]\s*\*{0,2}Phase\s+(\d+[A-Z]?(?:\.\d+)*)[:\s*]/gi;
|
||||
let um: RegExpExecArray | null;
|
||||
while ((um = uncheckedPattern.exec(roadmapContent)) !== null) {
|
||||
for (const variant of phaseVariants(um[1])) notStartedPhases.add(variant);
|
||||
}
|
||||
|
||||
const diskPhases = new Set<string>();
|
||||
@@ -625,15 +646,18 @@ export const validateHealth: QueryHandler = async (args, projectDir, workstream)
|
||||
} catch { /* intentionally empty */ }
|
||||
|
||||
for (const p of roadmapPhases) {
|
||||
const padded = String(parseInt(p, 10)).padStart(2, '0');
|
||||
if (!diskPhases.has(p) && !diskPhases.has(padded)) {
|
||||
const variants = phaseVariants(p);
|
||||
const existsOnDisk = [...variants].some((variant) => diskPhases.has(variant));
|
||||
const isNotStarted = [...variants].some((variant) => notStartedPhases.has(variant));
|
||||
if (!existsOnDisk) {
|
||||
if (isNotStarted) continue;
|
||||
addIssue('warning', 'W006', `Phase ${p} in ROADMAP.md but no directory on disk`, 'Create phase directory or remove from roadmap');
|
||||
}
|
||||
}
|
||||
|
||||
for (const p of diskPhases) {
|
||||
const unpadded = String(parseInt(p, 10));
|
||||
if (!roadmapPhases.has(p) && !roadmapPhases.has(unpadded)) {
|
||||
const variants = phaseVariants(p);
|
||||
if (![...variants].some((variant) => roadmapPhaseVariants.has(variant))) {
|
||||
addIssue('warning', 'W007', `Phase ${p} exists on disk but not in ROADMAP.md`, 'Add to roadmap or remove directory');
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user