First leaf of Epic #12986 (the vdom delta-stream contract, graduated from Discussion #12942). The OQ1 grammar census (#12942, comment DC_kwDODSospM4BB6bh — source-grounded, peer-verified by @neo-gpt + @neo-fable) enumerated the complete op taxonomy and invariant classification at the DeltaUpdates boundary. Today that knowledge lives only in the census prose; nothing in src/ encodes it, so every guard, helper, capture tool, or registry the epic builds would re-derive (and drift from) the same contracts. The kernel makes the census executable: one pure module that names the vocabulary and checks the universal grammar.
Release classification: post-release (v13.0.0 tagged 2026-06-12; the contract layer is post-release hardening substrate).
The Problem
The delta wire format has strong implicit contracts and zero explicit ones:
8 actions + 1 implicit spelling — focusNode, insertNode, moveNode, removeAll, removeNode, replaceChild, updateNode, updateVtext; the differ's updateNode is action-less by construction (me[delta.action || 'updateNode'], src/main/DeltaUpdates.mjs:940) — the dominant spelling of the most common op is the absence of a field.
Enforcement today is three accidents: insertNode console.errors on a missing parent (DeltaUpdates.mjs:392,395), replaceChild can throw natively on bad child ids (:724-728), everything else silently no-ops. An unknown action string is a TypeError that aborts the batch mid-loop with partial application (src/Main.mjs:325-346 contains it per-operation) — leaving the DOM divergent from the worker's vnode model.
The misdirection hazard: an id-less updateNode resolves via getElementOrBody(undefined) → default param → document.body (src/main/DomAccess.mjs:481-487) — accidental writes silently style the body.
Ordering is a producer convention, not a checked contract: deltas.default.concat(deltas.remove) (src/vdom/Helper.mjs:833-835) — manual producers can violate remove-last undetected.
The Architectural Reality
The consumer dispatch surface: src/main/DeltaUpdates.mjs (per-op handlers + the :940 fallback + the insertNodeBatch coalescing optimization :911-936 that trusts sequential indices).
The producer field contracts: src/vdom/Helper.mjs — insertNode {action, parentId, index, vnode|outerHTML[, postMountUpdates]} with NO top-level id (the differ emits exactly the active renderer's payload key; the WIRE contract is at-least-one — manual producers may legally carry both) (:665-703); moveNode {action, id, parentId, index} (:730-769); removeNode {action, id[, parentId iff vtext/fragment]} pushed to the remove queue (:790-805); action-less updateNode accumulating all prop-diffs per node (:64-201); updateVtext {action, id, parentId, value} (:68-73); removeAll {action, parentId} (:291). replaceChild + focusNode have zero producers in src/ (consumer-only).
Sentinel encodings: null/'' attribute values mean remove ('' on value clears instead), voidAttributes boolean-coerce from strings, attributes.idrenames a live node (DeltaUpdates.mjs:768-769).
The three-sorted id space: element ids (getElementById / [data-neo-id]), fragment ids (comment-anchor ranges <!-- id-start/end -->), vtext ids (comment pairs) — getElement(id) === null does NOT imply not-live.
Sibling precedent for placement: src/vdom/util/ holds the pure vnode utils (DomApiVnodeCreator.mjs, StringFromVnode.mjs); src/main/DeltaUpdates.mjs:4 already imports the plain-module src/vdom/domConstants.mjs cross-realm — a pure src/vdom/util/DeltaGrammar.mjs is consumable from Main, the VDom worker, and the Node test env. Structural pre-flight: Stage 1 sibling fast-path match.
The Fix
New pure module src/vdom/util/DeltaGrammar.mjs (domConstants-style plain exports — NOT a Neo class; zero Neo-global requirements at import) + unit spec test/playwright/unit/vdom/DeltaGrammar.spec.mjs:
Vocabulary: the action set (8 + implicit-updateNode rule), per-op field contracts (required/optional, the insertNode payload contract (at least one of vnode/outerHTML, both legal, pinned renderer requires its active key), removeNode's conditional parentId), sentinel-encoding constants, the reserved-target id allowlist (window/document/document.body/body), and the three-sort id model (enum + per-op addressable-sort documentation — sort classification of arbitrary ids is NOT this module's job; that requires live-tree/ledger context owned by the future registry sub).
Universal predicates (pure functions, batch in → structured findings out, never throw): U1 action-validity; U2 per-op required fields; U3 remove-last ordering; U4 explicit-target updateNode (id present OR reserved). A validateBatch(deltas, {rendererFlag}) aggregator returns {valid, findings[]} — findings carry {rule, deltaIndex, detail} so future guard wiring (the guards sub) can reject atomically BEFORE first apply.
U5 (≤1 structural op per id per batch) ships as a documented CANDIDATE: exported, marked candidate: true, excluded from the default predicate set — per the #12942 Step-Back carry-through it stays non-guard-grade until a falsification run (owned by the guards sub).
JSDoc is the canonical spec text, self-contained behavior description (Anchor & Echo; no tracking-ticket refs in durable comments per check-ticket-archaeology — provenance lives in PR/commit).
Decision Record impact: none — first formalization of the delta wire contract; an ADR may graduate from Epic #12986 once the layer stabilizes (explicitly NOT this leaf's scope).
Acceptance Criteria
src/vdom/util/DeltaGrammar.mjs exports the action vocabulary (8 actions), the implicit-updateNode rule, per-op field contracts (incl. the insertNode payload contract — at least one of vnode/outerHTML, both legal, a pinned renderer requires its active key — and removeNode's conditional parentId), sentinel-encoding constants, the reserved-target allowlist, and the id-sort enum with per-op addressable-sort documentation.
Any modification to DeltaUpdates.mjs / Helper.mjs behavior.
Avoided Traps
Kernel as a Neo class: a Neo.core.Base subclass drags lifecycle + globals into Main-thread import paths; the domConstants plain-module precedent is the correct weight.
Magic id-sort classifier: an id string does not reveal its sort; pretending otherwise bakes a false oracle into the kernel. The kernel exports the model; classification needs live context (registry sub's job).
Throwing predicates: validators that throw recreate the partial-application hazard they exist to prevent; findings-out, never-throw.
Exact-count shape exemplars in the spec: the #12941 rot class; spec asserts findings presence/absence and rule identity, not delta counts.
Live latest-open sweep: checked latest 20 open issues at 2026-06-12T11:24Z; no equivalent found (closest: #12986 itself = the parent). A2A in-flight sweep: clean (last-30-min claims are @neo-gpt's #12971/#12985 review lanes; the only kernel-scope claims are this author's own broadcasts).
Evolution note (2026-06-12, review cycle 1): payload-contract wording aligned with source reality per PR review — manual producers legally emit insertNode with BOTH vnode and outerHTML (src/mixin/VdomLifecycle.mjs:590), so the contract is at-least-one with the pinned renderer requiring its active key; the prior XOR phrasing was the drift. Module export renamed payloadOneOf → payloadAnyOf accordingly.
tobiu referenced in commit 6dd5d4c - "feat(vdom): delta-grammar kernel — op vocabulary + universal batch predicates (#12987) (#12988) on Jun 12, 2026, 2:45 PM
Context
First leaf of Epic #12986 (the vdom delta-stream contract, graduated from Discussion
#12942). The OQ1 grammar census (#12942, commentDC_kwDODSospM4BB6bh— source-grounded, peer-verified by @neo-gpt + @neo-fable) enumerated the complete op taxonomy and invariant classification at theDeltaUpdatesboundary. Today that knowledge lives only in the census prose; nothing insrc/encodes it, so every guard, helper, capture tool, or registry the epic builds would re-derive (and drift from) the same contracts. The kernel makes the census executable: one pure module that names the vocabulary and checks the universal grammar.Release classification:post-release (v13.0.0 tagged 2026-06-12; the contract layer is post-release hardening substrate).The Problem
The delta wire format has strong implicit contracts and zero explicit ones:
focusNode,insertNode,moveNode,removeAll,removeNode,replaceChild,updateNode,updateVtext; the differ's updateNode is action-less by construction (me[delta.action || 'updateNode'],src/main/DeltaUpdates.mjs:940) — the dominant spelling of the most common op is the absence of a field.console.errors on a missing parent (DeltaUpdates.mjs:392,395), replaceChild can throw natively on bad child ids (:724-728), everything else silently no-ops. An unknown action string is aTypeErrorthat aborts the batch mid-loop with partial application (src/Main.mjs:325-346contains it per-operation) — leaving the DOM divergent from the worker's vnode model.getElementOrBody(undefined)→ default param →document.body(src/main/DomAccess.mjs:481-487) — accidental writes silently style the body.deltas.default.concat(deltas.remove)(src/vdom/Helper.mjs:833-835) — manual producers can violate remove-last undetected.The Architectural Reality
src/main/DeltaUpdates.mjs(per-op handlers + the:940fallback + theinsertNodeBatchcoalescing optimization:911-936that trusts sequential indices).src/vdom/Helper.mjs— insertNode{action, parentId, index, vnode|outerHTML[, postMountUpdates]}with NO top-level id (the differ emits exactly the active renderer's payload key; the WIRE contract is at-least-one — manual producers may legally carry both) (:665-703); moveNode{action, id, parentId, index}(:730-769); removeNode{action, id[, parentId iff vtext/fragment]}pushed to the remove queue (:790-805); action-less updateNode accumulating all prop-diffs per node (:64-201); updateVtext{action, id, parentId, value}(:68-73); removeAll{action, parentId}(:291).replaceChild+focusNodehave zero producers insrc/(consumer-only).null/''attribute values mean remove (''onvalueclears instead),voidAttributesboolean-coerce from strings,attributes.idrenames a live node (DeltaUpdates.mjs:768-769).getElementById/[data-neo-id]), fragment ids (comment-anchor ranges<!-- id-start/end -->), vtext ids (comment pairs) —getElement(id) === nulldoes NOT imply not-live.src/vdom/util/holds the pure vnode utils (DomApiVnodeCreator.mjs,StringFromVnode.mjs);src/main/DeltaUpdates.mjs:4already imports the plain-modulesrc/vdom/domConstants.mjscross-realm — a puresrc/vdom/util/DeltaGrammar.mjsis consumable from Main, the VDom worker, and the Node test env. Structural pre-flight: Stage 1 sibling fast-path match.The Fix
New pure module
src/vdom/util/DeltaGrammar.mjs(domConstants-style plain exports — NOT a Neo class; zero Neo-global requirements at import) + unit spectest/playwright/unit/vdom/DeltaGrammar.spec.mjs:vnode/outerHTML, both legal, pinned renderer requires its active key), removeNode's conditional parentId), sentinel-encoding constants, the reserved-target id allowlist (window/document/document.body/body), and the three-sort id model (enum + per-op addressable-sort documentation — sort classification of arbitrary ids is NOT this module's job; that requires live-tree/ledger context owned by the future registry sub).validateBatch(deltas, {rendererFlag})aggregator returns{valid, findings[]}— findings carry{rule, deltaIndex, detail}so future guard wiring (the guards sub) can reject atomically BEFORE first apply.candidate: true, excluded from the default predicate set — per the#12942Step-Back carry-through it stays non-guard-grade until a falsification run (owned by the guards sub).check-ticket-archaeology— provenance lives in PR/commit).Contract Ledger Matrix
src/vdom/util/DeltaGrammar.mjs(NEW)DC_kwDODSospM4BB6bhACTIONS, per-op field contracts, sentinel constants, reserved-target set, id-sort enum,U1–U4predicates,U5candidate,validateBatch()src/vdom/util/*+DeltaUpdates.mjs:4cross-realm importsrc/main/DeltaUpdates.mjsdispatch (EXISTING — encoded, not modified)DeltaUpdates.mjs:183-943read in full for the census;:940fallback verifiedsrc/vdom/Helper.mjsproducer contracts (EXISTING — encoded, not modified)Helper.mjs:64-201,291,665-805,833-835verifiedtest/playwright/unit/vdom/DeltaGrammar.spec.mjs(NEW)@summarytest/playwright/unit/vdom/*suiteDecision Record impact:none — first formalization of the delta wire contract; an ADR may graduate from Epic #12986 once the layer stabilizes (explicitly NOT this leaf's scope).Acceptance Criteria
src/vdom/util/DeltaGrammar.mjsexports the action vocabulary (8 actions), the implicit-updateNode rule, per-op field contracts (incl. the insertNode payload contract — at least one ofvnode/outerHTML, both legal, a pinned renderer requires its active key — and removeNode's conditionalparentId), sentinel-encoding constants, the reserved-target allowlist, and the id-sort enum with per-op addressable-sort documentation.validateBatch()aggregator returning structured findings ({rule, deltaIndex, detail}).candidate: true), excluded fromvalidateBatch()'s default rule set.vnode+outerHTMLboth present).Out of Scope
useDeltaGrammarGuardsconfig + the rejection/error surface (guards sub).attributes.idre-keying (registry sub).DeltaUpdates.mjs/Helper.mjsbehavior.Avoided Traps
Neo.core.Basesubclass drags lifecycle + globals into Main-thread import paths; the domConstants plain-module precedent is the correct weight.#12941rot class; spec asserts findings presence/absence and rule identity, not delta counts.Related
Epic #12986 (parent). Source Discussion
#12942(closed RESOLVED; censusDC_kwDODSospM4BB6bh). Adjacent leaves to follow: capture API, guards, signatures, registry. Sibling context:#12940(logDeltaUpdates docs),#12931(NL motion granularity).Live latest-open sweep: checked latest 20 open issues at 2026-06-12T11:24Z; no equivalent found (closest: #12986 itself = the parent). A2A in-flight sweep: clean (last-30-min claims are @neo-gpt's #12971/#12985 review lanes; the only kernel-scope claims are this author's own broadcasts).
Origin Session ID: 3e1566f6-6336-4db4-8726-189004578d8b
Retrieval Hint: "delta grammar kernel census vocabulary universal predicates validateBatch DeltaGrammar.mjs"
Evolution note (2026-06-12, review cycle 1): payload-contract wording aligned with source reality per PR review — manual producers legally emit insertNode with BOTH
vnodeandouterHTML(src/mixin/VdomLifecycle.mjs:590), so the contract is at-least-one with the pinned renderer requiring its active key; the priorXORphrasing was the drift. Module export renamedpayloadOneOf→payloadAnyOfaccordingly.