# WASM ships via WASI stdio, not a browser JS binding

**Status:** Accepted — implemented. `rust/src/kernel/{json,cli}.rs`, `rust/project/{json_codec,registry}.rb`, `bin/project_wasm`, branch `feat/rust-projection`. Narrows [§9 "WASM embedded Bluebook"](../HECKS_IMPLEMENTATION_PLAN.md) — see that section's own status note.

## Context

[0007](0007-rust-generates-code-not-ruby-source.md) already committed to one shape: "WASM is a compile target of the same generated artifact, not a separate implementation." §9 of the implementation plan sketched what that would look like — a browser-facing JS API (`const domain = await Hecks.load("domain.wasm"); await domain.dispatch(verb, args)`), per-era-compiled modules, and a host-adapter boundary keeping browser-only capabilities (IndexedDB, files, networking, rendering, auth tokens) outside the WASM module as narrow imports. That design was written before [0011](0011-rust-compiles-types-interprets-dispatch.md) landed and explicitly flagged itself as needing to be "re-scoped against the as-built architecture before starting" — the as-built generator has no era concept at all, and §9's `{era: 12}` dispatch option assumed one.

Separately, `rust/src/main.rs` had never been anything but a hardcoded demo — one Pizzas scenario built from Rust struct literals, printed with `println!`. `bin/rust_conformance` (the differential harness [0010](../../decisions/0010-ruby-is-the-reference-implementation.md) describes) already defined a real JSON contract — `{"steps": [...]}` in, `{"instances", "events", "refusals"}` out, the exact shape Ruby's `Fuzzing::Replay.call` produces — but its own header comment said outright it "does not invoke Rust itself... until then, 'give me a JSON file to compare against' is the whole interface," because there was no binary with a matching CLI contract to shell out to.

The immediate goal driving this pass wasn't a browser embedding — it was making the compiled Rust artifact runnable in non-browser WASM hosts (Wasmtime-based server/edge runtimes, the kind relevant to "run domain logic in workers" on a cloud platform), and doing it without adding Cargo dependencies to a crate that had none.

## Decision

**Give the compiled artifact ONE stdin/stdout JSON CLI contract — `bin/rust_conformance`'s own `{"steps"} -> {"instances","events","refusals"}` shape — and compile that SAME source for both `aarch64-apple-darwin` (native) and `wasm32-wasip1` (WASM via WASI preview 1).**

`rust/src/kernel/json.rs` is a hand-rolled `Json` value type, recursive-descent parser, and serializer — zero Cargo dependencies, the same constraint `expr.rs`/`dispatch.rs` already hold themselves to. `rust/project/json_codec.rb` extends the existing per-type code generation (`fielded.rb`'s own pattern) with paired `to_json`/`from_json` methods on every value object and command Args struct, `to_json` on every entity and aggregate record, and a per-aggregate `extract_id` that reads `identified_by`'s dotted paths straight off incoming JSON (the JSON-CLI counterpart to `mutations.rb`'s `identity_components`, which only ever ran for a creating command's typed args before this). `rust/project/registry.rb` emits one `Store` (an `InMemoryRepository` per aggregate) and one `dispatch_by_name(store, verb, args_json)` router per domain, covering every command the existing `command_skip_reason` gate already lets through — nothing new is skipped by this layer except a command whose identity needs an extra caller-supplied parameter no JSON step shape carries (`Bluebook::Command.Declare`'s `owner_id`, the one live example).

`rust/src/kernel/cli.rs` (hand-written, generic) drives all of it: parse stdin as `{"steps": [...]}`, dispatch each step's verb/args through `generated::active::dispatch_by_name` (`bin/project_rust`'s own domain-agnostic alias, rewritten on every regeneration), print the accumulated result in the exact shape `bin/rust_conformance` already expects. `rust/src/main.rs` is now just this loop plus stdin/stdout plumbing — nothing domain-specific, nothing that needs hand-editing when a different domain gets regenerated in its place.

Because that CLI touches nothing but `std` stdin/stdout — no filesystem, no network, no OS calls anywhere in the kernel — the identical source cross-compiles for `wasm32-wasip1` unchanged: WASI implements stdio the same way a native process does. `bin/project_wasm <domain>` is the only genuinely new piece of automation: check the target is installed, run `bin/project_rust <domain>` to regenerate, `cargo build --release --target wasm32-wasip1`, copy the artifact to `rust/dist/<domain>.wasm`. `bin/rust_conformance` gained a matching subprocess mode (`native` or a `.wasm` path) so the differential harness can now actually run either artifact instead of only diffing a pre-existing file — closing the gap its own header comment named.

## Consequences

- No new Cargo dependencies. The crate remains `std`-only; `Json` is a second, deliberately separate value type from `expr::Value` (which never needs Object/Array, and is never JSON-serialized).
- The WASM artifact and the native binary are the SAME compiled logic, verified by running both against an identical `script.json` and diffing structurally — `instances` matched byte-for-byte across Ruby (oracle), native Rust, and WASM-under-`wasmtime` on the first real cross-domain test (Banking: 10 aggregates, composite identities, closed-set data tables, all through the same registry).
- This is NOT §9's browser sketch. There is no `Hecks.load(...)`/`domain.dispatch(...)` JS API, no per-era module selection (the as-built generator still has no era concept — a real, separate, still-open gap §8 already named), and no host-adapter import boundary for IndexedDB/files/networking/rendering. A browser embedding remains a plausible FUTURE layer on top of this same WASI-stdio contract (a thin JS shim shelling JSON at it, or a separate `wasm-bindgen` build) — not what this decision builds.
- Two real, pre-existing fidelity gaps surfaced by actually running the differential harness live (not introduced by this decision — inherited from [0011](0011-rust-compiles-types-interprets-dispatch.md)'s kernel, unchanged here): generated `Event.payload` is `BTreeMap<String,String>` of `{:?}`-debug-formatted values, not Ruby's structured `args` hash, so `events[].payload` diffs even when everything else matches; and a `GivenNotMet` refusal's Rust wording is the bare `given` description, while Ruby prefixes it with the command name ("Debit refused — the balance covers it" vs "the balance covers it"). Both are message/shape fidelity gaps in already-built machinery, not something this WASM pass introduced or was scoped to fix — flagged here so they don't get mistaken for a WASM-specific defect later.
- A multi-field closed set (`emit_closed_set_table`'s "data table" shape — `Banking::StatementFrequency` is the live example) has `&'static str` fields, not `String`, because it's backed by a `pub const` array of struct literals. `from_json` for that shape can't construct a fresh instance from arbitrary runtime JSON at all — it can only SELECT one of the table's own fixed rows and clone it (`json_codec.rb`'s `emit_closed_set_table_codec`), the same match `bridging.rb`'s `literal_hash_bridgeable?` already performs for a literal `then_set`/`append` source.

## Rejected alternatives

- **`wasm-bindgen` + `js-sys`, targeting `wasm32-unknown-unknown`, per §9's original JS-facing sketch.** Would give a real ergonomic `domain.dispatch(name, args)` browser API directly, but adds the crate's first Cargo dependencies, only runs where a JS engine is present (rules out a Wasmtime-hosted worker with no JS layer), and would have meant designing a second CLI-adjacent boundary instead of reusing `bin/rust_conformance`'s already-designed contract. Not rejected forever — a plausible future layer ON TOP of the WASI contract this decision builds, per the Consequences note above.
- **Raw `wasm32-unknown-unknown` with a hand-rolled ABI (exported `i32`/pointer functions, manual memory marshaling).** Also zero-dependency and JS-independent, but throws away WASI's stdio for a bespoke memory-passing convention this project would have to invent and hand-maintain, for no benefit over reusing the stdin/stdout contract `bin/rust_conformance` already had.
- **A second, WASM-specific code generator, separate from `rust/project.rb`'s Rust output.** Rejected on the same grounds 0007 already rejected per-host runtimes generally: "WASM is a compile target of the same generated artifact, not a separate implementation." Everything new here (`json_codec.rb`, `registry.rb`) extends the existing generator's output for BOTH targets, not a WASM-only fork of it.
