Spec 004 — Views¶
Status: Drafting. v1-required. A view is
ui/defview— a pure function of one props map to a template. Templates are Reagent-familiar hiccup with the ambiguities removed; a compiler lowers every view to one normalized, serialisable template AST consumed by two emitters — direct React code for the browser, a structural render tree for the JVM. No interpreter ships. Event handlers are data (event vectors) by default. Every view is memoized by default. The CLJS realisation isre-frame.ui(artifactday8/re-frame2-ui, aliasui/); frames are created at host preflight (per 002), never from render. SSR (011) renders the same views on the JVM without React.
Abstract¶
A view is a pure function (props) → template, authored with ui/defview. The
pattern-level commitments:
- Pure and speculative-safe. A render may run, restart, or be abandoned; it reads values and builds a local capture. It MUST NOT dispatch, acquire ownership, mutate committed state, publish debug state, or create/seed frames.
- Frame-explicit, carried never guessed. Resolution is explicit pin → dynamic
binding → React context → loud
:rf.error/no-frame-context. There is no default frame and no cross-frame read spelling. Frames are created at host preflight (002); the view layer only scopes live frames. - The portability law. A portable view has one deterministic, serialisable template representation consumed by each host emitter. Emitted host values may be host-native and need not themselves be serialisable. One normalized template AST controls every emitter; parity between emitters is normalized structural equivalence (fingerprinted), not byte-identical output.
- Client markup is compiled, never interpreted. Literal templates lower to
jsx/jsxscalls; conversion is compile-time; static subtrees hoist. No hiccup walker, tag parser, camelizer, or component-shape detector ships in a browser bundle. - Handlers observe committed values. Event callbacks are per-site stable and read committed slots + the committed frame; the canonical handler is an event vector — data.
- Memoized by default. Every internal view is memoized on a generated
straight-line
rf=comparator over its declared prop slots. There is no opt-out.
These are pattern-level commitments across the eight in-scope JS-cross-compile hosts
(per 000 §The pattern).
The CLJS reference realisation is re-frame.ui; its forms (defview, sub, local,
effect, lease, and the ui/* interop surface) are ordinary namespace vars —
(:require [re-frame.ui :as ui :refer [defview sub]]) — referred bare in examples below
for readability.
The portability law and the template AST¶
A portable view has one deterministic, serialisable template representation consumed by each host emitter. Emitted host values may be host-native and need not themselves be serialisable.
- One AST.
defviewis.cljc. The compiler normalizes the template — including the control forms (§Template grammar) — into one closed-node-set AST. Every analyzer and every emitter consumes that AST; no emitter consumes raw source or another emitter's output. - Two emitters. The browser emitter generates direct React code (
jsx/jsxscalls, hoisted static subtrees, compile-time prop conversion). The JVM emitter generates the canonical serialisable structural render tree consumed by the existingday8/re-frame2-ssrartifact (per 011) — no second server product. That tree's versioned public node schema (v1: element / fragment / view-boundary / trusted-HTML / text — a closed set of plain serialisable maps in canonical form), the semantic normalizationNthat feeds parity and fingerprints, and theemit-ui-treeSSR consumption boundary (version-gated) are owned by 004B-UI-Tree-and-Conversion.md — referenced here, never restated. The optimizer/compiler AST stays private; the public contract is the tree plus the conversion table. - Parity. Equivalence between emitters is normalized structural equivalence over semantic nodes — tag/ns, attribute names + values, child order, escaping, keyed order, void/boolean handling, fragments, fallbacks — fingerprinted and generatively tested (per 008). Byte-identical HTML is NOT the contract.
- Serialisation boundary. The template AST's structure — tags, nesting, non-function
attribute values, and literal event vectors — is fully serialisable and survives a
print/read round-trip; event vectors are retained as data in the compiler manifest
and the JVM tree. Non-serialisable sites (
ui/event,ui/handler, bare fns,ui/raw-fn, foreign values) are explicit spellings recorded in the manifest with a:serializable?/:dynamicflag — escape hatches advertise their cost. - Closed node set. Escaping is structural: because the AST's node types are closed, there is no unknown-node fallback arm in either emitter. Template-string DSLs remain an invalid carrier — strings don't compose, don't diff, don't lint, don't round-trip.
- Non-React emitters are preserved as an option by an AST-shape gate (the IR must keep edit-list-sufficient information), not by a maintained implementation.
ui/defview — the one component form¶
(ui/defview product-card
"One product tile."
{:props [:map
[:product [:map [:id :int] [:name :string] [:price :double]]]]}
[{:keys [product]}]
(let [{:keys [id name price]} product
in-cart? (sub [:cart/contains? id])]
[:div.card
[:h3 name]
[:span.price (format-price price)]
[:button {:on-click (if in-cart? [:cart/remove id] [:cart/add id])
:disabled (sub [:cart/locked?])}
(if in-cart? "Remove" "Add to cart")]]))
- Zero or one argument — semantically a props map. Header destructuring (
:keys, namespaced:x/keys,:or, explicit bindings) lowers to direct property reads on the host props object; no CLJS map is materialized at entry.:asopts into materialization + generic comparison (a documented dev cost). There are no positional args. - Props ABI. Each prop keyword maps to a deterministic quoted JS property name
preserving namespace + name; it cannot collide with React's
key/ref/childrenslots.:keyis reserved (it feeds React's key slot) — an app prop literally named:keyis a compile error. Children arrive in the props map as:childrenand compare as one slot. The compiler manifest maps compact production slot indexes back to keywords. - Options map (closed for v1):
:props(Malli — literal call sites checked at compile time, dynamic values at dev runtime, elided in production),:id(registry override),:display-name. Nothing else. The following were considered and are deliberately absent::memo false(no demonstrated consumer; mutable foreign values belong at an explicit boundary);:on-mount/:on-unmount(domain events cannot ride mechanical React lifecycle — StrictMode replay, Activity, HMR, and error recovery make "once" semantics unrecoverable; domain visibility belongs to route/domain transitions, host sync toeffect);:catch/:fallback(error handling is the explicitui/error-boundarycomponent). - Registration.
defviewdefs a Var and registers in the registrar under the:viewkind: source metadata, template fingerprint, hook signature, capability bits. Default id derivation follows the family rule(keyword (str *ns*) (str sym))per Conventions;:idoverrides. Story mounts scenes by view id; render-keys are instance ids allocated at mount; the Pair hot-swaps a view like an event handler (§Hot reload). - Memo-by-default. Every internal view is memoized on a generated straight-line
rf=comparator over its declared prop slots. Scope stated honestly:rf=-equal props ⇒ no prop-driven repaint; subscription, local-state, and context changes still render.rf=is, per slot:Object.is(a,b) OR (= a b). CLJS data (anything withIEquiv, incl. records and js/Date) compares by value; host/foreign values (plain JS objects, arrays, functions, React elements) fall through to identity. Consequences pinned: fresh-but-equal CLJS literals ⇒ no repaint; in-place-mutated host objects ⇒ no repaint (mutable foreign values belong at an explicit boundary — consistent with the:memo falserejection);##NaNprops are repaint-stable via theObject.isbranch;-0/+0compare equal via the=branch (deliberate, harmless divergence from rawObject.is). Teach as "React.memo, except CLJS data compares by value". The identity check doubles as the generated fast path.
Template grammar¶
Reagent-familiar hiccup with the ambiguities removed. Control forms — let / letfn /
if / if-not / when / when-not / cond / case / statically-pure do / for —
normalize into the AST; all analyzers and both emitters see through branches.
| Form | Meaning |
|---|---|
[:div.cls#id {…} …] |
DOM element; literal head required |
[view-sym {…} & children] |
internal view (compile-resolved Var) |
[ForeignComponent {…} …] |
foreign React component (open props; JS values pass through) |
[:<> …] |
fragment |
(for [x xs] [item {:key …}]) |
keyed list → direct JS array; missing key = build failure |
(ui/presence …) |
declarative enter/exit retention (§Presence) |
strings / numbers / nil / false |
text / nothing |
Rejected at compile time (didactic messages naming the escape): dynamic tag heads;
markup-returning map; keywords in child position; raw lazy seqs; unkeyed list items;
sub/lease in loops (extract a keyed child view — sites must be finite). A bare
keyword head is a DOM/custom element, never a registry lookup (rf2-n82bbu; enforced at
compile time).
Expression positions are value-opaque but lexically audited — the closed macro
grammar (rf2-vxgfnd.100). Property values, condition tests, for collections,
handler-position expressions, and wrapper expressions hold ordinary Clojure values:
the compiler never interprets an expression's value as markup — opacity is with
respect to DOM/template interpretation, so a seq-producing call like (map f xs) in a
prop value or a for collection is just a value. The expression's lexical syntax,
however, is analyzed for finite reactive ownership: every (sub …) / (lease …) is a
compile-indexed lexical site, the manifest declares a view's sites, and optimized
production elides the ViewCell for a genuinely sub-free view — so the analysis must be
able to see every reactive call. An unaudited macro breaks that soundness: it can
inject, duplicate, or defer a ui/sub / ui/lease with no reactive token in the
authored invocation — the manifest would falsely declare a sub-free view, production
would elide its ViewCell, and the hidden read would go stale or escape ownership. The
expression grammar is therefore closed:
- Special / binder forms, handled structurally with binder-aware traversal:
quote(never traversed — quoted data is not executable),fn/fn*,let/let*,loop/loop*,letfn/letfn*,try. Binding patterns and destructuring:ordefaults may contain neither reactive calls nor unaudited macros — those positions are consumed by the host compiler, not expression rewriting, so they cannot own a lexical render site (hoist the read into the view body and bind its value). - The audited transparent macro set — position-aware
clojure.core/cljs.coremacros whose arguments are host-independent expression slots with no user-authored binders:or,and,when,when-not,cond,->,->>,some->,some->>,cond->,cond->>. Their spelling is preserved while their arguments are recursively analyzed — a(sub …)nested under(or … "")still lowers to its indexed site. A baresub/leasereference below one is rejected: a threading step such as(-> query sub)would become an unindexed reactive call only after expansion, so the explicit(sub query)call is required. - Ordinary function calls — any function. A simple symbol/keyword head (a
plain fn/special-form name, a keyword lookup op) is not itself an evaluated
expression, so it is preserved verbatim and the arguments are analyzed. A
computed callee — a seq/vector/map/set in head position, e.g.
((if (sub [:op]) inc dec) 1)— IS evaluated (Clojure evaluates the callee before its arguments), so it is analyzed under the same rules before the arguments: a visible(sub …)there lowers to its indexed site, and an immediately-invoked-fn / opaque-macro callee that could hide a read is rejected didactically. A visible reactive site is never dropped from the manifest. Helpers stay helpers: pass reactive values in. - Every other macro is rejected at compile time —
:rf.ui.compile/unsupported-form, didactically: macro X is outside the compiler's audited expression set and could inject, duplicate, or defer a reactive call after lexical site analysis; rewrite it with ordinary functions/control forms, or hoist the computation around the view template. This covers core macros outside the set (if-let,when-let,case,doto,forin expression position, …) and every user/library macro — macro authority is the host analyzer's own flag, not a name heuristic. Recovery paths, in preference order: use a supported core form; move the pure computation into an ordinary function and call it; pass reactive values into the helper (readsubin the view body, hand the value down); or make the reactive boundary adefviewof its own.
There is deliberately no generic double macroexpansion, no macro interpreter, no
runtime dynamic reactive site, no ViewCell overhead for sub-free views, and no
compatibility fallback. The set grows only by ruling, and any future addition requires
lexical-scope plus hidden-reactive-injection counterfixtures (the rf2-vxgfnd.100
hidden-sub / helper / binder-macro proofs in
re-frame.ui.compiler-macro-resolution-jvm-test are the template).
DOM prop spelling is pinned: hyphenated lowercase words mirroring React's camelCase
— :on-click, :on-key-down, :on-input (never :on-keydown). Handler-map options:
{:event […] :prevent-default true :stop-propagation true :capture true :passive true
:once true} — the DOM listener vocabulary is explicit, not implied.
Prop conversion is compile-time, contextual, and total: DOM attribute casing,
:style maps (keyword values stringify), :class string/vector/map-of-flags; component
props pass through untouched. One rule table serves static props, the single
dynamic-map conversion fn (ui/spread, v1), and both emitters — and that table now
exists as normative rows (namespaces, attribute names, boolean/booleanish/overloaded
sets, property-only + form-control forms, :style px/unitless/custom-property rules,
:class composition + .class#id sugar precedence, children/escaping):
004B-UI-Tree-and-Conversion.md §The DOM
conversion table is the owning contract. No #js on compiled
DOM/internal paths (foreign React interop may hand raw JS values through — that is the
boundary's job).
Custom elements (tag contains -): a bounded classification rule — literal props
compile to properties when the name matches a declared property (per an optional
ui/custom-element declaration), else attributes; booleans/:class/:style follow DOM
rules; native custom events ride the normal handler grammar. Never forced through
ui/raw. Declaration grammar:
(ui/custom-element tag {:properties #{...}}) — top-level, compile-resolvable,
registers like defview. The :properties set is the entire v1 grammar (options
map closed, per the defview options-map discipline); future keys (:events, per-prop
types, attribute reflection) are new rulings, not silent growth. Declared names →
JS properties, kebab keyword mapped to the camelCase property (:help-text →
helpText, mirroring the pinned DOM-spelling philosophy); undeclared names →
attributes; undeclared elements need no declaration (all-attributes default). SSR/JVM
emitter emits attributes only; property-props are applied at hydration. Rejected:
Lit-style rich schema (no consumer — demand bar), React-19-style runtime in check
(breaks compile-time totality + gives SSR no static answer), attribute-only-v1 deferral
(makes property-accepting web components unusable, hollowing "never through
ui/raw"). Export-at-S1, assert-at-S4: the name is in the blessed S1 export
set (re-frame.ui's S1 public surface, per API.md §Compiled views), so
ui/custom-element is exported and callable from S1 — declaring a name never errors as
an unknown symbol — but its property-vs-attribute classification behaviour and
stage-conformance assertion ship with the S4 epic (the API.md table stages the row
S4). ui/custom-element is added to the 12 §2 freeze table as the delta protocol's
first row-level delta.
Handlers are data — the callback law¶
Canonical: the event vector. A vector in an :on-* position is the event intent,
dispatched to the committed frame:
[:button {:on-click [:cart/add id]} "Add"]
[:input {:on-input [:form/typed :email :rf.ui/value]}]
[:input {:type :checkbox :on-change [:prefs/set :dark :rf.ui/checked]}]
Placeholder vocabulary (closed, v1, scalars only): :rf.ui/value,
:rf.ui/checked, :rf.ui/key. Placeholders splice at top-level positions of the vector
at dispatch time. :rf.ui/form-data and :rf.ui/event do not exist — form payloads
carry duplicate keys/files and are not EDN; a raw event is a host object; both cases
belong to ui/event. Vectors with only literal/placeholder content are data:
value-comparable, statically inspectable, JVM-testable, retained as data in the manifest
and the JVM tree. On the client they lower to normal React handlers (per-site stable,
committed-slot reading). No handler attributes are emitted into HTML and no resumability
is claimed (research-tier per R-5; the serialisability property is kept, the platform
built on it is not v1).
The decision table¶
| Form | Invoker → phase | Identity | Sees | Serialisable | Use for |
|---|---|---|---|---|---|
[:event … :rf.ui/value] |
DOM → after commit | per-site stable | committed slots + frame | yes | intent (the 90%) |
(ui/event [e] … [:vector …]) |
DOM/foreign → after commit | per-site stable | committed slots + the live event | no | event mechanics, form/file payloads, filtering (nil ⇒ no dispatch) |
(ui/handler [x] …) |
foreign → after commit | per-site stable | committed slots | no | imperative work, stable-identity change-callbacks |
(ui/render-fn [x] …) |
foreign → during its render | none promised | current render | no | item-key/comparator/render props; pure — no dispatch/sub/lease/hooks |
bare #(…) in a known native event property (:on-* on DOM/custom elements) |
DOM → after commit | per-site stable | its closure (committed render's values) | no | shorthand for ui/handler — legal because invoker + phase are known. Only there: not refs, not arbitrary fn-valued props |
| bare fn at a foreign-component boundary | unknown | unknown | unknown | no | compile error — choose ui/event/ui/handler/ui/render-fn/ui/raw-fn |
(ui/raw-fn f) |
foreign, identity-as-protocol | passed through | its closure | no | APIs that treat callback identity as data; also the callback-ref form |
The narrow bare-fn law (R-4). Bare fns are legal only in known native
event properties, where the invoker and phase are known. One callback never serves both
phases. A day-one strict lint — {:re-frame.ui/bare-handlers :warn} (or :error) —
lets a team adopt explicit-everywhere as policy without a language change; the language
itself stays permissive (flipping it later would break source; the lint is the honest
lever).
Refs. :ref is a reserved React slot, never an event property — the bare-fn
shorthand does NOT apply. Object refs are preferred. A callback ref MUST be explicit
(ui/raw-fn f): React invokes callback refs during commit before the owning view's
layout publication, so no committed-slot promise can be made — the explicit form marks
that. Internal views forward :ref only by declaring it; refs never appear in event
vectors or SSR output.
Dynamic handler expressions. Handler-position expressions are legal
((if in-cart? [:a id] [:b id]), a prop-forwarded event). Literal forms classify at
compile time; non-literal values classify at runtime by type (vector → dispatch; map
→ options form; compiled handler object → itself; fn → the boundary rules above; nil →
no handler). Two consequences, stated honestly: placeholders are compiled, so they are
recognized in literal vectors only — a placeholder keyword inside a runtime-forwarded
vector dispatches as an ordinary keyword argument, and dev warns
(:rf.warning/placeholder-in-dynamic-vector). And manifests mark value-classified sites
:dynamic — the static interaction surface covers literal and normalized-branch sites
and says "dynamic" for the rest.
Loops. A capture-free literal vector handler in a for body is legal and shares one
callback across rows. A vector that captures the loop binding is a compile error
with the extract-a-keyed-child-view fix — per-row committed slots need per-row
instances. The same rule covers ui/event/ui/handler in loops (they are sites too).
Bare fns in loops get the same diagnostic as a dev warning (they work, at per-row
closure cost, and defeat the data idiom — the nudge is deliberate).
Controlled inputs — the synchrony law. Dispatches from
:on-input/:on-change/:on-before-input sites on controlled DOM elements drain
synchronously within the DOM event — event → drain → commit → snapshot advance
before React's discrete-event re-render — so value round-trips cannot drop characters,
jump the caret, or break IME composition. This is the one sanctioned synchronous door;
everything else batches (one notification per cell per render batch — the
drain-quiescence boundary, not epoch close; I-6). Caret/IME
correctness gates first, latency second. The trigger predicate (confirmed by the
controlled-input door spike; the residual named gate is the G-8 real-browser input
matrix, not the predicate): the door applies where the compiler can prove the element
controlled — a literal :value/:checked prop co-present on the element with the
vector-handler site; dynamic props maps, ui/spread, and ui/event/bare-fn dispatches
at such sites fall back to standard batching with a dev diagnostic naming the sync-door
conditions.
Dev safety nets. Data handlers with unregistered event ids warn at render with the
element's coordinates (:rf.warning/unregistered-event-id). The registrar is
process-global — frames isolate state, not behaviour — so a lazily-loaded module that
registers later can produce a false positive; the warning names that possibility.
Reactive reads — sub¶
(sub [:query …]) returns the subscription's value — no deref, no manual memoization,
no deps arrays. Each lexical (sub …) is a compile-indexed site; all of a view's sites
share one React bridge (one useSyncExternalStore, one scalar revision snapshot,
one notification per render batch — I-3/I-4/I-6). Conditional reads are legal; sub in loops
is a compile error (sites must be finite — extract a keyed child view). Site indexing
is why expression positions carry a closed macro grammar (see §Template
grammar — expression positions): an unaudited macro could hide a
sub from the lexical analysis, so only the audited transparent set, special/binder
forms, and ordinary function calls are accepted around reactive reads. Literal queries
are module constants; parametric sites reuse the prior query object while args are
rf=; sites return the prior exact value when the new read is rf=. The observation
model — the six-operation port (resolve-target · probe · acquire! ·
current? · read · release!) over the target/evidence/lease split, the staged
transactional commit algorithm (acquire-before-release with rollback), and the
three-state lifecycle (:connected / :disconnected / :dead; Activity-hide vs
unmount are qualified retroactive annotations, never distinct runtime states) — is
owned by 006 (the R-2 observation port; shapes final
per the merged amendment — the observation-port amendment landed with the S2 slice);
this Spec owns only the call-site surface. sub never fetches (I-11).
Local state — local — and the placement rule (this Spec owns the rule)¶
(local init) → [value set!] — host component-local state, deliberately outside
re-frame2 epochs: not observed by subs, not revertible by epoch restore, re-renders
this view only. set! during render is a dev error
(:rf.warning/render-phase-set!). There is no frame-resident variant and none is
reserved — if frame-resident ephemera is ever built it will be a new name with its own
semantics.
The placement rule (this Spec remains its sole owner; the narrow law — the earlier "forbidden if any handler ever reads it" strictness is superseded):
- Default —
app-db. State with product meaning lives inapp-db, written by events, read by subs — observable, replayable, schema-checkable, headlessly testable. When in doubt,app-db. - The
localtier (the narrow law).localholds keystroke-latency view ephemera — in-flight field text, uncommitted IME composition, transient focus/hover, open/closed visual state. A local value MAY be read by same-view committed handlers: handlers read committed slots, and committed slots include local ephemera. The guide's search box — text held inlocal, submitted as[:search/run text]from the button's handler — is canonical and conforming; the seam where a local value crosses into an event vector is exactly where it becomes product state. When the field's every keystroke is product state (live filtering another view observes), dispatch placeholders (:rf.ui/value) instead of holding it locally. - The forbidden tier.
localis FORBIDDEN when the value needs cross-view observation, replay/persistence, schema or tool inspection, durable navigation semantics, or subscription-derived computation — those belong inapp-db. Loading flags are the classic example. The bias is deliberate: a value wrongly kept local is invisible to tools and unrecoverable on replay; a value inapp-dbthat turns out never to be read is merely slightly verbose. Prefer the recoverable failure.
Effects and leases — the view-side surface¶
(effect [node series] (draw! node series) #(destroy!)) ; rf= value deps; cleanup fn
(effect :connect (subscribe-external!)) ; runs at each connect; cleanup at disconnect
(lease {:resource :article/by-slug :params {:slug slug}})
(effect [deps…] body)is a passive host effect; deps compare byrf=(documented cost: broad values walk — keep deps narrow); the cleanup fn is honored on dep change, disconnect, and unmount. StrictMode dev replay is expected and MUST be idempotent-safe — that is what cleanup is for.(effect :connect body)runs at each connect, cleanup at each disconnect; there is deliberately no "once"/"mount" name — React's lifecycle has no "once", and pretending otherwise is the:on-mountbug. Effects synchronize with the host world; app state goes through events.- Stateful imperative libraries that own their own DOM subtree (D3, Mapbox, CodeMirror,
animation libraries) attach/detach inside
effectwith a ref — never in the render body (I-1). Host primitives the substrate does not wrap (setTimeout,fetch, RAF loops, WebSocket listeners) remain registered fx per Pattern-AsyncEffect — unchanged dataflow doctrine. (ui/dispatch-fn)is the stable committed-frame dispatcher for imperative callbacks; it fails loudly in every non-connected state (:rf.error/dispatch-disconnected).(lease descriptor)declares resource liveness; it is recorded at render and reconciled by one aggregated passive effect after commit (ensure/release-owner per 016). Reads stay passive ((sub [:rf/resource …]));leasein loops is rejected; routes/events/machines remain the preferred causal owners (I-11).
Loading state is explicit¶
Loading state is data in app-db (canonically
Pattern-RemoteData's :status); views read it and branch.
sub never fetches; lease declares liveness and acts only after connected commit;
routes, events, and machines are the preferred causal owners of fetching. Suspense as
loading state is a non-goal — it hides loading in the substrate where tools cannot see
it, SSR cannot replay it, and machines cannot govern it. Hiding a loading flag in
local is the forbidden-tier violation above.
Presence — declarative enter/exit¶
A presence primitive, deliberately bounded — not an animation system (anything beyond enter/exit retention is out of scope):
- Keyed children pass
:mounting → :present → :unmounting; an exiting child stays mounted until its transition/animation completes, with:timeout-msas the mandatory safety bound (unit-suffixed); then cleanup is terminal and exactly-once (all ownership released). Unkeyed children under a presence boundary are a build failure. (presence-phase)is the single phase read. Outside a presence boundary it returns:present, so presence-aware children stay reusable anywhere.- Removal-then-reinsertion of a key has deterministic interruption/re-entry; exiting
children are
inert/aria-hiddenby default; reduced-motion takes the immediate path; hydration does not fabricate enter transitions; tests advance transitions viaui.test/flush-presence!(no wall-clock sleeps). - The JVM emitter renders
:presentand exposes presence metadata structurally. Occurrence-paths (§View identity) identify retained exiting rows in tooling.
Interop and boundaries¶
| Surface | Contract |
|---|---|
(ui/raw react-element) |
embed an existing React element (child position; SSR paths need a client-only sibling fallback) |
[ForeignComponent {…}] |
foreign React head; open props, JS values pass through; callbacks per §The decision table |
(ui/->react view) |
export a view as a React component — the outward migration bridge. v1, lands S6 with the migration wave. Contract: the compat-boundary contract §3 (promotes as spec/004A-Reagent-Compat.md at the deletion wave) — memoised per view id (returns the stable shell), no new React root/manifest/preflight; the exported view scopes frames, never creates them |
(ui/element type props & children) |
runtime-chosen element/component [WAVE-2] |
(ui/view id) |
registry-addressed component; production use requires production registry entries (dev-only string ids cannot serve prod lookup) [WAVE-2] |
(ui/spread base overrides) |
the one generic runtime prop-map conversion — v1; the conversion architecture's single dynamic-map path, driven by the owning rule table |
(ui/portal node child) |
React portal; frame context passes through [WAVE-2] |
(ui/client-only {:fallback tpl} client-tpl) |
browser-only subtree; the fallback is mandatory and MUST be capability-free (compiler-checked); the JVM and first hydration render the fallback, then one root phase-flip swaps all sites in a single update (per 011) |
(ui/html string) |
trusted markup, low-friction. Renders the string as HTML, explicitly. The spelling is the contract: the visible call marks the one place escaping is bypassed; manifests record the site; both emitters treat it identically. Strings anywhere else always escape. |
(ui/error-boundary {:fallback view :reset-key val :on-error [:ev …]} child) |
the explicit error component. Catches render/lifecycle throws below it (React does not catch event-handler or async errors — those keep their own typed paths); :on-error dispatches after the failing commit through a captured live frame (never during render, I-1); the fallback renders with :error + declared props and cannot recursively dispatch; changing :reset-key clears the caught error (retry = a state change that changes the key); the JVM/SSR renders the child under the server failure policy (per 011) — boundaries are a client recovery mechanism. |
re-frame.ui.data |
the interpreter for genuinely runtime-authored UI (CMS trees) — a separate artifact, never in a compiled browser bundle by accident |
Wave-2 rows ship only on the demand bar (a named consumer in the repo's examples, tools, or guide fixtures — guide examples authored by this project do not count as independent demand for platform-scale features).
Roots and mounting¶
(ui/mount root-form dom-node) / (ui/mount root-form dom-node opts) is a macro
over a literal root form — the compiler must see the root to keep the AST closed and
to extract frame plans; a runtime-assembled vector is a compile error pointing at
ui/view/ui/element. The opts map carries root identity — :root-id (authored
wins; a qualified keyword or a qualified-keyword-plus-scalar vector), :disambiguator,
:identifier-prefix, all compile-time literals — plus the host error callbacks
(:on-uncaught-error / :on-caught-error / :on-recoverable-error). Every root has
a root-id: authored, or derived from the mounted view's registered id (a single-root
page needs nothing; the same view mounted twice needs :disambiguator or an authored
id — anything else is a compile error). The compiler emits Root Descriptor v1
(:rf.root/schema-version 1) per mount site — the named, versioned compile-time
subset of the Stage-5 Root Manifest (strict superset the other way; readers ignore
unknown keys; additive keys never bump the version). Hosts needing control use
create-root / render! / hydrate-root / unmount!; hydrate-root takes its
identity from the manifest, never from client opts (supplying identity opts there is
:rf.error/root-manifest-invalid). The signature set, derivation + slug rules, element
locators, three-layer fail-loud duplicate/conflict detection, and the accepted
ui.test/render root forms are owned by
004C-Roots-and-Mount.md.
- Frames are created at host preflight, never from render (I-1). The compiler
extracts unconditional
frame-rootplans from the root form; before React (or the JVM renderer) is invoked, the host ensures the frames and drains:initial-events— exactly once, unaffected by abandoned renders, StrictMode replay, HMR, or error recovery. The emittedframe-rootcomponent then only scopes the already-live frame.frame-rootsites MUST sit in the top region of the root form (unconditional, compile-extractable); conditional, reactive, or list-generated sites are compile errors ("create frames in boot/event infrastructure; scope withframe-provider"). Frame identity, ENSURE semantics, andframe-provider(SCOPE) are owned by 002 (the R-7 staged frame chain). - Roots ≠ frames. A root is one React DOM render/hydration unit; a frame is one re-frame2 state world; roots ↔ frames are many-to-many. The root manifest, hydration contract, per-root failure isolation, and the static-root explicit policy are owned by 011; Root Descriptor v1 (above) is this Spec's compile-time subset of that manifest.
ui.test/flush!is the only test flush (per 008); on CLJS its zero/thunk arities andwith-rootare Promise boundaries and must be awaited.
View identity and the instrumentation surface¶
Five identities, never conflated: root-id (owned by 011), frame-id (owned by 002), render-key (one committed view instance — owned here), occurrence-path (a keyed repetition inside one instance — owned here), and observation-target (owned by 006). Sites get compile-time indexes + source anchors; identity under HMR is source anchor + structural path + generation, released/remounted on ambiguity.
- Compiler manifest — what can happen. Per view, dev: source coords, prop slots +
schema, template fingerprint, hook signature, capability bits, and every site (subs
with query shapes; events with event shapes +
:serializable?/:dynamicflags; leases; effects; presence sites; trusted-markupui/htmlsites) with source + template path. No runtime values; useful before mount — consumed by Xray, Story, editors, and agents. - Committed instance record — what did happen. Published only at connected commit;
speculative renders publish nothing (I-1/I-2). It carries render-key,
parent-render-key (direct hierarchy — no Fiber or DOM walking), root-id, frame-id,
view-id, generation, connection state, observations, and the
:rf.view/causesvector (mount / subscription / story-override / prop / local-state / frame / resource / hmr / hydration-correction / reconnect-correction / epoch-restore / foreign-or-react) — attribution is emitted at the cause site, never reconstructed. Every bounded buffer reports loss accounting (total/retained/dropped). - One catalogue. The evidence schemas, trace ops, and every error/warning id this
Spec names (
:rf.error/dispatch-disconnected,:rf.error/view-not-found,:rf.error/frame-payload-invalid,:rf.error/flush-in-open-epoch,:rf.error/ui-test-overlapping-act,:rf.error/jvm-host-op,:rf.warning/unregistered-event-id,:rf.warning/placeholder-in-dynamic-vector,:rf.warning/cross-frame-carried-op,:rf.warning/render-phase-dispatch/-set!, and the compile-error roster) get catalogue rows in 009 (the one-catalogue rule, rf2-cs0kd1). - Source ↔ DOM navigation. Compile-time
data-rf2-source-coord+ render-key (+ occurrence-path) annotation on compiler-owned host roots — today's attribute vocabulary, so existing Xray click-to-source works day one. Dev-gated; production builds carry none of it. - Production erasure is a proof (I-12). Compile-time defines + bundle-scan gates;
manifests, cause vectors, histories, warning text, and
data-rf2-*strings are on the scanned absence roster. The always-on Spec 009 error contracts remain. - [TRANSITION] The
[view-id instance-token]:render-keywire shape and the[:rf.view/anonymous nil]fallback for unregistered render fns remain emitted by the UIx/Helix/slim adapters until the deletion wave, and by the frozen stock-Reagent compatibility tier thereafter; the compiled substrate emits its own versioned evidence schema (integer render-key + separate:view-id+occurrence-path) in the 009 catalogue.
The JVM structural subset¶
defview is .cljc; the JVM emitter renders the defined structural subset. "If the
browser renders it, the server renders it" is scoped to exactly this table:
| Feature | JVM structural render |
|---|---|
structure, props, subs, branches, lists, event intent, ui/html |
full semantics (subs via the pure snapshot path — no ownership, no watches) |
local |
contributes its initial value; the setter is absent — invoking it in a JVM test raises :rf.error/jvm-host-op |
effect |
does not run; recorded as capability metadata |
| refs | absent |
portal / client-only |
explicit deterministic fallbacks |
error-boundary |
server failure policy (project error / status per 011), not client recovery |
presence |
renders :present; phase metadata exposed structurally |
The tree the JVM emitter returns is the versioned public ABI owned by
004B-UI-Tree-and-Conversion.md: five closed
node variants (element / fragment / view-boundary / trusted-HTML / text-as-string),
plain serialisable maps in canonical form, rooted with :rf.ui/tree-version 1. Node
reading (ruled): nodes are plain maps — field reads (:tag, :attrs, :events,
:children, :view-id) are public ABI; attribute reads go through the merged
projection (ui.test/attrs node) (attrs + events on elements, props on view-boundary
nodes) — (:on-click node) is a field miss, never an attribute read. Event vectors are
retained as data under :events. The node schema, canonical-form rules, semantic
normalization N (the parity/fingerprint input), and the SSR consumption boundary
(emit-ui-tree + the :rf.error/ssr-ui-tree-version-unsupported version gate) are that
contract's — summarised here, owned there. Selector semantics over these trees are
004D-UI-Test-Selectors.md's: view-id selectors match
the view-boundary node, so fragment-rooted and nil-rooted views are matchable.
Host-bearing features (state transitions, effects, refs, focus, portals, presence
timing, error recovery) require mounted (Tier-3) tests — the guide says this out loud.
Headless (Tier-1) tests of structure, subs, branches, lists, and event intent run on the
JVM against this subset via ui.test (per 008); Tier-1 requires the
events/subs a view touches to be .cljc — an authoring constraint the guide teaches.
Hot reload — the view-side contract¶
HMR is a designed contract with fixtures, not a hope; the REPL path is the HMR path
(defview re-evaluation re-registers and bumps the generation). The view-side surface:
- Stable shells.
defviewexports a stable component shell keyed by view id; the registry holds the current implementation descriptor. Re-evaluation replaces the descriptor; the shell identity never changes, so React state, refs, and cell identity survive. - Hook-signature hash decides preserve vs remount. Same signature: mounted cells
mark stale, the next render runs the new body, commit reconciles changed sites —
state preserved. Changed signature: the shell deliberately remounts — never a
corrupted hook order. Dev's fixed full hook skeleton exists precisely so adding your
first
subto a view is a same-signature edit (I-15). - Frames are untouched by reload — ENSURE ran at preflight; re-running the mount fn
finds the frames live and does not re-seed;
:initial-eventsre-run only on a genuinely new frame id. - The Pair's hot-swap is this same mechanism invoked over nREPL.
Cell/ownership reconciliation under reload is owned by 006 — including the guard that a re-registration landing in a cell's render→commit gap never paints the old body: commit checks body authority (the cell-local generation and the registered view revision) at both the render→commit boundary and again immediately before publication, releasing any newly-staged leases and re-rendering rather than committing a stale capture (006 §Body authority under hot reload). Compile budgets (expansion p95, watch-loop rebuild) are gated per 008 G-14.
Removed forms — normative absences¶
There is exactly one component form. The following do not exist in this contract:
- Form-1 / Form-2 / Form-3. No closure-form components, no class components, no
outer/inner render split; the Forms live on only in the frozen stock-Reagent
compatibility tier (live normative home: the compatibility appendix
spec/004A-Reagent-Compat.md, per §8 of the compat-boundary contract), taught on one migration page only. Form-2 local state islocal; Form-3 lifecycle work iseffect(+ refs) or a foreign-boundary component; setup-on-mount work is a frame's:initial-eventsor a route/domain transition — never a render-phase or mount-lifecycle dispatch. - The
reg-viewfamily.reg-view,reg-view*, the two registration lanes, and the(rf/view id)runtime lookup are absent from this contract — the family freezes with the stock-Reagent compatibility tier, and its live contract moves to the compatibility appendixspec/004A-Reagent-Compat.md(lands with the S7 deletion wave; the appendix retains the family's API/facade/Conventions rows under av1 (frozen — compat tier)status — the exports relocate, they are not removed; §8 of the compat-boundary contract).defviewis the one registration surface; the registrar:viewkind persists as the tooling read surface (Story mounts by view id; Xray lists by registry query). Runtime-chosen components areui/view/ui/element[WAVE-2], demand-gated, with production registry entries required for production lookup. - Positional view args. One props map. Call sites are
[view-sym {…}]. - Plain render fns as frame-aware views. There is no frame injection into
unregistered fns, no
capture-framerender affordance, and no[:rf.view/anonymous nil]trace fallback — every traced view is adefview; foreign React components are boundaries, not views. :on-mount/:on-unmount,:memo false,:catch/:fallbackoptions;:rf.ui/form-data/:rf.ui/eventplaceholders — considered and rejected (rationales in §ui/defviewand §Handlers).- The
hmacro; bare-keyword view heads. Carried absences — no compile-time hiccup walker rewriting keyword heads; a keyword head is always a DOM/custom element (rf2-n82bbu: dynamic tag heads are compile errors and the registry is never probed on the render path). - A second state model. No ratoms, cursors, reactions, signals, or query caches in the view tier — one reactive grammar, subscriptions.
- Suspense-as-loading, RSC,
startTransitionover app-db, general animation frameworks, resumability machinery — non-goals (resumability is research-tier per R-5).
[TRANSITION] Until the adapter deletion wave (proof/default/soak gates per the
08 §5 Adapters decision: RealWorld-resources green · Story + Xray green ·
SSR/hydration + HMR matrices green · production-specialization + bundle-absence gates
green · templates/docs/examples defaulted · zero repo-owned non-historical
UIx/Helix/slim imports · two consecutive green nightlies + one week with no fallback),
the UIx and Helix adapters (and reagent-slim) remain shipping surfaces governed by the
carried pre-rewrite contract text under these [TRANSITION] markers — the markers, not
git history, are the live contract during the transition (the git tag is provenance
only, never a normative home). The Reagent-tier forms — Form-1/2/3 and the reg-view
family — are not deleted at the wave: they freeze into the stock-Reagent compatibility
tier, whose live normative home is the compatibility appendix
spec/004A-Reagent-Compat.md (lands with the wave; carries the freeze rules, the
preserved Form/reg-view/Reagent-adapter/frame-context sections as live text, the
two-direction boundary contract, the retained API/facade rows, and the two-suite CI
surface — §8 of the compat-boundary contract). Correct but
frozen: contract suite + one smoke in CI; no new capabilities; taught on exactly one
migration page; ui/defview is the only taught component form. Old and new trees
co-mount at explicit boundaries during migration (per the migration guide); the
dataflow layer is untouched throughout. After the wave: Spec 006's host-neutral
contracts, the plain-atom substrate, and benchmark results + fixtures are kept, and a
git tag of UIx/Helix/slim is kept as provenance, not contract; those adapters are
not.
Transition annex — where the frozen family's live contract text is carried until S7.
The compatibility appendix spec/004A-Reagent-Compat.md lands with the deletion wave;
until then the carried pre-rewrite contract text for the frozen Form/reg-view family
lives in the committed homes that already hold it, and these markers name those homes
as the live carriers: 002 (reg-view injection + view ergonomics +
Form pointers), Conventions (the auto-id derivation rule + the
*-suffix pair), API.md (the reg-view / reg-view* / view rows with
shape + semantics notes), 009
(:rf.registry/handler-replaced), and Spec-Schemas (the view
registry-slot shape). Two governing sentences are carried here verbatim so the markers
are the live contract: the authoring-lane rule — a state-touching view MUST be a
registered view (reg-view), never a plain fn — governs the shipping adapters until the
wave; and (rf/view id) re-resolves the current registered implementation per call,
so a hot-reloaded view is picked up on the next render through the id handle.
Stage conformance profiles¶
R-1's staged merge needs an implementable meaning for "conforming". This section is that definition. It merges as part of the spec text and is the device that keeps an intermediate checked-in spec honest: rows tagged above the current implementation stage are declared, not yet asserted — their contract text is final, their enforcement rides their stage's conformance slice, which lands atomically with that stage's spec edits (the 12 §2b spec-landing rule).
Definition. Every normative section of this Spec is tagged with the stage (S1–S7) whose implementation slice first asserts it with conformance fixtures. "Stage-N-conforming" = every row tagged ≤ N passes its named assertions. A row with a "completes" note is asserted at its tagged stage to the tagged scope only; the completing stage extends the assertion. Stage assignments align with the authoritative surface matrix (12 §2b) and the stage contents (08 §2); a conflict is resolved in that order and is a defect in this table.
| Normative section (this Spec) | Stage | What that stage's fixtures assert |
|---|---|---|
| §The portability law and the template AST | S1 | one AST → two emitters; normalized structural equivalence (parity corpus v0); serialisation boundary; closed node set; AST-shape gate |
§ui/defview — grammar, props ABI, options map, registration, rf= comparator |
S1 | declaration arities + diagnostics; props ABI encoding + :key reservation; registrar :view entries; the ruled rf= comparator emitted and asserted against prop-driven re-render (subscription/local interplay asserts S2/S3; stable-shell identity is S2 HMR work) |
| §Template grammar — forms, control forms, rejection roster | S1 | table forms lower; compile-error roster with didactic messages |
| §Template grammar — expression positions (the closed macro grammar) | S2 | audited transparent set lowers with sites indexed below it; unaudited core/user macros rejected with the didactic escape (real-host-analyzer macro authority); bare sub/lease reference below a transparent macro rejected; binding-pattern/:or fences — the rf2-vxgfnd.100 hidden-sub/helper/binder-macro fixtures |
§Template grammar — prop conversion (the rule table; ui/spread) |
S1 | conversion-table fixtures consumed by both emitters (owning table: 004B-UI-Tree-and-Conversion.md); spread dynamic-map cases |
§Template grammar — custom elements (ui/custom-element, RULED grammar) |
S4 | property-vs-attribute classification; SSR attributes-only; W14 fixtures |
§Handlers — event vectors as structural data (manifest flags; JVM-tree :events) |
S1 | vectors/options-maps retained as data in tree + manifest; placeholder keywords retained as keywords |
| §Handlers — committed behaviour (decision table, bare-fn law + lint, dynamic classification, loops, refs, the synchrony door) | S3 | decision-table fixtures; sync-door fixture (input-door predicate; G-8 real-browser matrix is the residual named gate); loop/ref diagnostics |
§Reactive reads — sub |
S2 | one-ViewCell binding; stabilization; conditional reads (the loop rejection is a compile error from S1) |
§Process substrate — ui/adapter |
S2 | exact closed adapter map; canonical :rf.adapter/ui discriminator; copied-kind routing; dispose/re-init; watch re-arm; real provider/render/flush/dispose browser proof |
| §Local state + the placement rule | S3 | local semantics; narrow-law fixtures (same-view handler read conforming; forbidden-tier diagnostics) |
§Effects and ui/dispatch-fn |
S3 | rf= deps + cleanup + StrictMode replay; :connect semantics; loud non-connected failure |
| §Leases (view-side surface) | S2 → confirms S3 | the Resources ownership family (S2 — the resource-lease-reconcile fixtures): closed descriptor validation at finite compiler-owned lexical sites; render/abandonment owns nothing; one independent framework-minted owner per lexical site — an rf=-equal same-site descriptor retains its exact owner, movement retargets to a fresh owner; complete desired-set prevalidation before any mint or dispatch; every ensure queued before any release in deterministic per-frame order, drained as ordinary per-frame FIFO resource events — no global/cross-handler/cross-frame rollback of already-dispatched ensures is promised (transactional multi-acquire rollback is compiled sub observation acquisition's law — 006 §Transactional multi-acquire — not public lease); disconnect / teardown / frame- and root-destroy / Activity / StrictMode / HMR retention and release. View-level resource-lease confirmation fixture (S3) |
| §Loading state is explicit | S2 | sub never fetches; lease acts only after connected commit |
| §Presence | S4 | enter/exit retention; flush-presence! fake-clock fixtures; JVM :present |
§Interop — ui/raw |
S1 → completes S4 | compile form + opaque marker in the tree (S1); foreign-boundary corpus (S4) |
§Interop — ui/html |
S1 | dual-emitter agreement; the single escaping bypass; manifest site recording |
§Interop — ui/error-boundary |
S3 | phase semantics; :reset-key; server-policy contrast |
§Interop — ui/client-only |
S3 → completes S5 | capability-free fallback check (S3); SSR phase flip (S5) |
§Interop — ui/spread |
S1 | with the conversion-table row above |
§Interop — ui/->react |
S6 | compat-boundary fixtures, both nesting directions (the compat-boundary contract) |
§Interop — element / view / portal / re-frame.ui.data |
— | [WAVE-2]: no stage, no assertion, no v1 existence |
| §Roots and mounting — mount grammar, root identity, Root Descriptor v1, client host fns, duplicate Layers 1+3, static frame-plan extraction | S1 | the 004C-Roots-and-Mount.md §10 S1 row |
§Roots and mounting — frame preflight ENSURE (runtime) + frame-root/frame-provider scoping |
S2 | preflight-exactly-once, non-reseed, StrictMode/HMR-immune fixtures |
| §Roots and mounting — hydration + Root Manifest v1 | S5 | manifest extension keys; multi-root hydration + failed-root isolation |
ui.test surfaces this Spec references |
S1 core (render/find/find-all/text/attrs/frame over Tier-1 trees; query enforces the tier split) → S2 mounted semantics (dispatch!; Promise-backed with-root; native-CSS query; Promise-backed zero/thunk flush! on CLJS, synchronous nil on JVM; platform APIs for already-host-owned DOM mechanics, no gesture DSL) → S4 flush-presence! |
selector-grammar fixtures; JVM-subset enforcement; real React mount/query/total-teardown/open-drain/forgotten-await/fixed-point fixtures. Compiled event-vector delivery through native events rides the S3 handler row, not the S2 mount surface |
| §View identity and the instrumentation surface | S3 → budget/absence gates complete S6 | manifests, instance records, cause vectors, Xray consumption (compile-time site anchors exist from S1; the evidence schema asserts S3); production erasure G-7/G-11 |
§The JVM structural subset — structure/props/branches/lists/event intent/ui/html + :rf.error/jvm-host-op |
S1 | Tier-1 rendering against the tree contract |
| §The JVM structural subset — subs via the pure snapshot path | S2 | the Q32/Q22 answer: sub grammar compiles at S1, but no Stage-1 Tier-1 fixture exercises a sub read — a Tier-1 render through a sub site (frame or :sub-overrides) is an S2 assertion |
| §Hot reload — the view-side contract | S2 | the full HMR matrix (08 §2 places it with reactivity, deliberately early) |
| §Removed forms — the absences | S1 | absences are compile errors + export-surface checks from the first slice |
| §Removed forms — [TRANSITION] freeze + the 004A appendix | S7 | deletion-wave soak gates; spec/004A-Reagent-Compat.md lands |
The S1 profile — what the R-1 atomic merge requires: the rows tagged S1 above —
portability law + parity corpus v0 · defview grammar/props-ABI/registration/
comparator · template grammar + compile-error roster · prop conversion + spread ·
event vectors as structural data · raw and html compile forms · root identity +
Root Descriptor v1 + client mounts · ui.test Tier-1 core · the JVM subset's
non-reactive rows · the removed-forms absences. That set passing its named assertions
is "the first conforming Stage-1 slice".
Ripple-row timing (the atomic-merge sets for the inventory below): rows marked
[TRANSITION] and every "moves to 004A" row land at S7 with the appendix;
identity/naming/reservation rows (Conventions reserved :rf.ui/* namespace + artifact
registration + lint key, Ownership's new-surface rows, spec/API.md's re-frame.ui
additions, the 008 ui.test/selector rows) land at S1 with this rewrite;
behaviour rows land with the stage that asserts their subject (002 frame chain → S2,
006 observation port → S2, 009 evidence schema + catalogue rows → their features'
stages in small batches, 011 → S5).
Resolved decisions¶
- R-1 — staged merge. The portability law merges immediately (the interim amendment); this rewrite merges atomically with the first conforming Stage-1 slice — "conforming" is profile-defined: the S1 rows of §Stage conformance profiles.
- R-2 — shapes final. The observation port's six-operation target/evidence/lease ABI is final (the observation-port amendment, merged with the S2 slice, is the sole shape source; the merged 006 amendment carries it) — no provisional shapes remain anywhere in this contract.
- R-3 — naming.
re-frame.ui, aliasui, artifactday8/re-frame2-ui; supportingre-frame.ui.test/.react/ (if earned).data. Separate artifact on a lockstep release train initially (R-6). - R-4 — the narrow bare-fn law + strict lint (§Handlers).
- Presence ruling — wrapper form, no reserved nodes;
:timeout-msmandatory;presence-phasereturns:presentoutside a boundary (§Presence). - Refs policy —
:refreserved; object refs preferred; callback refs explicitui/raw-fn(§Handlers). :on-mount/:on-unmountrejected — mechanical React lifecycle cannot carry domain "once" semantics under StrictMode/Activity/HMR/error recovery;effect :connectis named for what it actually does.- Controlled-input synchrony door — committed; the trigger predicate is confirmed by the controlled-input door spike (the G-8 real-browser input matrix remains the residual named gate) (§Handlers).
- Push ownership committed (context for
sub's one-bridge contract — the pull alternative survives only as a falsification benchmark). - Carried from the checked-in 004: bare-keyword heads never resolve against the view
registry (rf2-n82bbu); no
hmacro; the ephemeral-state placement rule and its ownership by this Spec.