The authoritative wasm engine. One instance per WasmBackend; owns the slot registry, derived fn-table, observer registry, and the clock.

Constructors

  • Parameters

    • bridge: AuthoritativeBridge
    • imports: ComputeImportsModule
    • OptionalengineMode: "rust-ssot"

    Returns AuthoritativeWasmEngine

Properties

migrateFrom?: (snap: GraphSnapshot) => void

causl/causl-wasm-ts#401: the InjectedBackend.migrateFrom slot: the MIGRATION BOUNDARY the structural facade delegates to once its own four gates have passed, so the published ./internal _migrateFrom lands on the engine that runs the graph instead of on a TypeScript map nothing reads.

A property rather than a method because the owning WasmBackend installs it: the route (the engine's non-publishing adoption vs the publishing hydrate fallback on an artefact predating __adoptSnapshot) and the one-time warning that names the fallback are both per BACKEND, and this engine is what the constructor is handed. Left undefined on an engine nobody installed one on, in which case the facade keeps its own closure.

CMD_OBSERVER_PREFIX: "cmd-observer-" = 'cmd-observer-'

causl/causl-wasm-ts#380 — the prefix the engine mints a cmd-buf subscriber's ObserverId with, mirroring apply_commands::synthesise_observer_id's format!("cmd-observer-{callback_id}").

A literal on both sides of an FFI is a drift risk. It is a wire contract rather than a guess (apply_commands.rs's own doc designates the wasm-side dispatch as the inverse of the synthesis: "the wasm-side companion dispatch path will read the same u32 out of the FFI adapter's table to look up the matching JS callback"), but nothing hashes it: check-engine-parity.sh pins this repository's sources, not Rust. So a rename upstream would make #observerForRecord return undefined for every record and every observer would stop firing. That fails loudly in the suite (the whole subscribe surface goes red) and is counted through the #139 debug channel, but it names nothing. A cell asserting the observed id shape from a real fire would make the drift self-describing; it is not written yet and is the follow-up this paragraph exists to carry.

DERIVED_GEN: 1

The generation deriveds are minted at (distinct from inputs' gen 0).

Accessors

  • get engineMode(): undefined | "rust-ssot"

    promote-read (causl/causl-core-rs#169) — this engine's canonicality mode. The graph.ts read-side structural facade reads this to decide whether to route dependencies / dependents / commitLog / stats through THIS engine's Rust externs ('rust-ssot') or stay on the TS closure (absent discriminant, byte-identical).

    Returns undefined | "rust-ssot"

  • get now(): number

    Returns number

Methods

  • causl/causl-wasm-ts#399 / causl/causl-core-rs#353 — the MIGRATION BOUNDARY: take writes at snapTime WITHOUT publishing a commit.

    This is the operation hydrate is not. hydrate applies a snapshot as one 'hydrate'-labelled COMMIT: one clock tick, one commit-log row, Phase G/H firing. The boundary between two engines is not a commit — SPEC §3 monotonicity holds because the target is fresh, so now is SET to snapTime rather than advanced past it — and the engine makes that distinction rather than a wrapper subtracting the tick afterwards or snapshotting around the hydrate to restore the clock. Both of those are TypeScript reimplementing a decision about engine state the engine owns.

    The cmd-buf is the one commit encodes, byte for byte: the engine accepts ONLY BeginCommit / SetInput / EndCommit here and refuses anything else as a perfect no-op. The BeginCommit body is decoded and discarded on the adopt path (there is no commit record to carry an intent or an originatedAt breadcrumb), so it rides with the same intent string a hydrate's would and no breadcrumb.

    Nothing comes back. A Committed diff record is what positions NodeChanged rows, and emitting rows with no frame would invite a decoder to read a migration as a commit — which is the confusion this operation exists to remove. Subscriber firing does not ride the diff buffer either (the engine calls __causl_fire directly), and the boundary fires nothing, so there is no changed-set to publish and no Commit to return.

    Identical to commit's, and for the identical reason: the staged values MUST be in #valueCache BEFORE the FFI call, because Phase-D __causl_compute resolves dep values from the cache DURING it. So atomicity is a ROLLBACK on the throw path, replayed from the same journals — and the engine guarantees its own registry slot is byte-unchanged on EVERY error path, so the two sides stay in step.

    #nextIntentId is NOT consumed: no commit was published, so the next real commit rides the id it would have had. That is the same reasoning simulate uses.

    Parameters

    • writes: ReadonlyMap<string, unknown>

      The snapshot's input values, keyed by NodeId. Ids this engine does not carry as inputs are dropped by the engine, matching the TS reference (a foreign snapshot legitimately names nodes this graph does not have; that is a shape difference, not an error).

    • snapTime: number

      The GraphTime the snapshot was captured at. Marshals as a BigInt: the extern's parameter is a Rust u64.

    Returns void

    if the extern is absent — callers gate on hasAdoptSnapshot; if the engine is past its migration boundary (anything ever committed); CommitInProgressError on a same-engine re-entrant call; and the same typed Phase-D rejections commit raises.

  • causl/causl-wasm-ts#399 — the InjectedBackend.adoptsSnapshotFromRust capability probe. true only under rust-ssot AND with the rebuilt __adoptSnapshot extern present, so WasmBackend.__migrateFrom routes the migration boundary to the engine's non-publishing adoption (otherwise it keeps the publishing hydrate route).

    Returns boolean

  • causl-client#129 — build the graph.simulate reroute closure over the engine-closure hooks. The BODY lives on the wasm subpath (./simulate-facade.js) so the main-bundle createCausl cell stays inside its SPEC §14.2.2 budget; graph.ts supplies only the small context bag and delegates. Returns undefined when this engine cannot own the dry-run (a legacy artefact without the causl/causl-core-rs#320 extern) — the facade then keeps the TS dry-run.

    Parameters

    • ctx: SimulateRerouteContext

    Returns undefined | (intent: string, run: (tx: Tx) => void) => SimulateResult

  • Commit input writes authoritatively through the wasm engine. The wasm-produced diff drives the returned Commit (time, changed-set) and Phase-G fires the registered observers of the changed nodes.

    Parameters

    • intent: string
    • writes: ReadonlyMap<string, unknown>

    Returns Commit

  • §12.1 — the per-engine commit log (intent / time / changed nodes), oldest-first, decoded from the commit_log(engine_id) extern. The facade's commitLog derived publishes the most-recent-first Commit[]; this returns the wire shape and the facade adapts ordering / Commit fields.

    Returns readonly DecodedCommitLogRecord[]

    if the rebuilt structural extern is absent from the bridge.

  • #129 / causl/causl-core-rs#318 — the PUBLISHED adopter-facing commit-log window: frozen readonly Commit[], oldest-first, rows carrying the REAL adopter intent, the commit time, the hydrate-aware originatedAt breadcrumb, and the authoritative changed set (inputs

    • mirrored deriveds). Sourced from the commit_log_meta(engine_id) extern, with the #commitLogWindowCache copy-on-write cache making the per-commit consumer path O(1) amortised (no wire decode). Engine-internal rows (the legacy '__seed' flush; eliminated at source by #59, filtered here as defence-in-depth) never surface. Because setCommitHistoryCap threaded the adopter cap, the window is exactly the rows the TS commitHistory ring would have held — the graph.ts facade publishes it VERBATIM.

    Returns readonly Commit[]

    if the causl/causl-core-rs#318 extern is absent from the bridge (legacy artefact — callers must gate on ownsCommitLog).

  • pre-fire (causl-wasm#189/#190) — whether this engine delivers the commit's changed set BEFORE the first Phase-G observer byte (the __causl_pre_fire crossing; sidecar registry present). When false (legacy artefact) the facade's post-apply_commands union bump remains the fallback — commit-boundary nodeVersion reads stay byte-identical there; only the in-frame corner needs the crossing.

    Returns boolean

  • §12.1 — the depth-1 static dependency adjacency of id (a derived), decoded from the dependencies(engine_id, slot, gen) extern. An input (or an unregistered / disposed id) yields [], matching the facade's dependenciesOf for an input. The result is in the encoder's adjacency order; the facade sorts for its diffable projection.

    Parameters

    • id: string

    Returns readonly string[]

    if the rebuilt structural extern is absent from the bridge.

  • §12.1 — the DEPTH-1 dependents of id (the reverse-edge view the facade dependentsOf returns), structurally derived from the per-engine dep adjacency.

    IMPORTANT (ffi-1 note): the Rust dependents extern is the TRANSITIVE closure, NOT depth-1; routing it here would over-report. There is no depth-1 dependents extern. The depth-1 reverse edge is exactly "every registered derived d such that id ∈ dependencies(d)", so we resolve it from the depth-1 dependencies extern over the engine's live deriveds — every consulted buffer is the Rust-authoritative dep adjacency, so the result is resolved FROM the Rust side (not the TS closure) while matching the facade's depth-1 semantics exactly.

    Parameters

    • id: string

    Returns readonly string[]

  • lift-explain (causl/causl-core-rs#170) — §12.1 the lineage TOPOLOGY of id for one frame of the explain walk: the structural kind (input vs derived, from the per-engine input/derived slot registry) and the depth-1 dep adjacency (from the dependencies extern — [] for an input). Both come from the Rust side, so buildExplanation's tree SHAPE no longer consults the TS #graph entries map under rust-ssot.

    An id the engine does not own (unregistered / disposed out from under the walk) returns undefined, letting the walker emit the same defensive cycle marker the TS closure does (if (!entry) return cycle). The deps are returned lex-sorted (the dependencies extern already sorts), matching the TS walk's Array.from(entry.deps).sort() iteration order.

    Parameters

    • id: string

    Returns undefined | { deps: readonly string[]; kind: "input" | "derived" }

    if the rebuilt dependencies extern is absent (gated away by explainsLineageFromRust on a legacy artefact).

  • lift-explain (causl/causl-core-rs#170) — whether this engine resolves the explain lineage TOPOLOGY (node kind + depth-1 deps) from the Rust structural surface. true only when engineMode === 'rust-ssot' AND the rebuilt dependencies extern is present (the same extern explainNode decodes). The facade gates the buildExplanation topology reroute on this so a legacy artefact keeps the whole walk on the TS closure.

    Returns boolean

  • del-final (causl/causl-core-rs#170) — whether this engine resolves the explain per-node TIMESTAMPS (computedAt / contributedAt) and the via tag from the Rust node_meta extern. true only when engineMode === 'rust-ssot' AND the rebuilt node_meta extern is present. The facade gates the buildExplanation timestamp/via reroute on this so a legacy artefact (no node_meta) keeps them on the TS entries map. This is the LAST TS-closure read explain carried — with it true, no buildExplanation field reads the TS entries under rust-ssot.

    Returns boolean

  • lift-export (causl/causl-core-rs#170) — §18A.3 the whole CauslModel IR, decoded from the export_model(engine_id) extern. RUST-AUTHORITATIVE under rust-ssot: the nodes (+ values + dep adjacency), commit-log (+ the hydrate-aware originatedAt), time, and the IRSubscribe event stream all come from Rust — there is NO wrapped TS #graph consult.

    The graphId is supplied by the caller (the graph.ts facade's closure graphId, sourced from the adopter's createCausl({ name })) — a marshaling-layer concern (the adopter's graph name), NOT engine state. It stamps every node / commit / event and synthesises the single default scopes entry, byte-identical to the TS exportModel.

    The node VALUE + the serializable verdict are the THIN marshaling shim (JSON.stringify round-trip) the §18A.3 lift sanctions: it runs over the ENGINE's own value surface (read — the marshaling value cache holding the original adopter reference, or the decoded Rust value), NOT the #graph. A non-serialisable input (function / symbol) was marshaled to Rust as a NULL value record, but the original reference survives in the value cache — so the shim's verdict is false and the value null, byte-identical to the TS serialiseSafely / isSerializable pair.

    Parameters

    • graphId: string

      The adopter's graph name (the IR foreign key on every node / commit / event), supplied by the facade.

    • Optionalopts: ExportModelOptions

      Caller-supplied ExportModelOptions; maxCommits (default 100) bounds the exported commit-log window byte-identically to the TS floor's commitHistory.slice(-maxCommits). captureCallGraph is accepted but — like the TS floor — not yet emitted (IRCommit.callGraph is reserved).

    Returns CauslModel

    if the rebuilt export_model extern is absent from the bridge.

  • lift-export (#170) — the InjectedBackend.exportsModelFromRust capability probe. true only under rust-ssot AND with the rebuilt export_model extern present, so the graph.ts facade reroutes exportModel to the Rust deep export (otherwise it keeps it on the TS closure).

    Returns boolean

  • lift-subscribecommits (#170) — the InjectedBackend.firesCommitsFromRust capability probe. true only under rust-ssot AND with the rebuilt commit-handler registry present, so the graph.ts facade reroutes subscribeCommits to the Rust Phase-H channel (otherwise it keeps it on the TS closure).

    Returns boolean

  • Whether a NodeId is registered (input or derived).

    This is the injectedBackend.has(...) the graph.ts read / subscribe facade dispatches on (src/graph.ts:9585), and answering true for ENGINE_OWNED_COMMIT_LOG_ID is answering for a node this engine has no value for. The slot exists only because registerDerived mints an input slot for any unrecognised dep, and a plain derived(id, get => get(graph.commitLog)) declares exactly that dep — so a single such registration used to flip this predicate and redirect the adopter's graph.read(graph.commitLog) at an unmaterialised Rust cell, which decodes as null, permanently.

    MEASURED on wasmgc-classic, commitHistoryCap: 1000, one input and one commit: read(commitLog) is [] before the derived registers and null immediately after, and stays null across every later commit while stats().retainedCommits reports 1 — i.e. the log was there the whole time and the read was pointed away from it. The pure-TS floor returns the bounded window at every one of those points.

    Refusing the id here restores the documented fallback verbatim: the facade resolves it through the TS closure's readEntryFromResolved, whose COMMIT_LOG_ID arm rebuilds from buildCommitLogValue() — which is itself engine-sourced (engineCommitLogWindow()) under the #129 commit-SSOT gate and the TS ring otherwise. So the answer stays authoritative under BOTH shapes, and neither shape has to be decided here.

    This refusal names ONE string, and the defect it describes is general: registerDerived mints a phantom INPUT slot for ANY unrecognised dep, and derived() deliberately does not mirror a commit-metadata node either, so commitMetadataDerived('cm', …) named by any ordinary derived reproduced the whole paragraph above verbatim — read(cm) null for the rest of the graph's life, subscribe(cm) one fire carrying null, dependencies(cm) and dependents(a) empty.

    The general predicate now lives on the FACADE, where the ownership fact does (tsOwnedDerived in src/graph.ts, folded into rustOwns and applied at the read / subscribe / dependenciesOf / dependentsOf seams). This literal is kept as defence in depth for the id the engine itself has an opinion about — it is the one node the ENGINE owns the storage for while the TS closure owns the entry — and not as the mechanism that closes the class.

    Parameters

    • id: string

    Returns boolean

  • causl/causl-wasm-ts#399 / causl/causl-core-rs#353 — whether the rebuilt __adoptSnapshot extern is present on this bridge, i.e. whether this engine can take a snapshot's writes at the snapshot's time WITHOUT publishing a commit. Callers must gate adoptSnapshot on this; a legacy artefact keeps the hydrate route, which publishes.

    Returns boolean

  • lift-subscribecommits (#170) — whether the rebuilt sidecar carries the commit-level handler registry (__causl_set_commit_handler). The facade gates the rust-ssot subscribeCommits reroute on this so a legacy artefact (no commit channel) falls back to the wrapped TS #graph.

    Returns boolean

  • lift-export (#170) — whether the rebuilt export_model extern is present on this bridge. The facade gates the rust-ssot exportModel reroute on this so a legacy artefact (no §18A.3 deep-export extern) falls back to the wrapped TS #graph rather than throwing.

    Returns boolean

  • lift-readat (#170) — whether the rebuilt read_at_result extern is present on this bridge. The facade gates the rust-ssot readAt / snapshotAt reroute on this so a legacy artefact (no §12.2 discriminated extern) falls back to the wrapped TS #graph rather than throwing.

    Returns boolean

  • causl-client#129 / causl/causl-core-rs#320 — whether the engine-owned dry-run entry point is available on this artefact. The graph.simulate facade gates its reroute on this (plus its own fully-mirrored dynamic guard); a legacy artefact keeps the TS dry-run.

    Returns boolean

  • #78 (causl-client) — the InjectedBackend.hydrate op: apply a snapshot's input writes as ONE 'hydrate'-labelled commit through the SAME commit body (cmd-buf → apply_commands → diff) — exactly one clock tick, Phase-D recompute, Rust commit-log row, retention row, Phase G/H firing, and the same #178 commit-window rejection surface (typed CommitInProgressError; a throw means the engine applied NOTHING and the mirror journal rolled back, §5.2).

    originatedAt is the snapshot envelope's recorded time. On a causl/causl-core-rs#318-era artefact the Rust pipeline PERSISTS it (#129): commit rides it in the widened 16-byte cmd-buf BeginCommit body, so the commit_log_meta / export_model wires return it and every backend-sourced mouth (#onCommit Phase-H delivery, exportModel, commitLogWindow) publishes it straight off the wire — byte-identical to the TS reference, which publishes originatedAt: snap.time on hydrate-issued records. On a LEGACY artefact (no commit_log_meta extern, so the widened body would be rejected) this backend records the accepted commit's time → originatedAt in #hydrateOriginatedAt and the mouths stamp it from that map, exactly as before #129. The transient #pendingHydrateOriginatedAt covers the in-flight Phase-H crossing (it fires DURING apply_commands, before the map entry exists). One map entry per accepted hydrate — hydrates are boot/persistence-restore events, so growth is negligible.

    Parameters

    • writes: ReadonlyMap<string, unknown>
    • originatedAt: number

    Returns Commit

  • R1 (causl/causl-wasm-ts#59) — run the non-committing Phase-D materialise pass. Emits a Materialize cmd-buf op; the bridge runs recompute_affected over the eagerly-seeded graph, computing every derived's wasm cell value via the JS compute callback WITHOUT a commit (no clock tick / commit-log row / Phase-G fire). Called by the facade lazily, at the first read/subscribe/exportModel/commit that FOLLOWS a registration, so the deriveds + their wasm cells are materialised before any structural read (exportModel/dependencies) consults them. Replaces the old deferred __seed commit flush.

    causl/causl-wasm-ts#326 — the pass RE-PUBLISHES rather than RE-COMPUTES. The Rust Materialize op is graph-WIDE: it walks every derived reachable from a seeded input, not just the not-yet-materialised frontier (measured: registering one derived over input b re-invokes every derived hanging off the unrelated input a). Driving the user's compute lambdas from that walk is wrong twice over:

    • COST. It makes registration QUADRATIC if the pass runs per registration. K deriveds fanned out over one input cost K(K+1)/2 + 2K lambda invocations, and each pass is another Θ(K) FFI crossings.
    • CORRECTNESS. It re-mints an ALREADY-BASELINED node's last_value from whatever its container holds NOW. An adopter that mutates in place and then registers one more derived has that mutation quietly absorbed by the walk, and the next commit reports the node unchanged — the exact divergence causl/causl-wasm-ts#326 exists to close, re-opened by the pass meant to close it.

    So while the pass runs, #onCompute answers from #lastValueRecord: the snapshot frozen when the value was last genuinely produced (at registration, or by the last commit that recomputed it). The engine ends the pass holding exactly the values the host has published — no lambda runs, no baseline moves, and the cost is one walk per flush seam instead of one per registration.

    A node with no record (its eager registration compute threw) falls through to a real compute, which is the only answer available for it.

    Returns void

  • del-final (causl/causl-core-rs#170) — the per-node EXPLAIN metadata of id for one explain frame: the structural kind, the computedAt / contributedAt timestamps (from the Rust node_meta extern — the input lastWriteTime / the derived lastTime), and the via tag (from this backend's registration-time registerDerived record, which the Rust core does not retain). Composes the Rust-authoritative timestamps with the devtools tag so buildExplanation reads NO field off the TS entries map.

    An id the engine no longer owns resolves to kind: 'missing' (both timestamps 0), letting the walker skip it exactly as the TS closure's if (!childEntry) continue does.

    encode_node_meta stamps DerivedCell::last_time, and RegisterDerived never sets it: the engine registers fn + deps and does not compute the derivation until a later Phase D. So a derived whose most recent computation was its REGISTRATION decodes computed_at: 0. Measured on the shipped artefact, two commits deep, d0 = derived('d0', get => get(a) * 2) registered at now = 2: the extern answers 0 and the §12.1 canonical answer is 2.

    The engine is not wrong about it. It has not computed the node, and it was never told when this backend did: Cmd::RegisterDerived carries the baseline VALUE (causl/causl-wasm-ts#374) and no time. So the floor belongs to whoever holds the missing fact, which is this class — the same rule slice S3 applied to transientSubscribers and causl/causl-wasm-ts#380 to the pair count. Before S4 the floor lived in src/graph.ts as Math.max(rustMeta.computedAt, derivedEntry.lastTime), a max of an engine read against the TS entries mirror taken at the point of USE, which is why the gate's own doc could claim explain reads no mirror field while two of its stamps did.

    contributedAt is floored with it. The wire carries the two IDENTICAL by construction (encode_node_meta writes time twice) and the walk reads the same registration stamp for both, so one floor serves both fields.

    INPUTS are deliberately not floored: State::seed_input_inplace stamps last_write_time = self.now at registration, so the engine already carries the number the mirror carries, and a floor there would be a mask rather than a repair.

    RECORDED UPSTREAM, not filed: State::registered_at already holds this exact stamp engine-side (seed_input_inplace and the derived registration path both insert it, gated on now != 0), and encode_node_meta simply does not consult it. Flooring there would delete this map. It changes the bytes of a shipped extern that IS read, so it belongs to a scheduled engine revision alongside the four already recorded against causl/causl-core-rs (S2's foreign-dep wire slot, S6's evicted-record envelope time, and S3's two); #279 does not own it and this slice does no Rust.

    Parameters

    • id: string

    Returns DecodedNodeMeta & { tag: undefined | "live" | "commit-metadata" }

    if the rebuilt node_meta extern is absent (gated away by explainsTimestampsFromRust on a legacy artefact).

  • gap-5 (causl-wasm#169; causl-client#102) — register the facade's observer-error sink. Called once at graph construction; the observer fires once per surfaced Phase-G ObserverError, delivered by the Rust __causl_on_observer_error crossing, with the reconstructed (error, ctx) the facade re-fans to the adopter's onObserverError. Single slot — the facade is the only consumer.

    Parameters

    Returns void

  • pre-fire (causl-wasm#189/#190) — register the facade's pre-fire changed-set observer (the #71 nodeVersion union-bump hook). Called once at graph construction; the observer fires once per accepted commit, inside apply_commands, strictly before any Phase-G/H observer byte, with the commit's authoritative changed-set NodeIds (engine-internal commits filtered, matching the Phase-H delivery discipline).

    Parameters

    • observer: (changedNodes: readonly string[]) => void

    Returns void

  • #129 — true when the ENGINE is the single source of truth for the adopter-facing commitLog window: rust-ssot mode, a causl/causl-core-rs#318-era artefact (the commit_log_meta extern + persisted originated_at), AND the adopter's commitHistoryCap already threaded onto the engine horizon via setCommitHistoryCap. The graph.ts facade gates the TS Phase-F ring-append deletion and the commitLog re-source on this, so a legacy artefact keeps the TS ring byte-identically.

    Returns boolean

  • #252 — true when the ENGINE is the single source of truth for the readAt / snapshotAt retention window: rust-ssot mode, the §12.2 read_at_result extern present, AND the adopter's effective window already threaded (with the derived-row opt-in) via setSnapshotRetentionCap. The graph.ts facade gates the TS Phase-F.6 delta-build deletion and the derived readAt reroute on this, so a legacy artefact keeps the TS floor byte-identically.

    Returns boolean

  • #279 slice S2 — true when the ENGINE builds the present-time §12.2 GraphSnapshot envelope, so the facade stops walking its own entries map for it.

    The gate is rust-ssot and nothing else, and the absence of a capability probe beside it is the point: snapshot composes over read, whose read_cell_value extern is one of the two MANDATORY bridge members, and over now. There is no artefact this engine can be constructed against on which the composition is unavailable, so a probe would be a conjunct that is true whenever the constructor returned.

    Sibling of ownsRetention with one fewer conjunct, and deliberately so. The historical envelope needs the retention window threaded because its DISCRIMINANT (retained vs evicted) is a fact about that window; the present-time envelope has no discriminant and no window — it is the input set at now.

    Returns boolean

  • #279 slice S3 — true when this engine composes the whole §12.2 EngineTelemetry record, so the facade stops reading the seven-counter wire itself and stops holding the merge rule.

    Sibling of ownsSnapshot, with the capability conjunct that one does not need: stats is an OPTIONAL extern (twelve of the fourteen bridge members are), and a legacy artefact without it keeps the facade's own closure counters byte-identically rather than throwing out of a telemetry read.

    Returns boolean

  • causl-client#129 (write-SSOT cutover) — whether the ENGINE (+ this backend's #valueCache mirror) owns the committed input values, so the TS closure can stop publishing staged writes onto its outer input cells (the Phase A-C publication deletion / the AC2 retention drop). true only under rust-ssot when EVERY remaining TS-cell consumer has an engine-side (or backend-cache) resolution on this artefact:

    • simulate_commands (cw#320) — the TS dry-run walk reads live cells; the reroute replaces it;
    • read_at_result (cw#170/#198) — historical input reads + snapshotAt resolve from the Rust retention chain;
    • export_model (cw#170) — the TS exportModel fallback reads cells;
    • node_meta (cw#170 del-final) — the structural explain reroute (explainsTimestampsFromRust) is armed, so explain keeps only its #83 TS-mirror STAMP reads (lastWriteTime, preserved by the gated Phase C.5) and no VALUE reads.

    Everything else re-sources from this backend's #valueCache (the write-through mirror that also owns read() reference identity), which the commit path keeps byte-identical to the TS floor's cell.

    Returns boolean

  • Read a node's CURRENT value from wasm at full fidelity (inputs and deriveds). Returns the decoded JS value.

    Serves the cached current value (skipping read_cell_value + the O(rows) JSON.parse) when present; on a miss decodes from wasm and caches the result so the next read is fast.

    Parameters

    • id: string

    Returns unknown

  • lift-readat (causl/causl-core-rs#170) — §12.2 the DISCRIMINATED historical read of id at GraphTime time, decoded from the read_at_result(engine_id, slot, gen, time) extern into the exact RetentionResult shape the TS readAt(node, t) returns.

    The Rust resolver reproduces the TS decision tree — §3 Behavior-domain check (t < registrationTime) → retention-window check → genesis-seed fallback → retained value — so the discriminant (retained value + time, or evicted + oldestRetainedTime) is byte-identical to the pure-TS oracle. An unregistered id surfaces as evicted with oldestRetainedTime: 0 (the slot was never minted; there is no history to resolve), matching a never-written genesis node.

    Parameters

    • id: string
    • time: number

    Returns RetentionResult<unknown>

    if the rebuilt read_at_result extern is absent from the bridge.

  • lift-readat (#170) — the InjectedBackend.readsHistoryFromRust capability probe. true only under rust-ssot AND with the rebuilt read_at_result extern present, so the graph.ts facade reroutes readAt / snapshotAt to the Rust retention chain (otherwise it keeps the reads on the TS closure). This engine is the actual injectedBackend passed to createCausl({ injectedBackend }), so the facade calls this method directly.

    Returns boolean

  • Register a derived node: mint its slot, emit a RegisterDerived cmd-buf record (binding fn_id + dep slots in the engine) and bind the JS compute lambda in the fn-table the __causl_compute import dispatches to.

    deps is the ORDERED dependency list — the engine drives Phase-D in this order and passes dep values to __causl_compute in the same order, so the compute lambda maps each dep value back to its NodeId positionally.

    baseline (causl/causl-wasm-ts#374) is the value the caller's OWN eager evaluation already produced for this registration. See the parameter's documentation on BaselineValue for why handing it over is the whole fix.

    Parameters

    • id: string
    • deps: readonly string[]
    • compute: DerivedCompute
    • Optionaltag: "live" | "commit-metadata"
    • Optionalkey: (value: unknown) => unknown
    • Optionalbaseline: BaselineValue

      The registration value the caller already computed, or undefined to have this method evaluate the derivation itself.

    Returns void

  • Register an input id so reads resolve before the first commit.

    This used to gate the whole body on initial !== undefined, leaving an input(id, undefined) with a JS-side #slots entry and no Rust cell at all. The facade signature is input<T>(id, initial: T) — the seed is REQUIRED (graph.ts:3916) and the TS floor stores it verbatim as entry.value — so undefined there is the adopter's value, never a "no seed given" signal. Two cross-engine divergences fell out of the gate, both recorded as incidental findings under #261 and pinned by test/value-domain-151.test.ts:

    1. read() answered undefined on the TS floor and null on rust-ssot. With no #valueCache entry every read MISSED and fell through to read_cell_value, which decodes the unmaterialised cell as the NULL record. The value domain's undefined ≡ null collapse governs the CUTOFF/FIRE verdict only (src/value-domain.ts inputValueChanged) — it is NOT licence to hand the adopter back a value they never wrote, and SPEC §18A.5 preserves VALUE identity at a fixed GraphTime across both backends (only REFERENCE identity is off-contract). §18A.7 Criterion 5 makes the TS floor the side that wins a disagreement.
    2. subscribe() on such an input THREW Engine(NodeDisposed { slot: N }) once any derived had registered a dependency on it. The Rust liveness gate (state.rs is_disposed_slot) reads a slot as disposed when a resident cell has bumped PAST the queried generation; inputs and deriveds share the slot-index space, so derived slot N (gen DERIVED_GEN = 1) tombstoned the phantom input at (N, gen 0) — the input table had no cell to prove it live. Materialising the cell restores the generation match that gate is written around.

    Materialising the undefined seed is also what makes this backend mirror the floor's node set: the floor has no notion of a registered-but-absent input, so neither can an input the facade registered.

    Registering and seeding are two different events, and undefined is a perfectly good value to seed WITH, so initial !== undefined cannot tell them apart — which is the whole bug. The discriminator is therefore the ARGUMENT COUNT:

    • registerInput(id) — register only. The slot is minted host-side and materialised by the first commit; nothing is cached, so read() decodes the engine's default-null cell. The white-box mock-bridge tests take this arm deliberately.
    • registerInput(id, v) — seed with v, INCLUDING v === undefined. graph.ts:4017 (the only production caller) always passes two arguments because the facade's initial is required, so every adopter-registered input takes this arm.

    #306 (panel review) — the rule used to be implemented HERE and nowhere else, which made it a convention rather than a contract. Two of the three halves of that are closed:

    • js-fallback-backend.ts, the only other in-tree BackendEngine, honours the same rule, so the two implementations of one declared type no longer answer read() differently for the same call (measured before: registerInput(id) gave null here and undefined there).
    • the discriminator is arguments.length rather than a rest tuple, so the declared .length stays 2. A rest tuple reports 1, which a duck-typed arity probe reads as a different method than the one the interface declares; it also allocated a rest array on the hot seed path.

    What is still OPEN is the declaration: BackendEngine.registerInput (src/types.ts) is one initial?: unknown signature, and TypeScript defines f(id) and f(id, undefined) as the same call for an optional parameter, so the rule cannot be read off the type. The remedy is two overloads; it is deferred because src/types.ts sits inside both the bench measured-source closure and the engine-parity reference leg, so it lands with the baseline re-capture rather than ahead of it. Pinned by test/authoritative-publish-conformance-356.test.ts cell (a3), which goes RED the day it lands. Cells (a1)/(a2) gate the two halves that are closed.

    Parameters

    • id: string
    • Optionalinitial: unknown

    Returns void

  • #129 / causl/causl-core-rs#318 — thread the adopter's commitHistoryCap onto the engine's commit-log eviction horizon (State::commit_log_cap) via the SetCommitLogCap cmd-buf op, replacing the engine's hardcoded Some(1024) default. cap = 0 disables engine-side commit-log retention entirely (matching the facade's cap-0 "no observable history" contract, and freeing the default-cap ring's memory).

    Called by the graph.ts facade ONCE at construction, with the SAME resolved cap that gates Phases F/F.4/F.6, so the engine ring and the adopter-facing commitLog window share ONE horizon. No-op on a legacy artefact (the op would be rejected by the old decoder); the facade then keeps the TS ring because ownsCommitLog stays false.

    Parameters

    • cap: number

    Returns void

  • #252 / causl/causl-core-rs#321/#323 — thread the adopter's EFFECTIVE retention window (commitHistoryCap > 0 ? snapshotRetentionCap : 0, resolved by the facade) onto the engine's retention-chain eviction horizon (State::commit_history_cap, the SetSnapshotRetentionCap cmd-buf op), and opt in to derived-row retention (RetainDerivedRows) in the SAME atomic batch, replacing the engine's hardcoded 1024 default. Called by the graph.ts facade ONCE at construction, with the SAME resolved window that gates the TS floor's Phase F.6, so the engine chain and the adopter-facing readAt / snapshotAt window provably share ONE horizon for inputs AND deriveds.

    Unlike setCommitHistoryCap (whose causl/causl-core-rs#318 artefact co-shipped the commit_log_meta extern probe) the causl/causl-core-rs#321/#323 ops shipped with NO new extern, so artefact support is probed by APPLYING the batch: a legacy decoder rejects op 14/15 as UnknownOp and the whole batch rolls back atomically — the catch arm leaves ownsRetention disarmed and the facade keeps the TS floor's retention byte-identically (fail-safe).

    Parameters

    • cap: number

    Returns void

  • causl-client#129 (write-SSOT cutover) — engine-owned DRY-RUN of the commit the same writes map would produce, via the causl/causl-core-rs#320 simulate_commands extern. Encodes the SAME commit-window cmd-buf commit encodes (same staging: undefined collapse, container change-token minting, #valueCache write-through so the Phase-D __causl_compute crossings resolve staged dep values cache-first exactly as on the live commit), drives the engine's cloned dry-run pipeline, and returns the decoded prediction.

    OBSERVER-INVISIBLE BY CONSTRUCTION, on every exit path (success, predicted rejection, wiring error):

    • the ENGINE's registry slot is byte-untouched (cw#320: the clone is discarded; no clock tick, no commit-log row, no retention, zero Phase-G/H crossings);
    • the JS MIRROR rolls back unconditionally: every #cacheSet this call performed (the staging loop AND the dry-run compute crossings) is replayed from the journal, every container-epoch bump is restored, and any dynamic-dep rewires the dry-run compute discovered are DISCARDED (the TS floor's simulate rolls back deps the same way);
    • #nextIntentId is NOT consumed: the dry-run rides the id the NEXT real commit will use, predicting exactly that commit;
    • the #lastPhaseDTrace diagnostic is NOT overwritten.

    Parameters

    • intent: string
    • writes: ReadonlyMap<string, unknown>

    Returns { changedNodes: readonly string[]; time: number }

    the predicted commit time and changed (slot, gen)-mapped NodeIds, in DIFF order (engine Phase-B/D emission order — the same order commit would publish for the identical writes).

    the SAME typed errors the live commit path throws for the same rejection (CommitInProgressError, CycleError, the host-identity DerivedComputeError, WasmInstancePoisonedError) — the facade surfaces them on the SimulateResult failed arm.

  • causl-client#129 — the InjectedBackend.simulatesDryRunFromRust capability probe. true only under rust-ssot AND with the rebuilt simulate_commands extern present; the graph.ts facade gates the graph.simulate reroute on this (plus its fully-mirrored dynamic guard), keeping the TS dry-run for legacy artefacts.

    Returns boolean

  • #279 slice S2 — §12.2 the PRESENT-time GraphSnapshot: the engine's registered input set at now, byte-identical to the envelope the structural closure builds from its entries map.

    This is the composition snapshotAt already performs at a historical time, with read in place of read_at_result and now in place of the decoded row time. It needs no extern of its own: every ingredient is state this class already holds.

    Two things it does NOT do, both of which it would get wrong for free:

    • it does not walk isInput. registerDerived mints a phantom input slot for an unrecognised dep, so an isInput walk carries a commitMetadataDerived into the envelope with the null its unwritten cell decodes to, and the floor's envelope does not carry it at all. The walk is over SlotEntry.registeredInput, which is the registration fact rather than the slot table's read-table selector;
    • it does not derive schemaHash. That digest covers the FACADE's registered id-set, which is not this engine's: the closure's entries map carries the engine-owned __causl_commit_log__ derived (which has() refuses outright here) and calls a commit-metadata derived a derived where the slot table calls it an input. Measured on one graph, the facade's token set digests to 8e313ae4 and the slot table's to f4094507. So the digest arrives as a callback, exactly as snapshotAt takes it, and the facade stays its single source.

    The isSerializable gate is imported from src/value-domain.ts rather than reimplemented, because "byte-identical to the closure's envelope" is not a property two copies of a membership test can hold.

    Parameters

    • computeSchemaHash: () => string = ...

      The facade's digest over its registered id-set. Defaults to the empty string for the same reason snapshotAt's parameter does: a caller that is not the facade has no id-set to digest.

    • time: number = ...

      The clock to stamp the envelope with, defaulting to this engine's own. The facade supplies its own answer instead, because across the #77 dispatch window (the Phase-G/H fan runs INSIDE apply_commands, before this engine's #now is promoted) the facade's clock is one tick ahead, and the envelope's time is an adopter-facing clock read like any other. The rollback mirror measures it: a probe reading from inside a Phase-G observer wants 1 where #now still says 0.

    Returns GraphSnapshot

    The envelope, keyed by schema, time, inputs, schemaHash.

  • lift-readat (causl/causl-core-rs#170) — §12.2 the historical GraphSnapshot projection at GraphTime time, as a RetentionResult. The retention discriminant (retained vs evicted + oldestRetainedTime) is resolved FROM the Rust retention window — byte-identical to the TS snapshotAt(t). The retained arm's envelope is materialised by reading every registered input's value at time from the SAME Rust read_at_result extern, filtered to JSON-serialisable values (matching the TS snapshot() / snapshotAt() isSerializable gate). schemaHash is sourced from computeSchemaHash — a structural-identity digest over the registered id-set, NOT a retention concern.

    The window discriminant is node-INDEPENDENT (the TS snapshotAt evicts purely on t < oldest / empty-buffer → now, with no per-node §3 domain check), so it is read from a genesis-registered input probe (registered_at == 0, whose read_at_result evicted arm carries the window front exactly). A graph with no inputs has an empty retention window; the probe-less fallback returns evicted { now }, matching the TS empty-buffer arm.

    Parameters

    • time: number
    • computeSchemaHash: () => string = ...

    Returns RetentionResult<GraphSnapshot>

    if the rebuilt read_at_result extern is absent from the bridge.

    RetainedValueUnavailableError #114 — when a retained CONTAINER input in the window can no longer be recovered host-side (its reference has since changed). The envelope fails LOUD rather than silently omitting the input, symmetric to readAt's #100 arm.

  • §12.2 — the per-engine telemetry counters, decoded from the stats(engine_id) extern. The 7 counters are the subset of EngineTelemetry the Rust core tracks; the facade merges them with the TS-closure-only fields (commit observers, entries map size, …).

    Returns DecodedStats

    if the rebuilt structural extern is absent from the bridge.

  • Register a JS observer keyed by NodeId. On commit the wasm __causl_fire dispatch fires it exactly once when the node changed. Returns an unsubscribe thunk.

    Type Parameters

    • T

    Parameters

    Returns Unsubscribe

  • lift-subscribecommits (causl-wasm#170) — register a commit-LEVEL observer (Phase H). On every commit the Rust __causl_on_commit crossing fires it exactly once with the just-published Commit, AFTER the per-node Phase-G firing. Mirrors the per-node subscribe unsubscribe discipline: the returned thunk removes this observer; a removed observer never fires again. Registration order is preserved (insertion-ordered Set), so multiple commit observers fan out in the order they were added.

    Parameters

    • observer: (commit: Commit) => void

    Returns Unsubscribe

  • #279 slice S3 — the §12.2 EngineTelemetry record, composed from this engine's counters plus the facade's contribution.

    Parameters

    • facade: FacadeTelemetryContribution

      FacadeTelemetryContribution, passed through.

    Returns EngineTelemetry

    The seven-counter Rust wire (STATS_IDX_INPUTSSTATS_IDX_PENDING) answers ONE of the twelve adopter-facing fields outright (inputs) and co-answers four more; nodeVersions and pending are decoded and have no adopter-facing field at all. Everything else is a registry, and each registry belongs to whichever layer implements the affordance that fills it — which is the partition FacadeTelemetryContribution enumerates and this method's per-field notes justify.

    Three of the missing fields look like counters the engine "should" carry, and none of them is:

    • commitObservers — the registry IS a TypeScript Set in this class (#commitObservers, fed by subscribeCommits). The Rust core never sees a commit-level observer; the __causl_on_commit crossing hands it one callback and this class fans it. An extern for this would report a number Rust would have to be told.
    • transientSubscribersresolve_engine_stats hardcodes 0, and that is CORRECT for what it was given. { transient: true } never crosses the cmd-buf, because the facade emulates the iasbuilt/causl#766 one-shot itself over the options-free subscribe seam. A wire field would ask the engine to model a lifecycle it does not run.
    • entries — counts the ADOPTER-VISIBLE registry, which slice S2 measured is provably not the engine's slot table (the two id-sets digest to 8e313ae4 and f4094507).

    So no WIRE_VERSION implication and no additive twin is owed for any of them. What IS owed upstream is recorded in test/stats.test.ts and not fixed here: the stats extern is routed through with_history_state, so a compute-frame stats() throws CommitInProgress where the floor answers, and none of the seven counters it resolves is history.

    if the stats extern is absent (gated away by ownsTelemetry on a legacy artefact).