From 3d6d83e4171a44036478f72d6b036a186ac7754e Mon Sep 17 00:00:00 2001 From: 1fanwang <1fannnw@gmail.com> Date: Mon, 7 Sep 2026 18:29:51 -0700 Subject: [PATCH 1/2] fix(sdd): ignore fenced Markdown examples Task counters and clarification parsers previously only checked for unindented triple-backtick fences. Markdown examples inside tilde fences or indented code blocks were incorrectly counted as active tasks or clarification questions. Advance fence state across CommonMark fences. Signed-off-by: 1fanwang <1fannnw@gmail.com> --- .../extensions/sdd-canvas/sdd.mjs | 41 +++++++++++++------ .../extensions/sdd-canvas/tests/sdd.test.mjs | 6 +++ 2 files changed, 34 insertions(+), 13 deletions(-) diff --git a/plugins/spec-kit-copilot-sdd/extensions/sdd-canvas/sdd.mjs b/plugins/spec-kit-copilot-sdd/extensions/sdd-canvas/sdd.mjs index 2d5e814..26f4b5c 100644 --- a/plugins/spec-kit-copilot-sdd/extensions/sdd-canvas/sdd.mjs +++ b/plugins/spec-kit-copilot-sdd/extensions/sdd-canvas/sdd.mjs @@ -252,18 +252,35 @@ function constitutionStatus(text) { return placeholders > 0 ? "template" : "ratified"; } +function advanceMarkdownFence(line, openFence) { + const match = line.match(/^ {0,3}(`{3,}|~{3,})(.*)$/); + if (!match) return { openFence, isFenceLine: false }; + + const marker = match[1][0]; + const length = match[1].length; + const rest = match[2]; + if (!openFence) { + if (marker === "`" && rest.includes("`")) { + return { openFence: null, isFenceLine: false }; + } + return { openFence: { marker, length }, isFenceLine: true }; + } + if (marker === openFence.marker && length >= openFence.length && !rest.trim()) { + return { openFence: null, isFenceLine: true }; + } + return { openFence, isFenceLine: false }; +} + // Count task checkboxes in tasks.md to derive implementation progress. -function taskProgress(text) { +export function taskProgress(text) { if (typeof text !== "string") return { total: 0, completed: 0 }; let total = 0; let completed = 0; - let inFence = false; + let openFence = null; for (const line of text.split(/\r?\n/)) { - if (line.startsWith("```")) { - inFence = !inFence; - continue; - } - if (inFence) continue; + const fenceState = advanceMarkdownFence(line, openFence); + openFence = fenceState.openFence; + if (fenceState.isFenceLine || openFence) continue; const m = line.match(/^\s*[-*+]\s+\[([ xX])\]/); if (!m) continue; total++; @@ -519,13 +536,11 @@ export function readArtifact(projectRoot, featureInput, stageKey) { export function extractClarifications(text) { const clarifications = []; let section = ""; - let inCodeFence = false; + let openFence = null; for (const line of String(text || "").split(/\r?\n/)) { - if (line.startsWith("```")) { - inCodeFence = !inCodeFence; - continue; - } - if (inCodeFence) continue; + const fenceState = advanceMarkdownFence(line, openFence); + openFence = fenceState.openFence; + if (fenceState.isFenceLine || openFence) continue; const heading = line.match(/^#{1,6}\s+(.+?)\s*$/); if (heading) { section = heading[1].trim(); diff --git a/plugins/spec-kit-copilot-sdd/extensions/sdd-canvas/tests/sdd.test.mjs b/plugins/spec-kit-copilot-sdd/extensions/sdd-canvas/tests/sdd.test.mjs index f2a3b14..ba9af49 100644 --- a/plugins/spec-kit-copilot-sdd/extensions/sdd-canvas/tests/sdd.test.mjs +++ b/plugins/spec-kit-copilot-sdd/extensions/sdd-canvas/tests/sdd.test.mjs @@ -23,6 +23,9 @@ test("implementation progress scans the complete bounded tasks artifact", (t) => write(join(featureDir, "plan.md"), "# Plan\n", 2); const tasks = [ "# Tasks", + "~~~markdown", + "- [x] T000 Example only", + "~~~", "- [x] T001 Complete near the start", "padding".repeat(10_000), "- [ ] T002 Incomplete after the 64 KiB scan prefix", @@ -51,6 +54,9 @@ test("clarifications retain stable indices across supported markdown blocks", () "```text", "[NEEDS CLARIFICATION: Ignore code?]", "```", + " ~~~markdown", + "[NEEDS CLARIFICATION: Ignore indented tilde fence?]", + " ~~~", ].join("\n"); assert.deepEqual(extractClarifications(markdown), [ From cd8a3e6abe70bf9953de6072058d30dd80783ffa Mon Sep 17 00:00:00 2001 From: 1fanwang <1fannnw@gmail.com> Date: Tue, 8 Sep 2026 16:08:39 -0700 Subject: [PATCH 2/2] test(sdd): pin task counting for fenced and indented checkboxes Signed-off-by: 1fanwang <1fannnw@gmail.com> --- .../extensions/sdd-canvas/tests/sdd.test.mjs | 26 ++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/plugins/spec-kit-copilot-sdd/extensions/sdd-canvas/tests/sdd.test.mjs b/plugins/spec-kit-copilot-sdd/extensions/sdd-canvas/tests/sdd.test.mjs index ba9af49..8a9d3a8 100644 --- a/plugins/spec-kit-copilot-sdd/extensions/sdd-canvas/tests/sdd.test.mjs +++ b/plugins/spec-kit-copilot-sdd/extensions/sdd-canvas/tests/sdd.test.mjs @@ -4,7 +4,7 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; import test from "node:test"; -import { extractClarifications, scanFeatures } from "../sdd.mjs"; +import { extractClarifications, scanFeatures, taskProgress } from "../sdd.mjs"; function write(path, content, mtimeSeconds) { writeFileSync(path, content); @@ -42,6 +42,30 @@ test("implementation progress scans the complete bounded tasks artifact", (t) => assert.equal(feature.nextStage, "implement"); }); +test("task counting ignores fenced examples but keeps indented list items", () => { + // A fence hides its contents whether the marker is plain, tilde, or indented + // up to the three spaces CommonMark allows. + assert.deepEqual(taskProgress([ + "- [x] T001 Real", + "```markdown", + "- [x] T900 Example in a backtick fence", + "```", + " ~~~markdown", + "- [x] T901 Example in an indented tilde fence", + " ~~~", + "- [ ] T002 Real", + ].join("\n")), { total: 2, completed: 1 }); + + // An indented checkbox is a nested list item, so it counts. Markdown also lets + // four spaces open a code block, and telling the two apart needs the block + // context a full CommonMark parser tracks. Counting is the safe side of that + // ambiguity: an extra task is visible in the dashboard, a dropped one is not. + assert.deepEqual(taskProgress([ + "- [ ] T001 Parent", + " - [x] T002 Nested child", + ].join("\n")), { total: 2, completed: 1 }); +}); + test("clarifications retain stable indices across supported markdown blocks", () => { const markdown = [ "## Requirements",