fix(3806): port W005/W006/I001 fixes from validate.ts to verify.cjs (#83)

PR #3479 fixed three false-positive classes in sdk/src/query/validate.ts
but the fixes never propagated to get-shit-done/bin/lib/verify.cjs —
the hand-maintained CJS runtime bundle that gsd-tools.cjs actually executes.

W005: widened phase-dir regex from \d{2} to \d{2,} so 3+-digit prefixes
like 999.1-foo are accepted.

W006: adds forEachArchivedPhaseToken call after collectDiskPhases so phases
whose directories live in a milestone archive are not flagged as missing.

I001: adds canonicalPlanStem helper and uses it in summaryBases Set
construction so 68-01-scaffolding-PLAN.md correctly matches 68-01-SUMMARY.md.

Adds five regression tests (TDD red→green) covering each false-positive path.

Fixes #3806

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Tom Boucher
2026-05-22 11:23:50 -04:00
committed by GitHub
parent 2b02786f50
commit b647f44eb0
3 changed files with 182 additions and 9 deletions

View File

@@ -830,6 +830,153 @@ describe('validate health --repair command', () => {
});
});
// ─────────────────────────────────────────────────────────────────────────────
// Regression: CJS bundle drift — W005/W006/I001 false positives (#3806)
// PR #3479 fixed these in sdk/src/query/validate.ts but never propagated to
// get-shit-done/bin/lib/verify.cjs. These tests fail on old verify.cjs and
// pass on the fixed version.
// ─────────────────────────────────────────────────────────────────────────────
describe('validate health — #3806 CJS bundle drift regressions', () => {
let tmpDir;
beforeEach(() => {
tmpDir = createTempProject();
});
afterEach(() => {
cleanup(tmpDir);
});
// W005 regression: \d{2} → \d{2,} so 999.1-foo is accepted (#3806)
test('does not emit W005 for a phase directory with a 3-digit prefix (999.1-foo)', () => {
writeMinimalProjectMd(tmpDir);
// Roadmap with no phases to avoid spurious W006
fs.writeFileSync(
path.join(tmpDir, '.planning', 'ROADMAP.md'),
'# Roadmap\n\nNo phases yet.\n'
);
writeMinimalStateMd(tmpDir, '# Session State\n\nNo phase refs.\n');
writeValidConfigJson(tmpDir);
// 999.1-foo should be valid under the widened \d{2,} pattern
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '999.1-foo'), { recursive: true });
const result = runGsdTools('validate health', tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const output = JSON.parse(result.output);
const w005s = output.warnings.filter(w => w.code === 'W005');
assert.strictEqual(
w005s.length, 0,
`W005 must not fire for "999.1-foo" (3-digit prefix is valid under \\d{2,}), got: ${JSON.stringify(w005s)}`
);
});
// W005 regression: additional multi-digit variants
test('does not emit W005 for phase directories with 4-digit and 2-digit prefixes', () => {
writeMinimalProjectMd(tmpDir);
fs.writeFileSync(
path.join(tmpDir, '.planning', 'ROADMAP.md'),
'# Roadmap\n\nNo phases yet.\n'
);
writeMinimalStateMd(tmpDir, '# Session State\n\nNo phase refs.\n');
writeValidConfigJson(tmpDir);
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '1000-backlog'), { recursive: true });
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '99-done'), { recursive: true });
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '100.2-feature'), { recursive: true });
const result = runGsdTools('validate health', tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const output = JSON.parse(result.output);
const w005s = output.warnings.filter(w => w.code === 'W005');
assert.strictEqual(
w005s.length, 0,
`W005 must not fire for multi-digit prefix dirs (\\d{2,} pattern), got: ${JSON.stringify(w005s)}`
);
});
// W006 regression: archived phases in milestones/*-phases/ must not trigger W006 (#3806)
test('does not emit W006 for a ROADMAP phase whose directory lives in a milestone archive', () => {
writeMinimalProjectMd(tmpDir);
// ROADMAP references Phase 1 in the current section
fs.writeFileSync(
path.join(tmpDir, '.planning', 'ROADMAP.md'),
[
'# Roadmap',
'',
'## v1.0.0',
'',
'### Phase 1: Setup',
'',
].join('\n')
);
writeMinimalStateMd(tmpDir, '# Session State\n\nPhase 1 complete.\n');
writeValidConfigJson(tmpDir);
// Phase 1 directory is in a milestone archive, NOT in the flat phases/ dir
const archiveDir = path.join(tmpDir, '.planning', 'milestones', 'v1.0.0-phases');
fs.mkdirSync(path.join(archiveDir, '01-setup'), { recursive: true });
// Ensure flat phases dir exists but does NOT contain phase 1
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases'), { 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.strictEqual(
w006s.length, 0,
`W006 must not fire for Phase 1 when its directory is in a milestone archive, got: ${JSON.stringify(w006s)}`
);
});
// I001 regression: FOO-PLAN.md + FOO-SUMMARY.md must match via canonicalPlanStem (#3806)
// e.g. 68-01-scaffolding-PLAN.md should match 68-01-SUMMARY.md (canonical stem = "68-01")
test('does not emit I001 when PLAN name has a descriptor suffix but SUMMARY uses canonical stem', () => {
writeMinimalProjectMd(tmpDir);
writeMinimalRoadmap(tmpDir, ['1']);
writeMinimalStateMd(tmpDir, '# Session State\n\nPhase 1 in progress.\n');
writeValidConfigJson(tmpDir);
// Create phase dir with descriptor-named PLAN and canonical-named SUMMARY
const phaseDir = path.join(tmpDir, '.planning', 'phases', '01-setup');
fs.mkdirSync(phaseDir, { recursive: true });
// PLAN has a descriptor suffix; SUMMARY uses the canonical stem only
fs.writeFileSync(path.join(phaseDir, '01-01-scaffolding-PLAN.md'), '# Plan\n');
fs.writeFileSync(path.join(phaseDir, '01-01-SUMMARY.md'), '# Summary\n');
const result = runGsdTools('validate health', tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const output = JSON.parse(result.output);
const i001s = output.info.filter(i => i.code === 'I001');
assert.strictEqual(
i001s.length, 0,
`I001 must not fire when SUMMARY stem (01-01) matches the canonical base of PLAN (01-01-scaffolding → 01-01), got: ${JSON.stringify(i001s)}`
);
});
// Confirm I001 still fires for a genuinely orphaned plan (no summary at all)
test('still emits I001 for a PLAN with no matching SUMMARY at all', () => {
writeMinimalProjectMd(tmpDir);
writeMinimalRoadmap(tmpDir, ['1']);
writeMinimalStateMd(tmpDir, '# Session State\n\nPhase 1 in progress.\n');
writeValidConfigJson(tmpDir);
const phaseDir = path.join(tmpDir, '.planning', 'phases', '01-setup');
fs.mkdirSync(phaseDir, { recursive: true });
fs.writeFileSync(path.join(phaseDir, '01-01-PLAN.md'), '# Plan\n');
// No SUMMARY file at all
const result = runGsdTools('validate health', tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const output = JSON.parse(result.output);
assert.ok(
output.info.some(i => i.code === 'I001'),
`I001 must still fire for an orphaned PLAN (no SUMMARY exists), got: ${JSON.stringify(output.info)}`
);
});
});
// ─────────────────────────────────────────────────────────────────────────────
// Graceful degradation when phasesDir is missing (#1973)
// ─────────────────────────────────────────────────────────────────────────────