Skip to content

SPIKE: package the engine's declarative plane as WebAssembly#249

Draft
sksizer wants to merge 1 commit into
mainfrom
spike/engine-wasm
Draft

SPIKE: package the engine's declarative plane as WebAssembly#249
sksizer wants to merge 1 commit into
mainfrom
spike/engine-wasm

Conversation

@sksizer

@sksizer sksizer commented Jul 14, 2026

Copy link
Copy Markdown
Owner

Spike — not for merge. De-risks packaging markdown-contract-engine as WebAssembly so the TS packages/core declarative-validation plane could become a thin wasm wrapper (engine-axis plan). Findings only; no production migration.

What's here

A throwaway crates/markdown-contract-engine/wasm-spike/ crate (its own Cargo workspace, publish = false) exposing one entry point via wasm-bindgen:

validate_document(source, contract_yaml, path) -> Finding[] JSON

over the engine's load_contract + validate. Built for wasm-pack nodejs / web / bundler; a Node harness proves the round-trip and a browser harness documents the async-init story.

The engine crate, its API, and its tests are untouched — the spike is an isolated workspace that depends on the engine with default-features = false (the fs-free core). cargo test -p markdown-contract-engine → 149 unit + 1 corpus golden, still green.

Headline findings (full detail in FINDINGS.md)

Recommendation GO — size is the only real cost, not a blocker
wasm size 1.42 MiB raw · 404 KiB brotli / 551 KiB gzip · +6–9 KB JS glue
vs TS bundle ~99 KiB gzip (esbuild-minified) → wasm is ~4–5× heavier on the wire
Init Node: fully synchronous (require, no await). Browser: async streaming, or initSync(bytes)
Targets one .wasm, per-target glue. bun runs the nodejs artifact unmodified (verified)
Boundary JSON string ↔ JSON.parse; Finding is the frozen interchange shape, so goldens hold by construction

Why it de-risks cleanly: the core compiled to wasm32-unknown-unknown on the first try — no getrandom / js-sys / std::fs / panic-hook surprises. The existing native feature gate already did the hard part.

Main risk: binary size (comrak + regex + serde). Irrelevant for CLI/daemon; a one-time cached download for the browser dashboard.

Verified

  • Node harness runs one validation (pass → [], fail → frontmatter/enum + structure/section-missing), and a malformed contract throws a JS Error.
  • Same output under bun.
  • Engine crate tests unaffected.

Tooling note

wasm-opt (binaryen 117) rejected the module until passed explicit --enable-bulk-memory/etc. flags — rustc 1.95 emits post-MVP wasm features and strip = true drops the target_features section it auto-detects from. Flags are set in Cargo.toml; a CI setup needs the same (or a newer binaryen).

Next step if go

Opaque Contract handle (compile once, validate many) → wasm build in packages/core's pipeline → a wasm-parity CI check over the shared corpus.

… findings

De-risk packaging `markdown-contract-engine` as WebAssembly for the TS
`packages/core` declarative-validation plane (engine-axis plan).

Adds a throwaway `wasm-spike/` crate (its own Cargo workspace, publish = false)
exposing one entry point via wasm-bindgen — `validate_document(source,
contract_yaml, path) -> Finding[] JSON` — over the engine's `load_contract` +
`validate`. Builds for wasm-pack nodejs/web/bundler targets; a Node harness
proves the round-trip and a browser harness documents the async-init story.

The engine crate, its API, and its tests are untouched (isolated workspace;
depends on the engine with default-features = false, i.e. the fs-free core).

FINDINGS.md is the deliverable: sizes (1.42 MiB wasm / 404 KiB brotli vs
~99 KiB gz for the TS bundle), sync Node / async browser init, one binary +
per-target glue, JSON-boundary ergonomics, and a GO recommendation.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant