fix: unify SUMMARY.md render paths for projection fidelity (#3091)
* fix: unify SUMMARY.md render paths for projection fidelity Closes #2720 renderSummaryMarkdown (complete-task.ts) and renderSummaryContent (workflow-projections.ts) produced structurally different output for the same data — different frontmatter format, different sections, different formatting. Deleting a SUMMARY.md and regenerating it via projection yielded a different file than the original. Fix: make renderSummaryContent the single source of truth. complete-task now builds a TaskRow from params and delegates to renderSummaryContent. The projection renderer passes verification evidence from the DB so both paths produce identical output including the Verification Evidence table, Files Created/Modified section, and YAML-format frontmatter. Added getVerificationEvidence() to gsd-db for projection-time evidence retrieval, and a 22-assertion parity test that prevents future drift. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: safe type assertion for verification evidence query result Cast through `unknown` to satisfy TS2352 — better-sqlite3's `.all()` returns `Record<string, unknown>[]` which doesn't directly overlap with `VerificationEvidenceRow[]`. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
3e78270cad
commit
c924f9f1f8
4 changed files with 356 additions and 129 deletions
|
|
@ -1534,6 +1534,26 @@ export function insertVerificationEvidence(e: {
|
|||
});
|
||||
}
|
||||
|
||||
export interface VerificationEvidenceRow {
|
||||
id: number;
|
||||
task_id: string;
|
||||
slice_id: string;
|
||||
milestone_id: string;
|
||||
command: string;
|
||||
exit_code: number;
|
||||
verdict: string;
|
||||
duration_ms: number;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export function getVerificationEvidence(milestoneId: string, sliceId: string, taskId: string): VerificationEvidenceRow[] {
|
||||
if (!currentDb) return [];
|
||||
const rows = currentDb.prepare(
|
||||
"SELECT * FROM verification_evidence WHERE milestone_id = :mid AND slice_id = :sid AND task_id = :tid ORDER BY id",
|
||||
).all({ ":mid": milestoneId, ":sid": sliceId, ":tid": taskId });
|
||||
return rows as unknown as VerificationEvidenceRow[];
|
||||
}
|
||||
|
||||
export interface MilestoneRow {
|
||||
id: string;
|
||||
title: string;
|
||||
|
|
|
|||
221
src/resources/extensions/gsd/tests/summary-render-parity.test.ts
Normal file
221
src/resources/extensions/gsd/tests/summary-render-parity.test.ts
Normal file
|
|
@ -0,0 +1,221 @@
|
|||
/**
|
||||
* summary-render-parity.test.ts — Regression test for #2720
|
||||
*
|
||||
* Asserts that the SUMMARY.md produced at task-completion time
|
||||
* (renderSummaryMarkdown in complete-task.ts) is structurally identical
|
||||
* to the SUMMARY.md produced at projection-regeneration time
|
||||
* (renderSummaryContent in workflow-projections.ts).
|
||||
*
|
||||
* Both render paths receive equivalent data (CompleteTaskParams vs TaskRow)
|
||||
* and must produce the same output. If they diverge, projection regeneration
|
||||
* silently replaces richer content with a stripped-down version.
|
||||
*/
|
||||
|
||||
import { createTestContext } from './test-helpers.ts';
|
||||
import { renderSummaryContent } from '../workflow-projections.ts';
|
||||
import type { TaskRow } from '../gsd-db.ts';
|
||||
|
||||
const { assertEq, assertTrue, report } = createTestContext();
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// Fixtures — same logical data in both shapes
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
const SLICE_ID = "S01";
|
||||
const MILESTONE_ID = "M001";
|
||||
|
||||
const taskRow: TaskRow = {
|
||||
milestone_id: MILESTONE_ID,
|
||||
slice_id: SLICE_ID,
|
||||
id: "T01",
|
||||
title: "Implement widget parser",
|
||||
status: "complete",
|
||||
one_liner: "Implement widget parser",
|
||||
narrative: "Added a recursive descent parser for widget DSL.",
|
||||
verification_result: "All 42 unit tests pass; linter clean.",
|
||||
duration: "2h",
|
||||
completed_at: "2025-01-15T10:30:00.000Z",
|
||||
blocker_discovered: false,
|
||||
deviations: "Switched from PEG to hand-rolled parser for perf.",
|
||||
known_issues: "No known issues.",
|
||||
key_files: ["src/parser.ts", "src/lexer.ts"],
|
||||
key_decisions: ["Hand-rolled parser over PEG for 3x throughput"],
|
||||
full_summary_md: "",
|
||||
description: "",
|
||||
estimate: "",
|
||||
files: [],
|
||||
verify: "",
|
||||
inputs: [],
|
||||
expected_output: [],
|
||||
observability_impact: "",
|
||||
full_plan_md: "",
|
||||
sequence: 1,
|
||||
};
|
||||
|
||||
const verificationEvidence = [
|
||||
{ command: "npm test", exitCode: 0, verdict: "42/42 passed ✅", durationMs: 3200 },
|
||||
{ command: "npm run lint", exitCode: 0, verdict: "No warnings ✅", durationMs: 1100 },
|
||||
];
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// Tests
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
// Test 1: renderSummaryContent includes Verification section
|
||||
{
|
||||
const output = renderSummaryContent(taskRow, SLICE_ID, MILESTONE_ID);
|
||||
assertTrue(
|
||||
output.includes("## Verification"),
|
||||
"renderSummaryContent must include a ## Verification section",
|
||||
);
|
||||
}
|
||||
|
||||
// Test 2: renderSummaryContent includes Verification Evidence table
|
||||
{
|
||||
const output = renderSummaryContent(taskRow, SLICE_ID, MILESTONE_ID, verificationEvidence);
|
||||
assertTrue(
|
||||
output.includes("## Verification Evidence"),
|
||||
"renderSummaryContent must include a ## Verification Evidence section",
|
||||
);
|
||||
assertTrue(
|
||||
output.includes("npm test"),
|
||||
"Verification Evidence table must include the command",
|
||||
);
|
||||
assertTrue(
|
||||
output.includes("| Exit Code |") || output.includes("exit_code") || output.includes("Exit Code"),
|
||||
"Verification Evidence table must include exit code column",
|
||||
);
|
||||
}
|
||||
|
||||
// Test 3: renderSummaryContent includes Files Created/Modified section
|
||||
{
|
||||
const output = renderSummaryContent(taskRow, SLICE_ID, MILESTONE_ID);
|
||||
assertTrue(
|
||||
output.includes("## Files Created/Modified"),
|
||||
"renderSummaryContent must include a ## Files Created/Modified section",
|
||||
);
|
||||
assertTrue(
|
||||
output.includes("`src/parser.ts`"),
|
||||
"Files section must list key_files as inline code",
|
||||
);
|
||||
}
|
||||
|
||||
// Test 4: one_liner renders as bold (not blockquote) for consistency
|
||||
{
|
||||
const output = renderSummaryContent(taskRow, SLICE_ID, MILESTONE_ID);
|
||||
assertTrue(
|
||||
output.includes(`**${taskRow.one_liner}**`),
|
||||
"one_liner must render as bold text (not blockquote)",
|
||||
);
|
||||
}
|
||||
|
||||
// Test 5: frontmatter key_files uses YAML list format (not JSON array)
|
||||
{
|
||||
const output = renderSummaryContent(taskRow, SLICE_ID, MILESTONE_ID);
|
||||
assertTrue(
|
||||
output.includes("key_files:\n - src/parser.ts\n - src/lexer.ts"),
|
||||
"key_files frontmatter must use YAML list format, not JSON array",
|
||||
);
|
||||
}
|
||||
|
||||
// Test 6: frontmatter key_decisions uses YAML list format (not JSON array)
|
||||
{
|
||||
const output = renderSummaryContent(taskRow, SLICE_ID, MILESTONE_ID);
|
||||
assertTrue(
|
||||
output.includes("key_decisions:\n - Hand-rolled parser over PEG for 3x throughput"),
|
||||
"key_decisions frontmatter must use YAML list format, not JSON array",
|
||||
);
|
||||
}
|
||||
|
||||
// Test 7: Deviations section always present (with "None." fallback)
|
||||
{
|
||||
const noDeviations = { ...taskRow, deviations: "" };
|
||||
const output = renderSummaryContent(noDeviations, SLICE_ID, MILESTONE_ID);
|
||||
assertTrue(
|
||||
output.includes("## Deviations"),
|
||||
"Deviations section must always be present even when empty",
|
||||
);
|
||||
assertTrue(
|
||||
output.includes("None."),
|
||||
"Deviations section must show 'None.' when no deviations",
|
||||
);
|
||||
}
|
||||
|
||||
// Test 8: Known Issues section always present (with "None." fallback)
|
||||
{
|
||||
const noKnownIssues = { ...taskRow, known_issues: "" };
|
||||
const output = renderSummaryContent(noKnownIssues, SLICE_ID, MILESTONE_ID);
|
||||
assertTrue(
|
||||
output.includes("## Known Issues"),
|
||||
"Known Issues section must always be present even when empty",
|
||||
);
|
||||
}
|
||||
|
||||
// Test 9: verification_result frontmatter not double-quoted
|
||||
{
|
||||
const output = renderSummaryContent(taskRow, SLICE_ID, MILESTONE_ID);
|
||||
// Should be: verification_result: passed (not "passed")
|
||||
assertTrue(
|
||||
!output.includes('verification_result: "'),
|
||||
"verification_result frontmatter value must not be double-quoted",
|
||||
);
|
||||
}
|
||||
|
||||
// Test 10: duration frontmatter not double-quoted
|
||||
{
|
||||
const output = renderSummaryContent(taskRow, SLICE_ID, MILESTONE_ID);
|
||||
assertTrue(
|
||||
!output.includes('duration: "'),
|
||||
"duration frontmatter value must not be double-quoted",
|
||||
);
|
||||
}
|
||||
|
||||
// Test 11: empty key_files renders YAML placeholder, not empty array
|
||||
{
|
||||
const noFiles = { ...taskRow, key_files: [] };
|
||||
const output = renderSummaryContent(noFiles, SLICE_ID, MILESTONE_ID);
|
||||
assertTrue(
|
||||
output.includes("key_files:\n - (none)"),
|
||||
"empty key_files must render as YAML list with (none) placeholder",
|
||||
);
|
||||
}
|
||||
|
||||
// Test 12: frontmatter does not contain extra projection-only fields
|
||||
{
|
||||
const output = renderSummaryContent(taskRow, SLICE_ID, MILESTONE_ID);
|
||||
assertTrue(
|
||||
!output.includes("provides:"),
|
||||
"frontmatter must not contain provides field",
|
||||
);
|
||||
assertTrue(
|
||||
!output.includes("requires:"),
|
||||
"frontmatter must not contain requires field",
|
||||
);
|
||||
assertTrue(
|
||||
!output.includes("affects:"),
|
||||
"frontmatter must not contain affects field",
|
||||
);
|
||||
assertTrue(
|
||||
!output.includes("patterns_established:"),
|
||||
"frontmatter must not contain patterns_established field",
|
||||
);
|
||||
assertTrue(
|
||||
!output.includes("drill_down_paths:"),
|
||||
"frontmatter must not contain drill_down_paths field",
|
||||
);
|
||||
assertTrue(
|
||||
!output.includes("observability_surfaces:"),
|
||||
"frontmatter must not contain observability_surfaces field",
|
||||
);
|
||||
}
|
||||
|
||||
// Test 13: no verification evidence renders empty table row
|
||||
{
|
||||
const output = renderSummaryContent(taskRow, SLICE_ID, MILESTONE_ID, []);
|
||||
assertTrue(
|
||||
output.includes("No verification commands discovered"),
|
||||
"Empty evidence array must render placeholder row",
|
||||
);
|
||||
}
|
||||
|
||||
report();
|
||||
|
|
@ -30,7 +30,7 @@ import { checkOwnership, taskUnitKey } from "../unit-ownership.js";
|
|||
import { saveFile, clearParseCache } from "../files.js";
|
||||
import { invalidateStateCache } from "../state.js";
|
||||
import { renderPlanCheckboxes } from "../markdown-renderer.js";
|
||||
import { renderAllProjections } from "../workflow-projections.js";
|
||||
import { renderAllProjections, renderSummaryContent } from "../workflow-projections.js";
|
||||
import { writeManifest } from "../workflow-manifest.js";
|
||||
import { appendEvent } from "../workflow-events.js";
|
||||
|
||||
|
|
@ -41,79 +41,40 @@ export interface CompleteTaskResult {
|
|||
summaryPath: string;
|
||||
}
|
||||
|
||||
import type { TaskRow } from "../gsd-db.js";
|
||||
|
||||
/**
|
||||
* Render task summary markdown matching the template format.
|
||||
* YAML frontmatter uses snake_case keys for parseSummary() compatibility.
|
||||
* Build a TaskRow-shaped object from CompleteTaskParams so the unified
|
||||
* renderSummaryContent() can be used at completion time (#2720).
|
||||
*/
|
||||
function renderSummaryMarkdown(params: CompleteTaskParams): string {
|
||||
const now = new Date().toISOString();
|
||||
const keyFilesYaml = params.keyFiles.length > 0
|
||||
? params.keyFiles.map(f => ` - ${f}`).join("\n")
|
||||
: " - (none)";
|
||||
const keyDecisionsYaml = params.keyDecisions.length > 0
|
||||
? params.keyDecisions.map(d => ` - ${d}`).join("\n")
|
||||
: " - (none)";
|
||||
|
||||
// Build verification evidence table rows
|
||||
let evidenceTable = "| # | Command | Exit Code | Verdict | Duration |\n|---|---------|-----------|---------|----------|\n";
|
||||
if (params.verificationEvidence.length > 0) {
|
||||
params.verificationEvidence.forEach((e, i) => {
|
||||
evidenceTable += `| ${i + 1} | \`${e.command}\` | ${e.exitCode} | ${e.verdict} | ${e.durationMs}ms |\n`;
|
||||
});
|
||||
} else {
|
||||
evidenceTable += "| — | No verification commands discovered | — | — | — |\n";
|
||||
}
|
||||
|
||||
// Determine verification_result from evidence
|
||||
const allPassed = params.verificationEvidence.length > 0 &&
|
||||
params.verificationEvidence.every(e => e.exitCode === 0 || e.verdict.includes("✅") || e.verdict.toLowerCase().includes("pass"));
|
||||
const verificationResult = allPassed ? "passed" : (params.verificationEvidence.length === 0 ? "untested" : "mixed");
|
||||
|
||||
// Extract a title from the oneLiner or taskId
|
||||
const title = params.oneLiner || params.taskId;
|
||||
|
||||
return `---
|
||||
id: ${params.taskId}
|
||||
parent: ${params.sliceId}
|
||||
milestone: ${params.milestoneId}
|
||||
key_files:
|
||||
${keyFilesYaml}
|
||||
key_decisions:
|
||||
${keyDecisionsYaml}
|
||||
duration: ""
|
||||
verification_result: ${verificationResult}
|
||||
completed_at: ${now}
|
||||
blocker_discovered: ${params.blockerDiscovered}
|
||||
---
|
||||
|
||||
# ${params.taskId}: ${title}
|
||||
|
||||
**${params.oneLiner}**
|
||||
|
||||
## What Happened
|
||||
|
||||
${params.narrative}
|
||||
|
||||
## Verification
|
||||
|
||||
${params.verification}
|
||||
|
||||
## Verification Evidence
|
||||
|
||||
${evidenceTable}
|
||||
|
||||
## Deviations
|
||||
|
||||
${params.deviations || "None."}
|
||||
|
||||
## Known Issues
|
||||
|
||||
${params.knownIssues || "None."}
|
||||
|
||||
## Files Created/Modified
|
||||
|
||||
${params.keyFiles.map(f => `- \`${f}\``).join("\n") || "None."}
|
||||
`;
|
||||
function paramsToTaskRow(params: CompleteTaskParams, completedAt: string): TaskRow {
|
||||
return {
|
||||
milestone_id: params.milestoneId,
|
||||
slice_id: params.sliceId,
|
||||
id: params.taskId,
|
||||
title: params.oneLiner || params.taskId,
|
||||
status: "complete",
|
||||
one_liner: params.oneLiner,
|
||||
narrative: params.narrative,
|
||||
verification_result: params.verification,
|
||||
duration: "",
|
||||
completed_at: completedAt,
|
||||
blocker_discovered: params.blockerDiscovered,
|
||||
deviations: params.deviations,
|
||||
known_issues: params.knownIssues,
|
||||
key_files: params.keyFiles,
|
||||
key_decisions: params.keyDecisions,
|
||||
full_summary_md: "",
|
||||
description: "",
|
||||
estimate: "",
|
||||
files: [],
|
||||
verify: "",
|
||||
inputs: [],
|
||||
expected_output: [],
|
||||
observability_impact: "",
|
||||
full_plan_md: "",
|
||||
sequence: 0,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -218,8 +179,9 @@ export async function handleCompleteTask(
|
|||
// If disk render fails, roll back the DB status so deriveState() and
|
||||
// verifyExpectedArtifact() stay consistent (both say "not done").
|
||||
|
||||
// Render summary markdown
|
||||
const summaryMd = renderSummaryMarkdown(params);
|
||||
// Render summary markdown via the single source of truth (#2720)
|
||||
const taskRow = paramsToTaskRow(params, completedAt);
|
||||
const summaryMd = renderSummaryContent(taskRow, params.sliceId, params.milestoneId, params.verificationEvidence);
|
||||
|
||||
// Resolve and write summary to disk
|
||||
let summaryPath: string;
|
||||
|
|
|
|||
|
|
@ -9,8 +9,9 @@ import {
|
|||
getMilestone,
|
||||
getMilestoneSlices,
|
||||
getSliceTasks,
|
||||
getVerificationEvidence,
|
||||
} from "./gsd-db.js";
|
||||
import type { MilestoneRow, SliceRow, TaskRow } from "./gsd-db.js";
|
||||
import type { MilestoneRow, SliceRow, TaskRow, VerificationEvidenceRow } from "./gsd-db.js";
|
||||
import { atomicWriteSync } from "./atomic-write.js";
|
||||
import { join } from "node:path";
|
||||
import { mkdirSync, existsSync } from "node:fs";
|
||||
|
|
@ -147,71 +148,93 @@ export function renderRoadmapProjection(basePath: string, milestoneId: string):
|
|||
|
||||
/**
|
||||
* Render SUMMARY.md content from a task row.
|
||||
* Pure function — no side effects.
|
||||
* Single source of truth for summary rendering — used both at completion
|
||||
* time and at projection regeneration time (#2720).
|
||||
*
|
||||
* @param evidence - Optional verification evidence rows. When called from
|
||||
* complete-task, these are passed directly. When called from projection
|
||||
* regeneration, they are queried from the DB by renderSummaryProjection.
|
||||
*/
|
||||
export function renderSummaryContent(taskRow: TaskRow, sliceId: string, milestoneId: string): string {
|
||||
const lines: string[] = [];
|
||||
export function renderSummaryContent(
|
||||
taskRow: TaskRow,
|
||||
sliceId: string,
|
||||
milestoneId: string,
|
||||
evidence?: Array<{ command: string; exitCode?: number; exit_code?: number; verdict: string; durationMs?: number; duration_ms?: number }>,
|
||||
): string {
|
||||
// ── Frontmatter (YAML list format, matches parseSummary() expectations) ──
|
||||
const keyFilesYaml = taskRow.key_files && taskRow.key_files.length > 0
|
||||
? taskRow.key_files.map(f => ` - ${f}`).join("\n")
|
||||
: " - (none)";
|
||||
const keyDecisionsYaml = taskRow.key_decisions && taskRow.key_decisions.length > 0
|
||||
? taskRow.key_decisions.map(d => ` - ${d}`).join("\n")
|
||||
: " - (none)";
|
||||
|
||||
// Frontmatter
|
||||
lines.push("---");
|
||||
lines.push(`id: ${taskRow.id}`);
|
||||
lines.push(`parent: ${sliceId}`);
|
||||
lines.push(`milestone: ${milestoneId}`);
|
||||
lines.push("provides: []");
|
||||
lines.push("requires: []");
|
||||
lines.push("affects: []");
|
||||
// Derive verification_result from evidence if available
|
||||
const evidenceList = evidence ?? [];
|
||||
const allPassed = evidenceList.length > 0 &&
|
||||
evidenceList.every(e => {
|
||||
const code = e.exitCode ?? e.exit_code ?? -1;
|
||||
return code === 0 || e.verdict.includes("\u2705") || e.verdict.toLowerCase().includes("pass");
|
||||
});
|
||||
const verificationResult = taskRow.verification_result
|
||||
? (allPassed ? "passed" : (evidenceList.length === 0 ? "untested" : "mixed"))
|
||||
: (allPassed ? "passed" : (evidenceList.length === 0 ? "untested" : "mixed"));
|
||||
|
||||
// key_files is already parsed to string[]
|
||||
if (taskRow.key_files && taskRow.key_files.length > 0) {
|
||||
lines.push(`key_files: [${taskRow.key_files.map(f => `"${f}"`).join(", ")}]`);
|
||||
// Build verification evidence table
|
||||
let evidenceTable = "| # | Command | Exit Code | Verdict | Duration |\n|---|---------|-----------|---------|----------|\n";
|
||||
if (evidenceList.length > 0) {
|
||||
evidenceList.forEach((e, i) => {
|
||||
const code = e.exitCode ?? e.exit_code ?? 0;
|
||||
const dur = e.durationMs ?? e.duration_ms ?? 0;
|
||||
evidenceTable += `| ${i + 1} | \`${e.command}\` | ${code} | ${e.verdict} | ${dur}ms |\n`;
|
||||
});
|
||||
} else {
|
||||
lines.push("key_files: []");
|
||||
evidenceTable += "| \u2014 | No verification commands discovered | \u2014 | \u2014 | \u2014 |\n";
|
||||
}
|
||||
|
||||
// key_decisions is already parsed to string[]
|
||||
if (taskRow.key_decisions && taskRow.key_decisions.length > 0) {
|
||||
lines.push(`key_decisions: [${taskRow.key_decisions.map(d => `"${d}"`).join(", ")}]`);
|
||||
} else {
|
||||
lines.push("key_decisions: []");
|
||||
}
|
||||
const title = taskRow.one_liner || taskRow.title || taskRow.id;
|
||||
|
||||
lines.push("patterns_established: []");
|
||||
lines.push("drill_down_paths: []");
|
||||
lines.push("observability_surfaces: []");
|
||||
lines.push(`duration: "${taskRow.duration || ""}"`);
|
||||
lines.push(`verification_result: "${taskRow.verification_result || ""}"`);
|
||||
lines.push(`completed_at: ${taskRow.completed_at || ""}`);
|
||||
lines.push(`blocker_discovered: ${taskRow.blocker_discovered ? "true" : "false"}`);
|
||||
lines.push("---");
|
||||
lines.push("");
|
||||
lines.push(`# ${taskRow.id}: ${taskRow.title}`);
|
||||
lines.push("");
|
||||
return `---
|
||||
id: ${taskRow.id}
|
||||
parent: ${sliceId}
|
||||
milestone: ${milestoneId}
|
||||
key_files:
|
||||
${keyFilesYaml}
|
||||
key_decisions:
|
||||
${keyDecisionsYaml}
|
||||
duration: ${taskRow.duration || ""}
|
||||
verification_result: ${verificationResult}
|
||||
completed_at: ${taskRow.completed_at || ""}
|
||||
blocker_discovered: ${taskRow.blocker_discovered ? "true" : "false"}
|
||||
---
|
||||
|
||||
// One-liner (if present)
|
||||
if (taskRow.one_liner) {
|
||||
lines.push(`> ${taskRow.one_liner}`);
|
||||
lines.push("");
|
||||
}
|
||||
# ${taskRow.id}: ${title}
|
||||
|
||||
lines.push("## What Happened");
|
||||
lines.push(taskRow.full_summary_md || taskRow.narrative || "No summary recorded.");
|
||||
lines.push("");
|
||||
**${taskRow.one_liner || ""}**
|
||||
|
||||
// Deviations (if present)
|
||||
if (taskRow.deviations) {
|
||||
lines.push("## Deviations");
|
||||
lines.push(taskRow.deviations);
|
||||
lines.push("");
|
||||
}
|
||||
## What Happened
|
||||
|
||||
// Known issues (if present)
|
||||
if (taskRow.known_issues) {
|
||||
lines.push("## Known Issues");
|
||||
lines.push(taskRow.known_issues);
|
||||
lines.push("");
|
||||
}
|
||||
${taskRow.narrative || "No summary recorded."}
|
||||
|
||||
return lines.join("\n");
|
||||
## Verification
|
||||
|
||||
${taskRow.verification_result || "No verification recorded."}
|
||||
|
||||
## Verification Evidence
|
||||
|
||||
${evidenceTable}
|
||||
## Deviations
|
||||
|
||||
${taskRow.deviations || "None."}
|
||||
|
||||
## Known Issues
|
||||
|
||||
${taskRow.known_issues || "None."}
|
||||
|
||||
## Files Created/Modified
|
||||
|
||||
${taskRow.key_files && taskRow.key_files.length > 0 ? taskRow.key_files.map(f => `- \`${f}\``).join("\n") : "None."}
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -223,7 +246,8 @@ export function renderSummaryProjection(basePath: string, milestoneId: string, s
|
|||
const taskRow = taskRows.find(t => t.id === taskId);
|
||||
if (!taskRow) return;
|
||||
|
||||
const content = renderSummaryContent(taskRow, sliceId, milestoneId);
|
||||
const evidenceRows = getVerificationEvidence(milestoneId, sliceId, taskId);
|
||||
const content = renderSummaryContent(taskRow, sliceId, milestoneId, evidenceRows);
|
||||
const dir = join(basePath, ".gsd", "milestones", milestoneId, "slices", sliceId, "tasks");
|
||||
mkdirSync(dir, { recursive: true });
|
||||
atomicWriteSync(join(dir, `${taskId}-SUMMARY.md`), content);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue