diff --git a/.gsd/milestones/M001/slices/S02/S02-PLAN.md b/.gsd/milestones/M001/slices/S02/S02-PLAN.md index f15f47944..856404f42 100644 --- a/.gsd/milestones/M001/slices/S02/S02-PLAN.md +++ b/.gsd/milestones/M001/slices/S02/S02-PLAN.md @@ -38,7 +38,7 @@ I’m splitting this into three tasks because there are three distinct failure boundaries and each needs its own proof. The highest-risk boundary is renderer compatibility: if the generated `PLAN.md` or task-plan markdown drifts from parser/runtime expectations, the rest of the slice is fake progress. That work goes first and includes the runtime contract around `skills_used` frontmatter and task-plan file existence. Once the render target is stable, the handler/registration work becomes straightforward because S01 already established the validation → transaction → render → invalidate pattern. The last task is prompt/tool-surface closure, which is intentionally small but necessary: without it, the system still has a gap between the new DB-backed implementation and the planning instructions/registrations the LLM actually sees. -- [ ] **T01: Add DB-backed slice and task plan renderers with compatibility tests** `est:1.5h` +- [x] **T01: Add DB-backed slice and task plan renderers with compatibility tests** `est:1.5h` - Why: This closes the main transition-window risk first: rendered plan artifacts must stay parse-compatible and satisfy runtime recovery checks before any new planning handler can be trusted. - Files: `src/resources/extensions/gsd/markdown-renderer.ts`, `src/resources/extensions/gsd/tests/markdown-renderer.test.ts`, `src/resources/extensions/gsd/tests/auto-recovery.test.ts`, `src/resources/extensions/gsd/files.ts` - Do: Implement `renderPlanFromDb()` and `renderTaskPlanFromDb()` using existing DB query helpers, emit slice/task markdown that preserves `parsePlan()` and `parseTaskPlanFile()` expectations, include conservative task-plan frontmatter (`estimated_steps`, `estimated_files`, `skills_used`), and add tests that prove rendered slice plans plus task plan files satisfy `verifyExpectedArtifact("plan-slice", ...)`. diff --git a/.gsd/milestones/M001/slices/S02/tasks/T01-SUMMARY.md b/.gsd/milestones/M001/slices/S02/tasks/T01-SUMMARY.md new file mode 100644 index 000000000..94f7c4808 --- /dev/null +++ b/.gsd/milestones/M001/slices/S02/tasks/T01-SUMMARY.md @@ -0,0 +1,55 @@ +--- +id: T01 +parent: S02 +milestone: M001 +key_files: + - src/resources/extensions/gsd/markdown-renderer.ts + - src/resources/extensions/gsd/tests/markdown-renderer.test.ts + - src/resources/extensions/gsd/tests/auto-recovery.test.ts + - .gsd/KNOWLEDGE.md +key_decisions: + - Rendered task-plan files use conservative `skills_used: []` frontmatter so execution-time skill activation remains explicit and no secret-bearing or speculative values are emitted from DB state. + - Slice-plan verification content is sourced from the slice `observability_impact` field when present so the DB-backed renderer preserves inspectable diagnostics/failure-path expectations instead of emitting a placeholder-only section. + - `renderPlanFromDb()` eagerly renders all child task-plan files after writing the slice plan so `verifyExpectedArtifact("plan-slice", ...)` sees a truthful on-disk artifact set immediately. +duration: "" +verification_result: mixed +completed_at: 2026-03-23T15:58:46.134Z +blocker_discovered: false +--- + +# T01: Add DB-backed slice and task plan renderers with compatibility and recovery tests + +**Add DB-backed slice and task plan renderers with compatibility and recovery tests** + +## What Happened + +Implemented DB-backed plan rendering in `src/resources/extensions/gsd/markdown-renderer.ts` by adding `renderPlanFromDb()` and `renderTaskPlanFromDb()`. The slice-plan renderer now reads slice/task rows from SQLite, emits parse-compatible `S##-PLAN.md` content with goal, demo, must-haves, verification, checklist tasks, and files-likely-touched, then persists the artifact to disk and the artifacts table. The task-plan renderer now emits `tasks/T##-PLAN.md` files with conservative YAML frontmatter (`estimated_steps`, `estimated_files`, `skills_used: []`) plus `Steps`, `Inputs`, `Expected Output`, `Verification`, and optional `Observability Impact` sections. Extended `markdown-renderer.test.ts` to prove DB-backed plan rendering round-trips through `parsePlan()` and `parseTaskPlanFile()`, writes truthful on-disk artifacts, stores those artifacts in SQLite, and surfaces clear failure behavior for missing task rows. Extended `auto-recovery.test.ts` to prove a rendered slice plan plus rendered task-plan files satisfies `verifyExpectedArtifact("plan-slice", ...)`, and that deleting a rendered task-plan file still fails recovery verification as intended. Also recorded the local verification gotcha in `.gsd/KNOWLEDGE.md`: the slice plan references `plan-slice.test.ts` / `plan-task.test.ts`, but those files are not present in this checkout, so the resolver-harness renderer/recovery/prompt tests are currently the inspectable proof surface for this task. + +## Verification + +Verified the task contract with the targeted resolver-harness command for `markdown-renderer.test.ts` and `auto-recovery.test.ts`; all renderer and recovery assertions passed, including explicit failure-path checks for missing task-plan files and stale-render diagnostics. Ran the broader slice-level resolver-harness command covering `markdown-renderer.test.ts`, `auto-recovery.test.ts`, and `prompt-contracts.test.ts`; it passed and confirmed the DB-backed planning prompt contract remains aligned. Attempted the slice-plan verification command for `plan-slice.test.ts` and `plan-task.test.ts`, then confirmed those referenced files do not exist in this checkout, so that command cannot currently execute here. This is a checkout/test-surface mismatch, not a regression introduced by this task. + +## Verification Evidence + +| # | Command | Exit Code | Verdict | Duration | +|---|---------|-----------|---------|----------| +| 1 | `node --import ./src/resources/extensions/gsd/tests/resolve-ts.mjs --experimental-strip-types --test src/resources/extensions/gsd/tests/markdown-renderer.test.ts src/resources/extensions/gsd/tests/auto-recovery.test.ts --test-name-pattern="renderPlanFromDb|renderTaskPlanFromDb|plan-slice|task plan"` | 0 | ✅ pass | 693ms | +| 2 | `node --import ./src/resources/extensions/gsd/tests/resolve-ts.mjs --experimental-strip-types --test src/resources/extensions/gsd/tests/plan-slice.test.ts src/resources/extensions/gsd/tests/plan-task.test.ts` | 1 | ❌ fail | 51ms | +| 3 | `ls src/resources/extensions/gsd/tests/plan-slice.test.ts src/resources/extensions/gsd/tests/plan-task.test.ts src/resources/extensions/gsd/tests/prompt-contracts.test.ts` | 1 | ❌ fail | 0ms | +| 4 | `node --import ./src/resources/extensions/gsd/tests/resolve-ts.mjs --experimental-strip-types --test src/resources/extensions/gsd/tests/markdown-renderer.test.ts src/resources/extensions/gsd/tests/auto-recovery.test.ts src/resources/extensions/gsd/tests/prompt-contracts.test.ts --test-name-pattern="plan-slice|plan-task|renderPlanFromDb|renderTaskPlanFromDb|task plan|DB-backed planning"` | 0 | ✅ pass | 697ms | + + +## Deviations + +Did not edit `src/resources/extensions/gsd/files.ts`; the existing parser contract already accepted the truthful renderer output. The slice plan’s referenced `plan-slice.test.ts` and `plan-task.test.ts` verification command could not be executed because those files are absent in the working tree, so I documented that local mismatch and used the existing resolver-harness renderer/recovery/prompt tests as the effective proof surface. + +## Known Issues + +The slice plan still references `src/resources/extensions/gsd/tests/plan-slice.test.ts` and `src/resources/extensions/gsd/tests/plan-task.test.ts`, but neither file exists in this checkout. Until those tests land, slice-level verification for planning work must rely on the existing `markdown-renderer.test.ts`, `auto-recovery.test.ts`, and related prompt-contract tests. + +## Files Created/Modified + +- `src/resources/extensions/gsd/markdown-renderer.ts` +- `src/resources/extensions/gsd/tests/markdown-renderer.test.ts` +- `src/resources/extensions/gsd/tests/auto-recovery.test.ts` +- `.gsd/KNOWLEDGE.md` diff --git a/src/resources/extensions/gsd/markdown-renderer.ts b/src/resources/extensions/gsd/markdown-renderer.ts index 6bff01c88..a497394ad 100644 --- a/src/resources/extensions/gsd/markdown-renderer.ts +++ b/src/resources/extensions/gsd/markdown-renderer.ts @@ -8,7 +8,7 @@ // Critical invariant: rendered markdown must round-trip through // parseRoadmap(), parsePlan(), parseSummary() in files.ts. -import { readFileSync, existsSync } from "node:fs"; +import { readFileSync, existsSync, mkdirSync } from "node:fs"; import { join, relative } from "node:path"; import { getAllMilestones, @@ -187,6 +187,228 @@ function renderRoadmapMarkdown(milestone: MilestoneRow, slices: SliceRow[]): str return `${lines.join("\n").trimEnd()}\n`; } +function renderTaskPlanMarkdown(task: TaskRow): string { + const estimatedSteps = Math.max(1, task.description.trim().split(/\n+/).filter(Boolean).length || 1); + const estimatedFiles = task.files.length > 0 + ? task.files.length + : task.expected_output.length > 0 + ? task.expected_output.length + : task.inputs.length > 0 + ? task.inputs.length + : 1; + + const lines: string[] = []; + lines.push("---"); + lines.push(`estimated_steps: ${estimatedSteps}`); + lines.push(`estimated_files: ${estimatedFiles}`); + lines.push("skills_used: []"); + lines.push("---"); + lines.push(""); + lines.push(`# ${task.id}: ${task.title || task.id}`); + lines.push(""); + + if (task.description.trim()) { + lines.push(task.description.trim()); + lines.push(""); + } + + lines.push("## Steps"); + lines.push(""); + if (task.description.trim()) { + for (const paragraph of task.description.split(/\n+/).map((line) => line.trim()).filter(Boolean)) { + lines.push(`- ${paragraph}`); + } + } else { + lines.push("- Implement the planned task work."); + } + lines.push(""); + + lines.push("## Inputs"); + lines.push(""); + if (task.inputs.length > 0) { + for (const input of task.inputs) { + lines.push(`- \`${input}\``); + } + } else { + lines.push("- None specified."); + } + lines.push(""); + + lines.push("## Expected Output"); + lines.push(""); + if (task.expected_output.length > 0) { + for (const output of task.expected_output) { + lines.push(`- \`${output}\``); + } + } else if (task.files.length > 0) { + for (const file of task.files) { + lines.push(`- \`${file}\``); + } + } else { + lines.push("- Update the implementation and proof artifacts needed for this task."); + } + lines.push(""); + + lines.push("## Verification"); + lines.push(""); + lines.push(task.verify.trim() || "- Verify the task outcome with the slice-level checks."); + lines.push(""); + + if (task.observability_impact.trim()) { + lines.push("## Observability Impact"); + lines.push(""); + lines.push(task.observability_impact.trim()); + lines.push(""); + } + + return `${lines.join("\n").trimEnd()}\n`; +} + +function renderSlicePlanMarkdown(slice: SliceRow, tasks: TaskRow[]): string { + const lines: string[] = []; + + lines.push(`# ${slice.id}: ${slice.title || slice.id}`); + lines.push(""); + lines.push(`**Goal:** ${slice.goal}`); + lines.push(`**Demo:** ${slice.demo}`); + lines.push(""); + + lines.push("## Must-Haves"); + lines.push(""); + if (slice.success_criteria.trim()) { + for (const line of slice.success_criteria.split(/\n+/).map((entry) => entry.trim()).filter(Boolean)) { + lines.push(line.startsWith("-") ? line : `- ${line}`); + } + } else { + lines.push("- Complete the planned slice outcomes."); + } + lines.push(""); + + if (slice.proof_level.trim()) { + lines.push("## Proof Level"); + lines.push(""); + lines.push(`- This slice proves: ${slice.proof_level.trim()}`); + lines.push(""); + } + + if (slice.integration_closure.trim()) { + lines.push("## Integration Closure"); + lines.push(""); + lines.push(slice.integration_closure.trim()); + lines.push(""); + } + + lines.push("## Verification"); + lines.push(""); + if (slice.observability_impact.trim()) { + const verificationLines = slice.observability_impact + .split(/\n+/) + .map((entry) => entry.trim()) + .filter(Boolean); + for (const line of verificationLines) { + lines.push(line.startsWith("-") ? line : `- ${line}`); + } + } else { + lines.push("- Run the task and slice verification checks for this slice."); + } + lines.push(""); + + lines.push("## Tasks"); + lines.push(""); + for (const task of tasks) { + const done = task.status === "done" || task.status === "complete" ? "x" : " "; + const estimate = task.estimate.trim() ? ` \`est:${task.estimate.trim()}\`` : ""; + lines.push(`- [${done}] **${task.id}: ${task.title || task.id}**${estimate}`); + if (task.description.trim()) { + lines.push(` ${task.description.trim()}`); + } + if (task.files.length > 0) { + lines.push(` - Files: ${task.files.map((file) => `\`${file}\``).join(", ")}`); + } + if (task.verify.trim()) { + lines.push(` - Verify: ${task.verify.trim()}`); + } + lines.push(""); + } + + const filesLikelyTouched = Array.from(new Set(tasks.flatMap((task) => task.files))); + if (filesLikelyTouched.length > 0) { + lines.push("## Files Likely Touched"); + lines.push(""); + for (const file of filesLikelyTouched) { + lines.push(`- ${file}`); + } + lines.push(""); + } + + return `${lines.join("\n").trimEnd()}\n`; +} + +export async function renderPlanFromDb( + basePath: string, + milestoneId: string, + sliceId: string, +): Promise<{ planPath: string; taskPlanPaths: string[]; content: string }> { + const slice = getSlice(milestoneId, sliceId); + if (!slice) { + throw new Error(`slice ${milestoneId}/${sliceId} not found`); + } + + const tasks = getSliceTasks(milestoneId, sliceId); + if (tasks.length === 0) { + throw new Error(`no tasks found for ${milestoneId}/${sliceId}`); + } + + const slicePath = resolveSlicePath(basePath, milestoneId, sliceId) + ?? join(gsdRoot(basePath), "milestones", milestoneId, "slices", sliceId); + const absPath = resolveSliceFile(basePath, milestoneId, sliceId, "PLAN") + ?? join(slicePath, `${sliceId}-PLAN.md`); + const artifactPath = toArtifactPath(absPath, basePath); + const content = renderSlicePlanMarkdown(slice, tasks); + + await writeAndStore(absPath, artifactPath, content, { + artifact_type: "PLAN", + milestone_id: milestoneId, + slice_id: sliceId, + }); + + const taskPlanPaths: string[] = []; + for (const task of tasks) { + const rendered = await renderTaskPlanFromDb(basePath, milestoneId, sliceId, task.id); + taskPlanPaths.push(rendered.taskPlanPath); + } + + return { planPath: absPath, taskPlanPaths, content }; +} + +export async function renderTaskPlanFromDb( + basePath: string, + milestoneId: string, + sliceId: string, + taskId: string, +): Promise<{ taskPlanPath: string; content: string }> { + const task = getTask(milestoneId, sliceId, taskId); + if (!task) { + throw new Error(`task ${milestoneId}/${sliceId}/${taskId} not found`); + } + + const tasksDir = resolveTasksDir(basePath, milestoneId, sliceId) + ?? join(gsdRoot(basePath), "milestones", milestoneId, "slices", sliceId, "tasks"); + mkdirSync(tasksDir, { recursive: true }); + const absPath = join(tasksDir, buildTaskFileName(taskId, "PLAN")); + const artifactPath = toArtifactPath(absPath, basePath); + const content = renderTaskPlanMarkdown(task); + + await writeAndStore(absPath, artifactPath, content, { + artifact_type: "PLAN", + milestone_id: milestoneId, + slice_id: sliceId, + task_id: taskId, + }); + + return { taskPlanPath: absPath, content }; +} + export async function renderRoadmapFromDb( basePath: string, milestoneId: string, diff --git a/src/resources/extensions/gsd/tests/auto-recovery.test.ts b/src/resources/extensions/gsd/tests/auto-recovery.test.ts index 206658d16..8c36c8cfe 100644 --- a/src/resources/extensions/gsd/tests/auto-recovery.test.ts +++ b/src/resources/extensions/gsd/tests/auto-recovery.test.ts @@ -13,9 +13,17 @@ import { selfHealRuntimeRecords, hasImplementationArtifacts, } from "../auto-recovery.ts"; -import { parseRoadmap, clearParseCache } from "../files.ts"; +import { parseRoadmap, parsePlan, parseTaskPlanFile, clearParseCache } from "../files.ts"; import { invalidateAllCaches } from "../cache.ts"; import { deriveState, invalidateStateCache } from "../state.ts"; +import { + openDatabase, + closeDatabase, + insertMilestone, + insertSlice, + insertTask, +} from "../gsd-db.ts"; +import { renderPlanFromDb } from "../markdown-renderer.ts"; function makeTmpBase(): string { const base = join(tmpdir(), `gsd-test-${randomUUID()}`); @@ -470,6 +478,143 @@ test("verifyExpectedArtifact execute-task passes for heading-style plan entry (# } }); +test("verifyExpectedArtifact plan-slice passes for rendered slice/task plan artifacts from DB", async () => { + const base = makeTmpBase(); + const dbPath = join(base, ".gsd", "gsd.db"); + openDatabase(dbPath); + try { + insertMilestone({ id: "M001", title: "Milestone", status: "active" }); + insertSlice({ + id: "S01", + milestoneId: "M001", + title: "Rendered slice", + status: "pending", + demo: "Rendered plan artifacts exist.", + planning: { + goal: "Render plans from DB rows.", + successCriteria: "- Slice plan parses\n- Task plan files exist on disk", + proofLevel: "integration", + integrationClosure: "DB rows are the source of truth for PLAN artifacts.", + observabilityImpact: "- Recovery verification fails if a task plan file is missing", + }, + }); + insertTask({ + id: "T01", + sliceId: "S01", + milestoneId: "M001", + title: "Render plan", + status: "pending", + planning: { + description: "Create the slice plan from DB state.", + estimate: "30m", + files: ["src/resources/extensions/gsd/markdown-renderer.ts"], + verify: "node --test markdown-renderer.test.ts", + inputs: ["src/resources/extensions/gsd/gsd-db.ts"], + expectedOutput: ["src/resources/extensions/gsd/tests/markdown-renderer.test.ts"], + observabilityImpact: "Renderer tests cover the failure mode.", + }, + }); + insertTask({ + id: "T02", + sliceId: "S01", + milestoneId: "M001", + title: "Verify recovery", + status: "pending", + planning: { + description: "Prove task plan files remain present for recovery.", + estimate: "20m", + files: ["src/resources/extensions/gsd/auto-recovery.ts"], + verify: "node --test auto-recovery.test.ts", + inputs: ["src/resources/extensions/gsd/auto-recovery.ts"], + expectedOutput: ["src/resources/extensions/gsd/tests/auto-recovery.test.ts"], + observabilityImpact: "Missing plan files surface as explicit verification failures.", + }, + }); + + const rendered = await renderPlanFromDb(base, "M001", "S01"); + assert.ok(existsSync(rendered.planPath), "renderPlanFromDb should write the slice plan"); + assert.equal(rendered.taskPlanPaths.length, 2, "renderPlanFromDb should render one task plan per task"); + + const planContent = readFileSync(rendered.planPath, "utf-8"); + const parsedPlan = parsePlan(planContent); + assert.equal(parsedPlan.tasks.length, 2, "rendered slice plan should parse into task entries"); + + const taskPlanContent = readFileSync(rendered.taskPlanPaths[0], "utf-8"); + const taskPlan = parseTaskPlanFile(taskPlanContent); + assert.deepEqual(taskPlan.frontmatter.skills_used, [], "rendered task plans should use conservative empty skills_used"); + + const result = verifyExpectedArtifact("plan-slice", "M001/S01", base); + assert.equal(result, true, "plan-slice verification should pass when rendered task plan files exist"); + } finally { + closeDatabase(); + cleanup(base); + } +}); + +test("verifyExpectedArtifact plan-slice fails after deleting a rendered task plan file", async () => { + const base = makeTmpBase(); + const dbPath = join(base, ".gsd", "gsd.db"); + openDatabase(dbPath); + try { + insertMilestone({ id: "M001", title: "Milestone", status: "active" }); + insertSlice({ + id: "S01", + milestoneId: "M001", + title: "Rendered slice", + status: "pending", + demo: "Rendered plan artifacts exist.", + planning: { + goal: "Render plans from DB rows.", + successCriteria: "- Slice plan parses\n- Task plan files exist on disk", + proofLevel: "integration", + integrationClosure: "DB rows are the source of truth for PLAN artifacts.", + observabilityImpact: "- Recovery verification fails if a task plan file is missing", + }, + }); + insertTask({ + id: "T01", + sliceId: "S01", + milestoneId: "M001", + title: "Render plan", + status: "pending", + planning: { + description: "Create the slice plan from DB state.", + estimate: "30m", + files: ["src/resources/extensions/gsd/markdown-renderer.ts"], + verify: "node --test markdown-renderer.test.ts", + inputs: ["src/resources/extensions/gsd/gsd-db.ts"], + expectedOutput: ["src/resources/extensions/gsd/tests/markdown-renderer.test.ts"], + observabilityImpact: "Renderer tests cover the failure mode.", + }, + }); + insertTask({ + id: "T02", + sliceId: "S01", + milestoneId: "M001", + title: "Verify recovery", + status: "pending", + planning: { + description: "Prove task plan files remain present for recovery.", + estimate: "20m", + files: ["src/resources/extensions/gsd/auto-recovery.ts"], + verify: "node --test auto-recovery.test.ts", + inputs: ["src/resources/extensions/gsd/auto-recovery.ts"], + expectedOutput: ["src/resources/extensions/gsd/tests/auto-recovery.test.ts"], + observabilityImpact: "Missing plan files surface as explicit verification failures.", + }, + }); + + const rendered = await renderPlanFromDb(base, "M001", "S01"); + rmSync(rendered.taskPlanPaths[1]); + + const result = verifyExpectedArtifact("plan-slice", "M001/S01", base); + assert.equal(result, false, "plan-slice verification should fail when a rendered task plan file is removed"); + } finally { + closeDatabase(); + cleanup(base); + } +}); + // ─── selfHealRuntimeRecords — worktree base path (#769) ────────────────── test("selfHealRuntimeRecords clears stale dispatched records (#769)", async () => { diff --git a/src/resources/extensions/gsd/tests/markdown-renderer.test.ts b/src/resources/extensions/gsd/tests/markdown-renderer.test.ts index edcb3fb72..ccb00cb7b 100644 --- a/src/resources/extensions/gsd/tests/markdown-renderer.test.ts +++ b/src/resources/extensions/gsd/tests/markdown-renderer.test.ts @@ -22,6 +22,8 @@ import { renderTaskSummary, renderSliceSummary, renderAllFromDb, + renderPlanFromDb, + renderTaskPlanFromDb, detectStaleRenders, repairStaleRenders, } from '../markdown-renderer.ts'; @@ -29,6 +31,7 @@ import { parseRoadmap, parsePlan, parseSummary, + parseTaskPlanFile, clearParseCache, } from '../files.ts'; import { clearPathCache, _clearGsdRootCache } from '../paths.ts'; @@ -433,6 +436,134 @@ console.log('\n── markdown-renderer: renderPlanCheckboxes bidirectional ─ } } +console.log('\n── markdown-renderer: renderPlanFromDb creates parse-compatible slice plan + task plan files ──'); + +{ + const tmpDir = makeTmpDir(); + const dbPath = path.join(tmpDir, '.gsd', 'gsd.db'); + openDatabase(dbPath); + clearAllCaches(); + + try { + scaffoldDirs(tmpDir, 'M001', ['S02']); + + insertMilestone({ id: 'M001', title: 'Milestone', status: 'active' }); + insertSlice({ + id: 'S02', + milestoneId: 'M001', + title: 'DB-backed planning', + status: 'pending', + demo: 'Rendered plans exist on disk.', + planning: { + goal: 'Render slice plans from DB state.', + successCriteria: '- Slice plan stays parse-compatible\n- Task plan files are regenerated', + proofLevel: 'integration', + integrationClosure: 'Wires DB planning rows to markdown artifacts.', + observabilityImpact: '- Run renderer contract tests\n- Inspect stale-render diagnostics on mismatch', + }, + }); + insertTask({ + id: 'T01', + sliceId: 'S02', + milestoneId: 'M001', + title: 'Render slice plan', + status: 'pending', + planning: { + description: 'Implement the DB-backed slice plan renderer.', + estimate: '45m', + files: ['src/resources/extensions/gsd/markdown-renderer.ts'], + verify: 'node --test markdown-renderer.test.ts', + inputs: ['src/resources/extensions/gsd/markdown-renderer.ts'], + expectedOutput: ['src/resources/extensions/gsd/tests/markdown-renderer.test.ts'], + observabilityImpact: 'Renderer tests cover stale render failure paths.', + }, + }); + insertTask({ + id: 'T02', + sliceId: 'S02', + milestoneId: 'M001', + title: 'Render task plan', + status: 'pending', + planning: { + description: 'Emit the task plan file with conservative frontmatter.', + estimate: '30m', + files: ['src/resources/extensions/gsd/files.ts'], + verify: 'node --test auto-recovery.test.ts', + inputs: ['src/resources/extensions/gsd/files.ts'], + expectedOutput: ['src/resources/extensions/gsd/tests/auto-recovery.test.ts'], + observabilityImpact: 'Missing task-plan files fail recovery verification.', + }, + }); + + const rendered = await renderPlanFromDb(tmpDir, 'M001', 'S02'); + assertTrue(fs.existsSync(rendered.planPath), 'slice plan written to disk'); + assertEq(rendered.taskPlanPaths.length, 2, 'task plan paths returned for each task'); + assertTrue(rendered.taskPlanPaths.every((p) => fs.existsSync(p)), 'all task plan files written to disk'); + + const planContent = fs.readFileSync(rendered.planPath, 'utf-8'); + clearAllCaches(); + const parsedPlan = parsePlan(planContent); + assertEq(parsedPlan.id, 'S02', 'rendered slice plan parses with correct slice id'); + assertEq(parsedPlan.goal, 'Render slice plans from DB state.', 'rendered slice plan preserves goal'); + assertEq(parsedPlan.demo, 'Rendered plans exist on disk.', 'rendered slice plan preserves demo'); + assertEq(parsedPlan.mustHaves.length, 2, 'rendered slice plan exposes must-haves'); + assertEq(parsedPlan.tasks.length, 2, 'rendered slice plan exposes all tasks'); + assertEq(parsedPlan.tasks[0].id, 'T01', 'first task parses correctly'); + assertTrue(parsedPlan.tasks[0].description.includes('DB-backed slice plan renderer'), 'task description preserved in slice plan'); + assertEq(parsedPlan.tasks[0].files?.[0], 'src/resources/extensions/gsd/markdown-renderer.ts', 'files list preserved in slice plan'); + assertEq(parsedPlan.tasks[0].verify, 'node --test markdown-renderer.test.ts', 'verify line preserved in slice plan'); + + const planArtifact = getArtifact('milestones/M001/slices/S02/S02-PLAN.md'); + assertTrue(planArtifact !== null, 'slice plan artifact stored in DB'); + assertTrue(planArtifact!.full_content.includes('## Tasks'), 'stored plan artifact contains task section'); + + const taskPlanPath = path.join(tmpDir, '.gsd', 'milestones', 'M001', 'slices', 'S02', 'tasks', 'T01-PLAN.md'); + const taskPlanContent = fs.readFileSync(taskPlanPath, 'utf-8'); + const taskPlanFile = parseTaskPlanFile(taskPlanContent); + assertEq(taskPlanFile.frontmatter.estimated_steps, 1, 'task plan frontmatter exposes estimated_steps'); + assertEq(taskPlanFile.frontmatter.estimated_files, 1, 'task plan frontmatter exposes estimated_files'); + assertEq(taskPlanFile.frontmatter.skills_used.length, 0, 'task plan frontmatter uses conservative empty skills list'); + assertMatch(taskPlanContent, /^# T01: Render slice plan/m, 'task plan renders task heading'); + assertMatch(taskPlanContent, /^## Inputs$/m, 'task plan renders Inputs section'); + assertMatch(taskPlanContent, /^## Expected Output$/m, 'task plan renders Expected Output section'); + assertMatch(taskPlanContent, /^## Verification$/m, 'task plan renders Verification section'); + + const taskArtifact = getArtifact('milestones/M001/slices/S02/tasks/T01-PLAN.md'); + assertTrue(taskArtifact !== null, 'task plan artifact stored in DB'); + assertTrue(taskArtifact!.full_content.includes('skills_used: []'), 'stored task plan artifact preserves conservative skills_used'); + } finally { + closeDatabase(); + cleanupDir(tmpDir); + } +} + +console.log('\n── markdown-renderer: renderTaskPlanFromDb throws for missing task ──'); + +{ + const tmpDir = makeTmpDir(); + const dbPath = path.join(tmpDir, '.gsd', 'gsd.db'); + openDatabase(dbPath); + clearAllCaches(); + + try { + scaffoldDirs(tmpDir, 'M001', ['S02']); + insertMilestone({ id: 'M001', title: 'Milestone', status: 'active' }); + insertSlice({ id: 'S02', milestoneId: 'M001', title: 'Slice', status: 'pending' }); + + let threw = false; + try { + await renderTaskPlanFromDb(tmpDir, 'M001', 'S02', 'T99'); + } catch (error) { + threw = true; + assertMatch(String((error as Error).message), /task M001\/S02\/T99 not found/, 'renderTaskPlanFromDb should fail clearly when task row is missing'); + } + assertTrue(threw, 'renderTaskPlanFromDb throws when the task row is missing'); + } finally { + closeDatabase(); + cleanupDir(tmpDir); + } +} + // ═══════════════════════════════════════════════════════════════════════════ // Task Summary Rendering // ═══════════════════════════════════════════════════════════════════════════