Two-engine architecture Enterprise
Causl ships two conformant implementations of the same denotational
contract: causl-ts, the pure-TypeScript engine that is the
open-source floor and reference oracle, and causl-wasm, the
Rust→WebAssembly production engine consumed through the thin
causl-client TypeScript API. Both expose the same public
surface and are held byte-identical by a cross-backend determinism gate.
The wasm path is an Enterprise feature: in causl-client it is
the unconditional production engine (rust-ssot default), while
causl-ts is retained as the §12 conformance reference. 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-client.
The reopen fired now — 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-API-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-client the
wasm core is the production engine (rust-ssot default), and the §18A.3
FFI structural lift means every adopter operation resolves from Rust.
causl-ts: the floor and the reference oracle
causl-ts is the TypeScript engine — 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 the
default engine in causl-client — there the production default
is the Rust→WASM engine (rust-ssot), and causl-ts
is retained internally as the §12 conformance reference and the
implicit-path capability fallback (§18A.13.1). It remains the
value-of-record and default backend in the causl-ts-wasm-engine
fork. It is also the reference oracle — the byte-identity
gate (below) measures the wasm engine against the TS engine's
output, so the TS engine defines what "correct" means at the public
boundary. An adopter who never imports @causl/core/wasm on the
OSS pure-TS distribution sees zero behavioural change and pays zero bytes
for the wasm path.
causl-wasm: the Rust core, reached over FFI
causl-wasm 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 TS 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.
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 — including dependencies and
dependents (and their transitive closures), the commit log,
commit metadata, handle/disposal/StaleTx validation, and stats. For the
thin TS API to satisfy the full surface with no JS-engine fallback
for any row, the wasm core must surface these as FFI externs; lifting that
structural surface (today internal to engine-rs-core) into the
public FFI contract is, per §18A.3, "the spine of the work" (≈12–15 new
#[wasm_bindgen] externs, each backing a named §12 row, all
routed through the existing wire-format codecs so the byte-identity gate
holds).
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-client.
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 CauslWasmNotPreloadedError if not
// preloaded; { fallbackToTs: true } returns
// createCauslTs() instead of throwing
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 CauslWasmNotPreloadedError, and passing
{ fallbackToTs: true } returns createCauslTs()
rather than throwing.
The retained async factory createCauslWasm(opts?): Promise<Graph>
is now re-expressed as preload ∘ createCauslWasmSync — one
codepath, not 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 an explicit factory trio, with engine choice made at the call site when you want it:
| Factory | Engine | Shape |
|---|---|---|
createCauslTs() |
The pure-TS floor. | Synchronous. No toolchain, no preload — runs anywhere JS runs. |
createCauslWasm() /createCauslWasmSync() |
The wasm engine. | Async / synchronous. The sync form needs a prior
preloadCauslWasm() on the browser target; both share one
codepath. |
createCausl() |
The default public factory. | Synchronous. In causl-client it routes to the wasm
engine (rust-ssot) once preloaded, and degrades
loudly to the retained internal
createCauslTs() on a WasmGC-unavailable host (§18A.13.1).
In the causl-ts-wasm-engine fork it routes to the TS
floor by default. See the causl-client
direction below. |
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.
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 is promoted to
insertion-order semantics concurrent with the FFI surface work.
causl-client: the thin TS API over the wasm core (§18A.4)
causl-client is the TypeScript-side FFI binding for the wasm
engine — the third actor at the BackendEngine seam, alongside
JsBackend (wrapping the TS engine) and WasmBackend
(wrapping the Rust core). The Graph facade delegates every
public method to the backend; 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-client is not: not a copy of the TS
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-client guide.
causl-client: wasm-default with a TS capability-fallback (§18A.13 + §18A.13.1)
causl-client ships wasm as its sole default and public
engine — but it is wasm-default with a TS
capability-fallback, not strictly "wasm-only." This is
shipped: gate-bypassed (a dated §18A.13 governance
amendment, on complexity-elimination grounds), enterprise-only, and
scoped to causl-client alone.
createCausl() routes to the wasm engine (rust-ssot default),
and the §18A.3 FFI structural lift landed
(causl-wasm#170),
so every adopter operation resolves from Rust. The public direct
createCauslTs() factory was un-exported (executed
wire-before-cut, epic #31), but createCauslTs is
retained internally as the §12 conformance reference.
§18A.13.1 (2026-06-23) re-extended a loud TS capability-fallback to
the implicit path. On a host where the WasmGC engine cannot
instantiate — Safari < 18 / macOS < 15,
policy-pinned pre-119 Chromium/WebView2, Node ≤ 20 — the implicit
createCausl() degrades to the retained internal
createCauslTs() (via JsFallbackBackend)
loudly: a one-time console.warn +
onCauslCapabilityFallback telemetry, never silent. The
explicit createCauslWasm() /
createCauslWasmSync() / engine: 'rust-ssot'
factories still fail loud
(CAUSL_WASM_ENGINE_UNAVAILABLE) — a consumer that explicitly
asked for wasm must never silently run on JS. The literal zero-TS core
(deleting createCauslTs outright) is therefore
dropped from near-term scope by §18A.13.1. Caveat: the
WasmGC capability probe is still a placeholder
(#691), so
the auto-degrade is wired in design but not yet a fully-runtime-proven probe.
The driver is complexity elimination, not speed:
causl-client accepts the wasm engine's runtime characteristics
as the single core and sheds the dual-engine branching. Performance is
explicitly accepted as immaterial for this decision — the
JS↔WASM wire tax (named at honest status) is paid
knowingly. The cut is a §18A.13 deviation from the §18A.7 promotion gate that
governs the default in the dual-engine repos; 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 under the
causljs org. 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 binding, and the TS value-of-record
floor are never confused for one another. The SPEC.md is kept
byte-identical across the repositories so each reads the same governing
contract.
| Repository | Role | Owns |
|---|---|---|
causljs/causl-wasm |
The wasm causl-core / engine repo — the source of truth for the
causl-wasm 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 &
packaging tooling (build_wasm.py +
package_wasm.py, §18A.11). |
causljs/causl-client |
The TypeScript API for causl-wasm — the thin TS
binding of §18A.4 (a binding over the FFI, not a copy of the
TS 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
wasm-adoption guide. Consumes the placed .wasm; ships no
Rust. |
causljs/causl-ts-wasm-engine |
The causl-ts reference engine — the TypeScript
value-of-record / §13.8 unconditional floor. |
The causl-ts engine (the floor until all §18A.7
criteria are met), the cross-backend benchmark / conformance harness
(the byte-identity gate's JS side), and the historical wasm-cutover
R&D. |
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.
Build & 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 / 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
causljs/causl-wasm — 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 + .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 + per-file-sha256 manifest
(causl-wasm.manifest.json). 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/runtime network (the engine is vendored
into the deploy artefact, 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 (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-client 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 can't 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. The node-loadability check
encodes a real bridge distinction: the gc-classic bridge loads
as --target nodejs, but gc-builtins emits
require("wasm:js-string") — which stock Node cannot resolve — so
gc-builtins is asserted bundler-target only.
Any of these failing is a non-zero exit with a clear message, never a silent
pass.
The read()-reference-identity migration (§18A.5)
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 TS engine caches values in its cells and returns the same JS
reference per read() within a commit window. A real Rust engine
deserialises values across the FFI boundary and returns a
fresh object per call. 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.
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
TS 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 backend
(await loadWasmBackend()), 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-wasm#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', verified in source at
packages/core/wasm/index.ts:423;
createCausl() / createCauslWasm() now route commit
through the Rust engine by default for the WASM path.
| # | Criterion | Status |
|---|---|---|
| 1 | Correctness — cross-backend byte-identity. TS 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). |
| 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-wasm#170); every adopter op resolves from Rust. |
| 3 | Performance floor — acceptable marshaling tax. Representative workloads stay under the named ceiling: single commit ≤ 250 µs p95 marshal overhead, batch / large mutation ≤ 5 ms p95. Gates "stays responsive" within the §14 RAIL budget, not "beats TS." | GO / RESOLVED (2026-06-19). 6/6 RAIL cells PASS; perf is immaterial to the flip. Was one of the five — all now satisfied. |
| 4 | Node target + real-world adoption. The
--target nodejs artefact + ESM/node:fs loader
ships (§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 TS engine could not also
accept. |
GO — the js-ssot floor is a one-line
per-graph opt and the value-of-record in the fork; §18A.13.1
re-extends it to causl-client's implicit fallback path. |
Reversibility & the governance revert (§18A.8)
The 2026-06-21 fail-safe-removal amendment (§18A.8, causl-wasm#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" vs "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-wasm#169),
DEFAULT_WASM_ENGINE_MODE = 'rust-ssot', and the per-flush byte-compare oracle + K=1 sticky-downgrade fail-safe were removed (§18A.8). Incausl-clientthe wasm engine is the production default; the TS floor is retained as the §12 reference and the implicit-path capability fallback. -
The
WasmBackendis the real Rust engine. The engine returned byloadWasmBackend()isengine-rs-corecompiled to WebAssembly — a from-scratch Rust engine, not a TS-engine wrapper. The §18A.3 FFI structural lift landed (causl-wasm#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/SSR the glue is synchronous at require-time. Only the browser bundler target needs the one-timeawait preloadCauslWasm()before the first render. -
causl-clientis wasm-default with a TS capability-fallback (§18A.13 + §18A.13.1). The public directcreateCauslTs()factory was un-exported, butcreateCauslTsis retained and wired as the implicitcreateCausl()path's WasmGC-unavailable fallback — degrading loudly (console.warn + telemetry), never silent. Explicit wasm still fails loud. The literal zero-TS core is dropped from near-term scope. Scoped tocausl-clientonly; the fork keeps both engines as the differential oracle. -
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-client. Thecommit/readspine stays synchronous. - The standing wire tax is named, not hidden. The JS↔WASM marshal cost is the documented ~78× wire tax — ~156.4 ms for 10k commits across the boundary vs ~2.017 ms TS median (§17.6 G.1). Perf is explicitly immaterial to the flip: the driver was complexity elimination, and per-commit wall-time stays inside the §14 RAIL responsiveness budget.
-
The WasmGC capability probe is still a placeholder.
§18A.13.1's capability-trigger is specified and scaffolded
(
JsFallbackBackend, thefallbackToTsflag, theCAUSL_WASM_ENGINE_UNAVAILABLEerror code all exist), but the actual WasmGC probe is a placeholder (#691). The auto-degrade is wired in design, not yet a fully-runtime-proven probe.
In short: causl-wasm via causl-client is the
production engine — the real Rust engine, rust-ssot by default, with the
byte-compare oracle and sticky-downgrade removed and the node loader + FFI
lift landed. causl-ts is the unconditional floor and the
differential oracle, retained in causl-client as the §12
reference and the implicit-path capability fallback (§18A.13.1, loud on a
WasmGC-unavailable host), and the value-of-record in the fork. Construction
is synchronous behind a one-time preload (§18A.12, shipped).
Related
- Integrating causl-client — the consumer-side guide:
loadWasmBackend(), the construction seam, and theWasmBackendUnavailableErrorfallback chain. - Wasm performance & the perf ceiling — the marshalling overhead, the FFI atomicity contract, and the §18A.7 Criterion-3 ceiling in detail.
- API Reference — the typedoc-generated reference for every
@causl/*package, including the@causl/core/wasmentry point. - SPEC §18A — the governing two-engine contract this page summarises.