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:
@@ -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)
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user