From c2de4eb17b8ce88174e1e5743dbf65d6dfdfa637 Mon Sep 17 00:00:00 2001 From: mastertyko <11311479+mastertyko@users.noreply.github.com> Date: Fri, 10 Apr 2026 16:40:08 +0200 Subject: [PATCH] fix(gsd): skip skipped slices in milestone prompts --- src/resources/extensions/gsd/auto-prompts.ts | 8 +- .../gsd/tests/validate-milestone.test.ts | 88 ++++++++++++++++++- 2 files changed, 93 insertions(+), 3 deletions(-) diff --git a/src/resources/extensions/gsd/auto-prompts.ts b/src/resources/extensions/gsd/auto-prompts.ts index 5e8bff3c4..22862fa38 100644 --- a/src/resources/extensions/gsd/auto-prompts.ts +++ b/src/resources/extensions/gsd/auto-prompts.ts @@ -1503,7 +1503,9 @@ export async function buildCompleteMilestonePrompt( try { const { isDbAvailable, getMilestoneSlices } = await import("./gsd-db.js"); if (isDbAvailable()) { - sliceIds = getMilestoneSlices(mid).map(s => s.id); + sliceIds = getMilestoneSlices(mid) + .filter(s => s.status !== "skipped") + .map(s => s.id); } } catch (err) { logWarning("prompt", `buildCompleteMilestonePrompt DB lookup failed: ${err instanceof Error ? err.message : String(err)}`); @@ -1597,7 +1599,9 @@ export async function buildValidateMilestonePrompt( try { const { isDbAvailable, getMilestoneSlices } = await import("./gsd-db.js"); if (isDbAvailable()) { - valSliceIds = getMilestoneSlices(mid).map(s => s.id); + valSliceIds = getMilestoneSlices(mid) + .filter(s => s.status !== "skipped") + .map(s => s.id); } } catch (err) { logWarning("prompt", `buildValidateMilestonePrompt slice IDs lookup failed: ${err instanceof Error ? err.message : String(err)}`); diff --git a/src/resources/extensions/gsd/tests/validate-milestone.test.ts b/src/resources/extensions/gsd/tests/validate-milestone.test.ts index 569abd796..7ba062229 100644 --- a/src/resources/extensions/gsd/tests/validate-milestone.test.ts +++ b/src/resources/extensions/gsd/tests/validate-milestone.test.ts @@ -9,10 +9,11 @@ import { deriveState, isValidationTerminal } from "../state.ts"; import { resolveExpectedArtifactPath, diagnoseExpectedArtifact } from "../auto-artifact-paths.ts"; import { verifyExpectedArtifact, buildLoopRemediationSteps } from "../auto-recovery.ts"; import { resolveDispatch, type DispatchContext } from "../auto-dispatch.ts"; -import { buildValidateMilestonePrompt } from "../auto-prompts.ts"; +import { buildCompleteMilestonePrompt, buildValidateMilestonePrompt } from "../auto-prompts.ts"; import type { GSDState } from "../types.ts"; import { clearPathCache } from "../paths.ts"; import { clearParseCache } from "../files.ts"; +import { closeDatabase, insertMilestone, insertSlice, openDatabase } from "../gsd-db.ts"; // ─── Helpers ────────────────────────────────────────────────────────────── @@ -25,9 +26,15 @@ function makeTmpBase(): string { function cleanup(base: string): void { clearPathCache(); clearParseCache(); + closeDatabase(); try { rmSync(base, { recursive: true, force: true }); } catch { /* */ } } +function openTestDb(base: string): void { + const dbPath = join(base, ".gsd", "gsd.db"); + assert.equal(openDatabase(dbPath), true, "test DB should open"); +} + function writeRoadmap(base: string, mid: string, content: string): void { const dir = join(base, ".gsd", "milestones", mid); mkdirSync(dir, { recursive: true }); @@ -218,6 +225,85 @@ test("buildValidateMilestonePrompt inlines ASSESSMENT evidence instead of UAT sp } }); +test("buildCompleteMilestonePrompt skips skipped slices from DB-backed summary inlining", async () => { + const base = makeTmpBase(); + try { + writeRoadmap(base, "M001", `# M001: Test Milestone + +## Vision +Test + +## Success Criteria +- It works + +## Slices + +- [x] **S01: First slice** \`risk:low\` \`depends:[]\` + > Done +- [ ] **S02: Skipped slice** \`risk:low\` \`depends:[]\` + > Intentionally skipped + +## Boundary Map + +| From | To | Produces | Consumes | +|------|-----|----------|----------| +| S01 | terminal | output | nothing | +`); + openTestDb(base); + insertMilestone({ id: "M001", title: "Test Milestone", status: "active" }); + insertSlice({ id: "S01", milestoneId: "M001", title: "First slice", status: "complete", depends: [], sequence: 1 }); + insertSlice({ id: "S02", milestoneId: "M001", title: "Skipped slice", status: "skipped", depends: [], sequence: 2 }); + writeSliceSummary(base, "M001", "S01", "# S01 Summary\nDelivered."); + + const prompt = await buildCompleteMilestonePrompt("M001", "Test Milestone", base); + assert.match(prompt, /S01 Summary/i, "prompt should inline non-skipped slice summaries"); + assert.doesNotMatch(prompt, /### S02 Summary/i, "prompt should not inline skipped slice summaries"); + assert.doesNotMatch(prompt, /not found — file does not exist yet/i, "prompt should not emit skipped-slice missing-file placeholders"); + } finally { + cleanup(base); + } +}); + +test("buildValidateMilestonePrompt skips skipped slices from DB-backed summary inlining", async () => { + const base = makeTmpBase(); + try { + writeRoadmap(base, "M001", `# M001: Test Milestone + +## Vision +Test + +## Success Criteria +- It works + +## Slices + +- [x] **S01: First slice** \`risk:low\` \`depends:[]\` + > Done +- [ ] **S02: Skipped slice** \`risk:low\` \`depends:[]\` + > Intentionally skipped + +## Boundary Map + +| From | To | Produces | Consumes | +|------|-----|----------|----------| +| S01 | terminal | output | nothing | +`); + openTestDb(base); + insertMilestone({ id: "M001", title: "Test Milestone", status: "active" }); + insertSlice({ id: "S01", milestoneId: "M001", title: "First slice", status: "complete", depends: [], sequence: 1 }); + insertSlice({ id: "S02", milestoneId: "M001", title: "Skipped slice", status: "skipped", depends: [], sequence: 2 }); + writeSliceSummary(base, "M001", "S01", "# S01 Summary\nDelivered."); + writeSliceAssessment(base, "M001", "S01", "---\nverdict: PASS\n---\n# Assessment\nEvidence captured."); + + const prompt = await buildValidateMilestonePrompt("M001", "Test Milestone", base); + assert.match(prompt, /S01 Summary/i, "prompt should inline non-skipped slice summaries"); + assert.doesNotMatch(prompt, /### S02 Summary/i, "prompt should not inline skipped slice summaries"); + assert.doesNotMatch(prompt, /not found — file does not exist yet/i, "prompt should not emit skipped-slice missing-file placeholders"); + } finally { + cleanup(base); + } +}); + // ─── Dispatch rule ──────────────────────────────────────────────────────── test("dispatch rule matches validating-milestone phase", async () => {