FN-7159: add canonical CE HTML checklist repair

Enable CE HTML document review to safely canonicalize malformed checklists.

- Add a DOM-safe checklist-repair operation that rewrites provable markdown-marker and checkbox list shapes into canonical CE checklist HTML.
- Refuse ambiguous, nested, state-bearing, or protected checklist-like markup while preserving report-only fallback semantics.
- Expand HTML mutation and CE doc-review tests plus operator docs and rendering guidance for the canonical checklist contract.

Files changed:
 .changeset/fn-7159-canonical-html-checklist.md     |   7 +
 docs/workflow-editor.md                            |   2 +-
 docs/workflow-steps.md                             |   5 +-
 .../src/__tests__/ce-doc-review-html-mode.test.ts  |   6 +-
 .../src/artifacts/__tests__/html-mutation.test.ts  | 135 ++++++++++++++-
 .../src/artifacts/html-mutation.ts                 | 186 ++++++++++++++++++++-
 .../ce-brainstorm/references/html-rendering.md     |  31 ++++
 .../src/skills/ce-doc-review/SKILL.md              |  13 +-
 .../skills/ce-ideate/references/html-rendering.md  |  31 ++++
 .../skills/ce-plan/references/html-rendering.md    |  31 ++++
 10 files changed, 436 insertions(+), 11 deletions(-)

Fusion-Task-Id: FN-7159
Fusion-Task-Lineage: bbde7003-2942-4363-98d5-53bc3a5976b8
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
gsxdsm
2026-06-28 09:20:17 -07:00
parent 68d2d41f37
commit 631e8fc675
10 changed files with 436 additions and 11 deletions

View File

@@ -0,0 +1,7 @@
---
"@runfusion/fusion": minor
---
summary: CE HTML docs can define and safely repair malformed checklists in ce-doc-review.
category: feature
dev: Adds parse5-backed canonical checklist repair with validation, atomic writes, and report-only fallback.

View File

@@ -134,7 +134,7 @@ Fusion ships built-in workflows as read-only references:
- `builtin:coding` — the default coding lifecycle and fallback for tasks without a workflow selection.
- `builtin:quick-fix` — a short path for trivial or no-commit/decision work.
- `builtin:review-heavy` — a standard execute/review/merge path with an additional gated security review.
- `builtin:compound-engineering` — a plugin-gated Compound Engineering pipeline: `/ce-plan` writes the CE plan doc, optional `ce-doc-review` can pressure-test plans (markdown gets autofix/Open Questions write-back; HTML uses DOM-safe helper mutations only when safety is proven, otherwise report-only with no write), `/ce-work` implements, `/ce-code-review` gates merge, and autoMerge-off projects route through the CE PR/feedback skills before Fusion's manual merge seam.
- `builtin:compound-engineering` — a plugin-gated Compound Engineering pipeline: `/ce-plan` writes the CE plan doc, optional `ce-doc-review` can pressure-test plans (markdown gets autofix/Open Questions write-back; HTML uses DOM-safe helper mutations, including canonical checklist repair, only when safety is proven and otherwise report-only with no write), `/ce-work` implements, `/ce-code-review` gates merge, and autoMerge-off projects route through the CE PR/feedback skills before Fusion's manual merge seam.
- `builtin:stepwise-coding` — a graph variant that models per-step parse, execute, review, and rework structure.
- `builtin:design` — a UI-heavy work path with a gated design/UX review before standard review and merge.

View File

@@ -149,9 +149,12 @@ The default built-in catalog entry `builtin:coding` is backed by the canonical `
<!--
FNXC:CompoundEngineering 2026-06-27-23:05:
FN-7149 changes the CE workflow note from HTML report-only-only to DOM-safe-or-report-only. Operators need to know markdown behavior is unchanged, while HTML writes are atomic, idempotent, DOM-validated, and refused back to report-only on any safety failure.
FNXC:CompoundEngineering 2026-06-28-09:03:
FN-7159 adds a canonical HTML checklist representation, allowing ce-doc-review to repair malformed HTML checklists through the same DOM-safe helper. Keep the operator-facing note explicit that checklist repair is canonical-shape-only and still refuses to report-only on any safety failure.
-->
`builtin:compound-engineering` is plugin-gated by `fusion-plugin-compound-engineering`. Its graph runs `/ce-plan` first and expects the CE plan document artifact under `docs/plans/`; an optional default-off `ce-doc-review` advisory step can then review plans without blocking merge. Markdown plans retain safe autofix and Append-to-Open-Questions behavior, while HTML plans support DOM-safe in-place mutation only after parse/anchor/visible-text/protected-region validation with atomic, idempotent writes; any safety failure falls back to report-only with no write. Implementation runs `/ce-work`, merge-blocking code review runs `/ce-code-review`, and the PR lane runs `/ce-commit-push-pr` then `/ce-resolve-pr-feedback` before Fusion's native merge seam. When project `autoMerge` is off, that merge seam no-ops into manual review instead of forcing an unattended board merge, so the CE-created pull request remains the human merge path.
`builtin:compound-engineering` is plugin-gated by `fusion-plugin-compound-engineering`. Its graph runs `/ce-plan` first and expects the CE plan document artifact under `docs/plans/`; an optional default-off `ce-doc-review` advisory step can then review plans without blocking merge. Markdown plans retain safe autofix and Append-to-Open-Questions behavior, while HTML plans support DOM-safe in-place mutation only after parse/anchor/visible-text/protected-region validation with atomic, idempotent writes. HTML checklist hygiene additionally supports canonical checklist repair when the helper proves the stable CE HTML checklist representation deterministically; any safety failure falls back to report-only with no write. Implementation runs `/ce-work`, merge-blocking code review runs `/ce-code-review`, and the PR lane runs `/ce-commit-push-pr` then `/ce-resolve-pr-feedback` before Fusion's native merge seam. When project `autoMerge` is off, that merge seam no-ops into manual review instead of forcing an unattended board merge, so the CE-created pull request remains the human merge path.
During triage/planning sessions, agents can call `fn_workflow_list` to discover available built-in and custom workflows and read their descriptions before routing work. They can call `fn_workflow_select` only when the user explicitly requested a workflow or when selecting a workflow for a task they created, and they can pass `workflow_id` when creating child tasks with `fn_task_create`; decision-only or investigation tasks can also set `noCommitsExpected` / `**No commits expected:** true` when no code changes are expected. The built-in triage thresholds, decision-only verb list, and default routing IDs are workflow-native typed settings resolved from the selected workflow.

View File

@@ -44,8 +44,10 @@ describe("ce-doc-review HTML report-only mode", () => {
expect(docReviewSkill).toMatch(/same document-quality and persona-lens checks/i);
expect(docReviewSkill).toMatch(/fall back to report-only \(`fixes_applied = 0`\)/i);
expect(docReviewSkill).toMatch(/Append-to-Open-Questions write-back that inserts markdown/i);
expect(docReviewSkill).toMatch(/Malformed-checklist HTML repair remains report-only/i);
expect(docReviewSkill).toMatch(/successful DOM-safe helper fixes may increment `fixes_applied` only for the allowlisted operations/i);
expect(docReviewSkill).toMatch(/malformed checklist repair to the canonical HTML checklist representation/i);
expect(docReviewSkill).toMatch(/including malformed-checklist repair when the helper can prove the canonical checklist form deterministically/i);
expect(docReviewSkill).toMatch(/Ambiguous or non-canonical checklist-like HTML that cannot be proven safe stays report-only/i);
expect(docReviewSkill).toMatch(/successful DOM-safe helper fixes may increment `fixes_applied` only for the allowlisted operations, including canonical checklist repair/i);
});
it("routes ce-plan HTML artifacts through report-only review instead of a skipped envelope", () => {

View File

@@ -160,7 +160,7 @@ describe("HTML mutation helper", () => {
expect(result).toMatchObject({ ok: false, fixesApplied: 0 });
});
it("refuses unsafe fragments and unsupported checklist repair without writing", () => {
it("refuses unsafe fragments and unknown checklist operations without writing", () => {
root = makeRepo();
const file = planPath(root);
writeFileSync(file, BASE_HTML);
@@ -213,6 +213,139 @@ describe("HTML mutation helper", () => {
expect(readdirSync(root).filter((entry) => entry.includes("html-mutation"))).toEqual([]);
});
it("leaves empty and populated canonical checklists unchanged", () => {
const canonical = '<!DOCTYPE html><html><head><title>Plan</title></head><body><main><section id="implementation-units"><h2>Implementation Units</h2><ul class="ce-checklist" aria-label="Checklist"></ul><ul class="ce-checklist" aria-label="Checklist"><li class="ce-checklist-item"><span class="ce-checklist-state">[ ]</span> <span class="ce-checklist-label">Unchecked</span></li><li class="ce-checklist-item"><span class="ce-checklist-state">[x]</span> <span class="ce-checklist-label">Checked</span></li></ul></section></main></body></html>';
const result = applyHtmlMutations(canonical, [{ type: "checklist-repair" }]);
expect(result).toEqual({ ok: true, html: canonical, fixesApplied: 0 });
});
it("repairs raw markdown checklist text to the canonical mixed-state shape", () => {
const html = '<!DOCTYPE html><html><head><title>Plan</title></head><body><main><section id="definition-of-done"><h2>Definition of Done</h2>\n- [ ] Ship alpha\n- [x] Verify beta\n- [X] Archive gamma\n</section></main></body></html>';
const result = applyHtmlMutations(html, [{ type: "checklist-repair" }]);
expect(result).toMatchObject({ ok: true, fixesApplied: 1 });
if (!result.ok) throw new Error(result.reason);
expect(result.html).toContain('<section id="definition-of-done"><h2>Definition of Done</h2><ul class="ce-checklist" aria-label="Checklist"><li class="ce-checklist-item"><span class="ce-checklist-state">[ ]</span> <span class="ce-checklist-label">Ship alpha</span></li><li class="ce-checklist-item"><span class="ce-checklist-state">[x]</span> <span class="ce-checklist-label">Verify beta</span></li><li class="ce-checklist-item"><span class="ce-checklist-state">[x]</span> <span class="ce-checklist-label">Archive gamma</span></li></ul></section>');
expect(result.html).not.toContain("- [ ] Ship alpha");
const second = applyHtmlMutations(result.html, [{ type: "checklist-repair" }]);
expect(second).toMatchObject({ ok: true, fixesApplied: 0, html: result.html });
});
it("repairs marker lists without adding, dropping, reordering, or rewording items", () => {
const html = '<!DOCTYPE html><html><head><title>Plan</title></head><body><main><section id="verification-contract"><h2>Verification Contract</h2><ol><li>[x] First &amp; ready</li><li>[ ] Second pending</li></ol></section></main></body></html>';
const result = applyHtmlMutations(html, [{ type: "checklist-repair" }]);
expect(result).toMatchObject({ ok: true, fixesApplied: 1 });
if (!result.ok) throw new Error(result.reason);
expect(result.html).toContain('<li class="ce-checklist-item"><span class="ce-checklist-state">[x]</span> <span class="ce-checklist-label">First &amp; ready</span></li><li class="ce-checklist-item"><span class="ce-checklist-state">[ ]</span> <span class="ce-checklist-label">Second pending</span></li>');
expect(result.html.indexOf("First &amp; ready")).toBeLessThan(result.html.indexOf("Second pending"));
});
it("repairs a single checked item to the canonical shape", () => {
const html = '<!DOCTYPE html><html><head><title>Plan</title></head><body><main><section id="definition-of-done"><h2>Definition of Done</h2><ul><li>[x] Sole task</li></ul></section></main></body></html>';
const result = applyHtmlMutations(html, [{ type: "checklist-repair" }]);
expect(result).toMatchObject({ ok: true, fixesApplied: 1 });
if (!result.ok) throw new Error(result.reason);
expect(result.html).toContain('<ul class="ce-checklist" aria-label="Checklist"><li class="ce-checklist-item"><span class="ce-checklist-state">[x]</span> <span class="ce-checklist-label">Sole task</span></li></ul>');
});
it("repairs all-unchecked and all-checked checklists without changing state", () => {
const unchecked = '<!DOCTYPE html><html><head><title>Plan</title></head><body><main><section id="verification-contract"><h2>Verification Contract</h2><ul><li>[ ] First</li><li>[ ] Second</li></ul></section></main></body></html>';
const checked = '<!DOCTYPE html><html><head><title>Plan</title></head><body><main><section id="verification-contract"><h2>Verification Contract</h2><ul><li>[x] First</li><li>[X] Second</li></ul></section></main></body></html>';
const uncheckedResult = applyHtmlMutations(unchecked, [{ type: "checklist-repair" }]);
const checkedResult = applyHtmlMutations(checked, [{ type: "checklist-repair" }]);
expect(uncheckedResult).toMatchObject({ ok: true, fixesApplied: 1 });
expect(checkedResult).toMatchObject({ ok: true, fixesApplied: 1 });
if (!uncheckedResult.ok) throw new Error(uncheckedResult.reason);
if (!checkedResult.ok) throw new Error(checkedResult.reason);
expect(uncheckedResult.html.match(/ce-checklist-state">\[ \]/g)).toHaveLength(2);
expect(uncheckedResult.html).not.toContain('ce-checklist-state">[x]');
expect(checkedResult.html.match(/ce-checklist-state">\[x\]/g)).toHaveLength(2);
expect(checkedResult.html).not.toContain('ce-checklist-state">[ ]');
});
it("repairs input-checkbox lists while preserving checked state and label text", () => {
const html = '<!DOCTYPE html><html><head><title>Plan</title></head><body><main><section id="implementation-units"><h2>Implementation Units</h2><ul><li><input type="checkbox" checked=""> Build one</li><li><input type="checkbox"> Test two</li></ul></section></main></body></html>';
const result = applyHtmlMutations(html, [{ type: "checklist-repair" }]);
expect(result).toMatchObject({ ok: true, fixesApplied: 1 });
if (!result.ok) throw new Error(result.reason);
expect(result.html).toContain('<ul class="ce-checklist" aria-label="Checklist"><li class="ce-checklist-item"><span class="ce-checklist-state">[x]</span> <span class="ce-checklist-label">Build one</span></li><li class="ce-checklist-item"><span class="ce-checklist-state">[ ]</span> <span class="ce-checklist-label">Test two</span></li></ul>');
expect(result.html).not.toContain('<input type="checkbox"');
});
it("preserves protected regions and never turns label text into script or style markup", () => {
const html = '<!DOCTYPE html><html><head><title>Plan</title><style>.x{color:red}</style></head><body><main><section id="implementation-units"><h2>Implementation Units</h2><ul><li>[ ] Render &lt;script&gt; as text</li><li>[x] Render &lt;style&gt; as text</li></ul><pre>- [ ] code checklist stays raw</pre></section></main><script>const marker = "- [ ] untouched";</script></body></html>';
const result = applyHtmlMutations(html, [{ type: "checklist-repair" }]);
expect(result).toMatchObject({ ok: true, fixesApplied: 1 });
if (!result.ok) throw new Error(result.reason);
expect(result.html).toContain('<style>.x{color:red}</style>');
expect(result.html).toContain('<script>const marker = "- [ ] untouched";</script>');
expect(result.html).toContain('<pre>- [ ] code checklist stays raw</pre>');
expect(result.html).toContain('Render &lt;script&gt; as text');
expect(result.html).toContain('Render &lt;style&gt; as text');
expect(result.html).not.toContain('<span class="ce-checklist-label">Render <script>');
});
it.each([
["ordinary list", '<ul><li>First</li><li>Second</li></ul>', /no provable malformed checklist/i],
["partial marker list", '<ul><li>[ ] First</li><li>Second</li></ul>', /ambiguous/i],
["nested checklist list", '<ul><li><input type="checkbox" checked=""> Parent<ul><li><input type="checkbox"> Child</li></ul></li></ul>', /ambiguous/i],
["id-bearing checklist-like list", '<ul id="keep"><li>[ ] First</li></ul>', /ambiguous/i],
["aria-checked item", '<ul><li aria-checked="true">[ ] Ship</li></ul>', /ambiguous/i],
["role-bearing item", '<ul><li role="checkbox">[ ] Task</li></ul>', /ambiguous/i],
["class-bearing item", '<ul><li class="done">[ ] Task</li></ul>', /ambiguous/i],
["style-bearing item", '<ul><li style="display:none">[ ] Task</li></ul>', /ambiguous/i],
["conflicting input aria state", '<ul><li><input type="checkbox" checked="" aria-checked="false"> Ship</li></ul>', /ambiguous/i],
])("refuses %s with report-only semantics", (_name, fragment, reason) => {
const html = `<!DOCTYPE html><html><head><title>Plan</title></head><body><main><section id="verification-contract"><h2>Verification Contract</h2>${fragment}</section></main></body></html>`;
const result = applyHtmlMutations(html, [{ type: "checklist-repair" }]);
expect(result).toMatchObject({ ok: false, fixesApplied: 0 });
if (result.ok) throw new Error("expected checklist repair to be refused");
expect(result.reason).toMatch(reason);
});
it("refuses non-round-trip-stable checklist repair and leaves the file byte-identical", () => {
root = makeRepo();
const file = planPath(root);
const unstable = "<html><body><main><section>- [ ] Missing close";
writeFileSync(file, unstable);
const result = writeHtmlMutationsToFile(file, [{ type: "checklist-repair" }], { rootDir: root });
expect(result).toMatchObject({ ok: false, fixesApplied: 0 });
if (result.ok) throw new Error("expected unstable checklist HTML write to be refused");
expect(result.reason).toMatch(/round-trip stability/i);
expect(readFileSync(file, "utf8")).toBe(unstable);
});
it("rolls back checklist repair atomically when post-write validation fails", () => {
root = makeRepo();
const file = planPath(root);
const html = '<!DOCTYPE html><html><head><title>Plan</title></head><body><main><section id="definition-of-done"><h2>Definition of Done</h2><ul><li>[ ] Done one</li></ul></section></main></body></html>';
writeFileSync(file, html);
const result = writeHtmlMutationsToFile(file, [{ type: "checklist-repair" }], { rootDir: root, validateWrittenHtml: () => false });
expect(result).toMatchObject({ ok: false, fixesApplied: 0 });
expect(readFileSync(file, "utf8")).toBe(html);
expect(readdirSync(root).filter((entry) => entry.includes("html-mutation"))).toEqual([]);
});
it("rejects symlink artifact targets", () => {
root = makeRepo();
const real = join(root, "real.html");

View File

@@ -1,7 +1,7 @@
import { lstatSync, readFileSync, realpathSync, renameSync, rmSync, writeFileSync } from "node:fs";
import { basename, dirname, isAbsolute, join, relative } from "node:path";
import { randomUUID } from "node:crypto";
import { parse, parseFragment, serialize, serializeOuter, type DefaultTreeAdapterTypes } from "parse5";
import { html, parse, parseFragment, serialize, serializeOuter, type DefaultTreeAdapterTypes } from "parse5";
const MAX_ARTIFACT_BYTES = 2_000_000;
const STABLE_SECTION_IDS = new Set([
@@ -43,6 +43,19 @@ const SAFE_OPEN_QUESTION_TAGS = new Set([
]);
const SAFE_OPEN_QUESTION_GLOBAL_ATTRS = new Set(["aria-label", "title"]);
const SAFE_OPEN_QUESTION_ATTRS = new Map<string, ReadonlySet<string>>([["a", new Set(["href", "title", "aria-label"])] as const]);
const CHECKLIST_REPAIR_BLOCK_TAGS = new Set([
"article",
"blockquote",
"details",
"div",
"dl",
"figure",
"ol",
"p",
"section",
"table",
"ul",
]);
type Document = DefaultTreeAdapterTypes.Document;
type DocumentFragment = DefaultTreeAdapterTypes.DocumentFragment;
@@ -53,6 +66,7 @@ type ParentNode = DefaultTreeAdapterTypes.ParentNode;
export type HtmlMutationOperation =
| { type: "append-open-question"; itemHtml: string }
| { type: "checklist-repair" }
| { type: "repair-heading-depth"; anchorId: string; fromLevel: 1 | 2 | 3 | 4 | 5 | 6; toLevel: 1 | 2 | 3 | 4 | 5 | 6 }
| { type: "normalize-duplicate-inter-block-whitespace" }
| { type: "replace-visible-text"; from: string; to: string; anchorId?: string };
@@ -191,6 +205,8 @@ function applySingleOperation(
switch (operation.type) {
case "append-open-question":
return appendOpenQuestion(document, operation.itemHtml);
case "checklist-repair":
return repairChecklists(document);
case "repair-heading-depth":
return repairHeadingDepth(document, operation);
case "normalize-duplicate-inter-block-whitespace":
@@ -222,6 +238,170 @@ function appendOpenQuestion(document: Document, itemHtml: string): { ok: true; a
return { ok: true, applied: true };
}
/**
* FNXC:CompoundEngineering 2026-06-28-08:41:
* FN-7159 permits HTML checklist write-back only after the CE rendering contract defines one canonical source-readable shape. Repair is limited to provable markdown-marker/list/input-checkbox variants, rejects ambiguous lists and subtrees with hidden/state-bearing attributes, and delegates round-trip, protected-region, visible-text, atomic-write, and rollback safety to the shared helper contract.
*/
function repairChecklists(document: Document): { ok: true; applied: boolean; expectedVisibleText?: string } | HtmlMutationRefusal {
const repairs: Array<{ target: ChildNode; items: ChecklistItem[] }> = [];
let sawCanonical = false;
let sawAmbiguous = false;
walkNodes(document, (node, ancestors) => {
if (isInsideProtectedOrRawText(ancestors)) return;
if (isText(node)) {
const parsed = parseRawMarkdownChecklistText(node.value);
if (parsed.kind === "repair") repairs.push({ target: node, items: parsed.items });
if (parsed.kind === "ambiguous") sawAmbiguous = true;
return;
}
if (!isElement(node) || (node.tagName !== "ul" && node.tagName !== "ol")) return;
if (isCanonicalChecklist(node)) {
sawCanonical = true;
return;
}
const parsed = parseMalformedChecklistList(node);
if (parsed.kind === "repair") repairs.push({ target: node, items: parsed.items });
if (parsed.kind === "ambiguous") sawAmbiguous = true;
});
if (sawAmbiguous) return refusal("checklist repair found ambiguous or unsafe checklist-like HTML");
if (repairs.length === 0) {
return sawCanonical ? { ok: true, applied: false } : refusal("no provable malformed checklist found");
}
for (const repair of repairs) {
const parent = repair.target.parentNode;
if (!parent || !("childNodes" in parent)) return refusal("checklist repair target has no mutable parent");
const index = parent.childNodes.indexOf(repair.target);
if (index < 0) return refusal("checklist repair target is not attached to its parent");
const replacement = makeCanonicalChecklist(repair.items);
replacement.parentNode = parent;
parent.childNodes[index] = replacement;
}
return { ok: true, applied: true, expectedVisibleText: getVisibleText(document) };
}
interface ChecklistItem {
checked: boolean;
label: string;
}
type ChecklistParseResult = { kind: "none" } | { kind: "ambiguous" } | { kind: "repair"; items: ChecklistItem[] };
function parseRawMarkdownChecklistText(value: string): ChecklistParseResult {
if (!/[\r\n]?\s*-\s*\[[ xX]\]/.test(value)) return { kind: "none" };
const lines = value.split(/\r?\n/);
const items: ChecklistItem[] = [];
for (const line of lines) {
if (line.trim() === "") continue;
const match = /^\s*-\s*\[([ xX])\]\s+(.+?)\s*$/.exec(line);
if (!match) return { kind: "ambiguous" };
items.push({ checked: match[1].toLowerCase() === "x", label: match[2] });
}
return items.length > 0 ? { kind: "repair", items } : { kind: "none" };
}
function parseMalformedChecklistList(list: Element): ChecklistParseResult {
if (hasUnsafeChecklistSubtree(list)) return looksChecklistLike(list) ? { kind: "ambiguous" } : { kind: "none" };
const items = list.childNodes.filter(isElement);
if (items.length === 0) return { kind: "none" };
if (items.some((item) => item.tagName !== "li") || list.childNodes.some((child) => !isWhitespaceText(child) && !isElement(child))) {
return looksChecklistLike(list) ? { kind: "ambiguous" } : { kind: "none" };
}
if (items.some(hasNestedChecklistRepairBlock)) return looksChecklistLike(list) ? { kind: "ambiguous" } : { kind: "none" };
const markerItems = items.map(parseMarkerListItem);
if (markerItems.every((item): item is ChecklistItem => Boolean(item))) return { kind: "repair", items: markerItems };
if (markerItems.some(Boolean)) return { kind: "ambiguous" };
const inputItems = items.map(parseInputCheckboxListItem);
if (inputItems.every((item): item is ChecklistItem => Boolean(item))) return { kind: "repair", items: inputItems };
if (inputItems.some(Boolean)) return { kind: "ambiguous" };
return { kind: "none" };
}
function hasNestedChecklistRepairBlock(item: Element): boolean {
return findElements(item, (element) => element !== item && CHECKLIST_REPAIR_BLOCK_TAGS.has(element.tagName)).length > 0;
}
function parseMarkerListItem(item: Element): ChecklistItem | null {
const visible = getVisibleText(item);
const match = /^\[([ xX])\]\s+(.+?)\s*$/.exec(visible);
if (!match) return null;
return { checked: match[1].toLowerCase() === "x", label: match[2] };
}
function parseInputCheckboxListItem(item: Element): ChecklistItem | null {
const first = item.childNodes.find((child) => !isWhitespaceText(child));
if (!first || !isElement(first) || first.tagName !== "input" || getAttr(first, "type")?.toLowerCase() !== "checkbox") return null;
const label = getVisibleText(item).trim();
if (!label) return null;
return { checked: hasAttr(first, "checked"), label };
}
function looksChecklistLike(root: ParentNode): boolean {
return /\[[ xX]\]|type=["']?checkbox/i.test(serialize(root));
}
function hasUnsafeChecklistSubtree(root: Element): boolean {
return findElements(root, (element) => {
if (PROTECTED_TAGS.has(element.tagName)) return true;
if (getAttr(element, "id")) return true;
return element.attrs.some((attr) => {
const name = attr.name.toLowerCase();
return name.startsWith("data-") || name.startsWith("on") || name.startsWith("aria-") || name === "role" || name === "class" || name === "style";
});
}).length > 0;
}
function isCanonicalChecklist(list: Element): boolean {
if (list.tagName !== "ul" || getAttr(list, "class") !== "ce-checklist" || getAttr(list, "aria-label") !== "Checklist") return false;
for (const child of list.childNodes) {
if (isWhitespaceText(child)) continue;
if (!isElement(child) || child.tagName !== "li" || getAttr(child, "class") !== "ce-checklist-item") return false;
const semantic = child.childNodes.filter((itemChild) => !isWhitespaceText(itemChild) || (isText(itemChild) && itemChild.value === " "));
if (semantic.length !== 3) return false;
const [state, spacer, label] = semantic;
if (!isElement(state) || state.tagName !== "span" || getAttr(state, "class") !== "ce-checklist-state") return false;
if (getVisibleText(state) !== "[ ]" && getVisibleText(state) !== "[x]") return false;
if (!isText(spacer) || spacer.value !== " ") return false;
if (!isElement(label) || label.tagName !== "span" || getAttr(label, "class") !== "ce-checklist-label" || !getVisibleText(label)) {
return false;
}
}
return true;
}
function makeCanonicalChecklist(items: readonly ChecklistItem[]): Element {
return makeElement(
"ul",
[
{ name: "class", value: "ce-checklist" },
{ name: "aria-label", value: "Checklist" },
],
items.map((item) =>
makeElement("li", [{ name: "class", value: "ce-checklist-item" }], [
makeElement("span", [{ name: "class", value: "ce-checklist-state" }], [makeText(item.checked ? "[x]" : "[ ]")]),
makeText(" "),
makeElement("span", [{ name: "class", value: "ce-checklist-label" }], [makeText(item.label)]),
]),
),
);
}
function makeElement(tagName: string, attrs: Element["attrs"], childNodes: ChildNode[] = []): Element {
const element: Element = { nodeName: tagName, tagName, attrs, namespaceURI: html.NS.HTML, childNodes, parentNode: null };
for (const child of childNodes) child.parentNode = element;
return element;
}
function makeText(value: string): TextNode {
return { nodeName: "#text", value, parentNode: null };
}
function repairHeadingDepth(
document: Document,
operation: Extract<HtmlMutationOperation, { type: "repair-heading-depth" }>,
@@ -510,6 +690,10 @@ function getAttr(element: Element, name: string): string | undefined {
return element.attrs.find((attr) => attr.name === name)?.value;
}
function hasAttr(element: Element, name: string): boolean {
return element.attrs.some((attr) => attr.name === name);
}
function countOccurrences(text: string, needle: string): number {
return text.split(needle).length - 1;
}

View File

@@ -586,6 +586,37 @@ looks identical. Compose so semantic understanding is reachable in source:
label scheme, and the field-label vocabulary do not break across
versions. Visual styling can change freely.
### Canonical checklist representation
<!--
FNXC:CompoundEngineering 2026-06-28-08:30:
FN-7159 defines one stable HTML checklist shape so ce-doc-review can DOM-safely repair malformed checklist markup instead of leaving all HTML checklist hygiene report-only. The checked state must be visible text, not an attribute-only fact, because downstream CE consumers read source text as the public API.
-->
Use exactly this shape for checklist content in CE HTML artifacts:
```html
<ul class="ce-checklist" aria-label="Checklist">
<li class="ce-checklist-item"><span class="ce-checklist-state">[ ]</span> <span class="ce-checklist-label">Unchecked item label</span></li>
<li class="ce-checklist-item"><span class="ce-checklist-state">[x]</span> <span class="ce-checklist-label">Checked item label</span></li>
</ul>
```
- The container is a `<ul class="ce-checklist" aria-label="Checklist">`. An empty checklist is this same container with zero `<li>` children; do not invent an alternate empty-state wrapper.
- Each item is exactly one direct `<li class="ce-checklist-item">` child containing exactly two visible spans in order: `<span class="ce-checklist-state">[ ]</span>` or `<span class="ce-checklist-state">[x]</span>`, then one ASCII space text node, then `<span class="ce-checklist-label">…</span>`.
- The checked state is the visible text `[ ]` for unchecked or `[x]` for checked. Attribute-only state (`checked`, `aria-checked`, `data-checked`, CSS-generated checkmarks, icon-only checkmarks) is not canonical because semantic state must be readable in source text.
- The label span contains the item label as visible text. Do not move label meaning into `title`, `aria-label`, `data-*`, SVG, or CSS. Inline semantic text markup inside the label is allowed only when it preserves the same visible words and does not carry checklist state.
- Mixed-state checklists are represented by mixing `[ ]` and `[x]` item state spans. The item order is document order and is stable.
- Already-canonical checklists are not rewritten. This exact element/class/state vocabulary is the public API for downstream consumers and DOM-safe repair; visual CSS may style it, but structure and visible state markers must remain stable across versions.
For `ce-doc-review` DOM-safe repair, a checklist is **provable** only when the existing HTML is already this canonical shape or one of these unambiguous malformed variants whose canonical form can be derived without judgment:
- Raw markdown checklist lines left as visible HTML body text, using `- [ ] label`, `- [x] label`, or `- [X] label` on one or more consecutive lines.
- A `<ul>` or `<ol>` whose every direct `<li>` begins with visible `[ ]`, `[x]`, or `[X]` text followed by the item label.
- A `<ul>` or `<ol>` whose every direct `<li>` has an `<input type="checkbox">` as its first non-whitespace child, with `checked` preserving checked state and the remaining visible text preserving the label.
Anything else is malformed-or-ambiguous and stays report-only: ordinary lists, partial checklist lists, icon-only state, labels split across unrelated DOM regions, mixed checklist/non-checklist children, missing labels, duplicate hidden state copies that disagree with visible state, or content whose parse/serialize round trip is unstable. Repair may normalize only structure and visible state markers; it must not add, drop, reorder, or reword items.
## Post-compose audit
Before returning the artifact, scan it for common slips:

View File

@@ -11,6 +11,9 @@ Review a Compound Engineering markdown or HTML plan/requirements document. Markd
<!--
FNXC:CompoundEngineering 2026-06-27-00:00:
FN-7147 requires HTML artifacts to receive the same persona-lens document review as markdown artifacts while suppressing markdown-based in-file mutation. FN-7149 permits only DOM-safe HTML mutations after parse5 round-trip, anchor, protected-region, visible-text, and atomic-write validation; any safety failure preserves the report-only path because markdown edit mechanics would corrupt rendered HTML plans.
FNXC:CompoundEngineering 2026-06-28-08:55:
FN-7159 defines a canonical CE HTML checklist shape, so malformed checklist hygiene may use the DOM-safe checklist-repair operation. Keep every refusal or validation failure report-only with fixes_applied = 0 because ambiguous checklist markup is still unsafe to rewrite.
-->
## Modes
@@ -31,7 +34,7 @@ FN-7147 requires HTML artifacts to receive the same persona-lens document review
1. Resolve the target path from the arguments. Prefer an explicit path. If no path is provided, inspect `docs/plans/` for the most recent markdown (`.md`) or HTML (`.html`) plan-like artifact and use that; if none exists, skip non-blockingly.
2. Classify the target type:
- Markdown (`.md`): review with markdown-safe mutation enabled for `safe_auto` fixes only.
- HTML (`.html`): review with DOM-safe mutation enabled only for the FN-7149 allowlist: append to an existing Open/Outstanding Questions list, provable stable-registry heading-depth repair, duplicate inter-block whitespace normalization, and exact visible-prose typo text-node fixes. Run the same document-quality and persona-lens checks, preserve classifications, and fall back to report-only with `fixes_applied`/`applied_fixes_count` set to `0` whenever the helper refuses. Do not run markdown `safe_auto` writes, `gated_auto`/`manual` apply-set edits, or Append-to-Open-Questions write-back that inserts markdown `##`/`###` headings.
- HTML (`.html`): review with DOM-safe mutation enabled only for the FN-7149/FN-7159 allowlist: append to an existing Open/Outstanding Questions list, provable stable-registry heading-depth repair, duplicate inter-block whitespace normalization, exact visible-prose typo text-node fixes, and malformed checklist repair to the canonical HTML checklist representation. Run the same document-quality and persona-lens checks, preserve classifications, and fall back to report-only with `fixes_applied`/`applied_fixes_count` set to `0` whenever the helper refuses. Do not run markdown `safe_auto` writes, `gated_auto`/`manual` apply-set edits, or Append-to-Open-Questions write-back that inserts markdown `##`/`###` headings.
- Any other type: skip non-blockingly and explain that only markdown and HTML CE plan/requirements artifacts are supported.
3. Read the document enough to evaluate structure and consistency. For long documents, scan headings first, then read the Goal Capsule/Product Contract/Plan/Implementation Units/Verification/Definition of Done sections as present. For HTML, use the rendered document text/semantic headings as review input; never rewrite tags with markdown text edits, inject markdown, or mutate outside the DOM-safe helper.
4. Check for:
@@ -39,15 +42,15 @@ FN-7147 requires HTML artifacts to receive the same persona-lens document review
- HOW gaps: implementation units lacking files, dependencies, risks, or verification scenarios.
- Coherence gaps: contradictory decisions, stale handoff instructions, duplicated or inconsistent artifact readiness metadata.
- Feasibility gaps: sequencing that cannot work, missing prerequisite decisions, or verification that cannot prove the stated Definition of Done.
- Hygiene that is safe to fix automatically in the target format: markdown-only broken heading levels, duplicate blank lines, malformed checklists, or typo-level wording for markdown; for HTML, only the four DOM-safe helper operations may apply. Malformed-checklist HTML repair remains report-only until CE defines a canonical HTML checklist representation.
5. In headless mode, apply only `safe_auto` fixes directly to markdown files. Do not apply changes that alter product scope, technical decisions, acceptance criteria, or verification obligations; report those as findings. In HTML mode, call the DOM-safe helper only for allowlisted operations; on any round-trip, anchor, protected-region, visible-text, validation, unsupported-operation, or write failure, apply nothing and return the review envelope with `fixes_applied = 0` and classifications intact.
- Hygiene that is safe to fix automatically in the target format: markdown-only broken heading levels, duplicate blank lines, malformed checklists, or typo-level wording for markdown; for HTML, only the DOM-safe helper operations may apply, including malformed-checklist repair when the helper can prove the canonical checklist form deterministically. Ambiguous or non-canonical checklist-like HTML that cannot be proven safe stays report-only.
5. In headless mode, apply only `safe_auto` fixes directly to markdown files. Do not apply changes that alter product scope, technical decisions, acceptance criteria, or verification obligations; report those as findings. In HTML mode, call the DOM-safe helper only for allowlisted operations; on any round-trip, anchor, protected-region, visible-text, checklist-equivalence, validation, unsupported-operation, or write failure, apply nothing and return the review envelope with `fixes_applied = 0` and classifications intact.
6. If findings remain, classify each as:
- `proposed_fix`: a safe but non-trivial improvement the user may accept.
- `decision`: a scope/technical judgment that needs human or planner choice.
- `fyi`: useful observation that does not need routing.
7. Interactive routing:
- Markdown: the caller may choose whether to apply proposed fixes, route decisions, or append unresolved items to Open Questions.
- HTML: skip apply and Append-to-Open-Questions choices entirely. Present the findings as a report-only summary, recommend regenerating markdown only if the user wants in-file autofix/write-back, and leave the HTML artifact unchanged.
- HTML: skip markdown apply and markdown Append-to-Open-Questions choices entirely. Present findings with DOM-safe mutation status; checklist repair may write only the canonical HTML checklist representation after helper validation, and every unsupported or unsafe finding remains report-only with the HTML artifact unchanged.
8. End with a concise summary and exactly one trailing JSON object on the final line.
## Output contract
@@ -62,6 +65,6 @@ Use this shape for the final line:
- `APPROVE_WITH_NOTES`: non-blocking observations or report-only HTML findings; this is the normal result for optional workflow use.
- `REVISE`: only for severe document issues that make downstream work unsafe or impossible. In the Fusion built-in workflow this skill is advisory/non-blocking, but the verdict still helps humans see severity.
For HTML reviews, the JSON must still be emitted. When DOM-safe mutation is refused or unsupported, `fixes_applied` must be `0`, and notes should mention that HTML fell back to report-only without autofix; successful DOM-safe helper fixes may increment `fixes_applied` only for the allowlisted operations.
For HTML reviews, the JSON must still be emitted. When DOM-safe mutation is refused or unsupported, `fixes_applied` must be `0`, and notes should mention that HTML fell back to report-only without autofix; successful DOM-safe helper fixes may increment `fixes_applied` only for the allowlisted operations, including canonical checklist repair.
Do not wrap the final JSON in markdown fences.

View File

@@ -586,6 +586,37 @@ looks identical. Compose so semantic understanding is reachable in source:
label scheme, and the field-label vocabulary do not break across
versions. Visual styling can change freely.
### Canonical checklist representation
<!--
FNXC:CompoundEngineering 2026-06-28-08:30:
FN-7159 defines one stable HTML checklist shape so ce-doc-review can DOM-safely repair malformed checklist markup instead of leaving all HTML checklist hygiene report-only. The checked state must be visible text, not an attribute-only fact, because downstream CE consumers read source text as the public API.
-->
Use exactly this shape for checklist content in CE HTML artifacts:
```html
<ul class="ce-checklist" aria-label="Checklist">
<li class="ce-checklist-item"><span class="ce-checklist-state">[ ]</span> <span class="ce-checklist-label">Unchecked item label</span></li>
<li class="ce-checklist-item"><span class="ce-checklist-state">[x]</span> <span class="ce-checklist-label">Checked item label</span></li>
</ul>
```
- The container is a `<ul class="ce-checklist" aria-label="Checklist">`. An empty checklist is this same container with zero `<li>` children; do not invent an alternate empty-state wrapper.
- Each item is exactly one direct `<li class="ce-checklist-item">` child containing exactly two visible spans in order: `<span class="ce-checklist-state">[ ]</span>` or `<span class="ce-checklist-state">[x]</span>`, then one ASCII space text node, then `<span class="ce-checklist-label">…</span>`.
- The checked state is the visible text `[ ]` for unchecked or `[x]` for checked. Attribute-only state (`checked`, `aria-checked`, `data-checked`, CSS-generated checkmarks, icon-only checkmarks) is not canonical because semantic state must be readable in source text.
- The label span contains the item label as visible text. Do not move label meaning into `title`, `aria-label`, `data-*`, SVG, or CSS. Inline semantic text markup inside the label is allowed only when it preserves the same visible words and does not carry checklist state.
- Mixed-state checklists are represented by mixing `[ ]` and `[x]` item state spans. The item order is document order and is stable.
- Already-canonical checklists are not rewritten. This exact element/class/state vocabulary is the public API for downstream consumers and DOM-safe repair; visual CSS may style it, but structure and visible state markers must remain stable across versions.
For `ce-doc-review` DOM-safe repair, a checklist is **provable** only when the existing HTML is already this canonical shape or one of these unambiguous malformed variants whose canonical form can be derived without judgment:
- Raw markdown checklist lines left as visible HTML body text, using `- [ ] label`, `- [x] label`, or `- [X] label` on one or more consecutive lines.
- A `<ul>` or `<ol>` whose every direct `<li>` begins with visible `[ ]`, `[x]`, or `[X]` text followed by the item label.
- A `<ul>` or `<ol>` whose every direct `<li>` has an `<input type="checkbox">` as its first non-whitespace child, with `checked` preserving checked state and the remaining visible text preserving the label.
Anything else is malformed-or-ambiguous and stays report-only: ordinary lists, partial checklist lists, icon-only state, labels split across unrelated DOM regions, mixed checklist/non-checklist children, missing labels, duplicate hidden state copies that disagree with visible state, or content whose parse/serialize round trip is unstable. Repair may normalize only structure and visible state markers; it must not add, drop, reorder, or reword items.
## Post-compose audit
Before returning the artifact, scan it for common slips:

View File

@@ -592,6 +592,37 @@ looks identical. Compose so semantic understanding is reachable in source:
label scheme, and the field-label vocabulary do not break across
versions. Visual styling can change freely.
### Canonical checklist representation
<!--
FNXC:CompoundEngineering 2026-06-28-08:30:
FN-7159 defines one stable HTML checklist shape so ce-doc-review can DOM-safely repair malformed checklist markup instead of leaving all HTML checklist hygiene report-only. The checked state must be visible text, not an attribute-only fact, because downstream CE consumers read source text as the public API.
-->
Use exactly this shape for checklist content in CE HTML artifacts:
```html
<ul class="ce-checklist" aria-label="Checklist">
<li class="ce-checklist-item"><span class="ce-checklist-state">[ ]</span> <span class="ce-checklist-label">Unchecked item label</span></li>
<li class="ce-checklist-item"><span class="ce-checklist-state">[x]</span> <span class="ce-checklist-label">Checked item label</span></li>
</ul>
```
- The container is a `<ul class="ce-checklist" aria-label="Checklist">`. An empty checklist is this same container with zero `<li>` children; do not invent an alternate empty-state wrapper.
- Each item is exactly one direct `<li class="ce-checklist-item">` child containing exactly two visible spans in order: `<span class="ce-checklist-state">[ ]</span>` or `<span class="ce-checklist-state">[x]</span>`, then one ASCII space text node, then `<span class="ce-checklist-label">…</span>`.
- The checked state is the visible text `[ ]` for unchecked or `[x]` for checked. Attribute-only state (`checked`, `aria-checked`, `data-checked`, CSS-generated checkmarks, icon-only checkmarks) is not canonical because semantic state must be readable in source text.
- The label span contains the item label as visible text. Do not move label meaning into `title`, `aria-label`, `data-*`, SVG, or CSS. Inline semantic text markup inside the label is allowed only when it preserves the same visible words and does not carry checklist state.
- Mixed-state checklists are represented by mixing `[ ]` and `[x]` item state spans. The item order is document order and is stable.
- Already-canonical checklists are not rewritten. This exact element/class/state vocabulary is the public API for downstream consumers and DOM-safe repair; visual CSS may style it, but structure and visible state markers must remain stable across versions.
For `ce-doc-review` DOM-safe repair, a checklist is **provable** only when the existing HTML is already this canonical shape or one of these unambiguous malformed variants whose canonical form can be derived without judgment:
- Raw markdown checklist lines left as visible HTML body text, using `- [ ] label`, `- [x] label`, or `- [X] label` on one or more consecutive lines.
- A `<ul>` or `<ol>` whose every direct `<li>` begins with visible `[ ]`, `[x]`, or `[X]` text followed by the item label.
- A `<ul>` or `<ol>` whose every direct `<li>` has an `<input type="checkbox">` as its first non-whitespace child, with `checked` preserving checked state and the remaining visible text preserving the label.
Anything else is malformed-or-ambiguous and stays report-only: ordinary lists, partial checklist lists, icon-only state, labels split across unrelated DOM regions, mixed checklist/non-checklist children, missing labels, duplicate hidden state copies that disagree with visible state, or content whose parse/serialize round trip is unstable. Repair may normalize only structure and visible state markers; it must not add, drop, reorder, or reword items.
## Post-compose audit
Before returning the artifact, scan it for common slips: