hecks

A separate crate carries the wasm-bindgen browser projection

Status: Accepted — implemented. rust/src/lib.rs, rust/web/, bin/project_wasm_browser, branch feat/rust-projection. Builds the layer 0012 explicitly deferred: “a plausible FUTURE layer on top of this same WASI-stdio contract… not what this decision builds.”

Context

0012 gave the compiled Rust artifact one stdin/stdout JSON CLI contract and cross-compiled it unchanged for wasm32-wasip1 — real progress for non-browser WASM hosts (Wasmtime, edge workers), but it rejected wasm-bindgen outright for the browser case: “adds the crate’s first Cargo dependencies… would have meant designing a second CLI-adjacent boundary instead of reusing bin/rust_conformance’s already-designed contract.” That left an actual browser embedding — the original §9 sketch this whole arc traces back to — still unbuilt. Nothing about the reasons WASI stdio was chosen changes that: a browser has no stdin/stdout to speak to, only a JS call boundary, and wasm32-wasip1 modules aren’t naturally callable as JS functions the way a wasm-bindgen-processed wasm32-unknown-unknown module is.

The question this decision answers is narrower than “should the browser layer exist” (0012 already said yes, eventually) — it’s “does building it mean touching the crate 0012 deliberately kept std-only and dependency-free.” It doesn’t have to: rust/src/kernel/cli.rs::run(input: &str) -> String is already the complete, generic, domain-agnostic contract every projector wants — WASI’s main.rs is a two-line stdin/stdout wrapper around it, and a browser wrapper is exactly as thin, just wrapped in #[wasm_bindgen] instead.

Decision

Split generated/kernel into rust/src/lib.rs (this crate’s library face), and add rust/web/ — a second, separate Cargo package that depends on rust by path and owns the ONE new dependency (wasm-bindgen) plus the ONE new crate-type (cdylib). rust/web/src/lib.rs is nine lines: #[wasm_bindgen] pub fn dispatch(input: &str) -> String { rust::kernel::cli::run(input) } — the exact same run the WASI main.rs calls, now reachable as dispatch(json) from JS instead of over stdio. rust/web/Cargo.toml pins wasm-bindgen = "=0.2.126" (exact version — the wasm-bindgen CLI that post-processes the .wasm must match the crate version bit-for-bit, or it fails at CLI-run time, not build time). Each package gets its own [workspace] boundary (rust/web’s is the empty table; rust itself declares none) so they keep separate target/s and Cargo.locks rather than Cargo’s implicit-workspace auto-inclusion silently merging them.

bin/project_wasm_browser <domain> is the new projector, deliberately a SEPARATE script from bin/project_wasm rather than a branch inside it — mirroring the shape of the underlying decision: regenerate rust/src/generated/ through the same bin/project_rust every projector shares, cargo build --release --target wasm32-unknown-unknown inside rust/web, then run the (version-checked) wasm-bindgen CLI with --target web to produce rust/dist/browser/<domain>/{<domain>.js,<domain>_bg.wasm,*.d.ts} — an ES module usable directly in a <script type="module"> with no bundler: import init, { dispatch } from "./<domain>.js"; await init(); dispatch(JSON.stringify({steps: [...]})).

Verified the way 0012 verified WASI: the full spec/corpus/banking.json fixture (231 steps, the flagship “every declared command and query, entities, read models, process managers, policies” corpus) run through the native binary and through this browser build in an actual Chrome tab, both landing on 41 instances / 137 events / 126 refusals, and instances/events/refusals matching byte-for-byte between the two — not merely “it compiled.”

Consequences

Rejected alternatives