Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 28 additions & 13 deletions plugins/spec-kit-copilot-sdd/extensions/sdd-canvas/sdd.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Comment thread
1fanwang marked this conversation as resolved.
total++;
Expand Down Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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",
"~~~",
Comment thread
1fanwang marked this conversation as resolved.
"- [x] T001 Complete near the start",
"padding".repeat(10_000),
"- [ ] T002 Incomplete after the 64 KiB scan prefix",
Expand All @@ -39,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",
Expand All @@ -51,6 +78,9 @@ test("clarifications retain stable indices across supported markdown blocks", ()
"```text",
"[NEEDS CLARIFICATION: Ignore code?]",
"```",
" ~~~markdown",
"[NEEDS CLARIFICATION: Ignore indented tilde fence?]",
" ~~~",
].join("\n");
Comment on lines 78 to 84

assert.deepEqual(extractClarifications(markdown), [
Expand Down