Summary
Residual of the #27 fail-closed structural gate (which shipped in #29). The gate rejects the concrete #27 failure modes — pure prose (0 elements), code fences, and bare preamble text as a top-level sibling — but a model that wraps its entire conversational answer in a single element still clears the floor and finalizes.
The gate (html-export-sanitize.ts, behind requireStructuralDocument) requires:
bodyElementCount >= HTML_MIN_BODY_ELEMENT_NODES (floor = 1), and
- no non-whitespace top-level text node (HTML-ASCII-whitespace predicate).
So Creating a landscape… (pure prose) is rejected, and Sure:<h1>…</h1> (top-level text sibling) is rejected, but:
<div>Creating a landscape document about … Here is your export:</div>
has exactly one element and no top-level text node → passes sanitize → quarantine → finalize, saving the narration as the "export".
Why it is hard (accepted design boundary today)
A single <div>…prose…</div> is structurally indistinguishable from a legitimate intro paragraph / lede that a real document may open with. Hard-rejecting single-element-with-only-text bodies risks false-positives on valid short documents. The current mitigation is the direct-prompt rider forbidding conversational preamble/narration (bare or element-wrapped), plus the top-level-text and element-floor checks. Architect + Grok-QA review classified the element-wrapped case as an accepted residual, not a security hole (no XSS/containment impact — it is a content-quality/trust issue in the family of #27).
Options to consider
- Heuristic content-identity signal (e.g. reject a body that is a single block element whose only content is text and whose text matches narration markers) — risks false-positives; needs a careful corpus.
- Provider-side: ensure the Grok/Claude CLI HTML surface disables tool/plan narration so the model cannot emit a chat wrapper in the first place (verify
--disallowed-tools / prompt on current CLI versions).
- Accept as-is and rely on the prompt rider + the existing floor/top-level-text checks (current state).
References
Summary
Residual of the #27 fail-closed structural gate (which shipped in #29). The gate rejects the concrete #27 failure modes — pure prose (0 elements), code fences, and bare preamble text as a top-level sibling — but a model that wraps its entire conversational answer in a single element still clears the floor and finalizes.
The gate (
html-export-sanitize.ts, behindrequireStructuralDocument) requires:bodyElementCount >= HTML_MIN_BODY_ELEMENT_NODES(floor = 1), andSo
Creating a landscape…(pure prose) is rejected, andSure:<h1>…</h1>(top-level text sibling) is rejected, but:has exactly one element and no top-level text node → passes sanitize → quarantine → finalize, saving the narration as the "export".
Why it is hard (accepted design boundary today)
A single
<div>…prose…</div>is structurally indistinguishable from a legitimate intro paragraph / lede that a real document may open with. Hard-rejecting single-element-with-only-text bodies risks false-positives on valid short documents. The current mitigation is the direct-prompt rider forbidding conversational preamble/narration (bare or element-wrapped), plus the top-level-text and element-floor checks. Architect + Grok-QA review classified the element-wrapped case as an accepted residual, not a security hole (no XSS/containment impact — it is a content-quality/trust issue in the family of #27).Options to consider
--disallowed-tools/ prompt on current CLI versions).References
src/main/html-export-sanitize.ts(requireStructuralDocument,HTML_MIN_BODY_ELEMENT_NODES, top-level HTML-whitespace check); pipeline wiringhtml-export-pipeline-service.ts.html-export-direct-prompt.ts.