fix(gsd): reject empty roadmap stubs as milestone plans (#4063)

This commit is contained in:
mastertyko 2026-04-13 12:47:53 +02:00 committed by GitHub
parent ff42dccb58
commit df4e8245df
2 changed files with 72 additions and 0 deletions

View file

@ -272,6 +272,16 @@ export function verifyExpectedArtifact(
if (!isValidationTerminal(validationContent)) return false;
}
if (unitType === "plan-milestone") {
try {
const roadmap = parseLegacyRoadmap(readFileSync(absPath, "utf-8"));
if (roadmap.slices.length === 0) return false;
} catch (err) {
logWarning("recovery", `plan-milestone roadmap verification failed: ${err instanceof Error ? err.message : String(err)}`);
return false;
}
}
// plan-slice must produce a plan with actual task entries, not just a scaffold.
// The plan file may exist from a prior discussion/context step with only headings
// but no tasks. Without this check the artifact is considered "complete" and the

View file

@ -0,0 +1,62 @@
import { test } from "node:test";
import assert from "node:assert/strict";
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { tmpdir } from "node:os";
import { verifyExpectedArtifact } from "../auto-recovery.ts";
function createFixtureBase(): string {
const base = mkdtempSync(join(tmpdir(), "gsd-plan-milestone-artifact-"));
mkdirSync(join(base, ".gsd", "milestones"), { recursive: true });
return base;
}
function writeRoadmap(base: string, milestoneId: string, content: string): void {
const milestoneDir = join(base, ".gsd", "milestones", milestoneId);
mkdirSync(milestoneDir, { recursive: true });
writeFileSync(join(milestoneDir, `${milestoneId}-ROADMAP.md`), content, "utf-8");
}
test("#3405: plan-milestone roadmap stub does not count as a verified artifact", () => {
const base = createFixtureBase();
try {
writeRoadmap(base, "M001", [
"# M001: Placeholder",
"",
"**Vision:** Stub only.",
"",
"## Slices",
"",
"_TBD_",
"",
].join("\n"));
const result = verifyExpectedArtifact("plan-milestone", "M001", base);
assert.equal(result, false, "zero-slice roadmap stubs must fail verification");
} finally {
rmSync(base, { recursive: true, force: true });
}
});
test("#3405: plan-milestone roadmap with real slices still passes artifact verification", () => {
const base = createFixtureBase();
try {
writeRoadmap(base, "M001", [
"# M001: Real roadmap",
"",
"**Vision:** Real work.",
"",
"## Slices",
"",
"- [ ] **S01: First slice** `risk:low` `depends:[]`",
" > After this: a real slice exists.",
"",
].join("\n"));
const result = verifyExpectedArtifact("plan-milestone", "M001", base);
assert.equal(result, true, "real roadmap slices should keep passing verification");
} finally {
rmSync(base, { recursive: true, force: true });
}
});