Causl is a state engine for applications whose model is a live graph of facts whose derivations cascade. Atomic commits, automatic dependency tracking, dynamic-dep cleanup, glitch-free diamonds, and pre-runtime race detection — held by property tests and a bounded model checker, not by promises.
import { createCausl } from '@causl/core'
const graph = createCausl()
const a = graph.input('a', 1)
const b = graph.input('b', 2)
const sum = graph.derived('sum', g => g(a) + g(b))
const sum1 = graph.derived('sum+1', g => g(sum) + 1)
graph.subscribe(sum1, v => log(v))
// 4
graph.commit('bump-a', tx => tx.set(a, 10))
// 13
graph.commit('bump-both', tx => {
tx.set(a, 100); tx.set(b, 200)
})
// 301 — exactly one notification, not two
Spreadsheets, CMMS, BIM-style asset graphs, capital planning tools, scheduling systems, scenario planners, configuration editors, large operational consoles — applications whose state is a live graph of facts whose derivations cascade.
A single user edit fans out through derived values, and some dependencies change which inputs they read as the user navigates. Static selector graphs can’t express this; manual memoisation drifts as the model grows.
A fetch returns after the dependency it was launched against
has already moved. AbortController is part of the answer; the
rest is treating freshness as a typed property of the result,
not an ad-hoc if in the resolver.
Re-renders that fire in the wrong order produce visible-but-inconsistent intermediate frames. When that state is also persisted, the bug is not a render bug — it’s corruption that ships to disk and to other users.
Each commitment names an unavoidable engine concept; together they bound what the public API is allowed to grow into.
A derived value’s meaning is a function of its inputs at a
given commit time: Behavior a = GraphTime → a.
Glitch-freedom is then a theorem, not a scheduler trick.
All writes happen inside graph.commit(intent, tx → …).
Outside, the graph is read-only. There is no concurrent-write
API to misuse.
A derivation that today reads assetA and tomorrow
reads assetB no longer fires on
assetA writes — proven by property tests, not
promised by docs.
Resource fetch, conflict status, transaction phases, and interaction modes share one chart with shared event vocabulary. No more parallel string enums sprinkled across object fields.
The user’s information model, the editor’s controller state, and the engine’s substrate live in separate identifier namespaces and separate packages — enforced at the package boundary, not in the docs.
Optional fields that hide state machines are forbidden. Impossible states cannot be represented; the type checker is the first reviewer.
A typed Msg union dispatched through
update : Msg → Model → Commit. Transactions are
the engine room; messages are the front door.
Two Rust-backed CI tools ship today: causl-check
(twelve-pass static IR linter) and
causl-enumerate (SPEC §16.4 bounded model
checker, with an Apalache differential runner against the
EPIC-7 TLA+ corpus).
Honest about where existing libraries are strictly better (✓), where they cover the concern in some form (~), and where the concern is missing (✗). The Causl column uses ✓ for what currently ships and ∗ for in-flight or planned future work.
| Concern | Redux + RTK | MobX | Jotai | Recoil | Zustand | Valtio | TanStack Query | XState | Causl |
|---|---|---|---|---|---|---|---|---|---|
| Transactional commits (atomic write boundary) | ✓ | ~ | ✗ | ✗ | ✗ | ✗ | ~ | ~ | ✓ |
| Automatic dependency tracking on reads | ✗ | ✓ | ✓ | ✓ | ✗ | ~ | ~ | ✗ | ✓ |
| Dynamic-dep cleanup proven correct | n/a | ~ | ~ | ~ | n/a | ~ | n/a | n/a | ✓ |
| Glitch-free diamond as a guarantee | ✗ | ~ | ~ | ~ | ✗ | ✗ | ✗ | ✗ | ✓ |
| Denotational semantic specification | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ~ | ✓ |
| Composite statechart for all lifecycles | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✓ | ✓ |
| Stale-async protection by version | ~ | ✗ | ✗ | ~ | ✗ | ✗ | ✓ | ✗ | ✓ |
| Conflict records as queryable state | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ~ | ✗ | ✓ |
| Discriminated-union state (impossible states) | ~ | ✗ | ~ | ~ | ~ | ✗ | ~ | ✓ | ✓ |
| Strict model / controller / engine layering | ~ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ~ | ✓ |
| MVU-shaped typed Msg dispatch | ✓ | ✗ | ✗ | ✗ | ~ | ✗ | ✗ | ✓ | ✓ |
| Pre-runtime race detection in CI/CD | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ~ | ✓ |
| Live derivation editing in devtools | ~ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ~ | ✓ |
| Spreadsheet-grade dependency cascades | ✗ | ~ | ~ | ~ | ✗ | ✗ | ✗ | ✗ | ✓ |
| Excellent at: small global state | ~ | ✓ | ✓ | ~ | ✓ | ✓ | n/a | ~ | ~ |
| Excellent at: server cache / fetch dedupe | ~ | ✗ | ~ | ~ | ~ | ✗ | ✓ | ✗ | ~ |
| Excellent at: hierarchical UI state machines | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✓ | ~ |
Legend: ✓ strictly handles · ~ covers in some form · ✗ does not address · ∗ future goal · n/a not in scope. For the full reading-the-table notes, see the README.
Causl is over-engineered for simple apps and the only way to ship the complex ones without losing your mind. Pick the right tool.
causljs/causl-ts workspace at a glance.Each package owns one concern. Adopters install only the pieces they need; the engine has no React dependency. Six sibling repos round out the cross-org topology (Rust engine, WASM bridge, TS fork that defaults to it, bench harness, static analysis, this site) — see the documentation page.
useCausl, useDispatch, useCauslFamily, MVU runner, SSR.PersistenceError reporting.causl-check — the Rust-backed static IR linter (twelve passes).
Two Rust crates ship in
causljs/causl-check
as CI gates, not runtimes:
causl-check (static IR linter,
now with adopter-defined race classes per RFC 0001 and federated
cross-repo runs per RFC 0002) and
causl-enumerate
(SPEC §16.4 bounded model checker, with an Apalache differential
runner against the EPIC-7 corpus and a Tier-3 Apalache TLA+
corpus for the SPEC.async §9.1.1 S-rows).
main. Pre-1.0.The Phase-8 SPEC compliance audit is closed; the bounded enumerator’s full §16.4.1 type surface is implemented. APIs are stable but not version-locked until 1.0.
Atomicity, glitch-freedom, dynamic-deps, replay determinism, cycle detection — held by 1000-trial property suites in packages/core/test/properties/.
useCausl, useDispatch, useCauslFamily, Suspense + SSR — tested under StrictMode mount/unmount cycles.
causl-check + causl-enumerate run in CI against the spreadsheet and async demos. docs/apalache-diff-report.md regenerates on every CI run.
Full SPEC §16.4.1 surface — 10-field State, 8-arm Action, transition_phased with per-step events + phases, Oracle::check, Tier-1/2/3 Bound presets, enumerate_with_script entry point. 43 enumerator test binaries.
Configurable via CAUSL_BFS_FRONTIER_CAP / CAUSL_BFS_TRACES_CAP / CAUSL_BFS_RACES_CAP env vars. Conservative defaults stay until adopter empirical data supports retuning.
The Monaco playground and 100-cell spreadsheet demo ship as static HTML pages under causl-org/. Both load @causl/core, @causl/formula, and @causl/devtools live from esm.sh — no build step required.
SPEC §10 — two inputs, one derived value, one diamond
derivation, one subscriber, two commits, three observed
propagations. Pinned as an acceptance test in
packages/core/test/spec-10-worked-example.test.ts.
# prerequisites: Node 24.x + pnpm 10
pnpm add @causl/core
pnpm add @causl/react # React bindings (optional)
The repository pins Node via .nvmrc and pnpm via
packageManager in the root package.json.
Run pnpm validate before committing — it runs
typecheck + build + test, the same toolchain CI runs.
import { createCausl } from '@causl/core'
const graph = createCausl()
const a = graph.input('a', 1)
const b = graph.input('b', 2)
const sum = graph.derived(
'sum',
g => g(a) + g(b)
)
graph.commit('bump-both', tx => {
tx.set(a, 100)
tx.set(b, 200)
})
// Subscribers fire exactly once.
The documentation bundles the rendered SPEC, per-package API references generated from the source, and the changelog. The repository carries the eight-commitment audit trail.