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.”
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.
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.”
rust (the native/WASI crate) is UNCHANGED in kind: still std-only, still zero Cargo dependencies — the split into lib.rs/main.rs is a pure reshuffle (main.rs shrank to a stdin/stdout wrapper around rust::kernel::cli::run, same as before), not a new dependency surface. wasm-bindgen lives entirely in rust/web’s manifest.rust/ where 0007/0011/0012 all describe “the crate” in the singular. That’s a real departure worth naming plainly: rust/web is not a fork of the generator’s OUTPUT (both packages compile the identical rust/src/generated/ — 0007’s “WASM is a compile target of the same generated artifact” still holds), but it is a second compiled ARTIFACT with its own manifest, its own dependency, its own build script. Justified because a wasm-bindgen cdylib and a std-only WASI binary are genuinely different compilation products, not two flags on the same one.dispatch(json), not the richer Hecks.load(...)/per-command-method sketch from the original §9 design. No per-era module selection, no host-adapter import boundary for IndexedDB/files/networking — both still open, exactly as 0012 flagged them; this decision closes “can a browser call into the compiled domain at all,” nothing wider.wasm-bindgen-cli is a machine-local install (cargo install wasm-bindgen-cli --version 0.2.126 --locked), not a project dependency bundle/npm install pulls automatically — bin/project_wasm_browser checks the installed CLI’s version against the pinned crate version and aborts with the install command if they don’t match, the same pattern bin/project_wasm already uses for the wasm32-wasip1 rustup target.wasm-bindgen directly to rust/Cargo.toml, gated to wasm32-unknown-unknown via [target.'cfg(...)'.dependencies]. Keeps the dependency out of native/WASI builds too, and was seriously considered — but it still means the ONE crate 0012 built as “the compiled artifact” carries a second crate-type (cdylib alongside the existing bin) and a conditional dependency a reader has to mentally filter by target to understand what any given build actually pulls in. A separate package makes “what does the native/WASI build depend on” answerable by reading rust/Cargo.toml alone, unconditionally.bin/project_wasm as a flag (bin/project_wasm --browser <domain>). Rejected on the same “if it’s a separate binary it should be a separate projection” grounds the rest of this design follows: the two scripts share almost no machinery below “regenerate rust/src/generated/ first” — different target triple, different crate, different post-processing step (wasm-bindgen CLI vs. a plain file copy), different output shape (a JS+wasm pair vs. a single .wasm). A shared flag would be two scripts wearing one name.