OptionalengineMode: "rust-ssot"Optionalmigratecausl/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.
Static ReadonlyCMD_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.
Static ReadonlyDERIVED_The generation deriveds are minted at (distinct from inputs' gen 0).
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).
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.
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).
The GraphTime the snapshot was captured at. Marshals as
a BigInt: the extern's parameter is a Rust u64.
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).
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.
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.
§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.
#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
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.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.
§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.
§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.
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.
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.
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.
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.
The adopter's graph name (the IR foreign key on every node / commit / event), supplied by the facade.
Optionalopts: ExportModelOptionsCaller-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).
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).
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).
Whether a NodeId is registered (input or derived).
commitLog id is REFUSEDThis 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.
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.
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.
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.
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.
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.
#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.
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:
K(K+1)/2 + 2K lambda invocations, and each pass is another Θ(K)
FFI crossings.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.
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.
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.
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).
#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.
#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.
#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.
#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.
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.
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.
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.
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.
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.
Optionaltag: "live" | "commit-metadata"Optionalkey: (value: unknown) => unknownOptionalbaseline: BaselineValueThe registration value the caller already computed, or
undefined to have this method evaluate the derivation itself.
Register an input id so reads resolve before the first commit.
undefined seed is a SEED, not the absence of oneThis 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:
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.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).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.
Optionalinitial: unknown#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.
#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).
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):
#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;#lastPhaseDTrace diagnostic is NOT overwritten.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.
#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:
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;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.
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.
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.
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.
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, …).
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.
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.
#279 slice S3 — the §12.2 EngineTelemetry record, composed from this engine's counters plus the facade's contribution.
FacadeTelemetryContribution, passed through.
The seven-counter Rust wire (STATS_IDX_INPUTS … STATS_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.transientSubscribers — resolve_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).
The authoritative wasm engine. One instance per WasmBackend; owns the slot registry, derived fn-table, observer registry, and the clock.