Spec-Internal Shape Descriptions¶
Type: Schemas The canonical shapes of the spec's runtime data, written in Malli (for the CLJS reference). Shape description is required (so AIs and tools can read shapes); the mechanism is not. Among the eight in-scope JS-cross-compile hosts (per 000 §The pattern), dynamically typed hosts (CLJS, Squint) realise these shapes as runtime schemas — Malli (CLJS reference) or Zod (Squint, via JS-FFI). Statically typed hosts (TypeScript, Melange / ReScript / Reason, Fable, Scala.js, PureScript, Kotlin/JS) realise the same shapes as types in the language's own type system, generally without a runtime schema library. Both are first-class.
Scope¶
The Malli forms below are the canonical shape descriptions for the CLJS reference. For a different in-scope host:
- Schema-bearing dynamic host (CLJS+Malli; Squint+Zod): translate each Malli form into the host's schema language. The shape is identical; the syntax differs.
- Statically typed host (TypeScript, Melange / ReScript / Reason, Fable, Scala.js, PureScript, Kotlin/JS): translate each Malli form into a type definition. The shape is identical; runtime validation is unnecessary if the type system enforces correctness throughout. A boundary validator (e.g., Zod for incoming JSON) may still be useful at system edges.
A port can translate the Malli forms below mechanically. The CLJS canonical and TypeScript transcription:
;; Malli (CLJS reference)
(def DispatchEnvelope
[:map
[:event [:vector :any]]
[:frame :keyword]
...])
// TypeScript equivalent (no runtime schema; types only)
type DispatchEnvelope = {
event: ReadonlyArray<unknown>;
frame: Id;
fxOverrides?: Record<string, Id | ((m: Envelope, args: unknown) => unknown)>;
interceptorOverrides?: Record<string, Id | null>;
traceId?: string;
source?: string; // closed enum — see the canonical Malli `[:enum ...]` on the `:source` row of `:rf/dispatch-envelope` below for the authoritative value set
// open: additional keys are tolerated
[k: string]: unknown;
};
The shape is the same in both; the mechanism is local to the host. The remaining in-scope hosts (Melange / ReScript / Reason, Fable, Squint, Scala.js, PureScript, Kotlin/JS) follow the same shape, expressed in the host's native type-or-schema vocabulary.
Non-normative background. A Python/Pydantic, Ruby/dry-rb, or Rust transcription of the same shape would be straightforward, but server-side hosts are out of scope as first-class implementation targets per 000 §The pattern. The shape-discipline contract this doc pins applies only to the eight in-scope JS-cross-compile hosts.
Schema convention¶
All spec-internal schemas:
- Are open maps by default (
:closed false, equivalent to Malli's default behaviour). Unknown keys are tolerated; producers may add new keys additively. - Are namespaced under
:rf/...to avoid colliding with user schemas. - Are registered at runtime via
reg-app-schemafor inspectability via(app-schema-at [:rf/...]). - Use the lightest schema that captures the shape — preferring
[:map ...]over more specific Malli grammars.
Traceability metadata¶
This catalogue is a projection of shapes that originate in the owning per-Spec docs — per Ownership.md, Spec-Schemas.md is non-canonical for the semantics; it carries the shape and points at the owner for the contract. To make that projection auditable and to satisfy SPEC-AUTHORING §SA-3 (every wire / example shape MUST have an entry here), each schema section MUST carry the following header lines after ###
| Header | Purpose | Required |
|---|---|---|
> **Layer:** |
Already present — names one of Runtime / Public / Conformance (per §Schema layers). | Yes |
> **Owner:** |
The canonical owning spec doc — write as a markdown link, e.g. [002-Frames](002-Frames.md) or [Ownership.md](Ownership.md). The owner carries the load-bearing semantics; this catalogue carries the shape. |
Yes |
> **Status:** |
One of v1-required / v1 (optional capability) / post-v1 / dev-tier. Mirrors the API.md status vocabulary so a reader can map a schema row to its API surface. |
Yes |
> **Conformance:** |
Pointer to the conformance fixture(s) (spec/conformance/fixtures/<name>.edn) or per-artefact test (implementation/<artefact>/test/...) that asserts the schema holds. Optional when no harness asserts the schema directly (rare — most v1-required shapes have fixture coverage). |
When fixture/test exists |
SA-3 audit pointer. AI-Audit.md §SA-3 schema-coverage report carries the corpus-wide cross-reference table: every shape referenced in the numbered specs (as an example block, a wire payload, a returned-shape) MUST map to either a :rf/<id> schema entry in this catalogue OR an explicit host-type exemption (the per-host primitives that don't need cross-host schemas). The report is generated rather than hand-maintained per SA-3's enforcement obligation.
Migration scope. rf2-baj2g pins this rule and demonstrates the metadata on five load-bearing schemas (:rf/dispatch-envelope, :rf/effect-map, :rf/trace-event, :rf/epoch-record, :rf/hydration-payload). The full sweep across the remaining ~32 schema sections is tracked at rf2-vpu5c.
Schema layers¶
Each schema in this catalogue belongs to exactly one of three layers. The layer tells consumers what role the schema plays in the contract:
- Runtime — shapes the framework produces or consumes during normal operation (the dispatch envelope the runtime constructs, the effect-map a handler returns, the snapshot a machine writes, the trace event a tool reads). Implementations must match these on the wire; they are observable to every layer above.
- Public — shapes the user passes into
reg-*metadata or(rf/configure! ...)opts. These describe what tooling reads when introspecting registrations: the metadata map shape, the frame-meta returned by(frame-meta id), the route-metadata accepted byreg-route. Tools target this surface; implementations validate user input against it. - Conformance — shapes that exist for the conformance corpus and capability-tagging machinery. The handler-body DSL, the fixture-file format, the capability-tagging convention — none of these flow through the runtime; they exist so a host-agnostic test harness can drive any implementation.
Layer membership is disjoint: a schema is exactly one of Runtime, Public, or Conformance. Each schema entry below carries a > Layer: header naming its layer.
v1 vs post-v1 contracts¶
The schemas below are scoped to the v1 contract. Where a shape declares optional keys that are reserved for post-v1 features, those keys are flagged inline with a (post-v1) annotation. v1 implementations emit only the v1 keys and tolerate (per the open-map convention) but do not require post-v1 keys.
The hydration payload is the canonical example: v1 ships with a small required set; post-v1 will extend it with sub-warmups, machine-snapshot wire forms, and similar. The v1-required set is held stable across the v1 lifetime; post-v1 keys appear additively.
Schemas¶
:rf/dispatch-envelope¶
Layer: Runtime Owner: 002-Frames §Routing Status: v1-required Conformance:
spec/conformance/fixtures/dispatch-*.edn+implementation/core/test/re_frame/router_test.cljc
Carried internally by every dispatch. User-facing event vector remains a vector; the envelope wraps it.
(def DispatchEnvelope
[:map
[:event [:vector :any]] ;; the user-facing event vector
[:frame :keyword] ;; target frame id
[:fx-overrides {:optional true} [:map-of :keyword :any]] ;; id-valued at the pattern level; CLJS reference also accepts function values
[:interceptor-overrides {:optional true} [:map-of :keyword :any]]
[:interceptors {:optional true} [:vector :any]]
[:trace-id {:optional true} :any]
[:source {:optional true} [:enum :ui :frame-init :machine-spawn :machine-action :always :after-timer :fx-dispatch :fx-dispatch-later :http :router :ssr-hydration :test :tool :websocket :repl :unknown :other]] ;; trigger kind — default `:unknown` (envelope-construction); per rf2-1ve9h the `:rf/dispatch-origin` axis was collapsed into `:source` (Mike-approved 2026-05-28). Substrate-internal stamp sites: `:ui` (UI handlers), `:frame-init` (frame `:on-create`), `:machine-spawn` (spawn fx — actor bootstrap), `:machine-action` (machine handler's `:dispatch`(-later) — actor-message path, rf2-c3990), `:always` (machine `:always` microstep marker), `:after-timer` (machine `:after` timer fire), `:fx-dispatch` / `:fx-dispatch-later` (ordinary handler's `:dispatch` / `:dispatch-later` fx), `:http` (managed-HTTP reply settle), `:router` (routing-internal dispatches), `:ssr-hydration` (`:rf/hydrate` boot), `:test` (test-harness fixtures), `:tool` (tool / REPL / story dispatches), `:websocket` (reserved — app websocket adapters opt in), `:repl` (REPL eval), `:unknown` (the default — un-stamped dispatch site, per rf2-hxj0d), `:other` (escape hatch)
[:origin {:optional true} :keyword] ;; actor identity (default :app) — per [002 §Dispatch origin tagging]
[:dispatched-at {:optional true} :any]]) ;; CLJS reference may add an impl-specific timestamp; tools tolerate
:rf/dispatch-opts¶
Layer: Public Owner: 002-Frames §Routing Status: v1-required
The opts map a user passes to (dispatch event opts) / (dispatch-sync event opts) / (subscribe query-v opts). The runtime promotes these into a :rf/dispatch-envelope. The opts schema is a subset of the envelope — opts the user supplies are user-facing; envelope keys the runtime adds (:event itself, :dispatched-at) are internal.
(def DispatchOpts
[:map
[:frame {:optional true} :keyword] ;; defaults to :rf/default
[:fx-overrides {:optional true} [:map-of :keyword :any]]
[:interceptor-overrides {:optional true} [:map-of :keyword :any]]
[:interceptors {:optional true} [:vector :any]]
[:trace-id {:optional true} :any]
[:source {:optional true} [:enum :ui :frame-init :machine-spawn :machine-action :always :after-timer :fx-dispatch :fx-dispatch-later :http :router :ssr-hydration :test :tool :websocket :repl :unknown :other]]
[:origin {:optional true} :keyword]]) ;; actor identity tag — defaults to :app when omitted
The promotion is structural: (dispatch event opts) → envelope is (merge {:event event :frame :rf/default :dispatched-at (now)} opts). The runtime asserts :event and :frame are present after the merge.
:rf/registration-metadata¶
Layer: Public Owner: 001-Registration §Registration grammar Status: v1-required
Common shape for the metadata map every reg-* accepts in its middle slot.
(def RegistrationMetadata
[:map
[:doc {:optional true} :string] ;; SHOULD per [001 §:doc is dev-warned when absent]; structurally optional so re-registrations and programmatic paths still validate
[:schema {:optional true} :any] ;; Malli schema (or implementation equivalent) — canonical key per
[:ns {:optional true} :symbol] ;; auto-supplied by macros — flat per [§`:rf/source-coord-meta`](#rfsource-coord-meta)
[:line {:optional true} :int]
[:column {:optional true} :int]
[:file {:optional true} :string]
[:tags {:optional true} [:set :keyword]] ;; user-defined tags
[:platforms {:optional true} [:set [:enum :server :client]]] ;; for reg-fx / reg-cofx; per [011](011-SSR.md)
[:sensitive? {:optional true} :boolean] ;; privacy flag — every reg-* accepts it; per [009 §Privacy / sensitive data in traces](009-Instrumentation.md#privacy--sensitive-data-in-traces) and the `:sensitive?` registration-metadata key contract therein. When `true`, the runtime stamps `:sensitive? true` on every trace event whose in-scope handler carries the flag and listeners default-drop those events (per Spec 009).
])
Per-kind extensions (sub-specific, fx-specific, view-specific) are additive maps that conform to RegistrationMetadata's open shape. Each kind has its own narrowed schema enumerated below — :rf/event-handler-meta, :rf/sub-meta, :rf/fx-meta, :rf/cofx-meta, :rf/view-meta, :rf/machine-meta, :rf/flow-meta, :rf/app-schema-meta, :rf/head-meta, :rf/error-projector-meta, :rf/http-interceptor-meta, and the route-shaped :rf/route-metadata further below — and tools that need the per-kind shape look up the schema by registered id (e.g. via (app-schema-at [:rf/event-handler-meta])).
:doc is {:optional true} in the schema but normatively SHOULD appear on every registration. The dev runtime surfaces missing-:doc registrations through :rf.warning/missing-doc (emitted at most once per (kind, id) pair; production-elided) — see 001 §:doc is dev-warned when absent and 009 §Where trace emission lives for the emission contract. The schema stays {:optional true} so programmatic re-registration paths and tooling that compose metadata maps without :doc still validate; the warning is the nudge, not a structural gate.
The reg-event-* interceptor chain is not a metadata-map key — it is the positional vector slot between the metadata-map and the handler. Per 001-Registration §Allowed forms of the middle slot and Conventions §:interceptors is positional, not metadata, :interceptors inside this map is silently ignored and the runtime emits :rf.warning/interceptors-in-metadata-map. (reg-frame's metadata-map does carry an :interceptors key — that's a per-kind extension defined in Spec 002 §:interceptors.)
Per-kind refinements¶
Each per-kind schema below :merges :rf/registration-metadata and adds the keys the kind cares about. Open-map convention applies — hosts and tools may attach further keys additively without breaking conformance. (rf/handler-meta kind id) returns a value conforming to the corresponding per-kind schema; AI scaffolders (Construction-Prompts) and conformance harnesses validate against these shapes at registration time.
:rf/event-handler-meta¶
Layer: Public
The metadata map accepted by reg-event-db / reg-event-fx / reg-event-ctx. The :event/kind discriminator names which arity-flavour fed the entry (per 001 §Registry model); machine-handler registrations stamp :rf/machine? and :rf/machine per 005 §Registration-metadata stamp.
(def EventHandlerMeta
[:merge
RegistrationMetadata
[:map
[:event/kind {:optional true} [:enum :db :fx :ctx]] ;; runtime stamps this; user code MUST NOT set it
[:rf/machine? {:optional true} :boolean] ;; true iff this :event entry is a machine handler (reg-machine path)
[:rf/machine {:optional true} [:ref :rf/machine-spec]] ;; the captured machine spec (when :rf/machine? true); see [005](005-StateMachines.md)
]])
The interceptor chain is positional (not a metadata-map key) — see the §Registration-metadata section above and Conventions §:interceptors is positional, not metadata. :event/kind is runtime-stamped by the dispatch macro; explicit user assignment is silently overwritten. :rf/machine? / :rf/machine are stamped by reg-machine / reg-machine* only.
:rf/sub-meta¶
Layer: Public
The metadata map accepted by reg-sub. The :<- chain is not a metadata-map key — it is the alternating-keyword/query-vector positional arg between the metadata-map and the body fn (per 006 §Layer-1, layer-2, layer-3 sub semantics). Tools recover the input topology from the runtime-stamped :rf/inputs slot below.
(def SubMeta
[:merge
RegistrationMetadata
[:map
[:rf/inputs {:optional true} [:vector [:vector :any]]] ;; runtime-stamped: the resolved :<- chain as a vector of query-vectors
[:rf/layer {:optional true} [:enum :layer-1 :layer-2+]] ;; runtime-stamped: derived from :rf/inputs at registration time
]])
:rf/inputs and :rf/layer are stamped by the runtime at registration time from the :<- positional args — user code MUST NOT set them. Static topology queries (sub-topology, per 006) read :rf/inputs back to project the :<- graph.
:rf/fx-meta¶
Layer: Public
The metadata map accepted by reg-fx. Carries :platforms per 011 §:platforms metadata on reg-fx.
(def FxMeta
[:merge
RegistrationMetadata
[:map
[:platforms {:optional true} [:set [:enum :server :client]]] ;; default if absent: #{:server :client} (universal); per [011](011-SSR.md)
]])
:platforms absence defaults to universal (#{:server :client}). The fx resolver consults the active platform per 011 §:platforms metadata on reg-fx.
:rf/cofx-meta¶
Layer: Public
The metadata map accepted by reg-cofx. Carries :platforms mirroring reg-fx per 011 §:platforms metadata on reg-fx. The handler fn's arity (1-arity (fn [cofx]) vs 2-arity (fn [cofx arg])) is fn-shape, not metadata — the cofx resolver detects arity at injection time and routes the optional inject-cofx second-arg accordingly (per API.md §inject-cofx). Tools that need the arity discriminator inspect the fn's arity directly.
(def CofxMeta
[:merge
RegistrationMetadata
[:map
[:platforms {:optional true} [:set [:enum :server :client]]] ;; default if absent: #{:server :client}; mirrors :rf/fx-meta
]])
:rf/view-meta¶
Layer: Public
The metadata map accepted by reg-view / reg-view*. The ^{:rf/id ...} symbol-meta override on the reg-view symbol surfaces in the stamped registry slot as :rf/id per 004 §Shape.
(def ViewMeta
[:merge
RegistrationMetadata
[:map
[:rf/id {:optional true} :keyword] ;; explicit id override (auto-derived from *ns* + symbol when absent)
[:rf/args {:optional true} [:vector :symbol]] ;; the macro-captured args-vector symbols (defn-shape introspection)
[:rf/form {:optional true} [:enum :form-1 :form-2 :form-3]] ;; the view body's Reagent form discriminator
[:rf/props {:optional true} :any] ;; Malli schema for the view's props (when supplied); the canonical props-schema slot, resolved first-match over `[:rf/props :schema]` (not composed — `:rf/props` wins outright over `:schema` for the view-args boundary) per [010](010-Schemas.md)
]])
:rf/args / :rf/form are stamped by the reg-view macro at expansion time; reg-view* (the plain-fn surface) carries neither — programmatic registrations have no args-vector to capture (per 004 §reg-view* — the plain-fn escape hatch). :rf/props is an optional user-supplied props schema; in dynamic hosts the framework can validate props against it at render-time-boundary in dev builds (per 010).
Note — :schema is canonical. Story's earlier reading of :schema (as a legacy alias to the framework's :spec) is now in line with the canonical name — both Story and the framework speak :schema. See MIGRATION §M-54 for the v1→v2 rename.
:rf/machine-meta¶
Layer: Public
The metadata stamped on the :event registry slot by reg-machine / reg-machine* (per 005 §Registration-metadata stamp). This is the registry-slot metadata — :rf/machine? discriminates a machine handler from an ordinary event handler in (registrations :event) queries; the captured machine spec rides at :rf/machine and conforms to :rf/transition-table extended with the root-only :guards / :actions / :data / :doc slots per 005 §Transition table grammar.
(def MachineMeta
[:merge
EventHandlerMeta
[:map
[:rf/machine? [:= true]] ;; required true on machine-handler registrations
[:rf/machine [:ref :rf/transition-table]] ;; the captured machine spec — a TransitionTable rooted at the machine. Carries :initial, :states, :guards, :actions, optional :data / :doc / :tags / :meta. When the macro path stamped it, each :guards / :actions / :on-spawn-actions entry co-locates :source-coords / :source-code on its `{:fn ..}` map, and each :states-tree map node (state-node / transition map) co-locates its own reference-site :source-coords directly (per [005 §Source-coord stamping](005-StateMachines.md#source-coord-stamping)).
]])
Two surfaces; one stamp. The registry slot exposes the machine through two query fns, which read the same underlying entry:
| Lens | Returns | Implementation |
|---|---|---|
(handler-meta :event machine-id) |
the full registry-slot metadata — base RegistrationMetadata (:doc, :schema, :ns/:line/:file, :tags, :platforms) plus :event/kind, :rf/machine? true, and :rf/machine <spec>. Conforms to this MachineMeta. |
direct registrar lookup |
(machine-meta machine-id) |
the machine spec — the value at :rf/machine. The transition table (:initial, :states), the root-only :guards / :actions maps (whose entries co-locate :source-coords / :source-code when macro-stamped), the initial :data map, and (when macro-stamped) the reference-site :source-coords co-located on each :states-tree map node. |
(:rf/machine (handler-meta :event machine-id)) |
Visualisers walking the transition table consume (machine-meta id); tools needing source-coords on the reg-machine call site itself (file/line of the declaration) use (handler-meta :event id). The two surfaces are independent and complementary — see 005 §Querying machines and the reference implementation at implementation/machines/src/re_frame/machines/lifecycle_fx.cljc (machines / machine-meta).
:rf/flow-meta¶
Layer: Public
The registration-shape accepted by reg-flow. Unlike the other kinds, reg-flow takes the flow as a single map (no separate metadata-map / handler slot) — the map carries both the wiring (:id, :inputs, :output, :path) and the registration metadata (:doc, :schema, source coords) per 013 §The registration shape.
(def FlowMeta
[:merge
RegistrationMetadata
[:map
[:id :keyword] ;; required: the flow id
[:inputs [:vector [:vector :any]]] ;; required: vector of app-db paths; positional args to :output
[:output fn?] ;; required: pure fn (in-1, ..., in-n) → output
[:path [:vector :any]] ;; required: app-db path to write output to
]])
:id, :inputs, :output, :path are required at registration time; the base :rf/registration-metadata keys (:doc, :schema, :ns/:line/:file, :tags) compose additively. reg-flow rejects malformed maps with one of four distinct error keys — :rf.error/flow-missing-id, :rf.error/flow-bad-inputs, :rf.error/flow-bad-output, :rf.error/flow-bad-path — surfaced via 009 §Error contract; see also 013 §The registration shape.
:rf/app-schema-meta¶
Layer: Public
The metadata stamped on the schemas artefact's per-frame side-table entry by reg-app-schema (per 010 §reg-app-schema; per rf2-cq1ak app-db schemas are NOT a registrar kind — the schemas artefact's per-frame side-table is the single source of truth). The :path and :schema fields are runtime-stamped from the positional args — user code passes (rf/reg-app-schema path schema) rather than (rf/reg-app-schema id {:path ... :schema ...}).
(def AppSchemaMeta
[:merge
RegistrationMetadata
[:map
[:path [:vector :any]] ;; runtime-stamped from positional arg; the app-db path the schema validates
[:schema :any] ;; runtime-stamped; the Malli (or equivalent) schema value
[:frame :keyword] ;; runtime-stamped; the frame the schema registers against (`(or (:frame opts) (current-frame-id))`)
]])
reg-app-schema is per-frame (per 010 §Per-frame app-db schemas); the :frame slot records which frame this slot belongs to so tools enumerating across frames don't conflate registrations.
Per-slot metadata vocabulary¶
Inside the Malli schema value passed to reg-app-schema, individual slots may carry per-slot metadata maps (the {:optional ... :hint ...} shape Malli accepts on a property slot). The framework's reserved per-slot keys are catalogued below; user-defined keys live alongside them under the open-map invariant.
| Per-slot key | Type | Used for | Spec |
|---|---|---|---|
:large? |
boolean | Size-elision nomination — when true, the path the slot occupies is registered into [:rf/runtime :elision :declarations] with :source :schema at boot, so the rf/elide-wire-value walker (per API.md §rf/elide-wire-value) substitutes the :rf.size/large-elided marker at the wire boundary. The schema-driven nomination path catalogued in 009 §Size elision in traces. |
009 |
:hint |
string | A free-form short description of the slot. When :large? true rides alongside, the value is copied verbatim into the :rf.size/large-elided marker's :hint slot. |
009 |
:sensitive? |
boolean | Path-level privacy declaration sibling to :large? — when true, the path the slot occupies is marked sensitive and the schema-validation emit-site redacts the failing :value / :explain / :fx-args slots with the framework-reserved sentinel :rf/redacted (mirroring the 009 §Privacy registration-metadata :sensitive? key, composed most-specific-wins). The schemas artefact's walker (extract-sensitive-paths-from-schema) hydrates the [:rf/runtime :elision :sensitive-declarations] registry slot so consumers can (get-in db [:rf/runtime :elision :sensitive-declarations <path>]) in O(1). Per 010 §:sensitive? — privacy in schema-validation error traces. |
010 |
The reserved set is fixed-and-additive: new per-slot keys ship by spec change. Per-slot metadata not in the reserved set is tolerated under the open-shape invariant; the framework ignores it.
:rf/head-meta¶
Layer: Public
The metadata map accepted by reg-head (per 011 §Mechanism — registered head function + route metadata). The head fn itself is (fn [db route] head-model) — pure, JVM-runnable, value-shaped.
(def HeadMeta
[:merge
RegistrationMetadata
[:map
;; No required per-kind extras beyond the base shape — head registrations are
;; reflection-metadata-only at the slot level. The head model returned by the fn
;; conforms to :rf/head-model (defined below); a :schema key here may name that schema.
]])
:rf/error-projector-meta¶
Layer: Public
The metadata map accepted by reg-error-projector (per 011 §Server error projection). The projector fn itself is (fn [trace-event] public-error-map) — pure, value-shaped. The projector named in a frame's :ssr {:public-error-id ...} metadata is that frame's active projector.
(def ErrorProjectorMeta
[:merge
RegistrationMetadata
[:map
;; No required per-kind extras beyond the base shape. The projector input conforms to
;; :rf/trace-event (an :op-type :error refinement); the output conforms to :rf/public-error.
]])
:rf/http-interceptor-meta¶
Layer: Public
The metadata stored in the per-frame interceptor slot for a registration made via reg-http-interceptor (per 014 §Middleware). Per rf2-uheqq (shape iii) the call surface is (reg-http-interceptor id interceptor-map) — a single interceptor-map carrying at least one of :before / :after plus optional :frame and any :rf/registration-metadata keys. Unlike the other per-kind shapes, HTTP interceptors are stored in a per-frame side-table (keyed by :frame) rather than in the global registrar — the registrar slot for :http-interceptor is intentionally absent. The schema describes the shape of each slot in that side-table.
(def HttpInterceptorMeta
[:merge
RegistrationMetadata
[:map
[:id :keyword] ;; required, addressable for clear
[:before {:optional true} fn?] ;; optional, request-side transform: (fn [ctx] ctx')
[:after {:optional true} fn?] ;; optional, response-side transform: (fn [ctx response] response')
[:frame :keyword] ;; runtime-stamped from user-supplied :frame or :rf/default
]])
At least one of :before / :after MUST be present — a no-op slot is rejected at registration with :rf.error/http-bad-interceptor. The base RegistrationMetadata keys (:doc, :schema, :tags, :sensitive?, :ns / :line / :column / :file) flow through additively — supplied via the interceptor-map at the call site, except for source-coords which are auto-captured at the rf/reg-http-interceptor call site per Spec 001 §Source-coordinate capture. The slot is read by the runtime's run-interceptor-chain! (which pulls :id / :before for the request-side chain) and run-after-chain! (which pulls :id / :after for the REVERSE-order response-side chain); it is also the canonical surface tools introspect for "where is this interceptor declared?" lookups.
The route-shape — :rf/route-metadata — is defined separately further below in this catalogue (it predates this per-kind grouping). It composes with :rf/registration-metadata the same way the kinds above do; per §:rf/route-metadata.
:rf/source-coord-meta¶
Layer: Public Owner: 001-Registration §Source-coordinate capture Status: v1-required
The registration-metadata source-coord shape captured at reg-* macro-expansion time. The four keys (:ns / :line / :column / :file) are merged flat onto :rf/registration-metadata — the same level as :doc / :schema / :tags — so (rf/handler-meta kind id) and (rf/frame-meta id) returns expose them as top-level keys: (:line meta) / (:file meta) / (:ns meta) / (:column meta). Pair-shaped tools and IDE jump-to-source consumers read them for click-back-to-code resolution per Tool-Pair §Source-mapping UI clicks back to code. Trace events are the one shape that nests these keys under a :source-coord sub-map — see :rf.trace/trigger-handler on :rf/trace-event below — because traces carry coords for another handler (the in-scope trigger), so a sub-map keeps the trigger-handler shape self-contained alongside the trace's own keys.
(def SourceCoordMeta
[:map
[:ns :symbol] ;; the namespace symbol of the call site
[:line nat-int?] ;; integer source line
[:column nat-int?] ;; integer source column
[:file [:maybe :string]]]) ;; absolute or classpath-relative source file; nil when not captured
The four keys are the canonical source-coord shape. The CLJS reference fills all four from (meta &form) (:line, :column) and the compile-time *ns* / *file* (per Spec 001 §Source-coordinate capture). Programmatic registrations that bypass the macro path may carry a partial shape (e.g. only :ns resolved); consumers handle missing keys defensively. Companion shape: :rf/source-coord-attr below — a string-encoded contraction of this map suitable for DOM-attribute emission.
:rf/source-coord-attr¶
Layer: Public Owner: 006-ReactiveSubstrate §Attribute value format Status: v1-required
The DOM-attribute string contract emitted by Reagent / SSR adapters as the value of data-rf2-source-coord on rendered view roots (per Spec 006 §Attribute value format and 011 §Source-coord annotation under SSR). A 4-segment colon-separated string:
<ns>— the registered handler-id's namespace (string-encoded).<sym>— the registered handler-id's name (string-encoded). Note this is the registry handler-id, not a file path.<line>— integer source line, or the literal?for programmatic registrations whose macro-captured coord is absent.<col>— integer source column, or the literal?for programmatic registrations whose macro-captured coord is absent.
(def SourceCoordAttr
[:re #"^[^:]+:[^:]+:(?:\d+|\?):(?:\d+|\?)$"]) ;; pragmatic regex; consumers parse 4 segments
Consumers parse the four segments pragmatically (split on : from the right twice to recover <line> and <col>, then on : once more for <ns>/<sym>). To recover the full :rf/source-coord-meta shape — including :file — look up the handler-id via (rf/handler-meta :view <handler-id>) and read the flat top-level :ns / :line / :column / :file keys off the returned registration-metadata map; :file is not recoverable by parsing the attribute alone (it is not encoded in the 4-segment string).
The string format is committed as a public contract: pair-shaped tools, conformance harnesses, and CDP-driven test runners all parse it directly. Future extensions are additive — additional trailing segments may appear; consumers MUST handle the 4-segment shape and tolerate (ignore) trailing segments they do not recognise.
:rf/view-id-attr¶
Layer: Public Owner: 006-ReactiveSubstrate §View tagging contract Status: v1-required (fallback path; primary path is the Fiber walker per View-Hierarchy-Capture.md)
The DOM-attribute string contract emitted by Reagent / UIx / Helix adapters as the value of data-rf-view on rendered view roots (per Spec 006 §View tagging contract). The stringified registry id keyword:
For a namespaced keyword id, the attribute value is (str id) — the leading : is preserved so the walker can distinguish (keyword (subs s 1)) from a non-keyword id. For a non-keyword id (unusual but legal at the registrar layer) the attribute carries the raw string repr.
(def ViewIdAttr
[:re #"^:?[^/]+(?:/.+)?$"]) ;; permissive; consumers prefer (keyword (subs s 1)) when leading-colon
Consumers (the fallback view-walker) read the attribute and call (keyword (subs s 1)) when the string starts with :; otherwise the raw string is used as the id.
The view-id attribute pairs with data-rf2-source-coord (:rf/source-coord-attr above) — both attributes land on the same root element of every registered view, ride the same interop/debug-enabled? elision gate, and elide together under :advanced + goog.DEBUG=false. Per Spec 006 §View tagging contract: data-rf-view is the FALLBACK; the primary path for runtime view-hierarchy capture is the Fiber walker.
:rf/effect-map¶
Layer: Runtime Owner: 002-Frames §Effect resolution Status: v1-required (closed shape — only
:dband:fx) Conformance:spec/conformance/fixtures/effect-map-*.edn+implementation/core/test/re_frame/effects_test.cljc
The return value of reg-event-fx handlers. Only two keys: :db and :fx.
(def EffectMap
[:map {:closed true}
[:db {:optional true} :any] ;; new app-db (replace)
[:fx {:optional true} [:vector [:tuple :keyword :any]]] ;; effects: [[fx-id args] ...]
])
Every effect — including dispatching another event, scheduling a delayed dispatch, HTTP requests, navigation, anything — goes through :fx as a registered-fx-id + args pair:
{:db (assoc db :counter 1)
:fx [[:dispatch [:counter/saved]]
[:dispatch-later {:ms 1000 :dispatch [:counter/cleanup]}]
[:http {:method :get :url "/api/items"}]
[:localstorage/set {:key "counter" :value 1}]]}
The single-form rule lets the runtime walk one ordered list of effects, and fits the pattern's regularity-over-cleverness principle (Principles.md §Regularity). Migrating from earlier re-frame versions: see MIGRATION.md.
Note the schema is closed — unlike most spec-internal shapes which are open maps. The effect map is a contract between handler and runtime; novel keys would be silently ignored, which is exactly the kind of footgun the closed shape rules out.
Normative ordering and atomicity. Beyond the shape, the effect map carries a runtime-ordering contract that conformant implementations must produce: :db is the first side effect (when present, committed atomically before any :fx entry); :fx entries are processed in source order, serially (entry N's fx-handler returns before entry N+1's begins); subscriptions observe the post-:db state from the first :fx entry onwards; if an fx-handler throws, subsequent :fx entries continue to run and each error is traced as :rf.error/fx-handler-exception. See 002 §:fx ordering and atomicity guarantees for the full rules and rationale.
:rf/trace-event¶
Layer: Runtime Owner: 009-Instrumentation §Trace event shape Status: v1-required (dev-tier emit gated on
re-frame.interop/debug-enabled?) Conformance:spec/conformance/fixtures/trace-*.edn+spec/conformance/fixtures/error-event-*.edn+implementation/core/test/re_frame/trace/*_test.cljc
Universal trace event shape, including error events.
(def TraceEvent
[:map
[:id :any] ;; auto-incrementing per-process counter
[:operation :keyword] ;; what this trace describes
[:op-type :keyword] ;; discriminator (open vocabulary; see below)
[:time :any] ;; emit timestamp (host clock)
[:tags {:optional true} [:map-of :keyword :any]] ;; op-type-specific payload
[:source {:optional true} :keyword] ;; (when present) the trigger source
[:recovery {:optional true} [:enum :no-recovery :replaced-with-default :retried :skipped :warned-and-replaced :logged-and-skipped :ignored]]
[:rf.trace/trigger-handler {:optional true} ;; (when present) the in-scope handler at emit time
[:map
[:kind [:enum :event :sub :fx :cofx :view]]
[:id :keyword]
[:source-coord [:map
[:ns {:optional true} :symbol]
[:file {:optional true} :string]
[:line {:optional true} :int]
[:column {:optional true} :int]]]]]])
The runtime emits event-at-a-time, not span-shaped: there is no :start/:end/:duration pair and no :child-of parent-id. Cascade correlation rides on :rf.trace/dispatch-id under :tags of every trace event emitted inside a cascade; :rf.trace/parent-dispatch-id rides under :tags of :rf.event/dispatched events only (it documents inter-cascade lineage). These cross-cutting correlation keys live under :rf.trace/* (per Conventions §:rf.trace/*). Per 009 §Dispatch correlation. Per-event frame attribution rides under [:tags :frame] (the deliberate bare carve-out). Per-event handler attribution rides under the top-level :rf.trace/trigger-handler slot when a handler is in scope at emit time — :rf.fx/handled carries the fx handler's coord, :rf.machine/transition carries the machine's coord, every :rf.error/* carries the responsible handler's coord, every emit inside an event handler's chain carries the event handler's coord. Per 009 §:rf.trace/trigger-handler — naming the in-scope handler.
The :op-type vocabulary is open — implementations and tools may add new values additively per Spec-ulation. The canonical reserved values used by the framework — the family-level discriminators a consumer branches on — are enumerated below; every framework op-type is :rf.<family> and every :operation is :rf.<family>/<op> (the severity discriminators :error / :warning / :info are the bare exception). Per-emit-site :operation keywords (e.g. :rf.machine/transition, :rf.machine.timer/scheduled, :rf.epoch/snapshotted, :rf.error/handler-exception) ride under each op-type family; the authoritative cross-reference is 009 §:op-type vocabulary and the 009 §Error event catalogue.
Severity discriminators (every error / warning / advisory event carries one of these):
:op-type |
Used for | Spec |
|---|---|---|
:error |
Any :rf.error/* operation — a failure the runtime halted or recovered. Refines into :rf/error-event (below) |
009 |
:warning |
Non-error advisories the runtime emitted alongside continuing default behaviour (e.g. :rf.warning/plain-fn-under-non-default-frame-once, :rf.fx/skipped-on-platform, :rf.cofx/skipped-on-platform). Refines into :rf/error-event |
009 |
:info |
Informational advisories without warning/error severity (e.g. :rf.http/retry-attempt, :rf.http/aborted-on-actor-destroy, :rf.http.interceptor/registered, :rf.http.interceptor/cleared) |
009 / 014 |
Cascade-body discriminators (the success-path / lifecycle traces emitted inside the run-to-completion drain):
:op-type |
Used for | Spec |
|---|---|---|
:rf.event |
Top-level event-handler invocation (:rf.event/dispatched, :rf.event/db-changed, etc.) |
009 |
:rf.sub |
Subscription family — :rf.sub/create (first reference / registration into the reactive graph), :rf.sub/run (input changed; output recomputed), :rf.sub/skip (memo-hit; body did not re-run) |
009 |
:rf.view |
View-substrate family — :rf.view/render (per-render marker), plus the :rf.view/rendered per-render cascade-attribution / per-view ACTION+REASON marker (carries :rf.view/mount?, :rf.view/deref-subs, and :rf.view/render-args — the latter elided as user data), its :rf.view/rendered-cap-reached truncation marker, and the :rf.view/unmounted instance-teardown marker. Per Spec 004 §Render-tree primitives and 009 §:op-type vocabulary |
004 / 009 |
:rf.fx |
Effect-substrate success-path / lifecycle family — :rf.fx/do-fx (the effects-resolution pass after the handler returns — folded in here from the former standalone :event/do-fx op-type), :rf.fx/handled, :rf.fx/override-applied. The universal discriminator for fx outcomes when not error/warning-shaped |
002 / 009 |
:rf.cofx |
Coeffect-substrate success-path family — :rf.cofx/run (a cofx handler ran to success during the interceptor :before chain; carries :rf.cofx/id + :rf.cofx/value + :rf.cofx/elapsed-ms). The cofx skip / error paths ride the :warning / :error severity discriminators (:rf.cofx/skipped-on-platform, :rf.error/no-such-cofx). Per |
002 / 009 |
Family-level discriminators (umbrella :op-type values whose per-emit-site :operation varies; consumers filter the whole family with one key):
:op-type |
Used for | Spec |
|---|---|---|
:rf.frame |
Frame-lifecycle family — :rf.frame/created, :rf.frame/re-registered, :rf.frame/destroyed, :rf.frame/drain-interrupted. Lifecycle events, not error-shaped. :tags carries :frame <id> (plus per-operation extras, e.g. :dropped-count on :rf.frame/drain-interrupted). Per 002 §Edge cases worth pinning |
002 |
:machine |
Machine-substrate family — state-machine activity (:rf.machine/transition, :rf.machine.microstep/transition, :rf.machine/done, :rf.machine/event-received, :rf.machine/snapshot-updated, :rf.machine.spawn/spawned, :rf.machine/destroyed, :rf.machine/system-id-bound, :rf.machine/system-id-released, every :rf.machine.timer/* operation, every :rf.machine.spawn-all/* operation, :rf.machine.spawn/cancelled-on-join-resolution). :rf.machine/destroyed carries :reason :rf.machine/finished / :explicit / :parent-unmount-cascade (the non-frame-exit causes; :parent-frame-destroyed rides on the :rf.machine.lifecycle/destroyed family below). Per 005 §Trace events |
005 |
:rf.machine.lifecycle/created |
Machine instance lifecycle — created half. Uniform create-emit shape used by lifecycle observers; :tags {:frame <id> :machine-id <id>} |
005 / 009 |
:rf.machine.lifecycle/destroyed |
Machine instance lifecycle — destroyed half. :tags {:frame <id> :machine-id <id> :last-state <state> :reason <:parent-frame-destroyed | :rf.machine/finished | :explicit | :parent-unmount-cascade>}. Frame-exit cascade emits one per active machine snapshot carrying :reason :parent-frame-destroyed (see 009 §:op-type vocabulary — Frame-exit machine teardown) |
005 / 009 |
:rf.registry |
Registrar-mutation family — :rf.registry/handler-registered, :rf.registry/handler-cleared, :rf.registry/handler-replaced (handler hot-reload paths). Spans every kind in the registry model (:event, :sub, :fx, :cofx, :view, :machine, :flow, …) |
001 / 009 |
:flow |
Flow lifecycle and evaluation events (per 013 §Flow tracing) — :rf.flow/registered, :rf.flow/computed, :rf.flow/skip, :rf.flow/cleared, :rf.flow/failed. All five carry :tags :flow-id and :tags :frame so tools can attribute and route per-frame; consumers filter :op-type :flow to subscribe to the whole stream |
013 |
:rf.epoch |
Epoch-history family — :rf.epoch/snapshotted, :rf.epoch/outcome ( — consumer-facing {:ok :blocked :error} summary paired with -snapshotted's detailed cause), :rf.epoch/restored, :rf.epoch/db-replaced (the latter is the pair-tool write surface; see Tool-Pair §Pair-tool writes). :tags {:frame <id> :rf.epoch/id <id> :rf.trace/event-id <id>? :outcome <enum>?} |
Tool-Pair |
:rf.epoch.cb |
Epoch-callback listener-silencing notifications — :rf.epoch.cb/silenced-on-frame-destroy. Emitted once per (frame, cb-id) pair when a frame previously observed by a register-epoch-listener! callback is destroyed so a tool whose previously-firing cb has gone silent learns why without polling registry state. Per Tool-Pair §Surface behaviour against destroyed frames and |
Tool-Pair |
:ssr |
Generic SSR-context family — server-render boundary traces (per 011). Distinct from :rf.ssr/* operations under :op-type :warning (:rf.ssr/hydration-mismatch etc.) which ride the severity channel |
011 |
Per-operation rows carry their own :op-type membership — e.g. :rf.machine/transition is an :operation whose :op-type is :machine; :rf.route.nav-token/stale-suppressed is an :operation whose :op-type is :error; :rf.fx/handled is an :operation whose :op-type is :fx. The 009 §Error event catalogue is the single normative cross-reference: every emit site is enumerated there with its :operation, :op-type, trigger, default :recovery, and :tags payload.
The error category schemas in 009 §Error event catalogue are refinements of TraceEvent for :op-type :error events. The unified error/warning envelope is captured by :rf/error-event (below).
Non-error refinements. A small set of TraceEvent refinements describe frame-lifecycle traces that ride the trace stream alongside the error/warning channel. The single one v1 ships is :rf.frame/drain-interrupted — emitted when a frame's drain loop detects the frame was destroyed mid-cycle and drops remaining queued events. The :tags schema is DrainInterruptedTags (per §Per-category :tags schemas below), shape {:category :rf.frame/drain-interrupted, :frame <keyword>, :dropped-count <int>}. Consumers branch on :operation = :rf.frame/drain-interrupted to filter; the :op-type :rf.frame discriminator places it alongside the rest of the :rf.frame/* lifecycle family (:rf.frame/created, :rf.frame/destroyed). Per 002 §Edge cases worth pinning and 009 §:op-type vocabulary.
:rf/error-event¶
Layer: Runtime Owner: 009-Instrumentation §Error contract Status: v1-required
A refinement of :rf/trace-event for the unified error/warning envelope. Every error or warning emitted by the runtime conforms to this shape; per-category schemas (one per row in 009 §Error event catalogue) further constrain :tags.
(def ErrorEvent
[:map
[:id :any]
[:operation :keyword] ;; one of the reserved :rf.error/* / :rf.warning/* / :rf.fx/* / :rf.ssr/* operations
[:op-type [:enum :error :warning]] ;; :error for failures; :warning for advisories
[:time :any] ;; emit timestamp (host clock)
[:source {:optional true} :keyword]
[:recovery {:optional true} [:enum :no-recovery :replaced-with-default :retried :skipped :warned-and-replaced :logged-and-skipped :ignored]]
[:rf.trace/trigger-handler {:optional true}
[:map
[:kind [:enum :event :sub :fx :cofx :view]]
[:id :keyword]
[:source-coord [:map
[:ns {:optional true} :symbol]
[:file {:optional true} :string]
[:line {:optional true} :int]
[:column {:optional true} :int]]]]]
[:rf.trace/call-site {:optional true} ;; invocation coord stamped by the
[:map ;; macro form of dispatch / dispatch-sync /
[:ns {:optional true} :symbol] ;; subscribe / inject-cofx
[:file {:optional true} :string]
[:line {:optional true} :int]
[:column {:optional true} :int]]]
[:tags [:map
[:category :keyword] ;; same value as :operation, for consumer convenience
[:failing-id {:optional true} :any]
[:frame {:optional true} :keyword]
[:reason {:optional true} :string]]]]) ;; remaining keys are category-specific
The :op-type discriminates severity: :error halts or recovers a specific operation; :warning is an advisory the runtime emitted alongside continuing default behaviour. Consumers branch on :op-type for severity routing and on :operation for category-specific handling.
The optional :rf.trace/trigger-handler slot (top-level, NOT under :tags) names the handler whose execution produced the error and carries its registration-site source-coord. Inherited from the universal TraceEvent shape — the slot rides on every trace event emitted while a handler is in scope, not just errors (per — success-path traces like :rf.fx/handled and :rf.machine/transition carry it too). Present when a handler is in scope at emit time (event handler running, sub recomputing, fx handler dispatching, cofx injecting, view rendering); absent when no handler is in scope (e.g. outermost-dispatch :rf.error/no-such-handler, depth-exceeded drain rollback). Source-coord values come from the registrar slot stamped by the kind-specific reg-* macro at registration time; programmatic registration paths (the underlying registration fns called without the macro wrapping) carry no coord, in which case the slot is omitted rather than populated with placeholder data. Tools render click-to-jump-to-handler links by reading [:rf.trace/trigger-handler :source-coord]. Not elided in production — :rf.error/* traces are not elided (unlike :rf.assert/*) and this slot rides along with them.
The optional :rf.trace/call-site slot (top-level, sibling of :rf.trace/trigger-handler) names the invocation line of the user-facing surface that triggered the error — the (rf/dispatch [:bad-event]) line, the (rf/subscribe [:bad-sub]) line, the (rf/inject-cofx :missing) line. Where :rf.trace/trigger-handler answers "where is the failing handler defined?", :rf.trace/call-site answers "where is the failing handler called?". Tools that consume both render two clickable links per error: registration-site jump and invocation-site jump. Present when the surface was reached through its macro form (dispatch, dispatch-sync, subscribe, inject-cofx); absent when reached through the runtime-callable fn form (dispatch*, dispatch-sync*, subscribe*, inject-cofx*) — HoF use, programmatic / REPL paths, view-render closures captured by (rf/frame-handle)'s :dispatch / :subscribe ops — and absent under :advanced + goog.DEBUG=false builds (per Q3=B: dev-only elision; the macro's stamp branch DCEs and the literal map vanishes). Per.
The canonical category vocabulary is fixed-and-additive (Spec-ulation): existing categories cannot be renamed or removed; new categories are added by extending the operation namespace. The current set is enumerated in 009 §Error event catalogue — the single source of truth for the :operation enum domain (every row of the catalogue corresponds to one reserved keyword in this enum). API.md §Error contract points consumers at the catalogue rather than reproducing it. Reserved operation namespaces:
| Namespace | Severity | Used for |
|---|---|---|
:rf.error/* |
:error |
Runtime failures (handler/sub/fx exceptions, missing handlers, schema failures, drain depth, override fallthrough, adapter misuse) |
:rf.warning/* |
:warning |
Authoring-time advisories the runtime can detect but does not abort on (e.g. plain Reagent fn under non-default frame) |
:rf.fx/* |
:warning |
Effect-resolution advisories (e.g. fx skipped because of :platforms) |
:rf.ssr/* |
:warning |
SSR-specific advisories (hydration mismatch and similar) |
:rf.epoch/* |
:error |
Epoch-history restore failures (per Tool-Pair §Time-travel) |
:rf.http/* |
:warning / :info |
Managed-HTTP advisories (key-ignored-on-jvm, retry-attempt) per 014 |
:rf.route.nav-token/* |
:error |
Stale-result-suppression on async navigation (per 012 §Navigation tokens) |
:rf.frame/<operation> |
:frame |
Frame-lifecycle trace operations emitted by the router and frame lifecycle (e.g. :rf.frame/drain-interrupted, :rf.frame/destroyed) per 002. |
:rf.frame/<gensym> |
n/a | Identifier namespace, NOT a trace-operation prefix — anonymous frame ids minted by make-frame (e.g. :rf.frame/123). Carried as the value of the :frame key on trace events, never as the :operation. Listed here so consumers parsing operation namespaces don't mis-route a gensym'd frame id as an operation. |
Per-category :tags schemas¶
Layer: Runtime
Each error / warning category enumerated in 009 §Error event catalogue has a registered Malli schema describing its :tags payload, so consumers can validate without ad-hoc parsing. The schemas below are the canonical CLJS-reference shapes; ports translate them mechanically into the host's schema language (per §Scope).
Common keys (:category, :failing-id, :reason, :frame) are inherited from the :rf/error-event envelope above; the per-category schemas below describe the additional category-specific keys. Open-map convention applies — implementations may add fields additively without breaking consumers (per §Schema convention).
;; --- runtime: handler / sub / fx / interceptor exceptions ---
(def HandlerExceptionTags
;; The EVENT HANDLER itself threw (the terminal :before). Scoped to the
;; handler per rf2-mszrz — coeffect / user-interceptor throws in the same
;; chain carry their own categories below. `:failing-id` and `:handler-id`
;; are both the event id; `:phase` is :before (the handler-wrapper's slot).
[:map
[:category [:= :rf.error/handler-exception]]
[:failing-id :keyword]
[:reason :string]
[:event [:vector :any]]
[:event-id {:optional true} :keyword]
[:frame {:optional true} :keyword]
[:handler-id :keyword]
[:phase {:optional true} [:enum :before :after :handler]]
[:exception {:optional true} :any]
[:exception-message :string]
[:exception-data {:optional true} :any]])
(def CoeffectExceptionTags
;; A registered coeffect's injection body threw during the :before chain
;; (rf2-mszrz). `:failing-id` is the fully-qualified cofx id; `:phase` is
;; :before. Distinct from NoSuchCofxTags (unregistered id, trace-and-
;; continue) — this is a registered cofx whose injection logic threw,
;; aborting the event. No `:handler-id` (the handler never ran).
[:map
[:category [:= :rf.error/coeffect-exception]]
[:failing-id :keyword]
[:reason :string]
[:event [:vector :any]]
[:event-id {:optional true} :keyword]
[:frame {:optional true} :keyword]
[:phase {:optional true} [:enum :before]]
[:exception {:optional true} :any]
[:exception-message :string]
[:exception-data {:optional true} :any]])
(def InterceptorExceptionTags
;; A user interceptor's :before or :after slot threw (rf2-mszrz).
;; `:failing-id` is the throwing interceptor's :id; `:phase` discriminates
;; :before (pre-handler) from :after (post-handler teardown / reshape).
;; No `:handler-id` (the failure was not the handler). Excludes framework
;; auto-wrappers (handler-wrapper → HandlerExceptionTags; cofx injector →
;; CoeffectExceptionTags).
[:map
[:category [:= :rf.error/interceptor-exception]]
[:failing-id :keyword]
[:reason :string]
[:event [:vector :any]]
[:event-id {:optional true} :keyword]
[:frame {:optional true} :keyword]
[:phase [:enum :before :after]]
[:exception {:optional true} :any]
[:exception-message :string]
[:exception-data {:optional true} :any]])
(def FxHandlerExceptionTags
[:map
[:category [:= :rf.error/fx-handler-exception]]
[:failing-id :keyword]
[:rf.fx/id :keyword]
[:rf.fx/args :any]
[:frame {:optional true} :keyword]
[:exception-message :string]])
(def SubExceptionTags
[:map
[:category [:= :rf.error/sub-exception]]
[:failing-id :keyword]
[:rf.sub/id :keyword]
[:sub-query [:vector :any]]
[:exception-message :string]])
(def NoSuchSubTags
[:map
[:category :keyword] ;; [:= :rf.error/no-such-sub] in a closed schema
[:rf.sub/query-v [:vector :any]]
[:frame {:optional true} :keyword]])
(def NoSuchHandlerTags
[:map
[:category :keyword]
[:rf.trace/event-id {:optional true} :keyword]
[:rf.event/v {:optional true} [:vector :any]]
[:frame {:optional true} :keyword]
[:url {:optional true} :string] ;; routing-side variant
[:kind {:optional true} :keyword]])
(def NoSuchFxTags
[:map
[:category :keyword]
[:rf.fx/id :keyword]
[:rf.fx/args :any]
[:frame {:optional true} :keyword]])
(def NoSuchCofxTags
[:map
[:category :keyword] ;; [:= :rf.error/no-such-cofx] in a closed schema
[:rf.cofx/id :keyword]
[:rf.cofx/value {:optional true} :any] ;; only present when the 2-arity inject-cofx was used
[:rf.trace/event-id {:optional true} :keyword]])
(def OverrideFallthroughTags
[:map
[:category :keyword]
[:failing-id :keyword]
[:overrides-map [:map-of :keyword :any]]
[:looked-up-id :keyword]])
(def UnwrapBadEventShapeTags
[:map
[:category :keyword]
[:event [:vector :any]]
[:expected :string]])
;; --- runtime: validation / drain / dispatch lifecycle ---
(def SchemaValidationTags
[:map
[:category [:= :rf.error/schema-validation-failure]]
[:failing-id :keyword]
[:reason {:optional true} :string]
[:where [:enum :event :sub-return :app-db :fx-args :cofx :flow-output :machine-data :sub-override]] ;; :machine-data is the `reg-machine` :schema boundary (Spec 005 §Schema validation, Spec 010 §Per-step recovery row 7); :sub-override is the `:sub-overrides` HIT boundary (Spec 006 §Sub-overrides, rf2-7pgiz)
[:path {:optional true} [:vector :any]]
[:value {:optional true} :any]
[:explain {:optional true} :any] ;; Malli explanation shape
[:rf.sub/query-v {:optional true} :any] ;; (:where :sub-return only) caller-supplied query vector; redacted to :rf/redacted when sub is :sensitive? — see Spec/010
[:rollback? {:optional true} :boolean] ;; (:where :app-db / :machine-data) true when :db was rolled back to pre-handler value; false for :where :machine-data :phase :spawn (nothing committed)
[:registered-path {:optional true} [:vector :any]] ;; (:where :app-db only) registration root; :path is the failing leaf — see Spec/010
[:machine-id {:optional true} :keyword] ;; (:where :machine-data only) the failing machine's id; mirrors :failing-id for domain clarity. [:phase {:optional true} [:enum :macrostep :bootstrap :spawn]] ;; (:where :machine-data only) lifecycle position of the violation: :macrostep (post-transition commit), :bootstrap (initial :data install on the first dispatch), :spawn (pre-install spawn rejection). [:received {:optional true} :any] ;; (:where :machine-data / :app-db / :event / :cofx / :sub-return / :fx-args) parallel to :value; the value the validator received. [:schema {:optional true} :any]]) ;; (:where :machine-data only) the registered schema verbatim, so consumers can render it inline next to the failing :data.
(def DrainDepthExceededTags
[:map
[:category :keyword]
[:frame :keyword]
[:depth :int]
[:queue-size :int]
[:last-event {:optional true} [:vector :any]]
[:rollback? {:optional true} :boolean]])
(def DispatchSyncInHandlerTags
[:map
[:category :keyword]
[:frame :keyword]
[:rf.event/v [:vector :any]]
[:reason :string]])
(def FrameDestroyedTags
[:map
[:category :keyword]
[:frame :keyword]
[:rf.event/v {:optional true} [:vector :any]]
[:rf.sub/query-v {:optional true} [:vector :any]]])
(def EffectMapShapeTags
[:map
[:category :keyword]
[:failing-id :keyword]
[:rf.trace/event-id :keyword]
[:rf.event/v [:vector :any]]
[:offending-key :keyword]
[:value :any]
[:reason :string]])
(def EffectHandlerBadReturnTags
[:map
[:category [:= :rf.error/effect-handler-bad-return]]
[:event-id {:optional true} :keyword]
[:event [:vector :any]]
[:returned :any]
[:returned-type :any]
[:reason :string]
[:recovery [:= :no-recovery]]])
(def FlowEvalExceptionTags
[:map
[:category :keyword]
[:frame :keyword]
[:event [:vector :any]]
[:exception :any]])
;; --- runtime: state-machine errors (per [005](005-StateMachines.md)) ---
(def MachineActionExceptionTags
[:map
[:category [:= :rf.error/machine-action-exception]]
[:machine-id :keyword]
[:action-id :any]
[:state-path [:vector :any]]
[:transition :any]
[:event [:vector :any]]
[:failing-id :keyword]
[:handler-id :keyword]
[:frame :keyword]
[:exception-message :string]
[:exception-data {:optional true} :any]])
(def MachineRaiseDepthExceededTags
[:map
[:category :keyword]
[:machine-id :keyword]
[:depth :int]])
(def MachineAlwaysDepthExceededTags
[:map
[:category :keyword]
[:machine-id :keyword]
[:depth :int]
[:path [:vector :any]]])
(def MachineUnresolvedGuardTags
[:map
[:category :keyword]
[:guard :keyword]
[:machine-id :keyword]])
(def MachineUnresolvedActionTags
[:map
[:category :keyword]
[:action :keyword]
[:machine-id :keyword]])
(def MachineBadGuardFormTags
[:map
[:category :keyword]
[:guard :any]])
(def MachineBadActionFormTags
[:map
[:category :keyword]
[:action :any]])
(def MachineBadStateFormTags
[:map
[:category :keyword]
[:state :any]])
(def MachineBadOnClauseTags
[:map
[:category :keyword]
[:value :any]])
(def MachineActionWroteDbTags
[:map
[:category :keyword]
[:machine-id :keyword]
[:action-id :any]
[:state-path [:vector :any]]
[:offending-value :any]])
(def MachineGrammarNotInV1Tags
[:map
[:category :keyword]
[:machine-id :keyword]
[:feature :keyword]
[:substitute {:optional true} :string]])
;; The benign unhandled-event no-op (rf2-ugdas — xstate-v5 parity). Op-type
;; `:rf.machine`, operation `:rf.machine.event/unhandled-no-op`; NOT an error.
;; Retires the former `:rf.error/machine-unhandled-event` (`MachineUnhandledEventTags`).
(def MachineUnhandledNoOpTags
[:map
[:machine-id :keyword]
[:event [:vector :any]]
[:state :any]])
(def MachineStateNotInDefinitionTags
[:map
[:category :keyword]
[:machine-id :keyword]
[:state :any]])
(def MachineSnapshotVersionMismatchTags
[:map
[:category :keyword]
[:machine-id :keyword]
[:version-recorded :any]
[:version-current :any]])
(def MachineAlwaysSelfLoopTags
[:map
[:category :keyword]
[:state :keyword]
[:machine-id :keyword]])
(def MachineCompoundStateMissingInitialTags
[:map
[:category :keyword]
[:machine-id :keyword]
[:state :any]])
;; --- runtime: machine guard / action trace payloads
;; (per [005 §Trace events — guard evaluations and action runs] and) ---
(def MachineGuardEvaluatedTags
[:map
[:machine-id :keyword]
[:guard-id :any] ;; keyword OR inline fn
[:input [:map
[:data :any]
[:event [:vector :any]]]]
[:outcome [:enum :pass :fail :threw]] ;; :threw added per
[:exception {:optional true} :any]]) ;; present only on the :threw path
(def MachineActionRanTags
[:map
[:machine-id :keyword]
[:action-id :any] ;; keyword OR inline fn
[:phase [:enum ;; closed set
:exit :transition :entry
:always :after-action
:initial-entry :destroy-exit]]
[:input [:map
[:data :any]
[:event [:vector :any]]]]
[:outcome :any] ;; <return-value> | :ok | :rf.error/action-threw
[:exception {:optional true} :any]]) ;; present only on the throw path
;; --- runtime: machine `:after` timer cancelled trace payload
;; (per [005 §Trace events] and) ---
(def MachineTimerCancelledTags
;; the unified `:rf.machine.timer/cancelled` event
;; emitted on every cancellation path. Payload shape mirrors
;; `:rf.machine.timer/scheduled` for arm-fire-cancel pairing by
;; `(machine-id, state, epoch)`; `:reason` discriminates the
;; cancellation cause from the closed set below.
[:map
[:machine-id :keyword]
[:state :any] ;; keyword OR vector path
[:delay :int] ;; the resolved-ms the timer held
[:epoch :int]
[:reason [:enum
:on-exit ;; bearing state exited
:on-destroy ;; machine destroyed
:on-resolution ;; sub-vec delay re-resolved
:on-supersede ;; re-armed on a still-live slot
:on-frame-destroy]] ;; frame teardown
[:frame :keyword]
[:delay-source {:optional true} :any]
[:sub-id {:optional true} :any]]) ;; present when :delay-source = :sub
(def SystemIdCollisionTags
[:map
[:category :keyword]
[:frame :keyword]
[:system-id :any]
[:existing-machine :keyword]
[:rebound-to :keyword]
[:reason :string]])
;; --- runtime: routing errors (per [012](012-Routing.md)) ---
(def NoSuchRouteTags
[:map
[:category :keyword]
[:route-id :keyword]])
(def MissingRouteParamTags
[:map
[:category :keyword]
[:param :keyword]
[:route-id :keyword]])
(def DuplicateUrlBindingTags
[:map
[:category :keyword]
[:existing-frame :keyword]
[:offending-frame :keyword]])
(def RouteShadowedByEqualScoreTags
[:map
[:category :keyword]
[:route-id :keyword]
[:shadowed :keyword]
[:rank {:optional true} :any]])
(def StaleSuppressedTags
[:map
[:category :keyword]
[:carried-token :any]
[:current-token :any]
[:event-id {:optional true} :keyword]])
;; --- runtime: schemas / preset / adapter / SSR errors ---
(def BadAppSchemasArgTags
[:map
[:category :keyword]
[:received :any]
[:expected :string]])
(def UnknownPresetTags
[:map
[:category :keyword]
[:preset :any]
[:valid [:set :keyword]]])
(def AdapterAlreadyInstalledTags
[:map
[:category :keyword]
[:installed :any]
[:attempted :any]])
(def NoAdapterSpecifiedTags
;; (rf/init! …) raised because the consumer did not pass
;; an adapter spec map. The only legal call shape is (rf/init! adapter-map);
;; calling (rf/init!) with no args, nil, or a non-map argument (e.g. a
;; keyword) raises this error. Surfaced as a thrown ex-info, not a trace.
[:map
[:category :keyword]
[:where [:or :symbol :string]]
[:received {:optional true} :any]
[:expected {:optional true} :string]
[:recovery {:optional true} :keyword]
[:reason :string]])
(def AdapterMap
;; The substrate adapter spec map per Spec 006 §The reactive-substrate
;; adapter contract. Nine fn entries plus a :kind discriminator
;; keyword (per Spec 006 §Adapter introspection — surfaced by
;; `(rf/current-adapter)`). Consumers pass this map to
;; (rf/init! adapter-map) — each adapter ns
;; (re-frame.adapter.{reagent,reagent-slim,uix,helix}, re-frame.ssr,
;; re-frame.substrate.plain-atom) exports an `adapter` Var of this shape.
[:map
[:kind :keyword]
[:make-state-container fn?]
[:read-container fn?]
[:replace-container! fn?]
[:make-derived-value fn?]
[:render fn?]
[:render-to-string fn?]
[:subscribe-container {:optional true} fn?]
[:register-context-provider {:optional true} fn?]
[:dispose-adapter! {:optional true} fn?]])
(def RenderOnHeadlessAdapterTags
[:map
[:category :keyword]
[:reason :string]])
(def NoHiccupEmitterBoundTags
[:map
[:category :keyword]
[:reason :string]
[:render-tree :any]])
(def SanitisedOnProjectionTags
[:map
[:category :keyword]
[:projector-id :keyword]
[:original-operation {:optional true} :keyword]
[:projection-failure-reason {:optional true} :string]
[:exception-message {:optional true} :string]
[:returned {:optional true} :any]
[:reason :string]])
;; --- runtime: flow errors (per [013](013-Flows.md)) ---
(def FlowCycleTags
[:map
[:category :keyword]
[:cycle [:vector :any]]])
(def FlowMissingIdTags
[:map
[:category :keyword]
[:flow :map]])
(def FlowBadInputsTags
[:map
[:category :keyword]
[:flow :map]
[:reason :string]])
(def FlowBadOutputTags
[:map
[:category :keyword]
[:flow :map]
[:reason :string]])
(def FlowBadPathTags
[:map
[:category :keyword]
[:flow :map]
[:reason :string]])
;; --- runtime: artefact-missing errors (per MIGRATION §M-31) ---
(def ArtefactMissingTags
;; Shared shape for the seven artefact-missing categories: flows, ssr,
;; routing, schemas, machines, http, epoch. Each surfaces as a thrown
;; ex-info with this payload; not a trace event.
[:map
[:category :keyword]
[:where [:or :symbol :string]]
[:reason :string]
;; per-artefact optional context keys
[:flow-id {:optional true} :keyword]
[:route-id {:optional true} :keyword]
[:machine-id {:optional true} :keyword]
[:path {:optional true} [:vector :any]]
[:id {:optional true} :keyword]
[:frame {:optional true} :keyword]])
;; --- runtime: epoch restore errors (per [Tool-Pair §Time-travel]) ---
(def RestoreUnknownEpochTags
[:map
[:category :keyword]
[:frame :keyword]
[:rf.epoch/id :any]
[:history-size :int]])
(def RestoreSchemaMismatchTags
[:map
[:category :keyword]
[:frame :keyword]
[:rf.epoch/id :any]
[:schema-digest-recorded :any]
[:schema-digest-current :any]
[:failing-paths [:vector :any]]])
(def RestoreMissingHandlerTags
[:map
[:category :keyword]
[:frame :keyword]
[:rf.epoch/id :any]
[:missing [:vector [:map [:kind :keyword] [:id :keyword]]]]])
(def RestoreVersionMismatchTags
[:map
[:category :keyword]
[:frame :keyword]
[:rf.epoch/id :any]
[:machine-id :keyword]
[:version-recorded :any]
[:version-current :any]])
(def RestoreDuringDrainTags
[:map
[:category :keyword]
[:frame :keyword]
[:rf.epoch/id :any]])
;; --- Tool-Pair §Pair-tool writes — reset-frame-db! ---
(def DbReplacedTags
;; :rf.epoch/db-replaced — fired by reset-frame-db! on the success
;; path. :op-type :rf.epoch (not :error). Carries the synthetic
;; record's epoch-id so consumers can correlate the trace with the
;; recorded epoch in epoch-history.
[:map
[:frame :keyword]
[:rf.epoch/id :any]])
(def ResetFrameDbDuringDrainTags
;; :rf.epoch/reset-frame-db-during-drain — failure mode: caller
;; invoked reset-frame-db! while the frame's drain was in flight.
;; Mirrors RestoreDuringDrainTags' shape (no :rf.epoch/id slot — the
;; injection was rejected before any synthetic record was assembled).
[:map
[:category :keyword]
[:frame :keyword]])
(def ResetFrameDbSchemaMismatchTags
;; :rf.epoch/reset-frame-db-schema-mismatch — failure mode: the
;; new-db argument failed the frame's currently-registered app-schema
;; set. :failing-paths enumerates the paths that did not validate.
[:map
[:category :keyword]
[:frame :keyword]
[:failing-paths [:vector :any]]])
;; --- Tool-Pair §Surface behaviour against destroyed frames ---
(def EpochCbSilencedOnFrameDestroyTags
;; :rf.epoch.cb/silenced-on-frame-destroy — emitted once per
;; (frame, cb-id) pair when a frame previously observed by a
;; register-epoch-listener! callback is destroyed. :op-type :rf.epoch.cb (not
;; :error). One-shot; subsequent destroys of the same frame do not
;; re-emit. The callback registration remains in place; eviction is
;; the consumer's call. Per Tool-Pair §Surface behaviour against
;; destroyed frames.
[:map
[:frame :keyword]
[:cb-id [:or :keyword :string]]])
;; --- warnings: SSR / authoring-time advisories ---
(def MultipleStatusSetTags
[:map
[:category :keyword]
[:writes [:vector :any]]
[:final-status :any]
[:frame {:optional true} :keyword]])
(def MultipleRedirectsTags
[:map
[:category :keyword]
[:writes [:vector :any]]
[:final-redirect :any]
[:frame {:optional true} :keyword]])
(def HeadMismatchTags
[:map
[:category :keyword]
[:server-hash :any]
[:client-hash :any]
[:head-id {:optional true} :keyword]])
(def HydrationMismatchTags
[:map
[:category :keyword]
[:server-hash :any]
[:client-hash :any]
[:first-diff-path {:optional true} [:vector :any]]])
(def InterceptorsInMetadataMapTags
[:map
[:category :keyword]
[:reg-fn :string]
[:id :keyword]
[:offending-keys [:vector :keyword]]
[:reason :string]])
(def PlainFnUnderNonDefaultFrameOnceTags
[:map
[:category :keyword]
[:fn-name :string]
[:rendered-under :keyword]
[:routed-to :keyword]])
(def NoClockConfiguredTags
[:map
[:category :keyword]
[:feature :keyword]
[:fallback {:optional true} :any]])
(def DispatchFromAsyncCallbackFellThroughTags
[:map
[:category [:= :rf.warning/dispatch-from-async-callback-fell-through-to-default]]
[:event [:vector :any]]
[:event-id :keyword]
[:routed-to [:= :rf/default]]
[:detected-at :int] ;; wall-clock ms
[:reason :string]
[:source-coord {:optional true} :any]]) ;; optional — `dispatch` is not macro-stamped, so the call-site coord may be absent
(def CrossFrameDispatchSyncDuringDrainTags
[:map
[:category [:= :rf.warning/cross-frame-dispatch-sync-during-drain]]
[:caller-frame :keyword] ;; `*current-frame*` at the call site, or `:rf/none` when unbound
[:target-frame :keyword] ;; the `dispatch-sync!`'s `:frame` opt (or resolved default)
[:other-frame :keyword] ;; an arbitrary mid-drain sibling — typically the caller's frame
[:event [:vector :any]]
[:reason :string]])
(def DecodeDefaultedTags
[:map
[:category :keyword]
[:request-id :any]
[:url :string]
[:content-type {:optional true} :string]
[:resolved-decoder :keyword]])
(def WriteAfterDestroyTags
[:map
[:category :keyword]
[:reason :string]])
;; --- warning: dev-mode advisory when the walker observes a large string at an unschema'd path ---
;; Schemas are the only nomination path for size elision; the walker does NOT auto-elide
;; unschema'd values, but it emits this warning once per (frame, path) to nudge authors toward
;; declaring `{:large? true}` on the slot's Malli schema. Per spec/009-Instrumentation.md
;; §Size elision in traces.
(def LargeValueUnschemadTags
[:map
[:category [:= :rf.warning/large-value-unschema'd]]
[:frame :keyword]
[:path [:vector :any]] ;; the app-db path the walker observed
[:bytes :int] ;; `pr-str` byte count that exceeded the dev threshold
[:hint {:optional true} [:maybe :string]]])
;; --- info: managed-HTTP retry advisories ---
(def CljsOnlyKeyIgnoredOnJvmTags
[:map
[:category :keyword]
[:key :keyword]
[:url :string]])
(def RetryAttemptTags
[:map
[:category :keyword]
[:request-id :any]
[:url :string]
[:attempt :int]
[:max-attempts :int]
[:failure :any] ;; one of the :rf.http/* failure-map shapes
[:next-backoff-ms {:optional true} [:maybe :int]]])
;; --- info: per-frame HTTP interceptor lifecycle ---
(def HttpInterceptorRegisteredTags
[:map
[:category :keyword]
[:frame :keyword]
[:id :keyword]])
(def HttpInterceptorClearedTags
[:map
[:category :keyword]
[:frame :keyword]
[:id :keyword]])
;; --- error: a request-interceptor :before threw ---
(def HttpInterceptorFailedTags
[:map
[:category :keyword]
[:frame :keyword]
[:interceptor-id :keyword]
[:url {:optional true} [:maybe :string]]
[:cause {:optional true} [:maybe :string]]])
;; --- value schemas for the per-frame request-side interceptor ---
(def HttpInterceptor
[:map
[:frame {:optional true} :keyword] ;; defaults to :rf/default
[:id :keyword] ;; addressable for clear-http-interceptor
[:before [:=> [:cat :map] :map]]]) ;; (fn [ctx] ctx')
(def HttpInterceptorContext
[:map
[:request :map] ;; the :request envelope per Spec 014
[:args :map] ;; the full :rf.http/managed args map
[:frame :keyword]
[:event {:optional true} [:maybe vector?]]])
;; --- fx-substrate event (warning-shaped per :rf/error-event table) ---
(def FxSkippedOnPlatformTags
[:map
[:category :keyword]
[:fx-id :keyword]
[:fx-args :any]
[:frame {:optional true} :keyword]
[:platform :keyword]
[:registered-platforms [:set :keyword]]])
(def FxHandledTags
[:map
[:category :keyword]
[:fx-id :keyword]
[:fx-args :any]
[:frame {:optional true} :keyword]])
;; --- frame-lifecycle event (op-type :frame, not error/warning) ---
(def DrainInterruptedTags
[:map
[:category [:= :rf.frame/drain-interrupted]]
[:frame :keyword]
[:dropped-count :int]])
Pattern-level: every implementation registers an equivalent set of schemas. The category vocabulary is fixed-and-additive per Spec-ulation: existing categories cannot be renamed or removed; new categories appear additively.
The schemas above are open (Malli's default [:map ...]) — consumers receive payloads that conform to the listed keys plus any additive keys the implementation adds. Validation against these schemas is non-fatal in dev: a validate failure is logged via the same trace stream (per 009) but does not abort the consumer. In production, both validation and the trace stream are compile-time elided (per 009 lead claim and Spec 000 C-000.35) — there is no runtime validation cost and no trace emission.
InterceptorContextErrorKeys — post-chain interceptor-context error contract¶
Layer: Runtime Owner: 002-Frames §Per-event drain Status: v1-required
When an interceptor's :before or :after function throws, the chain runner records the failure into the context map under two paired keys before continuing or short-circuiting. Each captured error is a {:phase :id :exception} record; per rf2-mszrz it additionally carries :rf/cofx-id when the throwing interceptor is an inject-cofx injector (so the router can attribute a coeffect-injection throw to the fully-qualified cofx id rather than the bare interceptor :id):
(def InterceptorContextErrorKeys
[:map
;; The FIRST error captured during chain execution — the original cause.
;; Trace code reads this (its captured `:id` / `:rf/cofx-id` / `:phase`)
;; to fire the component-attributed error category per rf2-mszrz —
;; `:rf.error/handler-exception` (the event handler), `:rf.error/coeffect-exception`
;; (a cofx injection), or `:rf.error/interceptor-exception` (a user
;; interceptor :before/:after). Singleton: once set, subsequent failures
;; do NOT overwrite it (preserves the root cause).
[:rf/interceptor-error {:optional true} :any]
;; ALL errors captured during chain execution, in occurrence order.
;; Vector: every `:before` and `:after` throw appends here, even after
;; the singleton above has been set. A later `:after`-phase failure that
;; would otherwise be hidden by an earlier `:before` failure is preserved
;; for post-hoc inspection (pair-tools, Xray).
[:rf/interceptor-errors {:optional true} [:vector :any]]])
Semantics (the contract ports must uphold):
- Singleton-FIRST / vector-ALL.
:rf/interceptor-erroris set once — to the first throw observed.:rf/interceptor-errorscollects every throw in order; subsequent entries append. :beforefailures short-circuit subsequent:beforestages. Remaining:beforeinterceptors are skipped; the handler is also skipped.:afterpass runs in full regardless of:beforefailures — interceptors that allocate cleanup-on-:afterresources must always get their:aftercall. An:afterthrow appends to:rf/interceptor-errorsbut does not abort the remaining:afterstages.- Trace emission tracks the singleton, attributed to the true failing component. The trace stream emits one error event per chain execution — keyed off
:rf/interceptor-error. The category is derived from the captured component identity (per rf2-mszrz)::rf.error/handler-exceptionwhen the throwing:idis the handler-wrapper (:rf/db-handler/:rf/fx-handler/:rf/ctx-handler),:rf.error/coeffect-exceptionwhen the captured error carries:rf/cofx-id(aninject-cofxinjection threw), and:rf.error/interceptor-exceptionotherwise (a user interceptor's:before/:after, with:phasediscriminating the two). The:failing-idtag carries the true component id (event id / cofx id / interceptor id), NOT a blanket event id. Consumers wanting the full failure set read:rf/interceptor-errorsfrom the post-drain context snapshot directly.
Both keys are namespaced under :rf/, so user-installed interceptors that read or write context entries don't collide with the runtime-owned slots. Per Conventions §Reserved namespaces, user code MUST NOT write to either key.
:rf/handler-body-dsl¶
Layer: Conformance Owner: spec/conformance/README Status: v1-required (conformance corpus)
Conformance-corpus event/sub/view handler bodies are described as data so any in-scope host (per §Scope) can interpret them without shipping CLJS lambdas. The DSL is a small fixed vocabulary of operations the harness in each host implements. Grammar:
(def HandlerBodyOp
[:or
;; --- db / context manipulation ---
[:tuple [:= :set] [:vector :any] :any] ;; [:set path value]
[:tuple [:= :update] [:vector :any] :any] ;; [:update path fn-spec] -- fn-spec is e.g. [:fn :inc]
[:tuple [:= :merge-into-db] :map] ;; [:merge-into-db {...}]
[:tuple [:= :get] [:vector :any]] ;; [:get path] -- read; used in sub bodies / preds
;; --- event / cofx access ---
[:tuple [:= :event-arg] :int] ;; [:event-arg N] -- the Nth element of the event vector
[:tuple [:= :event-arg] :int :any] ;; [:event-arg N default] -- the Nth element; default returned when the element is nil (default-for-nil; never type-dispatched)
[:tuple [:= :get-event-arg] :int :keyword] ;; [:get-event-arg N :key] -- (get (nth event N) :key)
[:tuple [:= :get-event-arg] :int :keyword :any] ;; [:get-event-arg N :key default] -- key-access with default if missing/nil
[:tuple [:= :cofx-without] :keyword] ;; [:cofx-without :db] -- assert :db absent (test fixture)
;; --- effects / dispatch ---
[:tuple [:= :dispatch] [:vector :any]] ;; [:dispatch [:event ...]]
[:tuple [:= :fx] [:vector :any]] ;; [:fx [[fx-id args] ...]] -- explicit effect-map :fx slot
;; --- view / sub return shapes ---
[:tuple [:= :hiccup] [:vector :any]] ;; [:hiccup [...]] -- view body returning hiccup
;; --- inline fn references ---
[:tuple [:= :fn] :keyword] ;; [:fn :inc] -- look up an interpreter-known fn
;; --- control / negative cases ---
[:tuple [:= :throw]] ;; [:throw] -- error-path fixtures
[:tuple [:= :noop]]]) ;; [:noop]
(def HandlerBody
[:vector HandlerBodyOp])
The body of a fixture event handler / sub computation is a vector of these ops, executed in order. Semantics:
| Op | Used in | Effect |
|---|---|---|
:set |
event-db, event-fx | (assoc-in db path value) |
:update |
event-db, event-fx | (update-in db path f) where f resolves a [:fn :name] form to a host-built-in fn |
:merge-into-db |
event-db, event-fx | (merge db m) |
:get |
sub, predicate | Read from current db — produces the sub's return value |
:event-arg |
event, sub | Selects an event-vector argument by index. An optional 3rd element is a default-for-nil — returned when the Nth element is nil. The 3rd element is never type-dispatched; for key-access into a map argument use :get-event-arg. |
:get-event-arg |
event, sub | (get (nth event N) :key) — single-key access into the Nth event arg; an optional 4th element is a default returned when the key is missing/nil |
:cofx-without |
event-fx | Asserts a cofx key is absent — test fixture |
:dispatch |
event-fx | Adds [:dispatch event] to the effect-map's :fx |
:fx |
event-fx | Adds entries to the effect-map's :fx directly |
:hiccup |
view | Returns the literal hiccup vector as the view's render-tree |
:fn |
as a value inside other ops | Names a built-in fn (e.g. :inc, :dec, :identity) |
:throw |
any | Throws — exercises :rf.error/* paths |
:noop |
any | Does nothing — used to anchor empty-body fixtures |
Built-in fns the [:fn :name] form resolves to: :inc, :dec, :identity, :not, :keyword?, :number?, :string?. Hosts may extend this set additively per a corpus revision.
The op vocabulary is closed for v1 of the corpus. The conformance corpus's :fixture/handlers shape (per conformance/README.md) consumes this DSL — :rf/handler-body-dsl and :rf/fixture-handler-body are synonyms; this entry is canonical.
State-machine transition-table guards and actions are referenced by inline fn or keyword reference into the machine's local :guards / :actions map in :rf/transition-table below; those are the runtime grammar for machine transitions per 005-StateMachines.md, not part of this conformance handler-body DSL. Transition slots take fn-valued or keyword-valued :guard and :action slots — keyword values resolve machine-locally against the spec's :guards / :actions map (no global registry); effects emitted by an action — including the reserved fx-id :raise and the canonical actor-lifecycle fx-ids :rf.machine/spawn / :rf.machine/destroy — appear inside the action's returned :fx vector.
:rf/transition-table¶
Layer: Public Owner: 005-StateMachines §Transition tables Status: v1-required
Grammar for state-machine transition tables (per 005). Public because the user supplies the transition table to make-machine-handler as registration data; tools introspect it via (machine-meta id). The v1 foundation (machine-transition / make-machine-handler and the rest of the machine-as-event-handler surface — see 005 §Disposition) interprets the grammar that maps to the v1 reference's claimed capability list. The CLJS reference claims flat FSM, hierarchical compound states, eventless :always, delayed :after, declarative :spawn, parallel regions, final states, and history states.
The schema below covers the flat FSM grammar, the hierarchical compound extension (per 005 §Hierarchical compound states), the eventless :always extension (per 005 §Eventless :always transitions), the delayed :after extension (per 005 §Delayed :after transitions), the declarative :spawn extension (per 005 §Declarative :spawn), and the history pseudo-state arm (per 005 §History states). All extensions are additive (open-map invariant) without breaking the Transition schema.
(def TransitionTable
[:ref ::state-node]) ;; a TransitionTable IS the root state-node (it just happens to be where :initial / :states begin)
;; A state-node is recursive — a leaf has no :states; a compound state declares
;; :states and MUST declare :initial. Per [005 §Initial-state cascading]
;; (005-StateMachines.md#initial-state-cascading).
;;
;; The :guards and :actions maps are root-only — they declare the machine's
;; named guard / action implementations. Transition-table keyword references
;; (`:guard :foo`, `:action :bar`) resolve **machine-locally** against these
;; maps; there is no global :machine-guard / :machine-action registry. See
;; [005 §Registration](005-StateMachines.md#registration--the-machine-is-the-event-handler)
;; and [005 §Inspectability bias](005-StateMachines.md#inspectability-bias).
;;
;; ELEMENT-ENTRY SHAPE (rf2-npvsx). Each :guards / :actions / :on-spawn-actions
;; entry value is a MachineElementEntry — a co-located `{:fn <fn> :source-coords
;; .. :source-code ..}` map where `:fn` is the callback the runtime invokes and
;; `:source-coords` / `:source-code` are DEBUG-only (absent in production; the
;; macro elides them). As-written user source and programmatic `reg-machine*`
;; specs carry a bare `fn?` value instead; the runtime accepts both (and a
;; `keyword?` indirection). The macro path co-locates source onto each entry —
;; superseding the former `:rf.machine/source-coords` + `:rf.machine/handler-
;; source` side-indexes (per [005 §Source-coord stamping](005-StateMachines.md#source-coord-stamping)).
;;
;; REFERENCE-SITE COORD (rf2-vqja2). Each :states-tree MAP node (a state-node,
;; a transition map under :on / :always / :after, a :spawn map) additionally
;; co-locates its OWN reference-site `:source-coords` directly on the node —
;; DEBUG-only, absent in production (the macro elides it). This supersedes the
;; former flat `:rf.machine/state-coords` side-index that paralleled :states.
;; Inline-fn / keyword slots (:entry / :exit / :guard / :action / :on-spawn)
;; hold a value, not a map, so they carry no coord of their own; a tool reads
;; the nearest enclosing map node's `:source-coords`. The runtime ignores the
;; key (it reads only :on / :states / :initial / :tags / … by name).
(def MachineElementEntry
[:or fn? ;; as-written / programmatic: a bare guard/action fn
:keyword ;; keyword indirection ({:short-name :registered-id})
[:map ;; macro-stamped co-located entry
[:fn fn?] ;; ALWAYS present — the callback the runtime invokes via (:fn entry)
[:source-coords {:optional true} [:ref :rf/source-coord-meta]] ;; DEBUG-only — absent in production
[:source-code {:optional true} :string]]]) ;; DEBUG-only — the pr-str of the fn-form; absent in production
(def StateNode
[:schema {:registry {::state-node
[:map
[:type {:optional true}
[:enum :single :parallel :history]] ;; controls how the runtime interprets the node. ROOT-ONLY values: absent / :single (the default — flat-or-compound shape disambiguated by whether `:states` declares nested `:states`); `:parallel` switches the spec to parallel-region mode — `:regions` (below) is required and `:states` / `:initial` MUST be absent (Nine States Stage 2, [005 §Parallel regions](005-StateMachines.md#parallel-regions)). CHILD-ONLY value: `:history` marks a HISTORY PSEUDO-STATE — declared under a compound's `:states`, never occupied, a transition target that resolves to the compound's recorded (or default) configuration. A `:type :history` node declares ONLY `:deep?` / `:default-target` (below) and none of the ordinary state-node keys; `make-machine-handler` rejects any other key, and a `:history` node at the machine root, on the parallel `:regions` map, with two-per-compound, or with an unresolvable `:default-target` at registration. Per [005 §History states](005-StateMachines.md#history-states-type-history--shallow--deep--default-target).
[:deep? {:optional true} :boolean] ;; history-pseudo-state-only (`:type :history`). `true` => DEEP history (restore the full recorded leaf path beneath the compound); absent / `false` => SHALLOW history (restore the recorded direct child, then cascade its `:initial` chain). Default shallow. Per [005 §History states](005-StateMachines.md#history-states-type-history--shallow--deep--default-target).
[:default-target {:optional true} TransitionTarget] ;; history-pseudo-state-only (`:type :history`). Target used when the owning compound has never been entered (nothing recorded yet) — a keyword (direct child of the compound) or vector (absolute path); MUST resolve to a real state. Absent => falls back to the owning compound's `:initial`. Per [005 §History states](005-StateMachines.md#history-states-type-history--shallow--deep--default-target).
[:regions {:optional true}
[:map-of :keyword [:ref ::state-node]]] ;; root-only — required iff `:type :parallel`. Each entry's value is a full state-node body (its own `:initial` + `:states`, optionally `:tags`, `:on`, etc.). Region names are keywords; region-name → state-tree. All regions are active simultaneously; the snapshot's `:state` is a map of region-name → keyword-or-vector-path. [:initial {:optional true} :keyword] ;; required iff :states is present (compound state); points to the cascade entry-point
[:states {:optional true} [:map-of :keyword [:ref ::state-node]]]
[:data {:optional true} :map] ;; root-only — initial extended-state data map; ignored on non-root nodes. §9.4 (Shared `:data`): parallel-region machines share one `:data` blob across every region. There is no per-region `:data` slot — apps that need per-region encapsulation register N independent machines (see [CP-5-MachineGuide §Substitutes](CP-5-MachineGuide.md#substitutes-for-skipped-features)).
[:guards {:optional true} [:map-of :keyword MachineElementEntry]] ;; root-only — machine-local guard implementations; keys are referenced from :guard slots. Values are MachineElementEntry (bare fn as-written; co-located `{:fn .. :source-coords .. :source-code ..}` after macro stamping, rf2-npvsx)
[:actions {:optional true} [:map-of :keyword MachineElementEntry]] ;; root-only — machine-local action implementations; keys are referenced from :action / :entry / :exit slots
[:on-spawn-actions {:optional true} [:map-of :keyword MachineElementEntry]] ;; root-only — optional map of named spawn-callbacks; consulted before :actions when an :on-spawn slot uses a keyword reference. See [005 §Registration](005-StateMachines.md#registration--the-machine-is-the-event-handler).
[:entry {:optional true} ActionRef] ;; one fn or one keyword reference into the machine's :actions map
[:exit {:optional true} ActionRef] ;; one fn or one keyword reference into the machine's :actions map
[:spawn {:optional true} InvokeSpec] ;; declarative spawn-on-entry / destroy-on-exit; at most one per state; see :rf/state-node §:spawn and [005 §Declarative :spawn](005-StateMachines.md#declarative-spawn)
[:spawn-all {:optional true} InvokeAllSpec] ;; spawn-N-children-and-join sugar; mutually exclusive with :spawn; see :rf/state-node §:spawn-all and [005 §Spawn-and-join via :spawn-all](005-StateMachines.md#spawn-and-join-via-spawn-all)
[:always {:optional true} ;; eventless transitions checked after entry (or after any transition landing here); first-match-wins; see [005 §Eventless :always transitions](005-StateMachines.md#eventless-always-transitions)
[:vector
[:map
[:guard {:optional true} GuardRef] ;; same shape as :on transition slot; resolves machine-locally against :guards map
[:target {:optional true} TransitionTarget] ;; keyword (sibling of declaring state) or vector (absolute path); same-state same-guard self-loops rejected at registration
[:action {:optional true} ActionRef]
[:meta {:optional true} :map]
[:source-coords {:optional true} [:ref :rf/source-coord-meta]]]]] ;; DEBUG-only — co-located reference-site coord of this :always transition map (rf2-vqja2)
[:after {:optional true} ;; delayed transitions; <delay> → transition spec where <delay> is pos-int? OR a subscription vector ([sub-id & args] resolved through subscribe; re-resolves on subscription change) OR (fn [{:keys [snapshot]}] ms) computed at state entry (unified context-map); epoch-based stale detection; SSR no-ops scheduling; see [005 §Delayed :after transitions](005-StateMachines.md#delayed-after-transitions) and [005 §Dynamic delay re-resolution](005-StateMachines.md#dynamic-delay-re-resolution). [:map-of
[:or pos-int? ;; literal milliseconds (default form)
[:vector :any] ;; subscription vector — [sub-id & args]; re-resolves on sub change
fn?] ;; (fn [{:keys [snapshot]}] ms) — local-data-derived delay; computed once at entry. [:or Transition ;; single transition — keyword-target sugar (desugars to {:target <kw>}), vector-path target, OR a full transition map ({:guard :target :action :meta}); same shape as an :on slot
[:vector Transition]]]] ;; guarded candidate-vector — first-guard-pass-wins at timer expiry, EXACTLY as an :on clause's multiple-candidate form (per [005 §Value shape](005-StateMachines.md#value-shape)). The `:after` VALUE grammar is identical to EventMap's value side — the runtime normalises both through one shared candidate-walk so the two slots can never drift.
[:on {:optional true} EventMap] ;; event → transition
[:tags {:optional true} [:set :keyword]] ;; runtime-projected onto snapshot's :tags — see [005 §State tags](005-StateMachines.md#state-tags); union of active-configuration tag sets is stamped at [:rf/runtime :machines :snapshots <id> :tags] on every transition commit. Reserved framework namespace (`:rf/*`, `:rf.*/*`) per Conventions.md §Reserved namespaces. [:final? {:optional true} :boolean] ;; leaf-only — entering this state terminates the machine. and [005 §Final states](005-StateMachines.md#final-states-final--on-done--output-key). A `:final?` state MUST NOT declare `:states`, `:initial`, `:on`, `:always`, `:after`, `:spawn`, or `:spawn-all` (`:entry` / `:exit` are permitted). For a `:spawn`d child: the runtime invokes the parent's `:spawn :on-done` with the child's `:data` slot named by `:output-key` (or `nil`), then auto-destroys synchronously. For a singleton: auto-destroys synchronously (singleton symmetry, D7).
[:output-key {:optional true} :keyword] ;; designates which `:data` key is reported back via the parent's `:on-done`. Requires `:final? true` (registration rejects `:output-key` on non-final states with `:rf.error/machine-output-key-without-final`). [:meta {:optional true} :map]
[:source-coords {:optional true} [:ref :rf/source-coord-meta]]]}} ;; DEBUG-only — co-located reference-site coord of this state-node (rf2-vqja2; absent in production; the macro elides it)
[:ref ::state-node]])
;; The :spawn spec on a state node. Per [005 §Declarative :spawn (sugar over spawn)]
;; (005-StateMachines.md#declarative-spawn), `make-machine-handler`
;; rewrites this slot into entry/exit actions emitting :rf.machine/spawn / :rf.machine/destroy fx
;; at registration time; the runtime sees only the desugared form. Constraint:
;; **exactly one of :machine-id or :definition** must be supplied — `make-machine-handler`
;; rejects any other shape at registration time as a malformed transition table.
(def InvokeSpec
[:map
[:machine-id {:optional true} :keyword] ;; registered machine id
[:definition {:optional true} [:ref ::state-node]] ;; inline transition table (root state-node)
[:data {:optional true} [:or :map fn?]] ;; literal initial data, OR (fn [{:keys [snapshot event]}] data) computed at entry time (unified context-map)
[:id-prefix {:optional true} :keyword] ;; defaults to :machine-id; base for the gensym'd actor id
[:on-spawn {:optional true} fn?] ;; (fn [{:keys [data id]}] _) — advisory callback fired with the spawned id; return is ignored (runtime tracks the id at [:rf/runtime :machines :spawned <parent> <invoke-id>]). [:on-done {:optional true} fn?] ;; (fn [{:keys [data result]}] new-data) — fires synchronously when the spawned child enters a `:final?` state. `result` is the child's `:data` slot named by the final state's `:output-key`, or nil when `:output-key` is absent. Returns the parent's new `:data` map. Per [005 §Final states](005-StateMachines.md#final-states-final--on-done--output-key).
[:start {:optional true} [:vector :any]] ;; event vector dispatched to the newborn after spawn
[:spawn-id {:optional true} :keyword] ;; explicit id instead of gensym (per-state singleton actor)
[:system-id {:optional true} :keyword]]) ;; per [005 §Named addressing via :system-id]; binds [:rf/runtime :machines :system-ids <sid>] in the spawning frame
;; The :timeout-ms / :on-timeout slots are DROPPED — wall-clock timeouts on
;; a :spawn-bearing state are expressed via the parent state's :after slot. See
;; [005 §Wall-clock timeouts on :spawn — use parent state's :after] and
;; [MIGRATION §M-44].
;; The :spawn-all spec on a state node — spawn-N-children-and-join. Per
;; [005 §Spawn-and-join via :spawn-all](005-StateMachines.md#spawn-and-join-via-spawn-all)
;; and . `make-machine-handler` walks the spec at construction time
;; and rewrites the slot into entry/exit actions emitting N parallel
;; :rf.machine/spawn fx (entry) and per-child :rf.machine/destroy fx (exit),
;; plus an internal join-state hook that intercepts :on-child-done /
;; :on-child-error events at the parent's handler boundary, updates the
;; runtime-owned join state at [:rf/runtime :machines :spawned <parent-id> <invoke-id> :join],
;; and dispatches the resolution event into the parent.
;;
;; Each child invoke-spec extends InvokeSpec with a required :id keyword
;; that names the child for join-state addressing. The :id is the second-
;; position payload arg the parent's :on-child-done / :on-child-error events
;; carry from the child back to the parent.
(def InvokeAllChildSpec
[:map
[:id :keyword] ;; user-supplied id for join-state addressing — REQUIRED
[:machine-id {:optional true} :keyword] ;; registered machine id (xor :definition)
[:definition {:optional true} [:ref ::state-node]] ;; inline transition table (xor :machine-id)
[:data {:optional true} [:or :map fn?]]
[:id-prefix {:optional true} :keyword]
[:on-spawn {:optional true} [:or :keyword fn?]]
[:start {:optional true} [:vector :any]]
[:spawn-id {:optional true} :keyword]
[:system-id {:optional true} :keyword]])
(def InvokeAllSpec
[:map
[:children [:vector InvokeAllChildSpec]] ;; vector of ≥ 1 child spec
[:join {:optional true}
[:or
[:enum :all :any]
[:map [:n pos-int?]]
[:map [:fn fn?]]]] ;; default :all
[:on-child-done :keyword] ;; child → parent event keyword (required)
[:on-child-error :keyword] ;; child → parent event keyword (required)
[:on-all-complete {:optional true} [:vector :any]] ;; required iff :join is :all (registration-time check)
[:on-some-complete {:optional true} [:vector :any]] ;; required iff :join is :any / {:n N} / {:fn ...}
[:on-any-failed {:optional true} [:vector :any]] ;; optional; if absent, child failures don't short-circuit
[:cancel-on-decision? {:optional true} :boolean]]) ;; default true
;; The :timeout-ms / :on-timeout slots are DROPPED — wall-clock timeouts on
;; a :spawn-all-bearing state are expressed via the parent state's :after slot. See
;; [005 §Wall-clock timeouts on :spawn — use parent state's :after] and
;; [MIGRATION §M-44].
;; The snapshot's location in app-db is the reserved path [:rf/runtime :machines :snapshots <id>]
;; — runtime-managed and not part of the transition-table grammar. See
;; [005 §Where snapshots live](005-StateMachines.md#where-snapshots-live).
;; Event map keys are event ids (keywords) or the wildcard :* (any-event fallback).
;; Values are a single Transition or a vector of Transition candidates evaluated first-match-wins.
(def EventMap
[:map-of [:or :keyword [:= :*]] [:or Transition [:vector Transition]]])
(def Transition
[:or
TransitionTarget ;; target shorthand — keyword OR vector path; see TransitionTarget below
[:map
[:target {:optional true} TransitionTarget] ;; one of: keyword (relative to declaring state), [:vector :keyword] (absolute path from root), or :same-state (external self-transition); omit for internal
[:guard {:optional true} GuardRef] ;; one fn or one registered id
[:action {:optional true} ActionRef] ;; one fn or one registered id (singular — no :actions vector)
[:meta {:optional true} :map]
[:source-coords {:optional true} [:ref :rf/source-coord-meta]]]]) ;; DEBUG-only — co-located reference-site coord of this transition map (rf2-vqja2; absent in production)
;; A transition's :target admits both forms per [005 §Target resolution]
;; (005-StateMachines.md#target-resolution--vector-vs-keyword).
;; - keyword form — relative to the state where the transition is DECLARED (sibling resolution)
;; - vector form — absolute path from the root
;; Plus the literal :same-state for external self-transitions.
(def TransitionTarget
[:or :keyword [:vector :keyword]])
;; Guards are one inline fn or one keyword reference resolved against the
;; machine's local :guards map. No compound data form ({:and ...} / {:or ...}
;; / {:not ...}) — compound logic is fn composition or a named entry in the
;; machine's :guards map (whose name carries semantic content visualisers and
;; AIs read).
(def GuardRef
[:or :keyword fn?])
;; Actions are one inline fn or one keyword reference resolved against the
;; machine's local :actions map, returning the {:data :fx} effect map. No
;; action-vector form ([a1 a2 a3]) — multi-step actions are fn composition.
;; The action's returned :fx may contain the reserved fx-id :raise (which
;; the machine handler routes locally) and the canonical actor-lifecycle
;; fx-ids :rf.machine/spawn / :rf.machine/destroy (which reach the standard
;; do-fx through :rf.fx/spawn-args below).
(def ActionRef
[:or :keyword fn?])
The recursive ::state-node ref is registered under the spec id :rf/state-node so individual nodes (and slices of a transition table) can be validated in isolation — cross-references target #rfstate-node directly. The TransitionTarget schema is registered as :rf/transition-target. Compound states without :initial are a registration error — emits :rf.error/machine-compound-state-missing-initial per 005 §Initial-state cascading.
Guard / action reference resolution. A GuardRef / ActionRef keyword is machine-local — it resolves to (get-in spec [:guards <id>]) / (get-in spec [:actions <id>]), where spec is the root ::state-node of the transition table. Resolution is performed at registration time: make-machine-handler walks the table (in :on, :always, :entry, :exit slots) and verifies each keyword reference resolves to a fn in the spec's :guards / :actions map. Unresolved references fail registration with :rf.error/machine-unresolved-guard (with :tags {:guard-id <id> :machine-id <id>}) or :rf.error/machine-unresolved-action (with :tags {:action-id <id> :machine-id <id>}). There is no global guard/action registry — each machine has its own :guards / :actions namespace. Cross-machine reuse is via Clojure vars referenced from each machine's map.
:spawn constraint. The :spawn slot's InvokeSpec declares both :machine-id and :definition as optional, but exactly one must be supplied for any actual :spawn slot — Malli alone cannot express the xor without a richer combinator, so make-machine-handler enforces it at registration time and rejects malformed slots as a transition-table error. :spawn is registration-time sugar — see 005 §Declarative :spawn for the desugaring rules; the runtime never sees a :spawn key at transition time.
:type :parallel constraint. A root state-node declaring :type :parallel MUST declare a non-empty :regions map and MUST NOT declare :initial or :states — those slots are mutually exclusive with :regions. Each region's value is itself a full ::state-node body (its own :initial + :states for the compound case, or no :states for a flat region). make-machine-handler validates the shape at registration time and rejects malformed declarations with :rf.error/machine-parallel-bad-shape. Nested parallel regions (a region whose own state-tree contains another :type :parallel) are not supported in v1; the validator rejects them with :rf.error/machine-parallel-nested-not-supported. Per (Nine States Stage 2) and 005 §Parallel regions.
:timeout-ms removed. Per, the pre-release :timeout-ms / :on-timeout slots on :spawn / :spawn-all are DROPPED. State-level :after on the parent state subsumes the wall-clock guard, with the standard exit-cascade destroying spawned children. make-machine-handler rejects any :timeout-ms or :on-timeout key on either slot at registration time with :rf.error/spawn-timeout-ms-removed. The retired error categories :rf.error/machine-spawn-timeout-without-on-timeout, :rf.error/machine-spawn-on-timeout-without-timeout, and :rf.error/machine-spawn-timeout-not-positive are no longer emitted. See 005 §Wall-clock timeouts on :spawn — use parent state's :after and MIGRATION §M-44.
:always constraints. The :always slot is checked at registration time for two registration-error categories:
:rf.error/machine-always-self-loop— an:alwaysentry whose:targetresolves to the declaring state itself (keyword target equal to the state's own key, or vector target equal to its own path) is rejected at registration time, with:tags {:state <state-keyword> :machine-id <id>}. An eventless self-:targetre-evaluates the same guard on the state it just re-entered — it would either spin to depth-exceeded or be a no-op; in both cases the author meant something else. The rejection is decidable from the:targetalone; a "re-enter on a changed condition" need is expressed by targeting a distinct state. An internal:always(no:target, only an:action) is permitted — that is the canonical action-microstep pattern. See 005 §Self-loop forbidden at registration.
A second :always-related category, :rf.error/machine-always-depth-exceeded, is a runtime error (not registration): emitted when the microstep loop exceeds its depth limit (default 16), with :tags {:machine-id <id> :depth <limit> :path [<state> ...]} and :recovery :no-recovery. The cascade halts with the snapshot uncommitted. See 005 §Bounded depth.
:after constraints. Per 005 §Delayed :after transitions, the :after slot's value is a map whose keys are one of three forms — positive-integer millisecond delays, subscription vectors ([:sub-id & args] resolved through subscribe's machinery; re-resolves on subscription change per 005 §Dynamic delay re-resolution), or fns of the entering snapshot returning a positive integer — and whose values admit the same three forms as an :on clause: keyword-target sugar ({5000 :timeout}), a full transition spec ({5000 {:guard :still-loading? :target :hard-error}}), or — parallel to :on — a vector of guarded transition candidates evaluated first-match-wins at timer expiry ({5000 [{:guard :ok? :target :done} {:target :failed}]}; the first candidate whose :guard passes fires, an unguarded candidate is the unconditional fallback). The :after value grammar is identical to the :on EventMap value ([:or Transition [:vector Transition]]) — the runtime normalises both through one shared candidate-walk, so the two slots can never drift. Per (#2053) and 005 §Value shape. Sugar normalises at registration time. Cancellation is not a separate fx — staleness is detected via a per-scheduling-node epoch map stored in :data under the reserved key :rf/after-epoch ({<decl-path-vector> <int>}; the :rf/-namespace within :data is reserved for runtime-managed bookkeeping). Per-node tracking is required by 005 §Hierarchy interaction: a leaf-only sibling transition leaves a still-active parent's entry — and its in-flight timer — untouched. The clock primitives live in re-frame.interop (now-ms, schedule-after!, cancel-scheduled!); tests swap the interop layer rather than configuring a framework-level clock. Hosts whose interop layer hasn't been wired with a clock emit :rf.warning/no-clock-configured when :after is exercised — an advisory-not-fatal: the runtime falls back to a host-native clock if available. Trace events: :rf.machine.timer/scheduled, :rf.machine.timer/fired, :rf.machine.timer/stale-after, :rf.machine.timer/cancelled (with :reason closed set per — replaces the :cancelled-on-resolution), :rf.machine.timer/skipped-on-server (added to the trace-op vocabulary above). Per.
:rf/machine-snapshot¶
Layer: Runtime Owner: 005-StateMachines §Snapshot shape Status: v1-required
The runtime snapshot of a machine instance. Per 005 §Snapshot shape, every conformant snapshot is print/read round-trippable so it survives the wire (SSR hydration, 011) and the time-axis (Tool-Pair epoch replay).
(def MachineSnapshot
[:map
;; :state has THREE arms — disambiguated by the machine's declared shape:
;; - keyword for flat machines (e.g. :idle)
;; - [:vector :keyword] for compound machines — root → active leaf path (e.g. [:authenticated :cart :browsing])
;; - [:map-of :keyword <region-state>]
;; for parallel-region machines (`:type :parallel`) — region-name → that region's keyword-or-vector-path. (Nine States Stage 2).
;; Implementations accept all three forms on read and may normalise the compound
;; arm to vector internally. Per [005 §Snapshot shape](005-StateMachines.md#snapshot-shape).
[:state [:multi {:dispatch (fn [v] (cond (keyword? v) :flat
(vector? v) :compound
(map? v) :parallel))}
[:flat :keyword]
[:compound [:vector :keyword]]
;; Region values are themselves a flat keyword or a compound path —
;; each region runs an independent state-tree. Nested parallel regions
;; are not supported in v1; a region's state value cannot itself be a map.
[:parallel [:map-of :keyword [:or :keyword [:vector :keyword]]]]]]
[:data {:optional true} :map] ;; the machine's extended state; closed under print/read
;; :tags is the runtime-projected union of every active state-node's
;; `:tags` set; recomputed on every transition commit. Optional —
;; implementations MAY elide the key when the union is empty (per
;; [005 §State tags §Snapshot shape change]
;; (005-StateMachines.md#snapshot-shape-change)). [:tags {:optional true} [:set :keyword]]
;; :rf/spawn-counter is the per-machine-id integer map the runtime uses
;; to deterministically allocate spawned-actor ids inside a pure
;; machine-transition call. Each declarative `:spawn` bump increments
;; the slot under the spawned child's `:machine-id`; the bumped value
;; is the suffix on the allocated id (`<machine-id>#<n>`). The slot is
;; runtime-owned (`:rf/`-namespaced) — user code MUST NOT write to it.
;; Seeded as `{}` by the runtime when a machine first comes into being
;; (`synthesise-initial-snapshot`); pure-call snapshots (the conformance
;; harness's hand-built input snapshots) may omit it — the reducer
;; defaults absent slots to 0 via `fnil`. [:rf/spawn-counter {:optional true} [:map-of :keyword :int]]
;; :rf/history is the recorded-history map for machines declaring a
;; `:type :history` pseudo-state (per [005 §History states]
;; (005-StateMachines.md#history-states-type-history--shallow--deep--default-target)).
;; Keyed by the COMPOUND DECLARATION PATH (the absolute prefix-path of the
;; history-bearing compound state-node) → that compound's recorded
;; configuration. NOT a single config: a machine may own several
;; history-bearing compounds, each recorded independently. Under
;; `:type :parallel` the keys are REGION-QUALIFIED — the head segment of
;; the path is the region name — so two regions with structurally-identical
;; compound paths never collide. The recorded value is:
;; - a [:vector :keyword] absolute LEAF PATH for a DEEP-history compound
;; (`:deep? true`), or
;; - a single :keyword DIRECT CHILD for a SHALLOW-history compound
;; (`:deep?` absent / false; the runtime cascades the child's :initial
;; chain on restore).
;; Written by the runtime during the compound's exit cascade; READ-ONLY for
;; users (invariant 7 below). Allocated lazily — absent until a
;; history-bearing compound is first exited; a machine with no history
;; pseudo-states never carries the slot. Vectors-and-keywords only — EDN-clean,
;; round-trips through pr-str / read-string and rides SSR hydration + Tool-Pair
;; epoch replay with the rest of the snapshot.
[:rf/history {:optional true}
[:map-of [:vector :keyword] ;; compound declaration path (region-qualified head under :type :parallel)
[:or [:vector :keyword] ;; DEEP — recorded absolute leaf path
:keyword]]] ;; SHALLOW — recorded direct child
[:meta {:optional true}
[:map
[:rf/snapshot-version {:optional true} :int] ;; bumped when definition shape changes incompatibly
]]]) ;; remaining :meta keys are user-defined and tolerated
Stability invariants the implementation upholds (see 005 §Snapshot shape):
(read-string (pr-str snapshot))returns an=-equal value — no functions, atoms, JS objects in:data(or:tags— but:tagsis a set of keywords, both of which are EDN-clean).:rf/spawn-counteris a map of keyword→int and round-trips cleanly;:rf/historyis a map of keyword-vectors to keyword-vectors-or-keywords and round-trips cleanly.- Snapshots represent committed state only; no in-flight microstate is captured.
- Hot-reloading a definition does not invalidate snapshots whose
:stateis still a member. The history analogue: a recorded configuration in:rf/historythat references a substate the reloaded definition removed is a dangling recorded path — on a restore-to-history transition the runtime discards it and falls back to the pseudo-state's:default-target(or the compound's:initial), never entering the dead path. Per 005 §Dangling recorded paths after hot reload and [rf2-wgfv0]. :rf/snapshot-versionmismatch between snapshot and definition emits:rf.error/machine-snapshot-version-mismatch(per Spec 009 §Trace events; older drafts spelled this:rf.warning/machine-snapshot-version-mismatch, the:rf.error/form is canonical).:tagsis read-only for users — actions cannot return:tagsin their{:data :fx}effect map; the runtime owns the slot and recomputes it from:stateat every commit.:rf/spawn-counteris read-only for users — the runtime owns the slot and bumps it on every declarative-:spawnspawn. Apps that need to address a spawned actor by id read it from[:rf/runtime :machines :spawned <parent-id> <invoke-id>](the runtime-owned registry) or via:on-spawnadvisory bookkeeping — never from the counter directly.:rf/historyis read-only for users — the runtime owns the slot and writes it during the history-bearing compound's exit cascade. Actions cannot return:rf/historyin their{:data :fx}effect map; the recorded configuration is derived from the active path at exit, not authored. Per 005 §The:rf/historysnapshot slot.
Effect-map note. A machine handler returns a standard :rf/effect-map (:db + :fx). The action-internal {:data :fx} shape is internal to the machine handler; the handler lowers :data to a single :db write at [:rf/runtime :machines :snapshots <id> :data] before returning. The closed :rf/effect-map contract (:db + :fx only) is preserved at the handler boundary.
:rf/runtime (reserved app-db key — the sole framework-owned root)¶
Layer: Runtime Owner: Conventions §Reserved app-db keys Status: v1-required
[:rf/runtime] is the single reserved key in every frame's app-db. The runtime owns it; user code MUST NOT write under it. All framework runtime state nests under this one root — four sub-containers, one per subsystem.
(def Machines
;; The machine runtime's four sub-containers — :snapshots is the per-machine
;; snapshot map, :system-ids is the system-id reverse index, :spawned is
;; the declarative-spawn/spawn-all registry, and :spawn-counter is the
;; hand-emitted-spawn fallback counter (per rf2-owvvr — the parallel slot
;; the declarative path tracks inside the parent's snapshot). All four are
;; allocated lazily — absent until the first write — so a frame that uses
;; no machines carries no machine sub-keys at all.
[:map
[:snapshots {:optional true} [:map-of :keyword :rf/machine-snapshot]]
[:system-ids {:optional true} [:map-of :any :keyword]] ;; <system-id> → <gensym'd-machine-id>
[:spawned {:optional true} [:map-of :keyword ;; parent-machine-id
[:map-of [:vector :keyword] ;; invoke-id (absolute prefix-path)
[:or :keyword ;; :spawn leaf — gensym'd spawned-id
InvokeAllJoinState]]]] ;; :spawn-all bookkeeping
[:spawn-counter {:optional true} [:map-of :keyword :int]]]) ;; per-machine-id integer counter for hand-emitted :rf.machine/spawn fxs
(def InvokeAllJoinState
;; Join bookkeeping for a :spawn-all invocation.
[:map
[:children [:map-of :keyword :keyword]] ;; child-id → spawned-id
[:done [:set :keyword]] ;; user-ids that signalled :on-child-done
[:failed [:set :keyword]] ;; user-ids that signalled :on-child-error
[:resolved? :boolean] ;; latch flips once the join condition resolves
[:spec :map]]) ;; back-reference for the join intercept
(def Routing
;; The routing runtime's per-frame state. :current is the live route slice;
;; :pending-navigation is the can-leave pending-nav slot; the remaining four
;; flat slots are the routing-internal counters and the scroll-restoration LRU.
;; All sub-keys are allocated lazily.
[:map
[:current {:optional true} :rf/route-slice]
[:pending-navigation {:optional true} :rf/pending-navigation]
[:scroll-positions {:optional true} [:map-of :string [:tuple :int :int]]]
[:scroll-positions-order {:optional true} [:vector :string]]
[:nav-token-counter {:optional true} :int]
[:pending-nav-counter {:optional true} :int]])
(def ElisionDeclaration
[:map
[:large? :boolean]
[:hint {:optional true} [:maybe :string]]
[:source [:enum :schema]]])
(def SensitiveDeclaration
[:map
[:sensitive? :boolean]
[:hint {:optional true} [:maybe :string]]
[:source [:enum :schema]]])
(def Elision
;; The wire-elision declaration registry. Consulted by `rf/elide-wire-value`
;; at every wire-boundary emit. Schemas are the only nomination path —
;; un-schema'd slots fire :rf.warning/large-value-unschema'd but are NOT elided.
[:map
[:declarations {:optional true} [:map-of [:vector :any] ElisionDeclaration]]
[:sensitive-declarations {:optional true} [:map-of [:vector :any] SensitiveDeclaration]]])
(def HydrationMetadata
;; Server-supplied hydration metadata stashed by the :rf/hydrate handler.
;; :server-hash is the carrier `verify-hydration!` reads after first client
;; render to drive :rf.ssr/hydration-mismatch; :version is the runtime version
;; consumed by the :rf.ssr/check-version fx.
[:map
[:server-hash {:optional true} :string]
[:version {:optional true} :int]])
(def Ssr
;; The SSR runtime's hydration metadata sub-container. Allocated lazily —
;; absent on frames that never hydrated.
[:map
[:hydration {:optional true} HydrationMetadata]])
(def Runtime
;; The framework-owned root. ALL framework per-frame state lives here.
;; The four sub-keys are allocated lazily — a frame that uses no machines,
;; no routing, no elision, and no SSR carries `:rf/runtime nil` (or absent).
[:map
[:machines {:optional true} Machines]
[:routing {:optional true} Routing]
[:elision {:optional true} Elision]
[:ssr {:optional true} Ssr]])
;; registered by the runtime at boot:
(rf/reg-app-schema [:rf/runtime] Runtime)
Four subsystems, four sub-containers:
:machines— owned by 005-StateMachines.md. Each machine's snapshot lives at[:rf/runtime :machines :snapshots <machine-id>]; the system-id reverse index lives at[:rf/runtime :machines :system-ids]; the declarative-spawn / spawn-all registry lives at[:rf/runtime :machines :spawned]; the hand-emitted-spawn fallback counter lives at[:rf/runtime :machines :spawn-counter](rf2-owvvr — declarative:spawn's counter is snapshot-internal, not here). The runtime composes the:snapshotsschema additively from registered machines' declared:datashapes.:routing— owned by 012-Routing.md. The live route slice ({:id :params :query :transition :error :fragment :nav-token}) lives at[:rf/runtime :routing :current]; the pending-navigation slot at[:rf/runtime :routing :pending-navigation]; the per-frame routing internals (scroll-positions LRU, monotonic counters) sit flat alongside under:routing.:elision— owned by 009-Instrumentation.md. The size-elision declaration registry lives at[:rf/runtime :elision :declarations]; the privacy sibling at[:rf/runtime :elision :sensitive-declarations].:ssr— owned by 011-SSR.md. Server-supplied hydration metadata lives at[:rf/runtime :ssr :hydration](:server-hashconsumed byverify-hydration!,:versionconsumed by:rf.ssr/check-version). Per rf2-f4j8x — the single-reserved-root contract; pre-eguy4 the slot lived at app-db root[:rf/hydration].
Per-frame isolation is automatic — each frame's app-db has its own :rf/runtime; the same machine id, route id, or elision path can exist in multiple frames without collision.
Frame revertibility is inherited from 000 §Frame state revertibility — every subsystem's state walks back atomically with app-db on a frame revert, including across restore-epoch (Tool-Pair time-travel) and SSR hydration (011-SSR.md) because both snapshot the whole app-db.
Cross-reference: :rf/machine-snapshot (above) is the value type for each entry under :machines :snapshots. :rf/route-slice (below) is the shape of :routing :current. :rf/pending-navigation (below) is the shape of :routing :pending-navigation. :rf/elision-marker (below) is the wire shape emitted by the walker when an entry in :elision :declarations says elide.
:rf/elision-marker¶
Layer: Public Owner: 009-Instrumentation §Wire marker —
:rf.size/large-elidedStatus: v1-required
The wire shape rf/elide-wire-value substitutes for an elided large value. Catalogued normatively at 009 §Size elision in traces and threaded through every tool that walks tree-typed payloads (per Tool-Pair.md).
(def ElisionMarkerBody
[:map
[:path [:vector :any]] ;; absolute path inside the slice's root value
[:bytes :int] ;; pr-str byte count
[:type [:enum :map :vector :set :scalar :string]] ;; top-level shape of the elided value
[:reason [:enum :schema]] ;; provenance — schemas are the only nomination path
[:hint [:maybe :string]] ;; verbatim from the declaration's :hint slot
[:handle [:tuple [:= :rf.elision/at] [:vector :any]]] ;; fetch-handle: [:rf.elision/at <path>]
[:digest {:optional true} :string]]) ;; sha256:<hex>; only when :rf.size/include-digests? true
(def ElisionMarker
;; The marker is a single-key map keyed by :rf.size/large-elided.
[:map [:rf.size/large-elided ElisionMarkerBody]])
Per-field MUST-level requirements (catalogued at 009 §Wire marker — :rf.size/large-elided):
:pathis absolute inside the snapshot slice — not relative to the elision site. An agent that asked for:path [:user]and got the marker back at:uploaded-pdfsees:path [:user :uploaded-pdf].:handleis an EDN vector (not a tagged literal). The default shape is[:rf.elision/at <path>]; markers riding inside a past-epoch payload (e.g. an:rf.mcp/diff-frompatch's:assocslot) carry the variant[:rf.elision/at <path> :as-of-epoch <epoch-id>]soget-pathresolves against that epoch's:db-aftersnapshot rather than now's.:digestis OPTIONAL and only present when the caller passed:rf.size/include-digests? true(per API.md §rf/elide-wire-value). Default off because the digest forces a full walk of the elided value, which negates the cost-saving.
The reserved sentinel :rf.elision/at (under the :rf.elision/* namespace per Conventions §Reserved namespaces) marks the handle as fetchable. Agents pattern-match on the leading :rf.elision/at keyword — no decoder needed.
:rf/route-pattern¶
Layer: Public Owner: 012-Routing §Path-pattern grammar Status: v1-required
The canonical path-pattern grammar for reg-route's :path value. Per 012 §Path-pattern grammar, this is the wire-form every conforming implementation parses and emits.
(def RoutePattern
;; The shape is a string — the schema below is descriptive (a regex constraint), not structural.
;; A formal data-form grammar (vector-of-segments) is post-v1; this string
;; form is the v1 contract.
[:and :string
[:re #"^(?:/|(?:/(?:[^:*{}/?][^/?{}]*|:[a-zA-Z][a-zA-Z0-9_-]*|\*[a-zA-Z][a-zA-Z0-9_-]*|\{/[^/{}]+\}\?))+/?)$"]])
Productions (per 012):
| Token | Meaning |
|---|---|
/ (root) |
Root pattern. |
/literal |
Literal segment. |
/:name |
Named param segment. |
{/:name}? or {/literal}? |
Optional segment group; final group only; not nested. |
/*name |
Catch-all (splat); must be the final segment; at most one per pattern. |
Implementations register this schema via reg-app-schema [:rf/route-pattern] so a route's :path value can be validated at registration time and the conformance harness can lint route tables.
:rf/route-rank¶
Layer: Runtime Owner: 012-Routing §Route ranking algorithm Status: v1-required
The structural-rank tuple match-url computes for each registered route, per 012 §Route ranking algorithm. Registrars attach the computed rank under :rf.route/rank on the route's metadata so tooling can read it via (rf/handler-meta :route route-id) and so AI scaffolds can render the precedence cascade without re-parsing patterns.
(def RouteRank
;; A vector of integers, lexicographically comparable. Higher = more specific.
;; The registrar's stable-sort by registration time provides rule 6.
[:tuple :int ;; rule 1 — static-segment count
:int ;; rule 2 — total segment count
[:enum 0 1] ;; rule 3 — splat? 0 = has splat; 1 = no splat (named params win)
[:enum 0 1] ;; rule 4 — catch-all? 0 = is "/*"; 1 = otherwise
[:enum 0 1]]) ;; rule 5 — has optional group? 0 = yes; 1 = no
Implementations rank candidates by descending route-rank then by ascending registration time (stable sort). Equal-score candidates emit :rf.warning/route-shadowed-by-equal-score at registration time per API.md §Error contract; the warning's :tags carry {:route-id <new> :shadowed <existing> :rank <RouteRank>}.
:rf/route-slice¶
Layer: Runtime Owner: 012-Routing §The route slice Status: v1-required
The shape of app-db's route slice — lives at [:rf/runtime :routing :current] per Conventions §Reserved app-db keys and §:rf/runtime. Schema-id retained as :rf/route-slice for stability of registered schema lookups.
(def RouteSlice
[:map
[:id :keyword] ;; current route id (e.g. :route/cart)
[:params {:optional true} :map] ;; path params (matches the route's :params schema)
[:query {:optional true} :map] ;; query params (matches the route's :query schema; includes :query-defaults)
[:fragment {:optional true} [:maybe :string]] ;; URL fragment (#section); nil when absent. Per [012 §Fragments](012-Routing.md#fragments).
[:transition {:optional true} [:enum :idle :loading :error]] ;; navigation transition state
[:error {:optional true} :any] ;; populated when :transition = :error; conforms to :rf/error per 009
[:nav-token {:optional true} :any]]) ;; per-navigation epoch token; per [012 §Navigation tokens](012-Routing.md#navigation-tokens--stale-result-suppression)
Open shape — implementations may add :rf.route/...-namespaced keys (e.g., the runtime's saved scroll-position cache might surface a :rf.route/saved-scroll key, opt-in).
:rf/route-metadata¶
Layer: Public Owner: 012-Routing §Reserved route-metadata keys Status: v1-required
The shape of the metadata map passed to reg-route. Reserved keys per 012 §Reserved route-metadata keys.
(def RouteMetadata
[:map
[:doc {:optional true} :string]
[:path :string] ;; conforms to :rf/route-pattern
[:params {:optional true} :any] ;; Malli schema for path params
[:query {:optional true} :any] ;; Malli schema for query/search params
[:query-defaults {:optional true} [:map-of :keyword :any]] ;; defaults for absent query keys
[:query-retain {:optional true} [:set :keyword]] ;; query keys carried through subsequent navigations
[:tags {:optional true} [:set :keyword]]
[:parent {:optional true} :keyword] ;; parent route id; used by :rf.route/chain sub
[:on-match {:optional true} [:vector [:vector :any]]] ;; events to dispatch when this route becomes active
[:on-error {:optional true} [:vector :any]] ;; event to dispatch if any :on-match event errors
[:can-leave {:optional true} :keyword] ;; sub-id; (subscribe [<sub-id>]) returns boolean — true means "OK to leave". Per [012 §Navigation blocking](012-Routing.md#navigation-blocking--pending-nav-protocol).
[:scroll {:optional true} [:or
[:enum :top :restore :preserve]
:map]]]) ;; map form is post-v1 / host-extensible
Per-host extension keys (:myapp/..., :rf.tooling/...) are tolerated — RouteMetadata composes with :rf/registration-metadata's open shape.
:rf/pending-navigation¶
Layer: Runtime Owner: 012-Routing §Navigation blocking — pending-nav protocol Status: v1-required
The shape of app-db's pending-navigation slot, set by the runtime when a navigation is blocked by a :can-leave guard. Lives at [:rf/runtime :routing :pending-navigation] per Conventions §Reserved app-db keys and §:rf/runtime. Per 012 §Navigation blocking.
(def PendingNavigation
[:map
[:id :string] ;; opaque pending-nav id (gensym); used by :rf.route/continue / :rf.route/cancel
[:requested-by-event [:vector :any]] ;; the original :rf/url-requested or :rf.route/navigate event vector
[:requested-url :string] ;; the URL the user was trying to reach
[:reason {:optional true} :string] ;; human-readable explanation for the dialog
[:rejecting-route :keyword] ;; the route id whose :can-leave guard rejected
[:rejecting-guard {:optional true} :keyword]]) ;; the sub-id of the rejecting guard (for tooling)
The slot is nil (or absent) when no navigation is pending. Cleared by :rf.route/continue (the navigation completes) or :rf.route/cancel (the navigation is abandoned). Open map — implementations may attach :rf.route/...-namespaced metadata; user code reads via (subscribe [:rf/pending-navigation]).
:rf.fx/with-nav-token-args¶
Layer: Runtime Owner: 012-Routing §Navigation tokens — stale-result suppression Status: v1-required
Args of the framework-supplied :rf.route/with-nav-token fx wrapper, per 012 §Navigation tokens. Threads the current :nav-token into a wrapped follow-up dispatch so the receiving handler can detect stale results.
(def WithNavTokenFxArgs
[:map
[:do [:vector :any]] ;; an fx entry to perform — typically [:dispatch [<event-id> args ...]]
[:nav-token :any]]) ;; the token captured at scheduling time (gensym or counter)
Registered under spec id :rf.fx/with-nav-token-args. The wrapped fx receives the carried token in cofx; on receipt, the framework-provided :nav-token cofx checks the carried token against the current route slice's ([:rf/runtime :routing :current]) :nav-token. Mismatch → suppress + emit :rf.route.nav-token/stale-suppressed trace.
:rf/hydration-payload¶
Layer: Runtime Owner: 011-SSR §Payload scope Status: v1 (optional capability — SSR; post-v1 keys are documented additive extensions) Conformance:
spec/conformance/fixtures/hydration-*.edn+implementation/ssr/test/re_frame/ssr_hydration_test.clj
Per 011 §The :rf/hydrate event. The canonical shape of the data crossing the wire from server to client. v1 and post-v1 are kept separate: the v1 schema below carries only v1 keys; the post-v1 extension is a separate schema that refines the v1 shape. v1 implementations emit and consume exactly the v1 shape; post-v1 keys in payloads from a future server are tolerated (open map) but ignored on a v1 client.
:rf/hydration-payload (v1):
(def HydrationPayload
[:map
[:rf/version :int] ;; pattern-protocol version (integer; v1 = 1)
[:rf/frame-id :keyword] ;; the frame id to seed
[:rf/app-db :any] ;; serialised app-db (authoritative)
[:rf/ssr-rendered-at {:optional true} :int] ;; ms-since-epoch the server completed render
[:rf/route {:optional true} ;; the matched route slice the server resolved
[:map
[:id :keyword]
[:params {:optional true} :map]
[:query {:optional true} :map]]]
[:rf/render-hash {:optional true} :string] ;; structural hash of the server-rendered render-tree, for mismatch detection. Covers both body and head — the runtime emits a single `:rf.ssr/hydration-mismatch` and discriminates head-vs-body via the `:failing-id` tag (per [011 §Hydration-mismatch detection](011-SSR.md#hydration-mismatch-detection)). The head-hash surface (a separate `:rf/head-hash` key) is reserved for the post-v1 `reg-head` payload extension and not part of the v1 wire.
[:rf/schema-digest {:optional true} :string] ;; hash of the server's registered app-schema set (per [010-Schemas.md](010-Schemas.md))
])
:rf/hydration-payload-postv1 (post-v1 extension):
;; Reserved for a future re-frame2.x. Keys appear additively on top of v1.
;; v1 implementations tolerate these on the wire but do not emit or consume them.
(def HydrationPayloadPostV1
[:merge
HydrationPayload
[:map
[:rf/machine-snapshots {:optional true} [:map-of :keyword :rf/machine-snapshot]] ;; per-machine snapshots keyed by machine-id; mirrors the in-app-db [:rf/runtime :machines :snapshots] map
[:rf/sub-warmups {:optional true} [:map-of [:vector :any] :any]] ;; pre-computed sub values (per [011-SSR.md](011-SSR.md))
]])
The split between v1 and post-v1 keeps the v1 contract auditable: a v1 conformance harness validates against HydrationPayload exactly; the post-v1 extension is a separate schema users opt into when they upgrade. The :rf/version integer increments when the post-v1 schema becomes the v2 contract.
Merge policy: the standard :rf/hydrate handler replaces the frame's app-db with (:rf/app-db payload). Server is authoritative for the initial client state. See 011 §The :rf/hydrate event for the transient-client-state pattern (seed before hydrate via :on-create; the replace clobbers it; if the user wants seeded transient client state to survive, the handler is opt-in customisable via re-registration of :rf/hydrate).
Why integer :rf/version (not string): integer comparison is cheaper for tools and hosts to do compatibility checks against; pattern-protocol versions are monotonic increments (1, 2, ...) rather than semver-style strings.
:rf/response¶
Layer: Runtime Owner: 011-SSR §HTTP response contract Status: v1 (optional capability — SSR)
The HTTP-response accumulator owned by the request frame during SSR. Per 011 §HTTP response contract. Populated during the drain by the standard :rf.server/* fx; consumed by the host adapter to build the wire response.
(def Response
[:map
[:status {:optional true} :int] ;; default 200 if no fx sets it
[:headers {:optional true} [:vector [:tuple :string :string]]] ;; ordered [name value] pairs; case-insensitive name match
[:cookies {:optional true} [:vector [:ref :rf.server/cookie]]] ;; structured cookies (per :rf.server/cookie below)
[:redirect {:optional true} [:maybe [:map
[:status {:optional true} :int] ;; default 302
[:location :string]]]]
[:content-type {:optional true} :string]]) ;; convenience accessor; mirrors headers' "content-type"
Open shape — implementations may attach :rf.response/...-namespaced keys (e.g., :rf.response/cache-tag) without breaking consumers. The canonical six fx (:rf.server/set-status, :rf.server/set-header, :rf.server/append-header, :rf.server/set-cookie, :rf.server/delete-cookie, :rf.server/redirect) write only the four canonical keys.
:rf.server/cookie¶
Layer: Runtime Owner: 011-SSR §Cookie shape Status: v1 (optional capability — SSR)
The structured-cookie shape that :rf.server/set-cookie and :rf.server/delete-cookie produce. Per 011 §Cookie shape. The host adapter serialises this to a Set-Cookie: header per RFC 6265.
(def Cookie
[:map
[:name :string]
[:value :string]
[:max-age {:optional true} :int]
[:expires {:optional true} :int] ;; ms-since-epoch; alternative to :max-age
[:secure {:optional true} :boolean]
[:http-only {:optional true} :boolean]
[:same-site {:optional true} [:enum :strict :lax :none]]
[:path {:optional true} :string]
[:domain {:optional true} :string]])
Either :max-age or :expires may be supplied (or neither — session cookie). User code does not build wire strings.
:rf/head-model¶
Layer: Runtime Owner: 011-SSR §Head/meta contract Status: v1 (optional capability — SSR)
The data model for SSR head/meta content. Per 011 §Head/meta contract. Pure data; the runtime emits <head>...</head> from this map in canonical key order.
(def HeadModel
[:map
[:title {:optional true} :string]
[:meta {:optional true} [:vector [:map-of :keyword [:or :string :int :boolean]]]]
[:link {:optional true} [:vector [:map-of :keyword :string]]]
[:script {:optional true} [:vector [:map-of :keyword [:or :string :int :boolean]]]]
[:json-ld {:optional true} [:vector :map]] ;; structured-data objects (raw maps, serialised as application/ld+json)
[:html-attrs {:optional true} [:map-of :keyword :string]] ;; attributes on <html>
[:body-attrs {:optional true} [:map-of :keyword :string]]]) ;; attributes on <body>
The shape is open — implementations may add :rf.head/...-namespaced keys (e.g., :rf.head/preload for resource hints) without breaking consumers.
:rf/public-error¶
Layer: Runtime Owner: 011-SSR §Server error projection Status: v1 (optional capability — SSR)
The sanitised, client-safe projection of an internal error trace event. Per 011 §Server error projection. The error projector consumes a :rf/error-event (per 009 §Error event shape) and returns this shape.
(def PublicError
[:map {:closed true} ;; closed in prod — extra keys are a leak risk
[:status :int]
[:code :keyword] ;; stable category (:not-found :bad-request :unauthorised :internal-error ...)
[:message :string] ;; one-sentence human-facing
[:retryable? :boolean]
[:details {:optional true} :any]]) ;; dev-only; the full trace event for developer view
The map is closed — production must not silently leak unknown keys. The :details key is dev-only (gated by :dev-error-detail?); production builds elide it.
Standard fx args schemas¶
Layer: Runtime
The :rf/effect-map's :fx is [[fx-id args] ...]. Each standard fx-id (the ones the runtime / standard libraries register) has a known args shape; this section registers them so the conformance corpus and AI scaffolding can validate fx-args at the call site. User-registered fx attach their own args schema via the :schema metadata on reg-fx (per 010 §Where schemas attach).
;; :dispatch — dispatches another event in the same frame
(def DispatchFxArgs
[:vector :any]) ;; an event vector
;; :dispatch-later — schedules a delayed dispatch (or dispatches a vector of them)
(def DispatchLaterFxArgs
[:or
[:map
[:ms :int] ;; non-negative
[:dispatch [:vector :any]]]
[:vector
[:map
[:ms :int]
[:dispatch [:vector :any]]]]])
;; :http — pattern-level HTTP fx (per Pattern-RemoteData). Args are user-supplied;
;; the framework treats them opaquely. Schema is recommendation, not contract.
(def HttpFxArgs
[:map
[:method [:enum :get :post :put :patch :delete :head :options]]
[:url :string]
[:body {:optional true} :any]
[:headers {:optional true} [:map-of :keyword :string]]
[:on-success {:optional true} [:vector :any]]
[:on-error {:optional true} [:vector :any]]])
;; :rf.nav/push-url — navigate to a URL via history.pushState. Client only.
(def NavPushUrlFxArgs :string)
;; :rf.nav/replace-url — replace history entry via history.replaceState. Client only.
(def NavReplaceUrlFxArgs :string)
;; :rf.nav/scroll — scroll-on-navigate. Client only. Per [012 §Scroll restoration].
(def NavScrollFxArgs
[:map
[:strategy [:or
[:enum :top :restore :preserve]
:map]] ;; map form is host-extensible (post-v1)
[:from {:optional true} [:map [:id :keyword] [:params {:optional true} :map] [:query {:optional true} :map]]]
[:to {:optional true} [:map [:id :keyword] [:params {:optional true} :map] [:query {:optional true} :map]]]
[:saved-pos {:optional true} [:tuple :int :int]]]) ;; runtime-captured saved position (for :restore)
;; :rf.machine/spawn — canonical actor-lifecycle fx-id (registered globally by
;; re-frame.machines); usable inside any event handler's :fx (machine actions
;; and ordinary handlers alike) to spawn a dynamic actor. Per
;; [005 §Spawning](005-StateMachines.md#spawning--dynamic-actors). The :raise
;; reserved fx-id (machine-internal, routed by the machine handler) takes a
;; bare event vector — same shape as :dispatch — and so does not need its own
;; args schema.
(def SpawnFxArgs
[:map
;; one of :machine-id (registered) or :definition (inline transition table)
[:machine-id {:optional true} :keyword]
[:definition {:optional true} :any] ;; an inline TransitionTable
[:id-prefix {:optional true} :keyword] ;; defaults to :machine-id; base for the gensym'd actor id
[:data {:optional true} :map] ;; initial data; overrides definition default
[:on-spawn {:optional true} fn?] ;; (fn [{:keys [data id]}] _) — advisory callback; return is ignored.
[:start {:optional true} [:vector :any]] ;; event vector dispatched to the new actor immediately after spawn
[:system-id {:optional true} :keyword] ;; per [005 §Named addressing via :system-id]; binds [:rf/runtime :machines :system-ids <sid>] in the spawning frame
;; Runtime-stamped on declarative-:spawn spawns (per ; not user-supplied).
;; The pair addresses the runtime-owned spawn registry slot at
;; [:rf/runtime :machines :spawned <parent-id> <invoke-id>]; absent on imperative from-action
;; spawns (those user-owned destroys are still hand-emitted with the actor id).
[:rf/parent-id {:optional true} :keyword] ;; parent machine's registration-id
[:rf/spawn-id {:optional true} [:vector :keyword]] ;; absolute prefix-path of the :spawn-bearing state node
[:rf/spawned-id {:optional true} :keyword]]) ;; resolved gensym'd id, threaded through so spawn-fx registers under the same id :on-spawn observed
;; The spawned actor's snapshot lives at [:rf/runtime :machines :snapshots <gensym'd-id>] in the
;; active frame's app-db — runtime-managed; not part of the spawn-spec.
;; :rf.machine/destroy — canonical actor-destroy fx-id (registered globally
;; by re-frame.machines); usable inside any event handler's :fx (machine
;; actions and ordinary handlers alike) to tear down a dynamic actor. Per
;; [005 §Spawning] and (Option A revised). Two argument shapes:
;; - a bare actor-id keyword — the legacy / imperative form (action emits
;; `[:rf.machine/destroy actor-id]` with the recorded id directly).
;; - a `{:rf/parent-id :rf/spawn-id}` map — the declarative-:spawn
;; exit-cascade form. The fx handler reads the spawned id back from
;; `[:rf/runtime :machines :spawned <parent-id> <invoke-id>]` at call time and tears down
;; whatever id is currently bound there.
(def DestroyMachineFxArgs
[:or :keyword
[:map
[:rf/parent-id :keyword]
[:rf/spawn-id [:vector :keyword]]]])
;; --- :rf.server/* fx — HTTP response contract per [011 §HTTP response contract] ---
;; :rf.server/set-status — set the response status code
(def SetStatusFxArgs :int) ;; e.g. 200 / 404 / 500
;; :rf.server/set-header — replace a header (case-insensitive name match)
(def SetHeaderFxArgs
[:map
[:name :string]
[:value :string]])
;; :rf.server/append-header — add another instance of a (possibly multi-valued) header
(def AppendHeaderFxArgs
[:map
[:name :string]
[:value :string]])
;; :rf.server/set-cookie — args is the :rf.server/cookie shape
(def SetCookieFxArgs
[:ref :rf.server/cookie])
;; :rf.server/delete-cookie — clear a named cookie at a path/domain
(def DeleteCookieFxArgs
[:map
[:name :string]
[:path {:optional true} :string]
[:domain {:optional true} :string]])
;; :rf.server/redirect — set status (default 302) and Location; truncates HTML body
(def RedirectFxArgs
[:map
[:status {:optional true} :int] ;; default 302
[:location :string]])
These are registered under spec ids:
| Spec id | Args of fx |
|---|---|
:rf.fx/dispatch-args |
:dispatch (and :raise, which takes the same event-vector shape) |
:rf.fx/dispatch-later-args |
:dispatch-later |
:rf.fx/http-args |
:http (recommendation; user-owned) |
:rf.fx/nav/push-url-args |
:rf.nav/push-url (per 012) |
:rf.fx/nav/replace-url-args |
:rf.nav/replace-url |
:rf.fx/nav/scroll-args |
:rf.nav/scroll |
:rf.fx/spawn-args |
:rf.machine/spawn (the canonical actor-lifecycle fx-id; emitted from any event handler's :fx and from machine actions; per 005) |
:rf.fx/destroy-machine-args |
:rf.machine/destroy (the canonical actor-destroy fx-id; per 005 and — accepts either a bare actor-id keyword or a {:rf/parent-id :rf/spawn-id} map) |
:rf.fx.server/set-status-args |
:rf.server/set-status (per 011 §HTTP response contract) |
:rf.fx.server/set-header-args |
:rf.server/set-header |
:rf.fx.server/append-header-args |
:rf.server/append-header |
:rf.fx.server/set-cookie-args |
:rf.server/set-cookie (the :rf.server/cookie shape) |
:rf.fx.server/delete-cookie-args |
:rf.server/delete-cookie |
:rf.fx.server/redirect-args |
:rf.server/redirect |
The :http schema is user-owned, not framework-owned — projects that ship their own HTTP integration register their own :schema on their own :http reg-fx. The schema here is a reasonable starting point (the conformance corpus uses it) but is not part of the locked pattern contract.
Per-fx args validation runs as part of the standard fx-arg validation (per 010 §Validation timing) when the reg-fx registration carries a :schema. The standard fx ship with :schema set to the corresponding schema above.
:rf/frame-meta¶
Layer: Public Owner: 002-Frames §Frame presets Status: v1-required
Returned by (frame-meta frame-id). The :preset field, when present, records which preset was applied (per 002 §Frame presets); the expanded keys are the effective metadata map. Composes with :rf/registration-metadata the same way every other per-kind shape does — base :doc / :tags / :schema / :ns / :line / :column / :file / :platforms / :sensitive? come from the merge; the keys below are the frame-specific additions.
(def FrameMeta
[:merge
RegistrationMetadata
[:map
[:id :keyword]
[:created-at :any] ;; timestamp
[:preset {:optional true} [:enum :default :test :story :ssr-server]] ;; per 002 §Frame presets
[:on-create {:optional true} [:vector :any]] ;; the init event vector
[:on-destroy {:optional true} [:vector :any]]
[:fx-overrides {:optional true} [:map-of :keyword :any]]
[:interceptor-overrides {:optional true} [:map-of :keyword :any]]
[:interceptors {:optional true} [:vector :any]]
[:drain-depth {:optional true} :int]
[:url-bound? {:optional true} :boolean] ;; per [012-Routing.md](012-Routing.md)
[:platform {:optional true} :keyword] ;; the frame's active platform; per [011-SSR.md](011-SSR.md). Single keyword (one platform per frame); compared against `reg-fx`'s `:platforms` set.
[:on-error {:optional true} :keyword] ;; error-projection target; per [011-SSR.md](011-SSR.md). The `:ssr-server` preset wires `:rf.error/server-projection`.
]])
:rf/preset-expansion¶
Layer: Public Owner: 002-Frames §Frame presets Status: v1-required
The fixed, closed expansion table for :preset values. Each preset expands to a metadata sub-map; the runtime merges user-supplied metadata over the expansion. Known presets: :default, :test, :story, :ssr-server. Unknown values raise :rf.error/unknown-preset at registration time.
(def PresetExpansion
[:map
[:default [:= {}]] ;; empty expansion
[:test [:map
[:fx-overrides [:= {:rf.http/managed :rf.http/managed-canned-success}]] ;; exact pair fixed by 002 §`:test` preset
[:drain-depth [:= 100]]]]
[:story [:map
[:fx-overrides [:= {:rf.http/managed :rf.http/managed-canned-success}]] ;; exact pair fixed by 002 §`:story` preset
[:drain-depth [:= 16]]]]
[:ssr-server [:map
[:platform [:= :server]]
[:on-error [:= :rf.error/server-projection]]]]])
The fully-expanded metadata returned from frame-meta conforms to :rf/frame-meta; the schema for the expansion table itself is :rf/preset-expansion. Implementations must produce the same expansion the table specifies, modulo user-supplied overrides.
:rf/variant¶
Layer: Public (post-v1 library) Owner: 007-Stories §Variant artefact contract Status: post-v1 (Story library — separate package)
The serialisable artefact contract for a story variant (post-v1 library; see 007 §Variant artefact contract). Variants are data, not functions — every key is a value-shape, no fn-valued slots.
(def Variant
[:map {:closed false}
[:variant-id :keyword] ;; :story.<path>/<variant>
[:doc {:optional true} :string]
[:extends {:optional true} :keyword] ;; parent variant id (composed)
[:events {:optional true} [:vector [:vector :any]]] ;; setup events (data only)
[:play {:optional true} [:vector [:vector :any]]] ;; post-render interaction sequence
[:args {:optional true} :map] ;; override or extend the parent story's args
[:argtypes {:optional true} :map] ;; per-arg control descriptions
[:tags {:optional true} [:set :keyword]] ;; from the registered tag vocabulary
[:decorators {:optional true} [:vector [:vector :any]]] ;; [decorator-id args...]; id-valued
[:loaders {:optional true} [:vector [:vector :any]]] ;; async setup events
[:platforms {:optional true} [:set [:enum :server :client]]]])
Composition. When :extends is present, the registrar resolves the parent variant's :rf/variant and merges (child wins key-by-key) before storing. The stored body is fully resolved — no further resolution at runtime.
No fn-valued slots. Decorators are id-valued ([decorator-id args...]); loaders are event vectors (the handler the loader event ids point to is the only fn-valued part — and it lives at the registration site, not in the variant body). Variants are wire-portable, storable as snapshots, and structurally diffable.
:rf/epoch-record¶
Layer: Runtime Owner: Tool-Pair §Time-travel Status: dev-tier (gated on
re-frame.interop/debug-enabled?; production builds elide entirely) Conformance:spec/conformance/fixtures/epoch-*.edn+implementation/epoch/test/re_frame/epoch_*.clj(build / restore / privacy / redact-fn / jvm-prod-gate)
Per-frame epoch snapshot, recorded per dequeued event in dev builds — one record per event, not per drain (per 002 §Drain versus event). A drain that settles several events back-to-back yields one record per settled event. Used by Tool-Pair for time-travel and post-mortem analysis. Production builds elide entirely (no schema validation needed in prod).
(def EpochRecord
[:map
[:epoch-id :any] ;; opaque, unique within a frame's history
[:frame :keyword]
[:committed-at :any] ;; timestamp
[:event-id :keyword] ;; the event that triggered the cascade
[:trigger-event [:vector :any]] ;; the full event vector
[:dispatch-id {:optional true} :any] ;; the settling cascade's opaque router dispatch-id, pinned from the `:event/run-start` tag — the stable cross-counter-space link from this epoch (epoch-id space) to the raw trace stream's cascade list (dispatch-id space); survives `:trace-events` elision + reactive back-fill; absent when the cascade carried no dispatch-id (rejected dispatch / pre-run-start halt / synthetic reset epoch)
[:db-before :any] ;; app-db before the cascade
[:db-after :any] ;; app-db the runtime settled to (see :outcome)
[:outcome [:enum :ok ;; the event's own cascade settled cleanly
:halted-depth ;; drain-depth limit tripped; halting event never ran (no whole-drain rollback) — :db-before = :db-after = durable last-settled db
:halted-destroy ;; frame destroyed mid-drain
:halted-handler-exception]] ;; reserved — current impl does not halt the drain on handler-exception, see §Outcomes below
[:halt-reason {:optional true} :any] ;; structured descriptor of the halt (operation + key tags), absent on :ok
[:schema-digest {:optional true} [:maybe :string]] ;; digest of the frame's app-schema set at record time, per [010 §Schema digest](010-Schemas.md#schema-digest); nil on hosts without a runtime schema layer
[:rf.epoch/sensitive? {:optional true} :boolean] ;; record-level rollup — true when ANY schema-declared sensitive path resolves to a non-nil leaf in `:db-before` / `:db-after`, OR any captured trace event carries `:sensitive? true`; computed from RAW signals BEFORE the `:redact-fn` runs
[:rf.epoch/redacted-modified-paths-count {:optional true} :int] ;; record-level count of schema-declared sensitive paths whose value differs between `:db-before` and `:db-after`; computed from RAW values BEFORE the `:redact-fn` runs, parallel to `:rf.epoch/sensitive?`. 0 when no schema-declared sensitive path mutated this cascade; absent (treat as 0) on hosts that ship no runtime schema layer
[:trace-events {:optional true} [:vector :any]] ;; the cascade's trace events (raw)
[:sub-runs {:optional true} [:vector
[:map
[:sub-id :any]
[:query-v [:vector :any]]
[:recomputed? :boolean]
;; value-change + cascade attribution threaded from the
;; reactive `:rf.sub/run` trace tag. Present on reactive recompute
;; entries; nil on entries derived from the pure `compute-sub`
;; emit (which omits the attribution). The wire-value slots are
;; redacted at the `marks/project-sub-tags` trace chokepoint
;; (process-scoped marks, no reactive read), so they may be
;; `:rf/redacted` for a schema-`:sensitive?` sub.
[:value-changed? {:optional true} [:maybe :boolean]]
[:prev-value {:optional true} :any]
[:value {:optional true} :any]
[:cascade? {:optional true} [:maybe :boolean]]
[:cause-sub {:optional true} [:maybe [:vector :any]]]
[:cause-event-id {:optional true} [:maybe :keyword]]]]] ;; per-sub activity in this cascade
[:renders {:optional true} [:vector
[:map
[:render-key [:tuple :any :any]] ;; [<view-id-or-:rf.view/anonymous> <instance-token>]
;; per-view cause + timing threaded from the
;; post-render :rf.view/rendered op (the projection source).
;; :mount? — true on the instance's first render.
;; :triggered-by — the sub-id that caused this re-render;
;; absent on a structural re-render (no own sub changed).
;; :elapsed-ms — the render duration (fractional ms).
;; :cause-event-id — the head keyword of the dispatching
;; cascade's trigger event vector (the event that invalidated
;; a reactive input this view deref'd); threaded from the
;; :rf.view/cause-event-id trace tag. OMITTED (key absent) for
;; a render outside any in-flight cascade (mount / structural
;; render) — same OMITTED-vs-nil semantics as the :sub-runs
;; row's :cause-event-id (rf2-9gquv / rf2-1cc03).
[:mount? {:optional true} :boolean]
[:triggered-by {:optional true} :any]
[:elapsed-ms {:optional true} :double] ;; fractional ms
[:cause-event-id {:optional true} [:maybe :keyword]]]]] ;; per-render activity in this cascade
[:effects {:optional true} [:vector
[:map
[:fx-id :keyword]
[:args :any]
[:outcome [:enum :ok :error :skipped-on-platform]]
[:error-trace {:optional true} :any]]]] ;; per-effect activity in this cascade
])
The :db-before / :db-after pair lets pair tools display diffs cheaply.
Structured slots are derived from :trace-events. The :sub-runs, :renders, and :effects slots are pre-computed projections of the underlying :trace-events stream, surfacing the per-sub / per-render / per-effect activity of the cascade in a shape pair-shaped tools can route off without re-folding the raw trace each time. The legacy :trace-events slot remains the raw underpinning; the structured slots derive from it.
:sub-runs— every sub the cascade re-ran.:recomputed?istruefor every entry: under the value-equality rule in Spec 006 §Invalidation algorithm, a sub whose inputs are value-equal to the prior call does not re-run its body and therefore does not emit:rf.sub/run, so cache-hit subs are absent from this projection. The slot answers "which subs moved this cascade?" without re-deriving from the trace. Per each reactive recompute entry additionally carries value-change + cascade attribution threaded from the:rf.sub/runtrace tag (per 009 §:rf.sub/run)::value-changed?((not= prev-value value)— distinguishes a recompute that re-ran but produced a=-equal value from one whose value actually moved; the "always-true" gap is now a concrete signal),:prev-value/:value(the before/after values, redacted at themarks/project-sub-tagstrace chokepoint so a schema-:sensitive?sub egresses them as:rf/redacted),:cascade?(truefor a layer-2+ sub recomputed because an upstream sub changed;falsefor a layer-1 sub driven by an app-db path change),:cause-sub(the upstream:<-query-vector that changed, for a cascade;nilfor a layer-1 sub or a first recompute), and:cause-event-id(the head keyword of the dispatching cascade's trigger event vector, naming WHICH event invalidated this sub's reactive input — same source the views path uses for:rf.view/cause-event-idper rf2-25zo2; threaded from:rf.sub/cause-event-idper rf2-okz1u). The:cause-event-idslot is OMITTED (key absent) for subs that ran outside any in-flight cascade — a post-settle reactive flush against no live drain, or a fixture-driven direct invocation. These slots are absent on entries derived from the purecompute-subemit, which has no prior cached value to diff and no reactive context to attribute against — consumers tolerate their absence (treat as no-attribution).:renders— every render that fired during the cascade.:render-keyis a tuple[<view-id> <instance-token>]. Per.1 the projection now sources from the post-render:rf.view/renderedop (not the render-START:rf.view/render), so each row additionally carries the per-view cause + timing Xray's Views panel needs::triggered-by(the single sub-id that caused this re-render — the first sub in the view's own read-set whose value changed; absent on a structural re-render where no own sub changed),:elapsed-ms(the render duration in fractional ms),:mount?(true on the instance's first render), and:cause-event-id(the head keyword of the dispatching cascade's trigger event vector — the event that invalidated a reactive input this view deref'd; threaded from the:rf.view/cause-event-idtrace tag per rf2-9gquv / rf2-1cc03, mirroring the:sub-runsrow's:cause-event-id). All four are optional — a structural render omits:triggered-by, a render outside any cascade may omit:elapsed-ms/:mount?if the emit lacked them, and:cause-event-idis OMITTED (key absent) for any render outside an in-flight cascade (a mount or a structural re-render), under the same OMITTED-vs-nil semantics the:sub-runsrow's:cause-event-iduses. (This supersedes the earlier decision to NOT carry per-render cause/timing, which held while the projection sourced from:rf.view/render— the START marker carries only:render-key. The richer:rf.view/renderedop carries everything and fires 1:1 per render, so re-sourcing closes the gap with no schema-perpetually-nil hazard. Note the:rf.view/renderedop is capped at 100 per cascade, so a full-page re-render storm truncates the:rendersprojection alongside the raw op, by design.):rf.view/rendered's richer:deref-subs(the full per-view read-set),:cause-subs(the cascade-wide sub list), and:render-args(the view's positional render args/props, elided as user data — rpgq8) remain on the raw op for tools wanting more than the single:triggered-bycause. (The:rendersprojection deliberately does NOT lift:render-args; the Xray VIEWS render-args diff column consumes the raw op.) The first render-key slot is thereg-viewregistry id, or:rf.view/anonymousfor plain Reagent fns (implementations may derive a tooling-friendly substitute from(.-displayName fn)when cheap); the second slot is an integer instance-token minted at mount time from a runtime counter atom. Tools that aggregate by view use the first slot; tools that distinguish per-mount activity use the second. Cross-run correlation (replay) is out of scope — instance-tokens regenerate per mount; alternative keys (positional path, parent context) are an open question if Tool-Pair replay grows that need.:effects— every effect dispatched in the cascade's:rf.fx/do-fxstep. Every dispatched fx surfaces exactly one entry, regardless of outcome — successes, warnings, and errors are all recorded so per-event fx attribution is available without re-folding the raw trace stream.:outcomeis:okon success,:errorif the effect threw or returned a structured error,:skipped-on-platformwhen the effect is registered with:platformsthat exclude the current host (per 011).:error-trace(when present, on:erroroutcomes) references the corresponding error trace event by:id. The:fx-ids of reserved runtime fx (:dispatch,:dispatch-later,:rf.fx/reg-flow,:rf.fx/clear-flow,:rf.machine/spawn,:rf.machine/destroy) appear in:effectsalongside user-registered fx — one entry per dispatched pair, in source order.:schema-digest— the canonical wire form (per 010 §Schema digest) of the frame's app-schema set at the moment this epoch was recorded. Pinned per-epoch sorestore-epoch's:rf.epoch/restore-schema-mismatchtrace can carry both the recorded digest and the frame's current digest, letting pair tools attribute restore failures to schema drift.nilon hosts that ship no runtime schema layer (the slot is optional and tolerated absent).:rf.epoch/redacted-modified-paths-count— record-level integer count of schema-declared sensitive paths (read from[:rf/runtime :elision :sensitive-declarations], populated from{:sensitive? true}per-slot schema props per 015 §The classification model) whose value differs between:db-beforeand:db-after. Computed from RAW values insidebuild-recordBEFORE the:redact-fnsubstitutes the:rf/redactedsentinel — parallel to the:rf.epoch/sensitive?rollup pattern; consumers (Xray's redacted-paths-modified chip pertools/xray/spec/004-App-DB-Diff.md, MCP wire pipeline, story recorders) read the exact figure without re-deriving from the post-redaction shape. Closes the:redact-fn⇒ "empty diff but something changed" gap: when the redact-fn substitutes the sentinel into both sides at a sensitive path, the structural diff sees:rf/redacted=:rf/redactedand emits no row; this counter surfaces the suppressed signal.0when no schema-declared sensitive path mutated this cascade. The slot is optional and tolerated absent — hosts that ship no runtime schema layer or builds with no:sensitive?declarations produce no count, and consumers treat absent as0. Egress projection:projected-recordpasses the count through unchanged (the integer is structurally non-sensitive bookkeeping).
:trace-events is optional because for long histories the per-epoch trace can be large — implementations may choose to drop traces from older epochs. The structured slots have the same per-epoch-storage tradeoff and may likewise be elided for older epochs in the ring buffer.
Redacted slot values. When the app installs an :epoch-history :redact-fn (per Tool-Pair §Time-travel and Security §Epoch privacy posture), any slot value the fn rewrites may be the :rf/redacted sentinel or an app-chosen redacted shape — the record schema is open to substitution at every leaf, and consumers MUST tolerate :rf/redacted (or arbitrary app-supplied shapes) in :db-before, :db-after, :trigger-event, :trace-events, and the structured projections.
Outcomes¶
The runtime commits one epoch record per dequeued event (per 002 §Drain versus event) — both clean per-event settles and the terminal record for an event whose drain halted. :outcome discriminates so devtools (Xray, re-frame2-pair) can render failing cascades with the partial-information shape they actually carry.
:outcome |
When the runtime commits | :db-before |
:db-after |
|---|---|---|---|
:ok |
The dequeued event's own six-domino cascade settled cleanly. The traditional record — one per dequeued event. | Pre-cascade snapshot. | Post-cascade snapshot. |
:halted-depth |
Drain hit the configured depth limit. Per Spec 002 §Run-to-completion dispatch rule 3 the atomicity unit is the event, not the drain: every already-settled event kept its own durable :ok epoch + db write (no whole-drain rollback), the remaining queued events are discarded, and this single trailing record marks the halting event — which never ran. |
The durable last-settled app-db (the value after the final :ok event). |
Equal to :db-before — the halting event made no write. |
:halted-destroy |
A handler called destroy-frame! on its own frame mid-cascade; the drain interrupts and drops remaining queued events per Spec 002 §Edge cases worth pinning §Frame disposal mid-drain. |
Pre-cascade snapshot. | The state at destroy-time — the partial cascade's writes survive in the recorded value, but the frame is gone so the live container can no longer be read. |
:halted-handler-exception |
Reserved. Spec 010 §Per-step recovery line 140 describes "cascade halts" on handler exception, but the reference runtime currently routes through the interceptor chain's error-capture seam: the failing handler's :db / :fx / flows do not apply (the chain caught the exception before :effects were populated), but the drain itself continues with the next queued event. No record carries this outcome under today's CLJS reference. Held for a future runtime path that aborts the drain on handler exception. |
— | — |
:halt-reason is a small structured map describing the halt — {:operation <error-op> :tags <selected-tags>} — sufficient for devtools to render a one-line summary without correlating against the raw trace stream. The slot is absent on :ok records and on the :halted-destroy path when no error trace is associated (a destroy is a deliberate lifecycle event, not an error).
Consumer-facing outcome tier. The four-value :outcome enum above is the detailed cause. The runtime additionally emits a paired trace op :rf.epoch/outcome at the same cascade-trailer point as :rf.epoch/snapshotted, carrying the coarse consumer-facing summary {:ok :blocked :error} derived from the cause via re-frame.epoch.assembly/outcome->consumer-facing. Tools that want the cause read :rf.epoch/snapshotted's :outcome (or this record's :outcome slot); tools that want the summary (Xray's Trace-panel close-row per tools/xray/spec/023-Trace-Panel.md §13, Story outcome chips, MCP wire consumers) read :rf.epoch/outcome's :outcome tag. The mapping table lives in Spec 009 §:rf.epoch/*.
Restore semantics. restore-epoch refuses non-:ok records, emitting :rf.epoch/restore-non-ok-record (per Tool-Pair §Time-travel). The "time-travel never lands you in a misleading state" invariant is preserved — halted records exist for devtools introspection, not as restore targets. Listeners (register-epoch-listener!) receive every record regardless of :outcome.
:rf/fixture-file¶
Layer: Conformance Owner: spec/conformance/README Status: v1-required (conformance corpus)
The host-agnostic conformance fixture format. Per conformance/README.md, each fixture is one EDN file describing a canonical interaction (registry, handlers-as-data, dispatches/calls, expected emissions).
(def FixtureFile
[:map
[:fixture/id :keyword]
[:fixture/spec-version :string]
[:fixture/doc {:optional true} :string]
[:fixture/registry
[:map
[:event {:optional true} [:map-of :keyword :map]]
[:sub {:optional true} [:map-of :keyword :map]]
[:fx {:optional true} [:map-of :keyword :map]]
[:cofx {:optional true} [:map-of :keyword :map]]
[:view {:optional true} [:map-of :keyword :map]]
;; Per rf2-cq1ak app-db schemas are NOT a registrar kind; the
;; `:app-schemas` fixture key carries `path → schema` for the
;; runner's `reg-app-schema` realisation step.
[:app-schemas {:optional true} [:map-of [:vector :any] :any]]
[:route {:optional true} [:map-of :keyword :map]]]]
;; Handler bodies are expressed in the :rf/handler-body-dsl grammar
;; defined above (single canonical definition).
[:fixture/handlers
[:map-of :keyword [:map-of :keyword HandlerBody]]]
[:fixture/frame-config {:optional true} :map]
[:fixture/dispatches {:optional true} [:vector [:vector :any]]]
;; `:fixture/calls` carries direct invocations of pure primitives (Mode B).
;; Each entry dispatches on `:call`; per-op record shapes match the
;; fixture-runner's case dispatch (`implementation/core/test/re_frame/conformance_test.clj` `run-call`).
;; The six operators cover state-machine transitions, URL ↔ route helpers, and SSR rendering.
[:fixture/calls
{:optional true}
[:vector
[:multi {:dispatch :call}
;; Pure machine-transition. Returns [next-snapshot effects].
[:machine-transition
[:map
[:call [:= :machine-transition]]
[:definition :any] ;; transition table per [005-StateMachines.md](005-StateMachines.md)
[:snapshot :any] ;; {:state :data} input snapshot
[:event [:vector :any]] ;; event vector to apply
[:expect-next-snapshot :any] ;; expected snapshot after transition
[:expect-effects [:vector :any]]]] ;; expected fx vector returned by the action
;; URL → route-match. `:expect` is the match map or `nil` for unmatched.
[:match-url
[:map
[:call [:= :match-url]]
[:url :string]
[:expect :any]]] ;; {:route-id :params :query :validation-failed?} or nil
;; route-id + params [+ query] → URL string.
[:route-url
[:map
[:call [:= :route-url]]
[:route-id :keyword]
[:params :map]
[:query {:optional true} :map] ;; 3-arity form when present
[:expect :string]]] ;; the rebuilt URL
;; Round-trip property: route-url ∘ match-url is identity for the URL.
[:round-trip
[:map
[:call [:= :round-trip]]
[:url :string]]]
;; Asserts winner's :rf.route/rank tuple compares greater than loser's
;; via lex compare. Per [012 §Route ranking algorithm](012-Routing.md#route-ranking-algorithm).
[:assert-rank-greater
[:map
[:call [:= :assert-rank-greater]]
[:winner :keyword] ;; route-id expected to outrank
[:loser :keyword]]] ;; route-id expected to be outranked
;; SSR pure render. `:input` is hiccup or a registered-view event vector.
[:render-to-string
[:map
[:call [:= :render-to-string]]
[:input :any] ;; hiccup or [:view-id args ...]
[:opts {:optional true} :map] ;; {:doctype? bool} etc.
[:expect :string]]]]]] ;; expected HTML output
[:fixture/expect
{:optional true}
[:map
[:final-app-db {:optional true} :any]
[:sub-values {:optional true} [:map-of [:vector :any] :any]]
[:sub-graph-topology {:optional true} :map]
[:trace-emissions {:optional true} [:vector :map]]
[:effects-routed {:optional true} [:vector :any]]]]]) ;; routed-fx pairs in declaration order, per [conformance/README.md](conformance/README.md) §Fixture lifecycle
:rf/fixture-handler-body is a synonym for :rf/handler-body-dsl (defined above) — the fixture format reuses the canonical DSL grammar rather than redefining it. Reserved built-ins are enumerated in conformance/README.md §Handler-body DSL builtins.
The schema is open by convention — fixture files may add :fixture/<key>-namespaced metadata keys.
Resolved decisions¶
Per-kind registration-metadata schemas (RESOLVED)¶
The open-shape :rf/registration-metadata describes the common keys every reg-* accepts; each registration kind additionally has its own narrowed shape. Per §Per-kind refinements, the catalogue ships :rf/event-handler-meta, :rf/sub-meta, :rf/fx-meta, :rf/cofx-meta, :rf/view-meta, :rf/machine-meta, :rf/flow-meta, :rf/app-schema-meta, :rf/head-meta, :rf/error-projector-meta, and the route-shaped :rf/route-metadata (defined separately above). The closure resolves the open-question carried in 001 §Resolved decisions (per-kind metadata schemas) and satisfies the SA-3/SA-4 commitment that every shape on the wire has a Spec-Schemas entry. AI scaffolders (Construction-Prompts) and conformance harnesses validate per-kind metadata at registration time against the corresponding refinement.
Conformance¶
An implementation conforms if every runtime shape it produces matches the structures described above. Multiple paths to conformance:
- Dynamically typed in-scope hosts with a runtime schema layer (CLJS+Malli; Squint+Zod): validate emitted shapes against registered schemas. Failures are surfaced as
:rf.error/schema-validation-failuretrace events (009 §Error contract). - Statically typed in-scope hosts (TypeScript, Melange / ReScript / Reason, Fable, Scala.js, PureScript, Kotlin/JS): the type system enforces shape correctness through the runtime. Mismatches at the boundary (incoming JSON, deserialised state) are caught by a small boundary validator if needed. Most internal shape errors don't arise at all because the compiler rejected them.
- Dynamically typed hosts without a runtime schema layer (rare; primarily early-stage prototypes): match shapes by convention. Conformance is verified by the fixture corpus alone.
The conformance test corpus is built from canonical interactions (a counter increment, a feature scaffolding, a state-machine transition, a server-side render + hydration round-trip) along with the expected emissions. The corpus format is itself data — an EDN/JSON file — so an AI can read it, generate test code in the host language, and report conformance scores.
All three paths pass the corpus; the differences are about when shape errors are detected (compile time vs. runtime) and what mechanism catches them.
Cross-references¶
- 000-Vision.md — the open-maps-with-schemas commitment.
- 009 §Error contract — error-event refinements of
TraceEvent. - 010-Schemas.md — the CLJS reference's schema integration (Malli); this doc applies the same shape discipline to the spec's own runtime data.
- 011-SSR.md — hydration payload context.
- 005-StateMachines.md — transition table grammar context.