Context
The markdown-VDOM lane from Epic #13012's plan-of-record (lane claimed via A2A 2026-06-12T20:57Z after the targeted lead-role request; operator named this peer for deep involvement). Resurrects the #8920 premise (Neo.component.markdown.VDom — VDOM-native parsing; closed stale/not_planned 2026-05-16, never implemented) as a fresh Fat Ticket per the prevent-reopen discipline — the harness transcript surface makes the premise load-bearing: agent/LLM responses are streaming markdown, and the renderer is on the hot path of every conversation turn.
First consumer: the harness transcript/activity surfaces (#13012 PoC→MVP staging names "markdown component v1 landing for activity/log surfaces"). Framework-level primitive — serves portal/docs later, but transcript-first scoping governs v1.
Release classification: post-release (harness product line; no v13.x blocking work).
The Problem
The current src/component/Markdown.mjs is structurally wrong for streaming, in three independent ways:
- Full re-parse + whole-blob replacement per value change (
afterSetValue → destroyComponents() + render() → marked.parse(content) → me.set({html})): every appended chunk re-parses the entire document and replaces the entire rendered tree. For an LLM response streaming N chunks this is O(N²) parse work and full DOM trashing per chunk — #8920's "avoids full DOM trashing on every character append" benefit, inverted.
- HTML-string rendering (the
html config = innerHTML semantics): the output never exists as a vdom tree, so the delta engine cannot do fine-grained patching — and the renderer is an XSS surface. The existing beforeSetValue guard only rejects accidental full HTML documents (404 pages), not hostile fragments. Acceptable for repo-authored docs; wrong default for transcripts rendering untrusted agent/LLM output (the hostile-content class the swarm has already been hit with, and the trust-tier boundary vega's containment work draws).
- Identity instability: even if the blob were diffed, nothing assigns stable identities to rendered blocks — the differ would see wholesale change. Stable per-block vdom ids are what make streaming cheap: the differ accumulates in-place changes into
updateNode and sequential appends ride DeltaUpdates' insertNode-batching optimization (sequential parentId+index appends batch natively).
The Architectural Reality
- VDOM primitives: vnode trees are plain
{tag/nodeName, id, cn, vtype} structures (src/vdom/VNode.mjs: vtype: 'vnode' | 'text'); a parser emitting these is renderer-agnostic by construction.
- The differ rewards id stability: unchanged subtrees with identical ids produce zero deltas; in-place changes accumulate into action-less
updateNode; src/main/DeltaUpdates.mjs#update() batches sequential same-parent inserts. A streaming append with a stable settled-prefix therefore costs exactly one insert-only tail batch.
- The delta-contract instruments exist NOW (the
#12986 arc): test/playwright/util/DeltaCapture.mjs (merged) gives epoch-windowed batch capture + kernel-vocabulary op counts — the executable form of this ticket's streaming ACs. The coherence registry (PR #13016, in review) flags two-nodes-one-id / retired-target incoherence live: a streaming incremental parser with id-reuse logic is exactly the producer class that motivated it, so this lane develops with useDeltaCoherenceRegistry: true as its dev harness and dogfoods the contract layer.
- The existing pipeline is load-bearing elsewhere:
Markdown.mjs's multi-pass embedded-block machinery (live-preview, mermaid, neo-component JSON blocks, HighlightJs <pre> injection) carries the portal/docs surfaces. It must keep working untouched — the migration boundary below is deliberate.
- Realm-pure parser = SSG-consumable: a parser with no
Neo globals and no DOM access (the src/vdom/util/DeltaGrammar.mjs plain-module discipline) is importable from the App Worker, Node unit tests, AND the Node-side SSR/SSG pipeline — markdown→vdom prerendering becomes available to the middleware for free.
The Fix
New component family at src/component/markdown/ (structural pre-flight, Stage-1 fast-path: src/component/ subfolder precedent exists — mwc/, wrapper/; #8920's own namespace was Neo.component.markdown.*; implementer re-runs the 30-second check at branch time):
src/component/markdown/Parser.mjs — plain module (no class lifecycle, no Neo globals; the DeltaGrammar.mjs discipline). Line-based block segmentation (paragraphs, headings, fenced code, lists, blockquotes, tables, thematic breaks) → per-block vdom subtrees; per-block inline pass (strong/em/inline-code/links) → cn arrays with vtype: 'text' leaves. No HTML string at any stage.
- Stable block identity: component-scoped block ids (
<componentId>__md-<blockSeq>) assigned at block birth and memoized against the block's source slice — an unchanged settled block re-emits its cached subtree (identical ids → differ no-ops). Block sequence numbers never recycle within one value lifetime (no two-nodes-one-id by construction).
- Streaming surface: reactive
value_ with append detection (newValue.startsWith(oldValue) → incremental path: only the open tail block — possibly an unclosed fence/paragraph — re-parses; anything else → full reset with fresh block sequence). The component stays a drop-in value-driven component; no bespoke streaming API in v1 (the append-detection heuristic IS the API — falsifiable in the spec).
- Security policy (transcript-grade by default):
- Raw HTML blocks/inline HTML render as escaped text (visible, inert) — no sanitized-subset pass in v1 (sanitizers are a follow-up leaf with its own threat-model review).
- Link protocol allowlist:
http(s), mailto, # fragments; javascript:/data:/unknown schemes render as plain text.
- Image sources: same allowlist; no inline
data: images in v1.
- Code blocks v1 = plain
<pre><code> with a language class for CSS — explicitly NO HighlightJs in v1 (it returns HTML strings; piping it in would reintroduce the innerHTML surface this ticket exists to remove — see Avoided Traps). Highlighting is a follow-up leaf (vdom-emitting highlighter or CSS-only).
Contract Ledger Matrix
| Target Surface |
Source of Authority |
Proposed Behavior |
Fallback |
Docs |
Evidence |
src/component/markdown/Parser.mjs (NEW) |
#8920 premise + Epic #13012 lane map |
Plain-module markdown→vdom: block segmentation, stable memoized ids, inline pass, security policy |
n/a (new surface) |
Module JSDoc = parser spec incl. id-stability contract |
VNode.mjs vtype shapes verified; plain-module precedent src/vdom/util/DeltaGrammar.mjs |
src/component/markdown/Component.mjs (NEW) |
Epic #13012 transcript staging |
value_-reactive component consuming the parser; append-detection incremental path |
n/a (new surface) |
Class JSDoc + streaming-model note |
Subfolder precedent src/component/{mwc,wrapper}/ verified |
src/component/Markdown.mjs (EXISTING — untouched) |
Portal/docs surfaces |
Zero changes in this ticket |
n/a |
n/a |
Migration boundary, Out of Scope |
Decision Record impact: none (component-layer primitive; epic-graduated direction).
Acceptance Criteria
Out of Scope
- Migrating
src/component/Markdown.mjs or any portal/docs surface (later leaf, possibly never — the docs pipeline's embedded-block features are a different product).
- Syntax highlighting (follow-up leaf; the hljs-html-string trap is documented below).
- Sanitized raw-HTML subset (follow-up leaf with its own threat model).
- GFM completeness beyond the transcript set (task lists, footnotes, autolinks etc. — organic follow-ups).
- The transcript APP surface itself (a #13015/#13012 leaf consuming this component).
Avoided Traps
- HighlightJs piping (innerHTML reintroduction):
HighlightJs.highlight() returns HTML strings; embedding them would silently reopen the XSS surface and break vdom-purity. v1 ships plain code blocks on purpose.
- Full-document re-parse per chunk — the O(N²) streaming trap the current component embodies.
- Id-per-render (fresh ids each parse): destroys the differ's no-op path and false-positives any coherence instrumentation; the memoized stable-id scheme is the load-bearing design element.
- Block-id recycling after reset: reusing ids across a value reset births two-nodes-one-id across batches — the exact
#12986 defect family; fresh sequence per reset.
marked dependency for the new path: string-coupled output contradicts the vdom-native premise; the lightweight block parser is the point (transcript markdown is a bounded dialect).
- Absorbing the docs pipeline in v1: 652 lines of load-bearing portal machinery would block the harness-critical core behind feature parity.
Related
Parent: Epic #13012 (cross-pillar framework leaf per its decomposition shape). Premise: #8920 (closed stale; this is the fresh-ticket resurrection). Instruments: #12986 / #12991 / PR #13014 (DeltaCapture, merged-pending), #13011 / PR #13016 (coherence registry, in review). Security adjacency: #10291 / #12995 (trust tiers), the hostile-content quarantine class. Transcript consumer staging: #13015.
Live latest-open sweep: checked latest 20 open issues at 2026-06-12T21:01Z; no equivalent found. A2A in-flight sweep: clean (only my own [lane-claim] 20:57Z on this scope).
Origin Session ID: e7e5274c-6486-45ec-9c5d-0933ca3f123a
Retrieval Hint: "markdown vdom streaming parser stable block ids transcript no innerHTML append-only tail batch"
Context
The markdown-VDOM lane from Epic #13012's plan-of-record (lane claimed via A2A 2026-06-12T20:57Z after the targeted lead-role request; operator named this peer for deep involvement). Resurrects the
#8920premise (Neo.component.markdown.VDom — VDOM-native parsing; closed stale/not_planned 2026-05-16, never implemented) as a fresh Fat Ticket per the prevent-reopen discipline — the harness transcript surface makes the premise load-bearing: agent/LLM responses are streaming markdown, and the renderer is on the hot path of every conversation turn.First consumer: the harness transcript/activity surfaces (#13012 PoC→MVP staging names "markdown component v1 landing for activity/log surfaces"). Framework-level primitive — serves portal/docs later, but transcript-first scoping governs v1.
Release classification:post-release (harness product line; no v13.x blocking work).The Problem
The current
src/component/Markdown.mjsis structurally wrong for streaming, in three independent ways:afterSetValue→destroyComponents()+render()→marked.parse(content)→me.set({html})): every appended chunk re-parses the entire document and replaces the entire rendered tree. For an LLM response streaming N chunks this is O(N²) parse work and full DOM trashing per chunk —#8920's "avoids full DOM trashing on every character append" benefit, inverted.htmlconfig = innerHTML semantics): the output never exists as a vdom tree, so the delta engine cannot do fine-grained patching — and the renderer is an XSS surface. The existingbeforeSetValueguard only rejects accidental full HTML documents (404 pages), not hostile fragments. Acceptable for repo-authored docs; wrong default for transcripts rendering untrusted agent/LLM output (the hostile-content class the swarm has already been hit with, and the trust-tier boundary vega's containment work draws).updateNodeand sequential appends rideDeltaUpdates' insertNode-batching optimization (sequentialparentId+indexappends batch natively).The Architectural Reality
{tag/nodeName, id, cn, vtype}structures (src/vdom/VNode.mjs:vtype: 'vnode' | 'text'); a parser emitting these is renderer-agnostic by construction.updateNode;src/main/DeltaUpdates.mjs#update()batches sequential same-parent inserts. A streaming append with a stable settled-prefix therefore costs exactly one insert-only tail batch.#12986arc):test/playwright/util/DeltaCapture.mjs(merged) gives epoch-windowed batch capture + kernel-vocabulary op counts — the executable form of this ticket's streaming ACs. The coherence registry (PR#13016, in review) flags two-nodes-one-id / retired-target incoherence live: a streaming incremental parser with id-reuse logic is exactly the producer class that motivated it, so this lane develops withuseDeltaCoherenceRegistry: trueas its dev harness and dogfoods the contract layer.Markdown.mjs's multi-pass embedded-block machinery (live-preview, mermaid, neo-component JSON blocks, HighlightJs<pre>injection) carries the portal/docs surfaces. It must keep working untouched — the migration boundary below is deliberate.Neoglobals and no DOM access (thesrc/vdom/util/DeltaGrammar.mjsplain-module discipline) is importable from the App Worker, Node unit tests, AND the Node-side SSR/SSG pipeline — markdown→vdom prerendering becomes available to the middleware for free.The Fix
New component family at
src/component/markdown/(structural pre-flight, Stage-1 fast-path:src/component/subfolder precedent exists —mwc/,wrapper/;#8920's own namespace wasNeo.component.markdown.*; implementer re-runs the 30-second check at branch time):src/component/markdown/Parser.mjs— plain module (no class lifecycle, noNeoglobals; theDeltaGrammar.mjsdiscipline). Line-based block segmentation (paragraphs, headings, fenced code, lists, blockquotes, tables, thematic breaks) → per-block vdom subtrees; per-block inline pass (strong/em/inline-code/links) →cnarrays withvtype: 'text'leaves. No HTML string at any stage.<componentId>__md-<blockSeq>) assigned at block birth and memoized against the block's source slice — an unchanged settled block re-emits its cached subtree (identical ids → differ no-ops). Block sequence numbers never recycle within one value lifetime (no two-nodes-one-id by construction).value_with append detection (newValue.startsWith(oldValue)→ incremental path: only the open tail block — possibly an unclosed fence/paragraph — re-parses; anything else → full reset with fresh block sequence). The component stays a drop-invalue-driven component; no bespoke streaming API in v1 (the append-detection heuristic IS the API — falsifiable in the spec).http(s),mailto,#fragments;javascript:/data:/unknown schemes render as plain text.data:images in v1.<pre><code>with a language class for CSS — explicitly NO HighlightJs in v1 (it returns HTML strings; piping it in would reintroduce the innerHTML surface this ticket exists to remove — see Avoided Traps). Highlighting is a follow-up leaf (vdom-emitting highlighter or CSS-only).Contract Ledger Matrix
src/component/markdown/Parser.mjs(NEW)#8920premise + Epic #13012 lane mapVNode.mjsvtype shapes verified; plain-module precedentsrc/vdom/util/DeltaGrammar.mjssrc/component/markdown/Component.mjs(NEW)value_-reactive component consuming the parser; append-detection incremental pathsrc/component/{mwc,wrapper}/verifiedsrc/component/Markdown.mjs(EXISTING — untouched)Decision Record impact:none (component-layer primitive; epic-graduated direction).Acceptance Criteria
opsInper chunk.updateNode-shaped, never remove+reinsert of the tail block.useDeltaCoherenceRegistry: trueproduces zero findings (documented in the PR).javascript:link → plain text;data:image → plain text; fenced code containing</pre><script>→ inert text inside the code block.DeltaGrammar.spec.mjsprecedent pins this).Markdown.mjssurfaces (portal/docs) byte-for-byte unaffected.Out of Scope
src/component/Markdown.mjsor any portal/docs surface (later leaf, possibly never — the docs pipeline's embedded-block features are a different product).Avoided Traps
HighlightJs.highlight()returns HTML strings; embedding them would silently reopen the XSS surface and break vdom-purity. v1 ships plain code blocks on purpose.#12986defect family; fresh sequence per reset.markeddependency for the new path: string-coupled output contradicts the vdom-native premise; the lightweight block parser is the point (transcript markdown is a bounded dialect).Related
Parent: Epic #13012 (cross-pillar framework leaf per its decomposition shape). Premise:
#8920(closed stale; this is the fresh-ticket resurrection). Instruments:#12986/#12991/ PR#13014(DeltaCapture, merged-pending),#13011/ PR#13016(coherence registry, in review). Security adjacency:#10291/#12995(trust tiers), the hostile-content quarantine class. Transcript consumer staging: #13015.Live latest-open sweep: checked latest 20 open issues at 2026-06-12T21:01Z; no equivalent found. A2A in-flight sweep: clean (only my own [lane-claim] 20:57Z on this scope).
Origin Session ID: e7e5274c-6486-45ec-9c5d-0933ca3f123a
Retrieval Hint: "markdown vdom streaming parser stable block ids transcript no innerHTML append-only tail batch"