fix(gsd): skip skipped slices in milestone prompts

This commit is contained in:
mastertyko 2026-04-10 16:40:08 +02:00
parent c0f101a217
commit c2de4eb17b
2 changed files with 93 additions and 3 deletions

View file

@ -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)}`);

View file

@ -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 () => {