EP-0035: Component-Library Readiness¶
Status: final Type: standards-track Created: 2026-07-16 Resolution: accepted 2026-07-16 (directed); final 2026-07-20
Abstract¶
re-com is the first and most important consumer test for re-frame.ui; the
substrate gets ready for it ahead of the port. The package is deliberately
small — correct three semantics, add two library contracts, clarify two rules,
gate the rest, never cross the wall — putting into the substrate only what a
library cannot build soundly itself; everything else is a guide convention or a
trigger-gated candidate with a recorded ruling.
The normative homes are the Spec 004 rewrite (handler synchrony law, local
placement law, render slots, spread),
spec/004B-UI-Tree-and-Conversion.md
(conversion table), spec/008-Testing.md (G-17/G-18
gate rows),
spec/conformance/S3-view-conformance-profile.md
(G-8/G-15/G-16 rows), and
spec/009-Instrumentation.md (diagnostic
rows); per EP-0009, where this EP and the spec differ, the spec governs. The
package shipped with the S3 conformance slice; the trigger-gated candidates
remain gated on the triggers recorded in Resolved Decisions.
Motivation¶
The tri-analysis of re-com (three analyses plus three cross-reviewed syntheses
of re-com at 0e2675a, no material divergence) found exactly three places
where the S3 contracts as first drafted would force a library into unsound
workarounds:
- Last-write-wins local state. re-com has 348
reset!/swap!lines in 27 files, with multi-writer idioms throughout. A setter computed from the committed render loses batched same-turn writes; a library cannot fix it. - The sync door excludes reusable inputs. The S-5 predicate admits only literal vector handlers; a prefix-receiving reusable input — exactly where the IME/caret guarantee matters most — forfeits it.
- Parameterized markup has no conforming home. Row/item/cell/part renderers (v-table alone injects nine) are compile errors as dynamic heads and cannot be unparameterized template values.
Libraries also need attr passthrough that provably cannot clobber owned
props (36 re-com :attr files) and one manifest projection replacing ~57
parallel args-desc Vars — public-contract decisions with rejected
alternatives; an EP, not a bead.
Specification¶
Scope. This EP amends the S3 contracts so a native component library builds
on re-frame.ui without unsound workarounds — before the port begins; keeps
the amendment minimal (substrate changes only where a library provably cannot
supply the semantics itself); and gates every speculative candidate behind a
named, measurable trigger with a recorded ruling, recording the doctrine and
the wall as durable rationale. It does not own the re-com port itself (a
separate directed program; its ruled product register is recorded here because
it anchors the triggers); it does not cross the wall — no
ratoms/cursors/reactions or generic IDeref, no runtime hiccup interpreter in
core, no parts interpreter, no dynamic heads or blanket ui/element, no
Form-2/3 / component-did-*, no render-phase mutation, no memo opt-out, no
theme registry / widget controllers / async loaders / focus policy, no dual
kwargs/map parser, no CSS/Bootstrap ownership; and it does not reopen the
blessed v1 API freeze — the row-level changes here entered under the freeze's
own delta protocol (deltas #4 and #5).
P0 — three semantic corrections¶
P0-1 · Atomic local updater. (local init) returns a three-tuple
[value set! update!]. set! stores its argument exactly — a stored
function is a value, never an updater. update! applies (f current &
args) to the latest host state, not the committed render's value, so
several same-turn host writers (key + pointer, timer + listener, observer +
handler) compose instead of last-write-wins. Both are legal in committed
handlers and effect callbacks; render-phase use remains the existing dev
error; update!'s writer composition is proven by G-15's writer composition
and render count — not a :local-state cause row, which is specified for S6;
it rides the HMR hook signature, and raises the same typed
:rf.error/jvm-host-op on the JVM.
Out: reset-key / derived local (a trigger-gated spike) and fn-overloaded
setters.
P0-2 · Sync-door widening: compiler-known ui/event vector outcomes. At
a compiler-proven controlled DOM site (the unchanged S-5 predicate: literal
:value/:checked co-present with the handler site), a synchronous
ui/event body whose result is an event vector rides the same synchronous
drain as a literal vector handler — (ui/event [e] (conj on-change (.. e
-target -value))) keeps the IME/caret guarantee for a prefix-receiving
library input. nil still means no dispatch; any other synchronous result
is a didactic diagnostic. The site proof stays static though prefix and
payload are runtime values; dynamic handler values still batch with the
existing diagnostic. The competing compiled event-template projection is
demoted — a second handler language — revisited only on proven JVM/Xray
inadequacy evidence.
P0-3 · Internal compiled render slots. ui/render-fn is valid for
internal library seams (not only foreign boundaries), invoked
exclusively through the compiler-owned form ui/slot. ui/slot accepts
only ui/render-fn values or nil; the callback body is lexically visible at
the consumer call site and therefore compiled (both emitters, closed
grammar inside); the body is pure render phase —
sub/local/effect/dispatch/hooks inside are didactic errors;
result normalization/error behaviour, keys/occurrence identity,
capability/fingerprint propagation, ui.test structural representation,
child-like memo cost, and manifest slot sites are all specified. The
slot-body grammar must permit statically-referenced internal view heads —
a stateful replacement part is a pure slot body mounting a static defview
that owns its own state; that coverage argument keeps registered ui/view
gated. This is the load-bearing middle of the customization taxonomy: data
props · pure render slots · registered stateful views (gated).
P1 — two library-facing contracts¶
P1-4 · Literal safe-spread policy. A policy form joins ui/spread as the
distinct sibling name ui/spread-safe — not a second spread arity —
with compiler-visible allow/deny: denied structural/controlled/identity keys
(:key :ref :value :checked owned :on-*) are rejected in every
build, never dev-only; allowed :on-* values classify through the handler
decision table; aria-*/data-*/title/class/style pass through per the
004B conversion table. A controlled site under the policy form retains
the sync door; general ui/spread remains the visible-cost escape and still
forfeits it.
P1-5 · Versioned docs/slot manifest projection. The dev/test view-manifest projection becomes a versioned public shape exposing the literal props schema, per-prop docs/defaults, declared part/render-slot metadata, source sites, and capability cost — the one projection Story, docs, Xray, and agents consume. No runtime handles egress; production absence is proven under G-7/G-11.
Clarifications (prose, not machinery)¶
- C-6 · Native-library layout/ref blessing.
ui/ref(the substrate DOM-node ref, promoted from there-frame.ui.reactref hook — rf2-u53yy.9) +react/use-layout-effect(call shapes frozen at S1) gain a second sanctioned audience: native-library measure-before-paint (popover/dropdown positioning, table geometry) — measure in the layout effect, compute placement with pure.cljcgeometry, apply, clean up exactly; guide recipe + StrictMode/reconnect/HMR/JVM fixtures ship with it. No second layout-effect spelling;component-did-*never returns. - C-13a · Internal fn-props ruling. Ordinary function props between
internal views are legal opaque values, identity-compared, with no
implicit invocation phase; special phases need
ui/handler/ui/render-fn. - C-13b · Library event convention (guide). Libraries accept event
vectors/prefixes and dispatch
(conj prefix payload…); placeholders are compile-time, never expanded inside vectors received through props;dispatch-conj-style helpers are library code, never core.
Gates¶
| Gate | Asserts |
|---|---|
| G-8 (widened) | the real-browser Chromium/WebKit matrix (IME composition, caret-on-restore, event ordering, pre-paint) gains a reusable event-prefix component arm — a library-shaped control receiving its event vector via props through the ui/event door; toy literals alone do not close G-8 |
| G-15 atomic-local writer matrix | N same-turn host writers through update! all land (key+pointer, timer+listener, observer+handler arms); fn-value set! stores exactly; mixed update!+dispatch; StrictMode replay; JVM typed failure; writer composition + render count (not a :local-state cause row — that cause is specified for S6); HMR ride |
| G-16 render-slot parity | client/JVM slotted output normalized-structurally equivalent; keyed reorder under slots; purity diagnostics fire inside slot bodies; manifest slot sites present |
| G-17 safe-spread ownership | owned-key rejection in dev and advanced builds; aria-*/data-* pass; policy form retains the sync door while general spread forfeits it |
| G-18 library façade isolation | an advanced build importing one view retains no unused sibling views — fixture-first: a packaging change is justified only if this fails structurally |
The component-library proof pack's six library views are rendered and
exercised by a mounted DOM suite (library-dom-cljs-test), not the parity
corpus — four are host-bearing (ui/sub, ui/local, ui/effect) and so
cannot be .cljc dual-emitter corpus rows (no re-com build dependency):
controlled input · selection controller · slotted list cell · safe-attrs form
control · schema-described component · inline popover; a single-view advanced
import proves façade isolation (G-18). EP-0034 owns the roster of record and
the wiring status.
The trigger-gated candidate register¶
Nothing below is pre-approved; each row's full ruling is in Resolved Decisions:
| Candidate | Disposition |
|---|---|
reset-key local |
SCHEDULED spike at port Wave-2 entry; key is an explicit caller revision, never the model value; initial re-evaluates at reset |
lexical ui/tpl |
Wave-3 checkpoint; if fired, pre-committed as literal-only sugar over zero-arg ui/render-fn + ui/slot, never a parallel primitive |
registered ui/view |
gate closed for re-com v1; trigger refined to runtime-open identity (never mere statefulness) |
ui/portal |
superseded-in-part: overlay plan of record is inline + native top-layer (dialog.showModal(), the popover attribute); graduation bar raised |
defview-alias |
fallback-first (canonical defviews in the public ns); if fired, a meta-copying macro; the bare-alias dev warning is ruled in and shipped |
| event-template projection | demoted (second handler language); revisit only on JVM/Xray inadequacy evidence from P0-2 |
| library DCE/packaging | fixture-first: only a structural G-18 failure justifies packaging work; the emitter already goog.DEBUG-gates registration (production arm is a direct React.memo) |
The doctrine and the wall¶
The future-proofing thesis is closed extensibility: complete a finite set of compiler-visible seams rather than making the runtime permissive — every executable value crossing a library boundary has an explicit phase, identity, ownership, host meaning, evidence shape, and production cost. A consumer that appears to need the wall crossed gets an API redesign or a named interop boundary, never a substrate exception.
Guide impact¶
The events guide gains the C-13b event-prefix convention; the interop guide
gains the C-6 measure-before-paint recipe and the safe-spread passthrough
pattern; the component-library authoring guide lands from port Wave 0 (R-6);
the four-lane content convention (see the ui/tpl ruling) is publishable
ahead of Wave 3.
Rationale¶
One test, applied three ways. Substrate or library? — only semantics a library cannot build soundly from outside become substrate (the passthrough policy and manifest projection qualify because every-build rejection and production absence are unfakeable from library land). Independent value? — every P0/P1 item is needed by grids, menus, and design systems generally; re-com is the demand-bar consumer, not the specification. Trigger or now? — anything contingent on port-wave evidence gets a gate with a measurable trigger and a pre-ruled shape, so firing a gate later is execution, not design reopening; each gate ruling also permanently rejected a permissive alternative, and those refusals are half an EP's value.
Backwards Compatibility¶
Pre-alpha; no shims. local's two-tuple destructuring sites migrate to the
three-tuple (the migration skill's swap! rewrite retargets to update! for
multi-writer idioms). The sync-door widening is
strictly additive at proven-controlled sites — no narrower door was ever
shipped law — and the v1 API freeze is not
reopened: deltas #4/#5 entered under the freeze's own delta protocol.
Erratum (2026-07-20, rf2-3qb5w). The pre-#6560 text of the clause above read "the migration skill's
swap!rewrite retargets toupdate!, retiring its MANUAL flag on multi-writer idioms." The MANUAL-flag language described the retired codemod-era plan; the shipped Reagent→re-frame.uimigration skill (W1,skills/reagent-migration— EP-0030) is an AI skill that reasons through judgment cases with the author and emits no MANUAL flag. Theswap!→update!migration fact is unchanged and preserved above; only the retired flag language is corrected. Meaning frozen, not bytes (EP-0009, rf2-r7ahi).
Resolved Decisions¶
Delegated rulings (2026-07-16):
- Reset-key
local— SCHEDULED, with the init ruling. The bounded(local initial reset-key)spike runs at port Wave-2 entry with the controlled-vs-commit/draft input classification (aborting if every internal-model case dissolves into redesign). Non-negotiable: the key is an explicit caller revision, never the model value — value-equality is provably blind to same-value rejection. Init ruling:initialis re-evaluated at reset, render-pure and retry-correct. Mechanism: the bounded compiler-emitted adjust-during-render idiom (effect-based reset, key-remount, and general render-time state setting all rejected). Public promotion needs a second distinct buffered consumer + all ten pins green. ui/tpl— demand-gated; pre-committed as slot sugar. Not in v1; runtime hiccup values / a general template function rejected permanently. If the measurable trigger fires (≥3 unrelated components + ≥20 materially-improved call sites + ≥1 defect beyond verbosity), the authorized spike is literal-only call-site sugar lowering onto zero-argui/render-fn+ui/slot. Primary checkpoint is Wave 3 (popovers); Wave 1 is early-warning only. The library-wide content convention is decided now, tpl-independently: rich content → child position/compound children; parameterized fragments →ui/render-fnviaui/slot; unparameterized named fragments → zero-argui/render-fn; strings stay data props.- Registered
ui/view— gate closed; trigger refined. Statefulness and runtime-chosen identity are orthogonal; only the latter needs a registry. A stateful replacement is already expressible as a pure slot body mounting a static stateful defview; runtime choice among a known finite set compiles as acaseover view heads (re-com has zero open-set-id part sites, and firing would create the substrate's first-ever production registry). The trigger fires only on irreducibly runtime-open identity (open-set ids from config/app-db/CMS) with finitecaseandre-frame.ui.databoth proven inadequate, or a parts-ceiling escalation; generalui/elementis rejected outright, forever. ui/portal— superseded-in-part by inline + top-layer. Overlay plan of record:<dialog>+showModal()for modals (native backdrop, inert background, Esc, focus return); thepopoverattribute + measured geometry for anchored overlays;position:fixed+ pure geometry elsewhere. re-com uses zero portals (parity needs none); the top layer is immune to its fixed-position emulation's failure modes. Graduation requires evidence the top layer cannot solve it (foreign-DOM-node rendering is the one portal-only capability); a thincreatePortalwrapper is rejected permanently.defview-alias— fallback-first; dev warning ruled in. The facade strategy is canonicaldefviews in the public namespace, impl namespaces demoted to helpers; try that first. A bare(def alias impl/view)renders correctly but silently loses compile-time props/children checks and view identity — so a dev-only warning when a foreign-classified head's runtime value is a registered view shell is ruled in, and has shipped. If the trigger fires (material facade-prototype harm, or a consumer incident from silent check-loss), the authorized shape is a meta-copying macro — no compiler naming graph; wrapper views / dynamic forwarding rejected permanently.
The port product register — R-1..R-10, all ruled:
| # | Ruling |
|---|---|
| R-1 packaging | same repo; native re-com/re-com-ui with re-com.ui.* (coordinate confirmed final 2026-07-17); the coexisting Reagent legacy keeps re-com.* |
| R-2 parts ceiling | data/style values + pure compiled slots only; slots receive the component's declared state; no registered stateful replacements in v1 |
| R-3 uncontrolled tier | narrow, per-component :default-value; :value XOR :default-value validated; type-based ownership inference rejected permanently |
| R-4 theme transport | explicit props + frame subscriptions; theme values plain data only (context graduation stays non-breaking) |
| R-5 tables/grids | foreign-virtualization-first behind a re-com-owned data/event/slot boundary; four named criteria to ever go native; engine selection is a Wave-4-entry decision |
| R-6 helper artifact | generic internals + the authoring guide from Wave 0; a public middle artifact only on a second consuming library |
| R-7 input ownership | ownership law approved now (controlled roster vs commit/draft vs local interaction state; rejected drafts identified only by explicit :reset-key); per-component roster frozen at Wave-2 entry |
| R-8 dates/times | host-neutral ISO-8601 strings as the canonical ABI (EP-0029 consistency); no goog.date/js/Date/java.time across the public ABI |
| R-9 accessibility | WAI-ARIA APG semantics per component class, shipped with each component's wave — legacy parity (one aria- attribute total) is not the bar |
| R-10 CSS/Bootstrap | retained through the parity waves as an isolated, removable theme-data layer; removal is a named post-parity decision |
Open Issues¶
None. Every gated candidate carries a recorded ruling and a measurable
trigger (Resolved Decisions); firing one later is execution, not design
reopening. The package's contract text lives in its normative homes with
spec/008/spec/009 carrying gates and catalogue rows; the port proceeds on
its ruled register — the substrate never waits on it, and the wall never
moves.
References¶
- EP-0030 — the
re-frame.uiprogram this package amends; the S3 stage carried the fold-in. - EP-0031 — the compiled-view
contracts this package amends: handler decision table + synchrony law,
local/effectplacement law, template grammar,ui/spread. - EP-0032 / EP-0033 / EP-0034 — siblings of this wave owning the adjacent touched surfaces — interop tier, tool/evidence projections, conformance/production gates; amendments land in their normative homes.
- EP-0029 — the ISO-8601 precedent: the port register's date/time ruling (R-8) deliberately matches the XState-v6 duration ruling.
- Specs:
spec/004D-Freehand-Compiled-Grammar.md,spec/004B-UI-Tree-and-Conversion.md,spec/006-ReactiveSubstrate.md,spec/008-Testing.md,spec/009-Instrumentation.md; the blessed table's delta record inspec/API.md§"re-frame.ui — blessed public-surface freeze". - Design provenance: the readiness delta document graduated into this EP and
the P0/P1 spec homes from the tombstoned synthesis tree
(
ai/findings/new-substrate-synthesis/); the tri-synthesis consensus analyses underai/findings/re-com-port/.