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:
parent
b2a88d5645
commit
752b26d542
5 changed files with 556 additions and 3 deletions
|
|
@ -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", ...)`.
|
||||
|
|
|
|||
55
.gsd/milestones/M001/slices/S02/tasks/T01-SUMMARY.md
Normal file
55
.gsd/milestones/M001/slices/S02/tasks/T01-SUMMARY.md
Normal 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 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`
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 () => {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue