test(S02/T01): Add DB-backed slice and task plan renderers with compati…

- 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
This commit is contained in:
TÂCHES 2026-03-23 09:58:52 -06:00
parent b2a88d5645
commit 752b26d542
5 changed files with 556 additions and 3 deletions

View file

@ -38,7 +38,7 @@
Im 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", ...)`.

View file

@ -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 plans 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`

View file

@ -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,

View file

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

View file

@ -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
// ═══════════════════════════════════════════════════════════════════════════