From bf0e3fb0e411c3e404f35a13e3111be2fbc9348f Mon Sep 17 00:00:00 2001 From: Tibsfox Date: Mon, 6 Apr 2026 18:59:00 -0700 Subject: [PATCH 1/2] fix(gsd): stop renderAllProjections from overwriting authoritative PLAN.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit renderAllProjections called renderPlanProjection which overwrote the complete PLAN.md (from markdown-renderer.js) with a simplified projection missing Must-Haves, Verification, Files Likely Touched sections and corrupting multi-line task descriptions. Remove the plan projection call from renderAllProjections — the authoritative renderer in plan-slice/replan-slice tools is the sole writer. The renderIfMissing recovery path is preserved for when the file is actually missing. Closes #3651 Co-Authored-By: Claude Opus 4.6 (1M context) --- src/resources/extensions/gsd/workflow-projections.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/resources/extensions/gsd/workflow-projections.ts b/src/resources/extensions/gsd/workflow-projections.ts index a77200917..15d39d55a 100644 --- a/src/resources/extensions/gsd/workflow-projections.ts +++ b/src/resources/extensions/gsd/workflow-projections.ts @@ -370,12 +370,10 @@ export async function renderAllProjections(basePath: string, milestoneId: string const sliceRows = getMilestoneSlices(milestoneId); for (const slice of sliceRows) { - // Render PLAN.md for each slice - try { - renderPlanProjection(basePath, milestoneId, slice.id); - } catch (err) { - logWarning("projection", `renderPlanProjection failed for ${milestoneId}/${slice.id}: ${(err as Error).message}`); - } + // PLAN.md is rendered by the authoritative markdown-renderer.js in + // plan-slice/replan-slice tools. Do NOT overwrite it here — the simplified + // projection is missing key sections (Must-Haves, Verification, Files + // Likely Touched) and corrupts multi-line task descriptions (#3651). // Render SUMMARY.md for each completed task const taskRows = getSliceTasks(milestoneId, slice.id); From 08ebf3387d942624babaf303bf3e7372f8b15ddc Mon Sep 17 00:00:00 2001 From: Tibsfox Date: Mon, 6 Apr 2026 22:26:06 -0700 Subject: [PATCH 2/2] test: add regression test for projection plan overwrite prevention Co-Authored-By: Claude Opus 4.6 (1M context) --- .../projection-no-plan-overwrite.test.ts | 83 +++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 src/resources/extensions/gsd/tests/projection-no-plan-overwrite.test.ts diff --git a/src/resources/extensions/gsd/tests/projection-no-plan-overwrite.test.ts b/src/resources/extensions/gsd/tests/projection-no-plan-overwrite.test.ts new file mode 100644 index 000000000..e87c3a4ca --- /dev/null +++ b/src/resources/extensions/gsd/tests/projection-no-plan-overwrite.test.ts @@ -0,0 +1,83 @@ +/** + * Regression test for #3651 — renderAllProjections must NOT call renderPlanProjection + * + * renderAllProjections previously called renderPlanProjection inside the slice + * loop, which overwrote the authoritative PLAN.md (produced by markdown-renderer.js + * in plan-slice/replan-slice tools) with a simplified projection that was missing + * key sections (Must-Haves, Verification, Files Likely Touched) and corrupted + * multi-line task descriptions. + * + * The fix removes the renderPlanProjection call from the renderAllProjections + * loop. The renderIfMissing recovery path is preserved. + */ + +import { describe, it } from 'node:test' +import assert from 'node:assert/strict' +import { readFileSync } from 'node:fs' +import { resolve } from 'node:path' + +// Use process.cwd() based resolution instead of import.meta.url +// to avoid tsx test runner path resolution issues +const src = readFileSync( + resolve(process.cwd(), 'src', 'resources', 'extensions', 'gsd', 'workflow-projections.ts'), + 'utf-8', +) + +describe('renderAllProjections must not overwrite PLAN.md (#3651)', () => { + it('renderAllProjections function body does NOT invoke renderPlanProjection', () => { + // Extract the renderAllProjections function body + const fnStart = src.indexOf('export async function renderAllProjections(') + assert.ok(fnStart !== -1, 'renderAllProjections function must exist') + + // Find the for-loop over sliceRows inside renderAllProjections + const loopStart = src.indexOf('for (const slice of sliceRows)', fnStart) + assert.ok(loopStart !== -1, 'slice loop must exist in renderAllProjections') + + // Find the closing of renderAllProjections (next section marker) + const fnEnd = src.indexOf('\n// ─── ', fnStart + 1) + assert.ok(fnEnd !== -1, 'section delimiter after renderAllProjections must exist') + + const fnBody = src.slice(loopStart, fnEnd) + + // The fix: renderPlanProjection must NOT appear as a function call. + // Strip comment lines before checking (comments may mention the function name). + const codeOnly = fnBody + .split('\n') + .filter(line => !line.trim().startsWith('//')) + .join('\n') + + const hasPlanCall = /renderPlanProjection\s*\(/.test(codeOnly) + assert.equal( + hasPlanCall, + false, + 'renderPlanProjection must not be called inside the renderAllProjections slice loop — ' + + 'authoritative PLAN.md is rendered only by plan-slice/replan-slice tools', + ) + }) + + it('renderPlanProjection is still defined (available for regenerateIfMissing)', () => { + assert.ok( + src.includes('function renderPlanProjection('), + 'renderPlanProjection function definition must still exist for on-demand recovery', + ) + }) + + it('renderAllProjections still renders ROADMAP, SUMMARY, and STATE projections', () => { + const fnStart = src.indexOf('export async function renderAllProjections(') + const fnEnd = src.indexOf('\n// ─── ', fnStart + 1) + const fnBody = src.slice(fnStart, fnEnd) + + assert.ok( + fnBody.includes('renderRoadmapProjection('), + 'renderRoadmapProjection must still be called', + ) + assert.ok( + fnBody.includes('renderSummaryProjection('), + 'renderSummaryProjection must still be called', + ) + assert.ok( + fnBody.includes('renderStateProjection('), + 'renderStateProjection must still be called', + ) + }) +})