Date: 2026-02-20 Reviewer: critic agent Scope: All 11 spec files, validated against statecharts library source code
The specs are ambitious and generally well-structured, but contain several critical inaccuracies when compared against the actual statecharts library API, architectural gaps in how the pieces compose, and missing coverage for important edge cases. The most serious issues are:
fops/assoc-alias signature is wrong throughout all specs -- the actual API uses keyword-argument pairs, not positional argsrstate/istate already handle most of what the specs propose to reimplementdefsc-form / defsc-report macro rewrites -- these are the most impactful changes for downstream usersQuality: Good. This is the most straightforward spec.
Issues:
spy and config). A full audit of RAD's timbre usage is needed before deciding.com.fulcrologic.guardrails.malli.core), while RAD uses the spec-based guardrails (com.fulcrologic.guardrails.core). These are different namespaces. Both should work side-by-side, but this should be verified.0.1.0-SNAPSHOT is correct. Tracking the fork version would cause confusion.Recommended changes: Add a timbre usage audit task. Add guardrails malli/spec coexistence verification.
Quality: Thorough analysis of the current UISM, good state mapping. Several API inaccuracies.
Critical Issues:
fops/assoc-alias signature is WRONG. The spec shows:
[(fops/assoc-alias :server-errors [{:message "error"}])]
But the actual signature is keyword-argument pairs (variadic & {:as kvs}):
[(fops/assoc-alias :server-errors [{:message "error"}])]
Wait -- this actually happens to work because a single key-value pair works as kwargs. But the spec also shows:
[(fops/assoc-alias :server-errors [])]
Which is also fine as kwargs. However, the conceptual framing is wrong. The spec treats it as (assoc-alias key value) -- it's actually (assoc-alias & {:as kvs}) meaning you can pass multiple at once: (assoc-alias :server-errors [] :route-denied? false). The specs should show the multi-key pattern.
Expression function arity inconsistency. The spec says expressions are (fn [env data event-name event-data]) (4-arg). The actual install-fulcro-statecharts! docstring says:
"The execution model for Fulcro calls expressions with 4 args: env, data, event-name, and event-data."
This is correct. BUT the routing code uses (fn [env data & _]) patterns extensively. The spec's attribute-changed-expr example shows 4-arg, which is correct. However, the spec doesn't mention that the _event data is ALSO available in data under the :_event key. The 4-arg form is a convenience. The spec should note both access patterns.
start! session-id handling. The spec proposes using form-ident as session-id:
:session-id form-ident ;; e.g. [:account/id #uuid "..."]
The actual start! function accepts ::sc/id type for session-id. Looking at the guardrails spec: [:session-id {:optional true} ::sc/id]. The ::sc/id spec needs to be checked -- if it requires a keyword, vectors won't work. This is a critical unknown that could break the entire session-id strategy.
fops/load signature mismatch. The spec shows:
[(fops/load form-ident FormClass
{::sc/ok-event :event/loaded
::sc/error-event :event/failed})]
The actual signature is:
(defn load [query-root component-or-actor {:keys [] :as options}])
The ok/error event keys are correct (::sc/ok-event, ::sc/error-event). The first arg query-root can be a keyword or ident, and the second is a component class or actor keyword. The spec's usage looks correct.
fops/invoke-remote signature mismatch. The spec shows:
[(fops/invoke-remote `save-form
{:params ...
:returning :actor/form
:ok-event :event/saved
:error-event :event/save-failed})]
The actual signature is:
(defn invoke-remote [txn {:keys [target returning ok-event error-event ...]}])
The first arg txn should be "A Fulcro transaction vector containing a single mutation", but the spec passes a symbol. The actual API expects a txn vector like [(save-form {...})] or possibly just the mutation call. Need to verify whether a bare symbol works or if it needs to be a txn vector. The spec should show the correct format.
:state/creating has an eventless transition to :state/editing, but the on-entry script is synchronous and sets up defaults. The eventless transition fires after on-entry completes. This is correct statechart semantics, but the spec should explicitly note that eventless transitions are evaluated AFTER entry actions complete.
Missing: view-mode? function conversion. The current view-mode? reads deeply into UISM internal storage:
(-> master-form comp/props ::uism/asm-id (get ...) ::uism/local-storage :options :action)
This is completely UISM-specific. The spec lists view-mode? as "unchanged" in the query functions section, but it absolutely requires rewriting. It needs to read from statechart session data instead.
Missing: defsc-form macro rewrite. The macro generates :will-enter, :will-leave, :allow-route-change?, query with [::uism/asm-id '_], etc. None of the specs detail exactly what the macro should generate in the new system. This is the single most impactful omission.
The :state/asking-to-discard-changes removal is premature. The spec says it's removed because route-denied? alias handles it. But the async confirmation pattern (where a modal asks "discard changes?") currently relies on the UISM being in a distinct state. With statecharts, this could be a child state of :state/editing, which would be cleaner than removing it entirely. The spec should address the async confirm UX pattern explicitly.
on-change trigger conversion is under-specified. The spec identifies three options but doesn't commit. The on-change trigger is used by real applications and its conversion needs a definitive answer. Option A (clean break) is correct but needs the full new signature specified.
Recommended changes: Fix all API signatures. Address view-mode?. Add defsc-form macro spec. Specify on-change trigger contract.
Quality: Good coverage of all three report variants. Good alias mapping.
Issues:
:state/processing as a transient state with eventless transition is problematic. The spec proposes:
(state {:id :state/processing}
(on-entry {} (script {:expr process-loaded-data-expr}))
(transition {:target :state/ready}))
An eventless transition fires immediately after on-entry. This means process-loaded-data-expr must be synchronous and complete before the transition. For large datasets, filter/sort can be expensive. The two-phase pattern (set busy, yield to render, then process) exists in the current UISM for a reason. The spec acknowledges this for sort/filter but not for the initial load processing.
Data storage strategy is unclear. The spec stores report data (raw-rows, filtered-rows, sorted-rows, current-rows) via aliases that point into Fulcro state at actor paths. But it also mentions ops/assign for page cache in server-paginated reports. This creates a split brain: some data in Fulcro state (via aliases), some in statechart session data (via assign). The spec should define a consistent strategy.
report-session-id function is referenced but never defined. Multiple specs reference it. Its implementation determines whether scf/send! can find the right session. This needs to be specified: is it (comp/get-ident report-class {}) (the current UISM pattern), or something else?
Missing: defsc-report macro rewrite. Same issue as defsc-form -- the macro generates query, ident, will-enter, initial-state. None of this is specified for the new system.
The incrementally-loaded report spec is thin. It says "same events as standard report" for :state/ready but doesn't enumerate them. This variant needs the full event list.
Missing: ro/machine override semantics. The spec says ro/machine becomes a statechart registry key, but doesn't explain how a user registers their custom chart, or how the defsc-report macro knows to use it.
Recommended changes: Define report-session-id. Detail data storage strategy. Add defsc-report macro spec. Flesh out incremental report.
Quality: Adequate for the simpler container system.
Issues:
Side effects in expression functions. The spec proposes calling scf/start! and scf/send! as side effects within expression functions:
(fn [env data]
(doseq [...] (report/start-report! app child-class ...))
[...ops...])
This is a pattern question: should expression functions have side effects? The statecharts model prefers operations (returned data) over side effects. scf/send! from within an expression during event processing could cause reentrancy issues if the event queue is in :immediate mode. The spec should note this risk and recommend :event-loop? true or at minimum acknowledge the ordering implications.
Missing: defsc-container macro rewrite. Same pattern as form/report.
Child cleanup strategy is weak. "Lazy cleanup" means statechart sessions persist indefinitely. The statecharts library GC's sessions that reach a final state, but report sessions never reach final state. This means memory will grow unboundedly as users navigate. The spec should specify explicit cleanup on route-exit.
Recommended changes: Address reentrancy risk. Specify cleanup. Add macro spec.
Quality: Good analysis, correctly identifies the cross-chart communication challenge.
Issues:
Duplicated event handling across states. The proposed chart has :event/authenticate, :event/logout, :event/session-checked duplicated in :state/idle, :state/gathering-credentials, and :state/failed. This is a classic anti-pattern -- these should be hoisted to a parent compound state:
(state {:id :state/auth :initial :state/idle}
;; Global events at parent level
(handle :event/session-checked handle-session-checked!)
(on :event/logout :state/idle (script {:expr handle-logout!}))
;; ... children ...
(state {:id :state/idle} ...)
(state {:id :state/gathering-credentials} ...)
(state {:id :state/failed} ...))
The reply-authenticated! side effect (sending an event to the source session) is a critical coordination point. The spec mentions scf/send! but doesn't detail HOW the source session-id gets to the auth chart. With UISM, it was source-machine-id. With statecharts, the source could be a routing session, a form session, etc. The spec should specify the full event data contract for :event/authenticate.
Multiple actors not fully addressed. The current auth machine has actors for each authority provider. The spec's data-model only stores metadata, but the actual UISM uses uism/swap-actor! to dynamically change which component serves as :actor/auth-dialog. The spec mentions fops/set-actor but doesn't show the full actor lifecycle.
"NOT PRODUCTION-READY" presents an opportunity. The spec asks whether to redesign. Recommendation: Do a minimal conversion now (keep same behavior), but flag it as a candidate for v2 redesign. Don't let scope creep here block the main conversion.
Recommended changes: Hoist common events. Specify event data contracts. Show actor swap pattern.
Quality: Best-written spec. Good comparison tables. But overcomplicates some things.
Critical Issues:
The spec RE-IMPLEMENTS what statecharts routing already provides. Looking at the actual routing.cljc source, rstate and istate already handle:
initialize-route!)update-parent-query!)establish-route-params-node)deep-busy?)busy-form-handler)The form spec proposes adding sfro/busy? and sfro/statechart to form components. But istate in the routing chart already does this:
(istate {:route/target :my.app/AccountForm})
And istate automatically reads sfro/statechart from the component and creates the invocation. The specs should not re-describe what the routing library already does. They should focus on what RAD needs to ADD to make forms/reports work with the existing routing infrastructure.
route-to! signature change is incomplete. The actual statecharts route-to! signature is:
(defn route-to! [app-ish target] ...)
(defn route-to! [app-ish target data] ...)
Where target is a component class, registry key, or keyword. RAD's current route-to! has:
[app options] ;; where options has :target, :route-params, etc.
[app-or-component RouteTarget route-params]
The mapping from old to new needs a compatibility function that extracts target and data from the old call patterns. The spec mentions this but doesn't specify the adapter.
update-route-params! removal is problematic. Reports use update-route-params! to persist sort/filter/page state in the URL. The spec says this is "removed" and "URL sync is automatic." But URL sync in statecharts routing only syncs the ROUTE (which page you're on), not arbitrary parameters stored in route state. The spec needs to explain how report parameters get into the URL. Looking at the routing code, establish-route-params-node stores params from event data into [:routing/parameters id], and the URL codec reads from there. So parameters CAN be in the URL, but they need to flow through the route event, not through a separate update mechanism. This needs explicit specification.
Missing: what happens to install-routing!? Currently apps call install-routing! to set up the RAD router. The spec says DELETE but doesn't specify what replaces it in the app setup. The answer is scr/start! + scr/install-url-sync!, but the spec should show the full app initialization sequence.
Missing: the actual routing chart definition. Each application needs to define its routing chart with rstate/istate for each form, report, and container. The spec doesn't show how defsc-form's fo/route-prefix maps to the routing chart's :route/segment. Is the routing chart auto-generated from form/report definitions, or does the user manually define it?
Recommended changes: Reduce redundancy with routing library. Show full app init sequence. Specify route-params-in-URL mechanism. Define whether routing chart is auto-generated or manual.
Quality: Excellent reference document. Most comprehensive spec.
Issues:
view-mode? is listed as "Unchanged" but IS UISM-DEPENDENT. As noted in the form spec critique, view-mode? reads from ::uism/asm-id and ::uism/local-storage. It requires a full rewrite to read from statechart session data. This is a breaking internal change even if the public signature stays the same.
clear-route-denied! and continue-abandoned-route! simplification. The spec changes these from [app-ish form-ident] to [app-ish]. This is correct because the routing system has a global session, but it means these functions are no longer form-specific -- they operate on the routing chart. The spec should note this semantic change.
start-form! visibility question is critical. The spec asks whether it should remain public. It must remain public for embedded forms (non-routed), which is a common pattern. But for routed forms, it should NOT be called directly -- istate handles it. The spec should clearly separate routed vs. embedded usage.
Missing: trigger! on forms. The current 4-arity trigger! takes form-ident as the session identifier. With statecharts, this could be the form-ident (if used as session-id) or a different session-id. The send-to-self! pattern from the routing library is the correct replacement. The spec should recommend send-to-self! rather than trying to preserve trigger!.
Missing: rendering env changes. rendering-env creates a map used for rendering. If it includes any UISM-derived data, it needs updating. The spec says "unchanged" but should verify.
Recommended changes: Fix view-mode? classification. Clarify start-form! public/private split. Recommend send-to-self!.
Quality: Good. Correctly identifies the minimal scope of changes.
Issues:
Session-id discovery is the real problem. run! currently uses (comp/get-ident instance) as both the UISM id and the target. With statecharts, this only works if the session-id convention uses the component ident. The spec acknowledges this dependency but doesn't lock it down. This convention MUST be specified in a cross-cutting decision, not left as an open question in each spec.
scf/send! first argument. The spec shows:
(scf/send! (comp/any->app instance) session-id :event/run)
But scf/send! can also accept a component instance directly (it's app-ish). The correct call could be:
(scf/send! instance session-id :event/run)
Actually, looking at the source: send! calls (statechart-env app-ish) which calls (impl/statechart-env app-ish). The app-ish type accepts ::fulcro-appish which includes components. So instance should work directly. Simpler code.
Event namespacing question is over-thought. :event/run should stay as-is. Namespacing it would break the contract between controls and reports/containers for no benefit.
Recommended changes: Lock down session-id convention. Simplify send! call.
Quality: Well-analyzed but this spec should be deprioritized or removed.
Critical concern: The blob system has ZERO UISM dependency. It uses Fulcro mutations with action, progress-action, and result-action. Converting it to a statechart is pure scope creep. The spec itself asks "Is the statechart conversion of blob worth the effort?" The answer is: No, not for v1.
If kept, issues include:
SubtleCrypto API). The spec doesn't address how to handle this in CLJ for headless testing.net/overall-progress which is deeply tied to Fulcro's networking layer. Redirecting progress events to a statechart adds complexity without clear benefit.Recommendation: Remove this spec from v1. The blob system works fine without UISM and doesn't need conversion. Add it to a "nice-to-have" list for v2.
Quality: Good structure. Good test examples. Some API inaccuracies.
Issues:
Test state names don't match the proposed statecharts. The test examples use :state/abandoned, :state/showing which don't appear in the form or report statechart definitions. The form spec uses :state/exited (not :state/abandoned) and the report uses :state/ready (not :state/showing). Tests must use the actual state names.
t/run-events! usage is potentially wrong. The test shows:
(t/run-events! env :event/created)
But the form statechart doesn't have an :event/created event. The :state/creating state has an eventless transition to :state/editing. The test should not need to send an event -- the eventless transition should fire automatically.
Missing: mock setup for Fulcro operations. The Tier 2 tests show scf/start! but don't explain how to mock the load/save operations. When the form statechart enters :state/loading, it fires fops/load. In a headless test, this load needs to either be mocked or use a loopback remote. The spec mentions "loopback remotes" but doesn't show how to set them up.
Container test example doesn't match container spec. The test creates actors :actor/form and :actor/report on the container, but the container spec uses :actor/container as its actor.
Tier 4 integration tests reference com.fulcrologic.fulcro.headless which may or may not exist. The spec should verify this namespace is available in the current Fulcro version.
Missing: how to run tests. The spec says "run via Kaocha in a REPL" but doesn't show the Kaocha configuration needed. The project may need a tests.edn for Kaocha.
Recommended changes: Fix state names to match specs. Fix event names. Show mock load/save patterns. Verify headless namespace.
Quality: Thorough analysis. Good comparison between use-form/use-report and use-statechart.
Issues:
use-statechart integration approach. The spec correctly identifies that use-statechart assumes a co-located chart on the component (via sfro/statechart option). For RAD hooks, the chart comes from the form/report module. The proposed "extract use-statechart-session" approach would require changes to the statecharts library itself, which is a separate project. This should be flagged as a cross-project dependency.
Alternative: just set sfro/statechart on the Form/Report component. If defsc-form sets sfro/statechart as a component option on the form class (which it should, per the routing integration), then use-statechart would work directly. The RAD hook just needs to:
use-statechart with the form instance{:form-factory, :form-props, :form-state} shapeThis is simpler than extracting a new lower-level hook.
:embedded? true semantics in statecharts. The spec asks how this flag translates. The answer: the form statechart should NOT have route-exit behavior when started via hooks. This means the chart needs to either:
:embedded? true) to skip route-related expressionsThe spec should specify which approach.
Missing: cleanup semantics. use-statechart sends :event/unmount on cleanup, which the chart can handle. But use-form currently calls uism/remove-uism! which fully destroys the session. The statechart equivalent would be to ensure the chart reaches a final state on unmount, triggering automatic GC. The spec should specify the unmount event handling in the form/report charts.
Recommended changes: Simplify to use sfro/statechart on component. Specify embedded-mode behavior. Specify cleanup.
Multiple specs independently propose session-id strategies without a unified decision:
report-session-id function (undefined)::auth-machine -> session-idcomp/get-identhooks/use-generated-id (random UUID)The ::sc/id type spec in the statecharts library must be checked. If it only accepts keywords or UUIDs, vector idents won't work. The start! function's guardrails spec says [:session-id {:optional true} ::sc/id]. We need to verify ::sc/id allows vectors.
Recommendation: Define ONE session-id convention document. Verify vector idents work. If they don't, use (keyword (str (first ident) "-" (second ident))) as a deterministic conversion.
defsc-form / defsc-report / defsc-container Macro Rewrites (CRITICAL)No spec covers what the macros should generate in the new system. These macros are the primary interface for downstream users. They currently generate:
[::uism/asm-id '_]):will-enter / :will-leave / :allow-route-change?In the new system, they need to generate:
sfro/statechart component option (pointing to the form/report chart)sfro/busy? component option (for forms: dirty check)sfro/initialize (:once for reports, :always for forms):route-segment for documentation/discoverability, but it's configured in the routing chartRecommendation: Add a new spec "macro-rewrites.md" covering all three macro changes.
The specs inconsistently use 2-arg (fn [env data] ...) and 4-arg (fn [env data event-name event-data] ...) patterns. The Fulcro integration ALWAYS uses 4-arg (per install-fulcro-statecharts! docs). The routing code uses (fn [env data & _] for flexibility.
Recommendation: Standardize on 4-arg in all specs. Show the & _ pattern for expressions that don't need event name/data.
Multiple specs call scf/send!, scf/start!, comp/transact! as side effects within expression functions. This is necessary in some cases but should be explicitly acknowledged as a pattern with caveats:
:immediate event-loop mode, scf/send! within an expression triggers synchronous processing, which can cause stack depth issuescomp/transact! within expressions bypasses the statechart's operation modelRecommendation: Document the "side effects in expressions" pattern with guidelines. Prefer operations over side effects where possible. Use senv/raise for internal events instead of scf/send!.
The specs say aliases are read via (scf/resolve-aliases data). Looking at the actual implementation, resolve-aliases is defined in fulcro_impl.cljc and is re-exported. The Fulcro data model automatically resolves aliases into the data map (per install-fulcro-statecharts! docs: "the data model will automatically resolve all aliases into the data map in expressions"). This means you can access alias values DIRECTLY on data without calling resolve-aliases. The specs should note this.
project-setup
<- form-statechart
<- report-statechart
<- auth-statechart
<- routing-conversion
<- control-adaptation
<- blob-statechart
report-statechart
<- container-statechart
form-statechart + report-statechart + routing-conversion
<- headless-testing
form-statechart + report-statechart
<- rad-hooks-conversion
Circular dependency: form <-> routing. The form spec depends on routing (for istate integration), and routing depends on form (for busy-form-handler). In practice, the routing library already handles both, so the actual dependency is: routing-conversion must be done BEFORE form conversion, because forms need to know how they're used as route targets.
Missing dependency: all specs -> macro-rewrites. The macro changes affect how forms/reports/containers are defined. This should be its own spec that depends on form, report, container, and routing specs.
Missing dependency: control -> report + container. Control's run! targets report/container sessions, so it needs their session-id convention finalized first.
Recommended implementation order:
defsc-form, defsc-report, defsc-container macro changes. See CC-2 above.
A short cross-cutting spec that defines:
report-session-id function implementationHow an application initializes with statecharts routing instead of DR:
install-fulcro-statecharts! call (with :on-save for URL sync)scr/start! with the routing chartscr/install-url-sync!install-routing! + install-route-history!rad/install! (the RAD application setup function)Not a code spec but essential for adoption:
The current UISM system allows assoc-in on the machine definition (used by incrementally-loaded-report). Statecharts don't support this. The spec needs to document the new extension patterns:
invoke vs independent chartsThe specs propose significant custom routing infrastructure on top of what the statecharts routing library already provides. The rstate, istate, routes, routing-regions, busy?, busy-form-handler functions in routing.cljc already handle 90% of what the RAD specs describe. The RAD layer should be THIN -- mostly option-to-option mapping and default behaviors.
Concern: The form and report specs describe building routing integration from scratch, when they should instead describe how to configure existing istate behavior for RAD's specific needs (e.g., how fo/route-prefix maps to :route/segment).
Reports store cached data (raw-rows, sorted-rows, etc.) in Fulcro state via aliases, but store pagination metadata in session data via ops/assign. This creates two sources of truth that must be kept in sync. A cleaner approach would be to store everything in Fulcro state (via aliases) since the report component needs to read it for rendering.
The blob system works. Converting it to statecharts for "consistency" adds risk and work without functional benefit. Remove from v1.
The auth spec notes the system is "NOT PRODUCTION-READY." Converting it as-is preserves existing (limited) functionality. Redesigning it risks scope explosion. Do the minimal conversion.
defsc-form, defsc-report, defsc-containerfops/assoc-alias usage -- show keyword-argument pair patternview-mode? -- mark as breaking internal change, not "unchanged"fops/invoke-remote first argument -- verify txn format& _ patternistate/rstate already doinstall-routing! bootstrapdataupdate-route-params! replacement -- how report params get into URLs| Metric | Value | Notes |
|---|---|---|
| Specs reviewed | 11 | All complete |
| Critical issues | 7 | Session-id, macros, API signatures |
| Important issues | 7 | Scope, naming, contracts |
| Suggested improvements | 5 | Documentation, patterns |
| Missing specs identified | 5 | Macros, session-id, init, migration, extension |
| API signature errors | 3+ | assoc-alias, invoke-remote, view-mode? |
| State name mismatches | 2+ | Test examples vs chart definitions |
| Unnecessary scope | 1 | Blob conversion |
Can you improve this documentation?Edit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |