fix(gsd): prevent double frontmatter in task SUMMARY.md from projection re-render (#2818)
renderSummaryContent() in workflow-projections.ts wraps full_summary_md (already a complete markdown doc with frontmatter) inside a second generated frontmatter/heading envelope. This produces double frontmatter, double H1 headings, and duplicate Deviations/Known Issues sections. The fix checks whether full_summary_md exists and starts with frontmatter delimiters. If so, it is used as the entire output. The fallback synthesis from individual DB columns only runs when full_summary_md is absent or lacks frontmatter. Adds 3 regression tests to projection-regression.test.ts.
This commit is contained in:
parent
110c01b8c6
commit
ff36c117dd
2 changed files with 104 additions and 1 deletions
|
|
@ -5,7 +5,7 @@
|
|||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
|
||||
import { renderPlanContent, renderRoadmapContent } from '../workflow-projections.ts';
|
||||
import { renderPlanContent, renderRoadmapContent, renderSummaryContent } from '../workflow-projections.ts';
|
||||
import type { SliceRow, TaskRow } from '../gsd-db.ts';
|
||||
|
||||
// ─── Helpers ─────────────────────────────────────────────────────────────
|
||||
|
|
@ -172,3 +172,98 @@ test('renderRoadmapContent: slice with status "pending" shows ⬜', () => {
|
|||
|
||||
assert.ok(content.includes('⬜'), 'pending slice should show ⬜');
|
||||
});
|
||||
|
||||
// ─── renderSummaryContent: double-frontmatter regression ─────────────────
|
||||
|
||||
test('renderSummaryContent: uses full_summary_md as-is when it contains frontmatter', () => {
|
||||
const existingSummary = [
|
||||
'---',
|
||||
'id: T01',
|
||||
'parent: S01',
|
||||
'milestone: M001',
|
||||
'key_files:',
|
||||
' - src/thing.ts',
|
||||
'verification_result: passed',
|
||||
'completed_at: 2026-01-01T00:00:00Z',
|
||||
'blocker_discovered: false',
|
||||
'---',
|
||||
'',
|
||||
'# T01: Did the thing',
|
||||
'',
|
||||
'**One-liner summary**',
|
||||
'',
|
||||
'## What Happened',
|
||||
'',
|
||||
'Narrative content here.',
|
||||
'',
|
||||
'## Deviations',
|
||||
'',
|
||||
'None.',
|
||||
'',
|
||||
].join('\n');
|
||||
|
||||
const task = makeTaskRow({
|
||||
id: 'T01',
|
||||
status: 'complete',
|
||||
title: 'Did the thing',
|
||||
one_liner: 'One-liner summary',
|
||||
narrative: 'Narrative content here.',
|
||||
full_summary_md: existingSummary,
|
||||
});
|
||||
|
||||
const result = renderSummaryContent(task, 'S01', 'M001');
|
||||
|
||||
// Must NOT produce double frontmatter
|
||||
const frontmatterCount = (result.match(/^---$/gm) || []).length;
|
||||
assert.equal(frontmatterCount, 2, `Expected exactly 2 frontmatter delimiters (one block), got ${frontmatterCount}`);
|
||||
|
||||
// Must NOT produce double H1 heading
|
||||
const h1Count = (result.match(/^# T01:/gm) || []).length;
|
||||
assert.equal(h1Count, 1, `Expected exactly 1 H1 heading, got ${h1Count}`);
|
||||
|
||||
// Content should match the full_summary_md exactly
|
||||
assert.equal(result, existingSummary);
|
||||
});
|
||||
|
||||
test('renderSummaryContent: synthesizes from DB columns when full_summary_md is empty', () => {
|
||||
const task = makeTaskRow({
|
||||
id: 'T01',
|
||||
status: 'complete',
|
||||
title: 'Did the thing',
|
||||
one_liner: 'One-liner summary',
|
||||
narrative: 'Built the feature.',
|
||||
full_summary_md: '',
|
||||
deviations: 'Deviated slightly.',
|
||||
known_issues: 'None.',
|
||||
});
|
||||
|
||||
const result = renderSummaryContent(task, 'S01', 'M001');
|
||||
|
||||
// Should have exactly one frontmatter block
|
||||
const frontmatterCount = (result.match(/^---$/gm) || []).length;
|
||||
assert.equal(frontmatterCount, 2, 'Should have one frontmatter block (2 delimiters)');
|
||||
|
||||
// Should contain synthesized sections
|
||||
assert.ok(result.includes('## What Happened'), 'Should have What Happened section');
|
||||
assert.ok(result.includes('Built the feature.'), 'Should use narrative for content');
|
||||
assert.ok(result.includes('## Deviations'), 'Should have Deviations section');
|
||||
assert.ok(result.includes('Deviated slightly.'), 'Should include deviation text');
|
||||
});
|
||||
|
||||
test('renderSummaryContent: synthesizes when full_summary_md has no frontmatter', () => {
|
||||
const task = makeTaskRow({
|
||||
id: 'T02',
|
||||
status: 'complete',
|
||||
title: 'Partial summary',
|
||||
narrative: 'Did some work.',
|
||||
full_summary_md: 'Just a plain text summary with no frontmatter.',
|
||||
});
|
||||
|
||||
const result = renderSummaryContent(task, 'S01', 'M001');
|
||||
|
||||
// Should synthesize with proper frontmatter since the stored md lacks it
|
||||
assert.ok(result.startsWith('---'), 'Should start with frontmatter');
|
||||
assert.ok(result.includes('id: T02'), 'Should have task ID in frontmatter');
|
||||
assert.ok(result.includes('## What Happened'), 'Should have What Happened section');
|
||||
assert.ok(result.includes('Did some work.'), 'Should use narrative');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -180,6 +180,14 @@ export function renderSummaryContent(
|
|||
milestoneId: string,
|
||||
evidence?: Array<{ command: string; exitCode?: number; exit_code?: number; verdict: string; durationMs?: number; duration_ms?: number }>,
|
||||
): string {
|
||||
// If the task already has a fully rendered summary (written by handleCompleteTask's
|
||||
// renderSummaryMarkdown), use it as-is. That content already includes frontmatter,
|
||||
// heading, and all sections. Re-wrapping it inside a second frontmatter/heading
|
||||
// envelope produces double frontmatter and duplicate sections.
|
||||
if (taskRow.full_summary_md && taskRow.full_summary_md.trimStart().startsWith("---")) {
|
||||
return taskRow.full_summary_md;
|
||||
}
|
||||
|
||||
// ── 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")
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue