S4 view-substrate conformance profile¶
Status: Reference · owned by rf2-yho9j (the S4 conformance gate).
This is the single authoritative catalogue of the frozen S4 surface of
re-frame.ui — the compiled-view substrate's presence and web-boundaries
stage. It fixes what S4 froze (the presence retention boundary, the ruled
custom-element classification, the compile-tier a11y roster, the foreign
boundary, and the explicit non-surfaces) and names, for each row, the suite or
gate that proves it. The runtime behaviour is proven by those homes (browser /
JVM / corpus suites), not re-asserted in prose here.
0. Inheritance — this profile is cumulative and rows only the S4 delta¶
"S4-conforming" means S3-conforming plus everything below. S1–S3 are
inherited by exact reference to the frozen S3 profile,
spec/conformance/S3-view-conformance-profile.md,
which remains the sole authority for the ten compiler-owned verbs, the
route-link view, and the seven-wrapper React interop tier. This document
does not restate any of it — restating a frozen roster is how the two
profiles drift apart. It rows only the S4 delta:
- presence (
ui/presence,ui/presence-phase,ui.test/flush-presence!); - the ruled custom-element property-vs-attribute classification;
- the completed
ui/rawforeign boundary (andui/html's no-sanitisation contract); - the ruled compile-tier a11y roster;
- the no-compiled-head structural policy, by reference to its ruling.
S4 adds no new verb to the re-frame.ui facade. Its forms were exported at
S1 for symbol resolution and assert at S4 — the stage froze behaviour
over an already-blessed surface. The drift guard composes with the S3 guard
rather than duplicating it: it reuses that guard's row-binding machinery and its
published rosters, and fails loudly on a new public surface, a broken
inheritance link, or a moved or deleted proof row.
The frozen surface and the proof references are kept honest by an executable
drift guard,
implementation/ui/test/re_frame/ui/s4_conformance_profile_jvm_test.clj: its
frozen-s4-forms, frozen-a11y-roster, frozen-s4-parity-cases and
s4-arm-gates maps are the source of truth this document restates. Every
roster is bound row by row — the guard fails if a §1 row is missing or
duplicated, if a row stops naming the error, kind, contract fragments or proof
homes its map entry fixes, if a named proof home is not a real test namespace,
if the §4 corpus row set drifts from the shipped parity cases, or if the §6
roster stops naming exactly the certified S4 arms. Deleting a row, hollowing
one out, or swapping a contract or proof home between rows is red.
1. Frozen S4 surface¶
Four closed rosters: the presence boundary and its phase read, the ruled custom-element classification, the compile-tier a11y diagnostics, and the foreign boundary.
1.1 Presence — declarative enter/exit retention¶
ui/presence is a bounded retention boundary, not an animation system. A
(ui/presence {:timeout-ms n} keyed-children) boundary tracks its children by
key: a key passes :mounting → :present; a key that leaves the incoming set is
retained in :unmounting for exactly :timeout-ms — the mandatory exit
retention duration and terminal bound — after which removal is terminal and
exactly-once (React unmounts the retained subtree, releasing every handle and
effect it owns). Removal-then-reinsertion interrupts the exit and re-enters at
:present.
Presence is DOM-agnostic and timeout-only (ruled, rf2-0ufty): the boundary
inserts no wrapper node, stamps no attributes, and observes no DOM events. A
presence-aware child owns its own exit styling and accessibility by reading
(ui/presence-phase).
These rows state the LANDED presence grammar. The two S4-epic bugs that changed the accepted grammar shipped in #6331:
rf2-vxgfnd.96.1(keyed literal children were wrongly rejected — the key-presence probe read an:element's key at the node rather than inside its analyzed props) andrf2-vxgfnd.96.2(duplicate ownership identities were wrongly accepted — two children of one boundary claiming one key now drop the later claimant under the existing:rf.error/ui-duplicate-key, rather than aliasing the first's phase and exit timer). The fragment correctionrf2-xoz1s(#6337) closed the last permissive path:[:<> {} …]reported its props map's presence as a key, so a props-bearing but unkeyed fragment passed the keyed-child requirement while offering the boundary no identity to retain. The rows below are therefore the grammar the shipped compiler enforces, and no presence grammar bug is open — which is what makes the §8 declaration honest.
| Form | Kind | Contract | Loud failure | Proven by |
|---|---|---|---|---|
ui/presence |
template form (compiler-owned); direct call fails loud | keyed enter/exit retention; :timeout-ms is mandatory — the exit retention duration and the terminal bound; terminal removal is exactly-once; re-insertion interrupts the exit; the boundary contributes no node of its own |
direct call :rf.error/ui-tree-malformed; a missing/non-positive :timeout-ms or a bad opts map is :rf.ui.compile/bad-presence; an unkeyed child is a build failure, :rf.ui.compile/presence-unkeyed-child |
JVM re-frame.ui.presence-jvm-test; CLJS re-frame.ui.presence-dom-cljs-test |
ui/presence-phase |
ordinary fn — the single phase read | :mounting / :present / :unmounting inside a boundary; :present outside one, so presence-aware children stay reusable anywhere; the JVM structural subset always yields :present |
none — a direct call is legal and returns :present |
JVM re-frame.ui.presence-jvm-test; CLJS re-frame.ui.presence-dom-cljs-test |
ui.test/flush-presence! |
test-surface driver | advances the presence fake clock so retained children reach their :timeout-ms removal with no wall-clock sleep: (flush-presence!) drains to quiescence, (flush-presence! ms) advances by ms; the sole and complete exit driver in tests; CLJS mounted host only — the JVM structural render has no presence lifecycle (it renders :present), so there is no JVM flush-presence! |
none | JVM re-frame.ui.presence-jvm-test; CLJS re-frame.ui.presence-dom-cljs-test |
The pure three-phase entry derivation (reconcile) is host-shared and unit-tested
in re-frame.ui.presence-reconcile-cljs-test.
1.2 Custom elements — the ruled property-vs-attribute classification¶
ui/custom-element exports at S1; its classification behaviour asserts at
S4. The declaration is the sole classifier — never an attribute-name
heuristic:
| Form | Kind | Contract | Loud failure | Proven by |
|---|---|---|---|---|
ui/custom-element |
def-level declaration macro | closed {:properties #{…}} grammar; the declaration is the sole classifier — a declared name is a PROPERTY under the ruled kebab → camelCase JS property name (:accent-color → accentColor), every other name is an ATTRIBUTE, an undeclared element is all-attributes, and a declared name that is also a standard HTML attribute spelling is still a PROPERTY |
:rf.ui.compile/bad-custom-element; two sources declaring one tag with different sets is :rf.ui.compile/custom-element-conflict |
JVM re-frame.ui.custom-element-classification-jvm-test; CLJS re-frame.ui.custom-element-classification-dom-cljs-test |
Host split. The JVM emitter carries attributes only into markup —
property-classified names are omitted by normalization N, because a server
cannot set a property — and the properties apply at hydration on the client,
where React assigns them to the mounted element under the camelCase name. The
:rf.ui/property-props marker that names the classification is a reserved
diagnostic and never reaches the semantic space.
1.3 The a11y compile-tier diagnostic roster¶
Four compile-tier warnings, minted by re-frame.ui.compiler.a11y. They are
:rf.ui.compile/* ids, which means: no Spec 009 catalogue row, no runtime emission,
and no production egress. A compile-roster finding is printed by the compiler
and recorded as a manifest :diagnostics fact. The roster is closed; an
addition is a roster change.
The charter is high confidence: a false positive is worse than a miss, so each check is proven in both directions and the silent direction is the load-bearing one.
| Diagnostic | Fires on | Author remedy |
|---|---|---|
:rf.ui.compile/a11y-missing-accessible-name |
an interactive element with no discernible accessible name | give it text content, aria-label, or aria-labelledby |
:rf.ui.compile/a11y-invalid-literal-aria |
a literal aria-* value outside the pinned WAI-ARIA vocabulary |
correct the literal; a runtime-computed value is never flagged |
:rf.ui.compile/a11y-click-non-interactive |
a click handler on a non-interactive element with no keyboard path | use a real control, or add role + keyboard handling |
:rf.ui.compile/a11y-presence-exit-interactive |
inline literal markup under a presence boundary carrying focusable content — the one shape where the author remedy is structurally unavailable, because inline props evaluate in the parent's render, outside the per-child phase Provider | extract a keyed child view that reads (ui/presence-phase) = :unmounting and stamps its own inert / aria-hidden |
Suppression. ^{:rf.ui/suppress {<id> "reason"}} silences the printed
warning while the finding remains a manifest :diagnostics fact carrying its
reason. A malformed suppression — an unknown id, a blank or non-string reason —
is the loud compile error :rf.ui.compile/bad-suppress, never a silent no-op.
Proven by re-frame.freehand.a11y-diagnostics-cljs-test (host-shared, pure analyzer);
the diagnostics reach tooling through the Xray diagnostics channel.
1.4 The foreign boundary¶
The boundary has two sides and one law each; ui/html is the third foreign edge.
| Form | Kind | Contract | Loud failure | Proven by |
|---|---|---|---|---|
ui/raw |
host-bearing template form | a foreign element that cannot render on the JVM; the raise is lazy — an untaken branch never evaluates its argument, and compiled siblings render normally around the absent boundary | rendered on the JVM: :rf.error/jvm-host-op naming :op :ui/raw; bad compile form :rf.ui.compile/bad-raw |
JVM re-frame.ui.raw-foreign-boundary-jvm-test; CLJS re-frame.ui.raw-foreign-boundary-dom-cljs-test |
ui/raw-fn |
host-bearing callback-ref form | a foreign callback in prop position; becomes the opaque {:rf.ui/opaque :ui/raw-fn} marker |
in child position :rf.ui.compile/raw-fn-child |
JVM re-frame.ui.raw-foreign-boundary-jvm-test; CLJS re-frame.ui.raw-foreign-boundary-dom-cljs-test |
| foreign component head | host-bearing call head | a resolvable var without view metadata; a distinct boundary from ui/raw and the error says so |
rendered on the JVM: :rf.error/jvm-host-op naming :op :foreign-component |
JVM re-frame.ui.raw-foreign-boundary-jvm-test; CLJS re-frame.ui.raw-foreign-boundary-dom-cljs-test |
ui/html |
trusted-markup leaf | the single escaping bypass. re-frame.ui sanitises nothing — no escaping, no tag filter, no attribute filter, no javascript:-scheme gate; the only runtime guard is a string? shape check (a non-string is :rf.error/ui-tree-malformed), never a content check; an empty string is a legal inert bypass |
non-string value :rf.error/ui-tree-malformed; bad compile form :rf.ui.compile/bad-html; not the sole child :rf.ui.compile/html-not-sole-child |
JVM re-frame.ui.raw-foreign-boundary-jvm-test; CLJS re-frame.ui.raw-foreign-boundary-dom-cljs-test |
A foreign value in prop position is opaque. It becomes the
{:rf.ui/opaque :foreign} (or {:rf.ui/opaque :ui/raw-fn}) marker and the value
itself is never evaluated and never enters the tree — so no host object can
leak into a structural tree, a fingerprint, or a serialised payload, and no prop
silently degrades to nil.
2. Host behaviour¶
JVM structural render (Tier 1). A presence boundary renders a fragment
carrying the reserved :rf.ui/presence {:phase :present :timeout-ms n}
diagnostic marker; the JVM has no lifecycle, so every keyed child renders and no
retention timer exists. An empty boundary still emits its marker fragment. The
marker is a droppable diagnostic — normalization N strips it, so it never
changes markup or a fingerprint. (ui/presence-phase) is :present.
Custom-element markup is attributes only. ui/raw and a foreign head raise
:rf.error/jvm-host-op; ui/html renders as an opaque verbatim leaf.
CLJS mounted render (Tier 3). The three-phase machine runs against a real
react-dom/client mount: a keyed child enters :mounting → :present; a removed
key is retained :unmounting until flush-presence! reaches :timeout-ms, then
it is removed and its ownership released exactly once; a partial advance
leaves a not-yet-due exit retained; re-insertion cancels the pending exit without
running its removal. Declared custom-element properties are applied to the
mounted element as JS properties under the camelCase name and are not
reflected as any attribute spelling; undeclared names ride as attributes.
Cross-host parity. The S4 rows the compiled-view parity corpus carries are catalogued in §4.
3. The document head is host-owned¶
No compiled structural form renders into <head>. There is no
head-targeting form, no ui/head, and no head channel in the view AST or the
JVM tree; re-frame.ui mounts into a root element inside <body>. ui/html
does not widen this — it is the sole child of a DOM element, and <head> is
neither a mount target nor reachable from a template. The document head belongs
to the host: the HTML shell the app serves and, for server-rendered apps,
Spec 011's structured reg-head / active-head channel.
ui/raw does widen it, and that is deliberate. A foreign value handed to
React verbatim may be a portal, and a portal renders into whatever container
the caller names, document.head included. Nodes placed that way are host
behaviour the caller owns — outside head ordering, de-duplication,
precedence, and every hydration guarantee, :rf/head-hash mismatch detection
included. Enumerability is not what survives (rf2-wjgle). The :raw
capability makes ui/raw's own sites listable in the manifest, but that hatch
is not the only way into host React: a runtime React element reaching an
ordinary dynamic child position is handed to React with nothing recorded at the
site, so the escapes a manifest can list are a strict subset of the escapes that
exist. The normative statement — and the open design question a real containment
would have to answer — is
004D §The document head is host-owned.
What does survive is the JVM's absence: the JVM tree has no such route —
:raw raises :rf.error/jvm-host-op — so a server-rendered head still comes
only from reg-head / active-head.
Normative ownership (ruled, rf2-3i7tr) — do not reopen. Spec 004
normatively owns the compiled-view structural absence (no ui/head, no head
target, no portable head channel, qualified at the ui/raw foreign boundary).
Spec 011 normatively owns the structured document-head mechanism (reg-head /
render-head / active-head, head-model shape, escaping, shell emission,
ordering, :rf/head-hash, mismatch behaviour). Spec 009 owns only the
trace/error projection. S4 adds no head machinery and takes no position
beyond citing that ruling.
This section states an absence of surface, not a behaviour, so it has no
behavioural fixture. What the drift guard binds is exactly the absence: the
re-frame.ui facade exposes no head-targeting public, and the compiler's closed
form set contains no head op.
4. W14 dual-emitter parity — the S4 corpus rows¶
The compiled-view parity corpus (parity_fixtures.cljc + the JVM/CLJS corpus
tests) proves normalized structural equivalence across the JVM tree and the
CLJS react-dom/server emitter. Parity is equivalence in normalization N's
semantic space — byte-identical HTML is not the contract. S4 contributes these
rows (the view-shape sweep, W14):
| Corpus case | The S4 claim it pins |
|---|---|
:presence-two |
a presence boundary contributes no node of its own and passes every incoming keyed child view through exactly once — the JVM marker fragment is stripped by N, the client's per-child phase Provider is invisible in markup, and both sides land on the same children in the same order |
:presence-empty |
an empty boundary is structurally inert on both hosts |
:presence-inline |
presence mid-tree over inline keyed literal markup — the second legal child shape — splices identically, and its siblings are undisturbed |
:ce-declared-attr-name |
the adversarial classification row: a declared name that is also a standard HTML attribute spelling (:tab-index) stays a PROPERTY on both emitters and is written into markup by neither, while undeclared siblings stay ordinary attributes |
:trusted-hostile |
ui/html crosses verbatim on both hosts — a javascript: scheme is not gated, and nothing about the string is inspected |
4.1 The intentional NON-parity facts¶
Some S4 facts are not structural-equivalence facts and must never be forced through the comparator — a fixture that pretends otherwise is a false green. Each is rowed here against the paired or compile-time proof home that really owns it:
| S4 fact | Why it is not a parity fact | Real proof home |
|---|---|---|
ui/raw / foreign component head |
a host escape — rendering it on the JVM raises :rf.error/jvm-host-op by contract, so there is no JVM side to compare |
paired: re-frame.ui.raw-foreign-boundary-jvm-test + re-frame.ui.raw-foreign-boundary-dom-cljs-test |
| the document head | an absence of surface (§3), not an emitted shape — there is nothing for either emitter to produce | the facade/op-set absence guards in this profile's drift guard |
| the a11y roster | compile-time analyzer facts — findings are produced during analysis and never reach either emitter's output | re-frame.freehand.a11y-diagnostics-cljs-test (host-shared, pure analyzer) |
| presence's three-phase machine | host-bearing — retention timers, and the phase a first render reports, are mounted questions (and, for the server, S5's) | re-frame.ui.presence-dom-cljs-test (mounted) + re-frame.ui.presence-jvm-test (structural) |
The corpus rows above carry only the portable half of each S4 surface — the structural output both emitters must agree on.
For presence the portable half is precisely this: the JVM emits its :present
marker fragment and N strips it; the client wraps each child in a phase
Provider, which is invisible in markup. Both therefore emit exactly the
children — the boundary's own contribution to the output is zero nodes on both
hosts. The phase value itself is not portable at first render: the JVM
structural subset reports :present, while an ordinary first client render
necessarily enters :mounting (the component starts with no committed entries),
which is correct pre-hydration. Reconciling those — so a server-adopted child
starts :present without fabricating an enter transition — is S5's obligation,
recorded as the rf2-d9yq3 rider in §7. No corpus row may assert a phase value.
5. Non-surfaces (the S4 wall)¶
S4 deliberately does not ship, and a consumer that appears to need one gets an API redesign or a named interop boundary, never a substrate exception:
- presence is not an animation system — no easing, no curves, no presence event bus; anything beyond enter/exit retention is out of scope;
- no presence wrapper node, clone/ref contract, or reserved DOM node; the boundary is DOM-agnostic;
- no automatic
transitionendearly completion, no automaticinert/aria-hiddenon exit, no framework-owned reduced-motion path — all three were retracted (rf2-0ufty); the presence-aware child owns them; - no optional
:timeout-ms— it is mandatory, and it is the exit duration, not a cap over a completion mechanism that does not exist; - no attribute-name heuristic for custom elements — the declaration is the sole classifier; no open/runtime property set;
- no sanitisation in
ui/html— no escaping, tag filter, attribute filter, or scheme gate; provenance is the author's obligation; - no head-targeting form,
ui/head, or head channel in the AST or JVM tree; - no Spec 009 runtime rows for the a11y roster, and no production egress of any diagnostic string; the a11y ids are compile-tier only;
- no silent a11y suppression — an unknown id or a blank reason is a loud compile error.
Deliberately deferred to S5 (rf2-vxgfnd.97) — S4 claims no conformance for
any of it: the Root Manifest (v1 extension keys, wire/discovery) and
hydration (preflight, idempotent payload install, locator/registry conflicts,
fingerprint/digest validation); render-static; the client-only phase
flip; multi-root hydration and failed-root isolation; and the
root-hydration half of W14. hydrate-root remains deliberately fail-loud until
that stage. A fixture here that appeared to prove any of them would be stage
collapse, not coverage.
6. The gate roster (S4 arms)¶
The full G-1..G-18 roster lives in docs/EP/EP-0034 §4 and the 07 §5 gate
roster. S4 introduces no new named gate; it grows the arms of two existing
ones:
| Gate | S4 arm |
|---|---|
| G-7 | dev↔prod equivalence for S4 shapes only — the S4 presence / custom-element corpus rows' generated structure agrees with debug off (Layer 1, via the advanced parity corpus); presence retention/removal and custom-element property application are mounted arms, named-open under the S6 leaf (rf2-55zsd); the a11y roster is compile-tier and emits nothing at runtime in either build |
| G-11 | S4 diagnostic / site / reload / test machinery absent from advanced production output — re-frame.ui.test (now including flush-presence!) and every compile-tier diagnostic string stay out of advanced production bundles |
The G-7 growth is bounded, deliberately. S4's presence and custom-element
corpus rows ride Layer 1 — the advanced parity-corpus prod-elision proof —
so their generated structure is proven dev↔prod-equivalent with debug off,
for S4 shapes only and no wider. What is not yet executable is the
mounted behaviour these shapes add: presence retention/removal over time and
custom-element DOM property application. Those arms are named-open under the S6
leaf rf2-55zsd (per-production-wrapper mounted causal fixtures); S4 does
not discharge them, and no row in this profile should be read as claiming the
mounted behaviour is proven. The broader retroactive S1–S3 equivalence-matrix
debt is likewise not in scope here. Layer 1 wires into the existing
required JVM / CLJS / DOM / parity / production-elision jobs — no new job, no new
named gate, and no S5 SSR build in the S4 matrix.
Everything else S4 froze is proven by the named suites in §1 and the parity corpus in §4, not by a gate arm.
7. Hand-off to S5¶
S5 (rf2-vxgfnd.97) already carries the obligations that S4's surfaces create.
This row cites them; it creates no parallel obligation:
Obligation (recorded on rf2-vxgfnd.97) |
Origin |
|---|---|
| S5-obligation (1) — root-manifest hydration + failed-root isolation fixtures | the residual named gate from the stage plan |
AUDIT RIDER — a multi-root fixture proving hydrated presence starts :present (while a client-only mount still observes :mounting → :present), with sibling-root isolation |
rf2-d9yq3 / PR #6261 |
| AUDIT RIDER — one real SSR-adoption custom-element fixture: server markup carries declared attributes only and omits the declared property under both spellings; hydrate without replacement; the declared property applies under camelCase | rf2-aozoi / PR #6276 |
S4 leaves the canonical semantic normalizer (Spec 004B's N) and the
render-fingerprint vocabulary as the contract S5 consumes — unchanged by this
stage, and the reason a hydration fixture can be written at S5 without
re-deriving the comparison space.
8. Conformance grading¶
An S4-conforming host emits, for the frozen surface above: the presence
retention contract with its mandatory :timeout-ms, exactly-once terminal
removal, and unkeyed-child build failure (§1.1); the ruled custom-element
classification with its JVM attributes-only / client properties-at-hydration
split (§1.2); the four compile-tier a11y diagnostics with their suppression round
trip (§1.3); the foreign-boundary laws and ui/html's no-sanitisation contract
(§1.4); the host behaviour of §2; a host-owned document head (§3); the parity
rows of §4; and none of the non-surfaces (§5). It must also be S3-conforming
— §0 inherits S1–S3 by reference, and a host that fails the S3 profile fails
this one. The gate arms in §6 and the suites named throughout are the executable
acceptance; scripts/test-fast-pr.sh plus the S4 CI matrix run them.
S4 IS DECLARED CONFORMING (rf2-vxgfnd.96.3). Every presence grammar bug
that gated this declaration has shipped: rf2-vxgfnd.96.1 and
rf2-vxgfnd.96.2 in #6331, and the [:<> {} …] fragment correction
rf2-xoz1s in #6337. §1.1's rows therefore state the grammar the shipped
compiler enforces, not a post-fix target, and the declaration rests on evidence
this profile already carries — the cumulative inheritance of §0, the named
proof homes of §1, the corpus rows of §4, and the two gate arms of §6, which
are all green. The declaration is bounded by §5: S4 claims conformance for
none of the S5 surfaces deferred there.