hecks

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” — see that section’s own status note.

Context

0007 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 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 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

Rejected alternatives