From df4e8245df04b7109ff01d80f84d57542daffd50 Mon Sep 17 00:00:00 2001 From: mastertyko <11311479+mastertyko@users.noreply.github.com> Date: Mon, 13 Apr 2026 12:47:53 +0200 Subject: [PATCH] fix(gsd): reject empty roadmap stubs as milestone plans (#4063) --- src/resources/extensions/gsd/auto-recovery.ts | 10 +++ ...an-milestone-artifact-verification.test.ts | 62 +++++++++++++++++++ 2 files changed, 72 insertions(+) create mode 100644 src/resources/extensions/gsd/tests/plan-milestone-artifact-verification.test.ts diff --git a/src/resources/extensions/gsd/auto-recovery.ts b/src/resources/extensions/gsd/auto-recovery.ts index 92086af16..3fb3d8336 100644 --- a/src/resources/extensions/gsd/auto-recovery.ts +++ b/src/resources/extensions/gsd/auto-recovery.ts @@ -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 diff --git a/src/resources/extensions/gsd/tests/plan-milestone-artifact-verification.test.ts b/src/resources/extensions/gsd/tests/plan-milestone-artifact-verification.test.ts new file mode 100644 index 000000000..eb2d90533 --- /dev/null +++ b/src/resources/extensions/gsd/tests/plan-milestone-artifact-verification.test.ts @@ -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 }); + } +});