Two-engine architecture Enterprise
Causl ships two conformant implementations of the same denotational
contract: a pure-TypeScript engine that is the open-source floor and
reference oracle, and causl-core-rs, the
Rust→WebAssembly production engine consumed through the thin
causl-wasm-ts TypeScript client. Both expose the same public
surface and are held byte-identical by a cross-backend determinism gate.
The two engines no longer live in one repository. Since the 2026-07-27
rename, causl-wasm-ts ships only the Rust engine,
and causl-core-ts is the repository that stays dual-engine
and remains the differential oracle. This page is the prose companion to
SPEC §18A.
Why two engines
For most of its history Causl committed to one engine, the
TypeScript value-of-record (SPEC §18 closes the arc on exactly that). The
§13.8 reopen trigger fired on 2026-06-19: a governance decision committed
the Rust-backed wasm engine as a conformant alternative
to the TypeScript engine, with a named promotion path to make it the
WASM-path default once the §18A.7 GO/NO-GO criteria were met. Those
criteria are now all GO (2026-06-21), so the full Rust
engine port has shipped. The wasm core is the production
engine in causl-wasm-ts.
The reopen fired then, not earlier and not as a slogan, because enough
scaffolding had landed (the EPIC #680 Phase-1 bridges, the #1493
batched-boundary infrastructure, the #1071/#1146 cross-backend
byte-identity gate, the §18A.3 FFI surface plan, the #1333/#1124
FFI-boundary invariants) that the engineered shape was no longer
speculative. The decision to commit is recorded in §17.6
Commitment 15 (two-engine byte-identity conformance and
the thin-TS-client-over-wasm-core architecture); §18A is its full
technical contract. Critically, §17.6 retired the earlier
"acceleration, not substitution" framing of Commitment 14 in favour of a
real second engine promoted under §18A.7. The thing behind the host-tier
bridges is a second engine, not merely an accelerator of the
first. That promotion has since landed: in causl-wasm-ts the
wasm core is the production engine (rust-ssot default), and the §18A.3
FFI structural lift means every adopter operation resolves from Rust.
The TypeScript floor and the reference oracle
The TypeScript engine is the value-of-record cell topology running
natively on the JS event loop. Per §13.8 and §18A.7 Criterion 5 it is
the unconditional floor: it stays supported and
byte-identical to the wasm engine for all adopter code, and any host that
runs JavaScript runs causl. It is not present in
causl-wasm-ts at all. There the production engine is the
Rust→WASM engine (rust-ssot) and nothing else. The
TypeScript engine is the value-of-record and default backend in
causl-core-ts, which still defaults to js-ssot,
and it ships as open source as causl-ts.
It is also the reference oracle. The byte-identity gate
(below) measures the wasm engine against the TypeScript engine's
output, so the TypeScript engine defines what "correct" means at the
public boundary. causl-wasm-ts pins
causl-core-ts by commit SHA (9c3d947e…) and runs
5,000 differential trials per pull request against it. An adopter on the
open-source pure-TypeScript distribution sees zero behavioural change from
any of this and pays zero bytes for the wasm path.
causl-core-rs: the Rust core, reached over FFI
causl-core-rs is the Rust engine (the
engine-rs-core pure-algorithm crate plus the
engine-rs-bridge JS↔WASM bridge crates) compiled to
WebAssembly and reached over FFI. Per §18A.1 it is a conformant
implementation of the same §3 semantic equation and the same §12 public
surface. The substrates differ. engine-rs-core carries real
Rust types: a generational NodeId { slot, gen } (#1151),
BTreeMap-keyed JSON values (#1078), and the
seven-named-struct cell shape (#1077). The behaviour at the
public surface does not differ; that is the whole point of the gate
(§18A.1.1).
The Rust crate is a from-scratch engine stood up against the same §0/§12 contract, not a line-by-line transliteration of the TypeScript engine. §18A.4 and §18A.9 are explicit that the canonical way is a from-scratch Rust engine proven byte-identical via the gate. A transliteration "carrying two copies of the same substrate assumptions in two languages" was considered and rejected.
The engine repo is called
causl-core-rs, and that is its only name. The old namecausl-wasmsurvives purely as a redirect on Gitea, so a link togithub.com/causljs/causl-wasmis stale. The engine repository isgit.opsite.ca/causl/causl-core-rs, and it is private, so throughout this page I name repositories, files and issues without linking to them; reading the source needs a Gitea account on that host.
One public surface, two engines
Both engines implement the same public Graph contract. The
seven-method spine that adopter code calls is identical regardless of
which engine is behind it:
input · derived · commit · read · subscribe · snapshot · explain
Alongside the canonical seven, the engines expose the §12.2 second-tier
structural surface: dependencies and dependents
(and their transitive closures), the commit log, commit metadata,
handle/disposal/StaleTx validation, and stats. For the thin TS client to
satisfy the full surface with no JS-engine fallback for any row,
the wasm core has to surface these as FFI externs. Per §18A.3 that
structural lift was the spine of the work, and it has
landed
(causl-core-rs#170):
every §12 row is now a named #[wasm_bindgen] extern routed
through the existing wire-format codecs, so the byte-identity gate still
holds over it, and every adopter operation resolves from Rust.
Because the surface is identical, the engine choice lives at the
BackendEngine seam
(packages/core/src/backend.ts) and nowhere else. Your model
code (input / derived / commit /
read / subscribe) is the same on either engine.
If application logic has to know which engine it is running on, that is a
bug. The one legitimate exception is the read()-identity
migration (below), which you fix once,
defensively.
Synchronous construction: the preload seam (§18A.12)
Constructing a wasm graph used to be unavoidably async,
because compiling the .wasm module is an await.
That single asynchrony leaked into every call site, and the §12
commit / read spine is and stays
synchronous (§18A.6), so a sync consumer (a React hook in
render, an iasbuilt/xldatagrid cell) had no clean way to
build a wasm graph without an await at the wrong place.
§18A.12 splits the one unavoidable async (the WebAssembly
compile) away from synchronous construction, so the seam offers a
fully synchronous build path. This is shipped in the
engine repo and in causl-wasm-ts.
The split is a preload step you run once at app init,
after which construction is a zero-await synchronous call:
// once, at app init: the single await lives here
await preloadCauslWasm() // compiles + caches the WebAssembly.Module
// (+ the _bg.js sidecar + compute-imports
// snippet), keyed by bridge. Idempotent:
// concurrent calls share one compile, and a
// transient failure drops the cache.
// anywhere after: fully synchronous, zero await, new instance from the cache
const graph = createCauslWasmSync() // throws CAUSL_WASM_NOT_PRELOADED if the
// preload never resolved
preloadCauslWasm(opts?) compiles and caches the
WebAssembly.Module (plus the _bg.js sidecar and
the compute-imports snippet), keyed by bridge. It is idempotent:
concurrent calls share a single compile, and a transient failure drops the
cache so a retry recompiles. Its synchronous peek companions,
isCauslWasmPreloaded(bridge?) and
getPreloadedCauslWasm(bridge?), let a caller branch without
awaiting. createCauslWasmSync(handle?, create?) then does a
new WebAssembly.Instance from the cached
Module with zero await. If
nothing is preloaded it throws with code
CAUSL_WASM_NOT_PRELOADED. There is no
fallbackToTs escape hatch any more; see
the withdrawn fallback.
The retained async factory
createCauslWasm(opts?): Promise<Graph> is now
re-expressed as preload ∘ createCauslWasmSync, one
codepath rather than two, so the async and sync constructors cannot
drift. On the server the seam is moot: Node's
--target nodejs glue is already synchronous end to end, so no
preload is needed there. It is the browser bundler target that needs the
one-time preload, which is exactly where the sync render constraint bites.
Construction therefore resolves into two live factories, with engine choice made at the call site when you want it:
| Factory | Engine | Shape |
|---|---|---|
createCauslWasm() /createCauslWasmSync() |
The wasm engine, explicitly. | Async / synchronous. The sync form needs a prior
preloadCauslWasm() on the browser target; both share
one codepath. |
createCausl() |
The default public factory. | Synchronous, and construct-or-throw. In
causl-wasm-ts it routes to the wasm engine
(rust-ssot) once preloaded and throws otherwise; two
statements, no branch. In causl-core-ts it routes to
the TypeScript floor by default (js-ssot). |
The equivalence contract: byte-identity (§18A.1)
"Two interchangeable engines" is a contract, not a slogan, because of the
cross-backend byte-identity gate (§18A.1.1). Given the
same action sequence, the two engines MUST produce
byte-identical Commit records, graph
snapshots, structural-query results (dependencies, dependents,
commit-log, commit-metadata, stats), and subscriber fire-order. The gate
runs at the §15.2 1000-trial property floor, JS-side in
cross-backend-determinism.property.test.ts and Rust-side in
bridge_byte_identity.rs, per-commit under the default
single-commit envelope (afterN=1) and per-flush under the
epic-#1493 batched-boundary opt-in. Zero-byte divergence on every replayed
commit is the pass condition; any divergence is a CI failure and
a §18A.7 Criterion-1 NO-GO. Since the split,
causl-wasm-ts runs the gate against a SHA-pinned
causl-core-ts at 5,000 trials per pull request.
Byte-identity is reachable on the Rust side only because the container
invariants are pinned: §5.1 Amendment 4 (#1333) requires the
per-node subscriber index to preserve insertion order
(IndexMap<NodeId, SmallVec<[ObserverId; 2]>>).
engine-rs-core's prior BTreeMap /
BTreeSet shape (which sorts by id) is non-conformant and was
promoted to insertion-order semantics alongside the FFI surface work.
causl-wasm-ts: the thin TS client over the wasm core (§18A.4)
causl-wasm-ts is the TypeScript-side FFI binding for the wasm
engine, the actor at the BackendEngine seam that wraps the
Rust core. The Graph facade delegates every public method to
the backend, and for the wasm path that backend marshals parameters
across the JS↔WASM boundary to the externs of §18A.3.
§18A.4 states plainly, so the boundary stays honest, what
causl-wasm-ts is not: not a copy of the
TypeScript engine's graph.ts; not the ~7.5k-LOC TS shell
under a new name; not "the same TS engine, just calling into WASM." It is
a thin adapter that delegates each public method to the wasm core over
FFI, marshals parameters across the boundary (the cost is measured, not
hidden), and binds to a placed .wasm artefact. It
ships no Rust source and no build tooling; those live in the engine repo
(below). The consumer-side framing for TS/Node apps is the
Integrating causl-wasm-ts
guide.
The withdrawn TypeScript fallback (§18A.13, and §18A.13.1 as a dated record)
causl-wasm-ts ships wasm as its sole engine.
This is shipped: gate-bypassed (a dated §18A.13
governance amendment, on complexity-elimination grounds), enterprise-only,
and scoped to this one package. createCausl() routes to the
wasm engine (rust-ssot default), and the §18A.3 FFI structural lift landed
(causl-core-rs#170),
so every adopter operation resolves from Rust.
For a period in 2026 that was not the whole story.
§18A.13.1 (2026-06-23) re-extended a loud TypeScript
capability-fallback to the implicit path: on a host where the wasm engine
could not instantiate, createCausl() would degrade to a
retained internal createCauslTs() with a one-time
console.warn plus telemetry. That section was
withdrawn at 0.5.0. Its own heading now says so, and it adds that
it should be read "as a dated record, not as a live obligation."
Two things killed it, and both are more interesting than the withdrawal itself:
-
The two engines actually disagreed. The fallback
engine and the primary diverged on in-place mutation of a committed
value
(
causl-wasm-ts#272), which is a §18A.1.1 MUST-be-identical surface. A fallback that is not byte-identical is not a fallback; it is a second set of semantics reached by accident. -
The implicit path was the accident path. Nobody
deliberately chose to run on the fallback; they landed on it because
createCausl()was the ordinary thing to call. 0.4.0 warned on it, and 0.5.0 throws.
So today createCausl() is construct-or-throw,
two statements in packages/core/src/graph.ts, and
createCauslTs is deleted from source. A
git grep createCauslTs over packages/*/src/**
returns zero hits; it survives only in tests that assert its absence and
in dated records like the paragraph above. Branch on
error.code and never on instanceof:
CAUSL_WASM_ENGINE_UNAVAILABLE (the wasm subpath was never
imported, or the host cannot instantiate the engine) and
CAUSL_WASM_NOT_PRELOADED (imported, but
preloadCauslWasm() never resolved).
The driver for the original cut was complexity
elimination, not speed. causl-wasm-ts accepts the
wasm engine's runtime characteristics as the single core and sheds the
dual-engine branching. Performance was explicitly accepted as
immaterial to that decision. The cut is a §18A.13 deviation from
the §18A.7 promotion gate that governs the default in the
dual-engine repository; it does not move that gate, it bypasses it for
this one consumer-facing package.
Repository topology (§18A.10)
The two-engine contract spans three first-party repositories, all now
hosted on Gitea at git.opsite.ca/causl/. Each owns exactly
one artefact of the §18A surface, and no repo duplicates another's role.
The split is recorded so the FFI source of truth, the thin TS client, and
the TypeScript value-of-record floor are never confused for one another.
All three were renamed on 2026-07-27; the old names are 301 redirects.
| Repository | Was | Role | Owns |
|---|---|---|---|
causl-core-rs |
causl-wasm |
The Rust engine repo, the source of truth for the Rust→WebAssembly engine. | The §18A.3 FFI surface, engine-rs-core,
engine-rs-bridge, the byte-identity gate's Rust side
(bridge_byte_identity.rs), and the Python build and
packaging tooling (build_wasm.py +
package_wasm.py, §18A.11). Publishes
@causl/core-rs. |
causl-wasm-ts |
causl-client |
The TypeScript client for the Rust engine, the thin TS binding of §18A.4 (a binding over the FFI, not a copy of the TS engine). Single-engine. | The §12 Graph contract as a thin binding over the
wasm FFI, the --target nodejs loader hook (§18A.2)
that resolves the placed artefact via node:fs, and the
adoption docs. Consumes the placed .wasm; ships no
Rust and no TypeScript engine. Publishes
@causl/causl-wasm-ts. |
causl-core-ts |
causl-ts-wasm-engine |
The TypeScript reference engine, the value-of-record and §13.8 unconditional floor. Stays dual-engine. | The TypeScript engine, the differential oracle for the
byte-identity gate's JS side, the benchmark and conformance
harness, and the historical wasm-cutover R&D. Still defaults to
js-ssot. causl-wasm-ts pins it by commit
SHA and runs 5,000 differential trials per pull request against
it. |
Named first-party integration consumers are
iasbuilt/xldatagrid and iasbuilt/webapp
(Node.js TS web apps). This topology is descriptive: it records where the
committed artefacts live and does not itself alter the §18A.7 GO/NO-GO
criteria.
SPEC.mdis no longer byte-identical across the repositories. It used to be, and §18A.10 used to promise it would stay that way. As measured on 2026-08-02 the two copies are 3,333 and 3,280 lines: thecausl-core-tscopy is missing §5.1 Amendments 5 to 7 and §5.7. Treat the copy incausl-wasm-tsas the current one, and treat the copy bundled with this site as a dated 3,280-line snapshot rather than the live contract.
Build and packaging tooling (Python, §18A.11)
A webapp must be able to take the --target nodejs artefact
and place it into its own deployment reproducibly:
build-once and place-where-told, not rebuild-per-app and not fetch-late.
Per §18A.11 the tooling is two stdlib-only CPython scripts (no
pip install, no third-party deps) that live in
causl-core-rs, the engine repo, because the engine repo owns
producing and placing its own artefact:
| Script | Role |
|---|---|
scripts/build_wasm.py |
The BUILD half. Compiles the Rust core to a size-optimised,
node-target .wasm (wasm-pack --target nodejs
then external wasm-opt -Oz), staging the optimised
.wasm, node glue and .d.ts typings
and the §18A.12 compute-imports snippet next to the
.wasm / _bg.js. This is the only place a
Rust toolchain is needed, the producer side. |
scripts/package_wasm.py |
The PACKAGE half, the script the Python webapp pipeline plugs in.
It places the node-consumable artefact set at a consumer-stated
destination (--dest, REQUIRED) and writes a
deterministic version plus per-file-sha256 manifest. No network.
Idempotent. Fails loudly (non-zero exit, clear stderr) on a missing
source, an unhashable file, or an unwritable destination. |
A Python pipeline states where the engine goes and calls the package script. It never authors a YAML kit or reasons about bundler integration:
import subprocess, sys
subprocess.run(
[sys.executable, "scripts/package_wasm.py", "--build", "--dest", WASM_TARGET_DIR],
check=True,
)
The contract the tooling must satisfy: the placement is versioned and
checksum-pinned; there is no deploy or runtime network (the engine is
vendored into the deploy artefact and is offline-resolvable); the placed
node-target artefact works across container, serverless and SSR-Node
deploy shapes without per-shape loader rework; a missing source,
unhashable file, unwritable destination or checksum mismatch is a hard,
loud failure and never a silent fall-through to a wrong or partial
engine; and the consuming pipeline needs only CPython stdlib, no
Rust toolchain, mirroring §18A.4's "causl-wasm-ts ships no
Rust source."
Both scripts fail loud on the §18A.12 sync seam, so a
build cannot quietly ship an artefact the synchronous constructor cannot
use. They assert that the new WebAssembly.Instance seam is
present (the sync path of §18A.12), that the compute-imports snippet was
placed next to the .wasm / _bg.js, and that the
artefact is node-loadable where it claims to be. Any of these
failing is a non-zero exit with a clear message, never a silent pass.
Only one bridge ships now. The node-loadability check used to encode a distinction between two bridges:
gc-classicloads as--target nodejs, whilegc-builtinsemittedrequire("wasm:js-string"), which stock Node cannot resolve, sogc-builtinswas asserted bundler-target only.gc-builtinswas subsequently deleted (causl-core-rs#355).gc-classicis the only bridge that ships. The ids aregc-classicandgc-builtins, neverwasmgc-classicorwasmgc-builtins.
The read()-reference-identity migration (§18A.5)
This is the one breaking change the wasm path introduces, and the one
thing an adopter must audit for before upgrading to a
real-Rust build. The TypeScript engine caches values in its cells and
returns the same JS reference per read() within a commit
window. Per the §15.1 amendment (#1124), reference identity
across commits is not contractually guaranteed;
value identity at a fixed GraphTime
is preserved by both backends.
Be precise about what the wasm engine does, because the obvious mental
model is wrong in a way that makes the bug harder to find. It is
not uniformly fresh. A ValueHandleCache
retains the original adopter reference, so read() identity is
stable on a cache hit and fresh only on the decode paths. The
break is therefore intermittent, and an intermittent
identity break is considerably harder to catch than a uniform one: a test
that reads twice in a row may well pass.
The fix is to key memoisation on commit.time (the
GraphTime on the published Commit,
byte-identical under both engines) or on the per-node version counter from
EngineTelemetry (the read_derived_version
extern, §18A.3), never on the read() return reference.
§18A.5's pre-migration checklist requires auditing every
React.memo(...) and
useMemo(() => transform(value), [value]) whose key is the
read() return itself. A property guard
(read-no-identity-contract.property.test.ts) wraps a backend
in a deep-clone-on-read decorator and stays green to prove no
engine-internal code relies on identity: the honest pre-disclosure of a
silent break, surfaced before the swap rather than after.
FFI single-tick atomicity (§18A.6)
Theorem 2 (glitch-freedom, "no intermediate time") is proven on the
TypeScript engine by single-threaded JS event-loop evaluation. That proof
does not survive an FFI boundary without additional machinery. The §3
Amendment (#1333) pins the contract: the marshal of a Commit
envelope across the host boundary MUST be atomic with respect to
JS-observable scheduling. No microtask,
requestAnimationFrame, MessageChannel callback,
or await-able continuation may run between engine-side
Phase E (the Commit sealed in engine memory) and host-side
Phase G (subscriber callbacks fired in the TS host).
The wasm bridge enforces this by construction: the FFI commit entry point
(apply_commands / commit_batch) is a
synchronous #[wasm_bindgen] extern, not
async. The async surface you await is loading the
engine (await preloadCauslWasm()), not committing
through it. Calling await apply_commands(...) from the TS
host is syntactically permitted, breaks the uninterruptibility contract,
and violates Theorem 2 silently, so do not await a
commit. graph.commit(intent, tx => …) is and stays
synchronous on both engines.
The 5-criterion GO/NO-GO gate, met 2026-06-21 (§18A.7)
The wasm engine became the default for the WASM path
(route through WASM first) when, and only when, all five
criteria below were met. On 2026-06-21 the named, dated
promotion
(causl-core-rs#169)
recorded all five criteria GO. This was a governance
decision by the authority that opened §13.8, not a CI auto-flip. The
mechanical flip is
DEFAULT_WASM_ENGINE_MODE: 'js-ssot' → 'rust-ssot', and
createCausl() / createCauslWasm() now route
commit through the Rust engine by default for the WASM
path.
| # | Criterion | Status |
|---|---|---|
| 1 | Correctness, cross-backend byte-identity. The TypeScript and wasm engines produce byte-identical commits, snapshots, structural-query results, and subscriber fire-order. (The §18A.1.1 gate.) | GO: 0-byte divergence over 1000+ trials (100k nightly), and 5,000 differential trials per pull request today. |
| 2 | Completeness, full FFI surface. Every §12.1 canonical-seven and §12.2 second-tier row is reachable over FFI with no JS-engine fallback. (The §18A.3 completeness fixture.) | GO: the §18A.3 FFI structural lift landed (causl-core-rs#170); every adopter operation resolves from Rust. |
| 3 | Performance floor, acceptable marshalling tax. Representative workloads stay under the named ceiling: single commit ≤ 250 µs p95 marshal overhead, batch or large mutation ≤ 5 ms p95. Gates "stays responsive" within the §14 RAIL budget, not "beats TS." | GO / RESOLVED (2026-06-19), and now known to have been the wrong bar to worry about. Marshalling is not the binding constraint: see wasm performance. |
| 4 | Node target plus real-world adoption. The
--target nodejs artefact and ESM/node:fs
loader ship (§18A.2), and at least one adopter has shipped
a production workload on the real Rust engine. |
GO: Node target shipped (epic #680);
iasbuilt/xldatagrid is the named adopter. |
| 5 | TS-engine floor maintained.
engine: 'js-ssot' stays supported and byte-identical:
no §12 row removed, no perf regression the TypeScript engine could
not also accept. |
GO, with the floor now located in
causl-core-ts, which still defaults to
js-ssot and hosts the differential oracle. The floor is
no longer reachable from inside causl-wasm-ts. |
Reversibility and the governance revert (§18A.8)
The 2026-06-21 fail-safe-removal amendment (§18A.8, causl-core-rs#169)
removed the per-flush TS-vs-Rust byte-compare oracle and
the V2.5/#1544 K=1 sticky-downgrade fail-safe. Under
engine: 'rust-ssot' the Rust commit_batch
post-state is applied and the Rust-derived Commit returned
unconditionally: no per-flush compare, no rollback, no
in-process demotion to js-ssot. The justification was 0-byte
divergence over 100,000 trials, far beyond the 1000-trial floor. The
CI-blocking cross-backend byte-identity gate (§18A.1.1) is now the
sole conformance guarantee and sole detector: a
divergence is a HALT-before-merge condition, and the revert path is a
governance revert (re-pin the floor), not a runtime
downgrade.
Honest status today
The discipline throughout §18A is, in its own words, "brutal honesty: the costs are named with the rationale, not buried in an adoption guide." So, preserving the SPEC's own "shipped" versus "planned" distinctions:
-
rust-ssot is the unconditional WASM-path default. All
five §18A.7 criteria are GO as of 2026-06-21; the promotion landed
(causl-core-rs#169),
DEFAULT_WASM_ENGINE_MODE = 'rust-ssot', and the per-flush byte-compare oracle plus K=1 sticky-downgrade fail-safe were removed (§18A.8). -
The wasm backend is the real Rust engine. What
causl-wasm-tsloads isengine-rs-corecompiled to WebAssembly, a from-scratch Rust engine and not a TypeScript-engine wrapper. The §18A.3 FFI structural lift landed (causl-core-rs#170), so every adopter operation resolves from Rust. The only JS in the hot path is the user's ownderived()compute lambda, by design. -
The Node target and loader shipped. The
--target nodejsartefact and the consumer loader are real and shipped (epic #680); on Node and SSR the glue is synchronous at require-time. Only the browser bundler target needs the one-timeawait preloadCauslWasm()before the first render. -
causl-wasm-tshas no TypeScript engine at all. §18A.13.1's capability-fallback was withdrawn at 0.5.0 andcreateCauslTsis deleted from source.createCausl()is construct-or-throw. The dual-engine floor and the differential oracle live incausl-core-ts. -
Synchronous construction is shipped. The §18A.12
preload seam (
await preloadCauslWasm()once at init, thencreateCauslWasmSync()with zeroawait) is live in the engine repo and incausl-wasm-ts. Thecommit/readspine stays synchronous. - The marshal tax is real but it is not the ceiling. A raw boundary crossing measures 1.6 ns on the benchmark host and value marshalling costs about 77 ns per read, so the older "78× wire tax" framing that used to sit here overstated the crossing by orders of magnitude and cited a serde bridge artefact that no longer ships. The costs that actually dominate are a per-commit clone that scales with graph size, a per-recompute constant, and a per-commit constant. The numbers, with their confidence caveats, are on wasm performance.
-
Two functions share the name
detectBridge(), and only one of them probes anything. The/wasmsubpath version is real (#426wired it to a floor module, and it throws on a failing host). The version re-exported from the main barrel is still a placeholder whose primitives throw "pending #692" and which reportsgc: falseeven where the engine runs, so the default import is the stub. Onlygc-classicships;gc-builtinswas deleted (causl-core-rs#355). The host floor is Safari 18.2, Chromium 119, Firefox 120 and Node 22, governed by typed function references rather than by WasmGC heap types. Nobody has run the artefact on a real boundary host, so treat those versions as engine release-note claims rather than as our own measurement.
In short: the Rust engine reached through causl-wasm-ts is
the production engine, rust-ssot by default, with the byte-compare oracle
and sticky-downgrade removed and the node loader plus FFI lift landed. The
TypeScript engine is still the unconditional floor and the differential
oracle, but it lives in causl-core-ts and no longer inside
the client. Construction is synchronous behind a one-time preload
(§18A.12, shipped).
Related reading
- Integrating causl-wasm-ts: the consumer-side guide, covering the Gitea-registry install, the preload plus synchronous construction pattern, the loader and host-tier matrix, and the two failure codes.
- Wasm performance: what the wasm path costs, measured, and which of the older claims the measurements refute.
- Enterprise overview: the tier, the private registry, and the install hazard around
@causl/core. - API reference: the TypeDoc-generated reference. It is built from an older package layout and lags the 2026-07-27 rename.
- SPEC (bundled copy): the governing two-engine contract this page summarises, as a dated 3,280-line snapshot. The current copy lives in
causl-wasm-ts.