Initial Commit
This commit is contained in:
97
openspec/changes/wasm-compiler-javy/proposal.md
Normal file
97
openspec/changes/wasm-compiler-javy/proposal.md
Normal file
@@ -0,0 +1,97 @@
|
||||
# TS→WASM Compiler (Javy-backed, WASI-stdio variant)
|
||||
|
||||
**Package**: `@suaveplan/wasm-compiler-javy`
|
||||
**Status**: PROPOSED (rewritten 2026-05-27 after architectural blocker — see §Why)
|
||||
**Type**: new-package (compilation tooling)
|
||||
**Depends on**: `api-sandbox-wasm-stdio-abi` (defines the wire protocol — SHIPPED at `cddccbdb7c`) AND `api-plugin-example-dual-shape` (provides the `/wasm` subpath the keystone test compiles — must ship first)
|
||||
|
||||
## Why
|
||||
|
||||
The original proposal sketched a direct-export ABI emitted by Javy (`__suaveplan_alloc`, `__suaveplan_handle_request`, etc.). The build agent surfaced an architectural blocker against Javy v8.1.1: Javy's `-C wit=...` flag restricts WIT-exposed exports to "function exports with no arguments and no return values", and Javy's host-interaction model is fundamentally WASI fd 0 / fd 1 (stdin / stdout). The direct-export ABI cannot be produced from a Javy-compiled JavaScript module without authoring a hand-encoded outer-WASM wrapper — a route the orchestrator rejected as too brittle.
|
||||
|
||||
The chosen path (orchestrator decision, 2026-05-27) is to extend `@suaveplan/api-sandbox-wasm-host-bridge` to accept a WASI-stdio ABI variant (sibling proposal `api-sandbox-wasm-stdio-abi`) and let this compiler be a **thin Javy wrapper**: bundle TS → inject a WASI-stdio ABI shim → invoke `javy build` → emit manifest. No outer-WASM authorship, no Component Model migration, no novel encoding work.
|
||||
|
||||
## Concrete consumer
|
||||
|
||||
- `@suaveplan/api-plugin-example` — TypeScript reference plugin. The keystone end-to-end test compiles it via this package and deploys the resulting WASM through the WASI-stdio bridge variant.
|
||||
- `e2e/foundation-stack-full` Stage-8 closure smoke (gate 2.8) — calls `compilePluginToWasm({ pluginEntry: "<api-plugin-example>/src/wasm/index.ts" })` and asserts the produced artifact responds to a real HTTP request via `host.installPlugin` + the WASI-stdio bridge.
|
||||
- Plugin operators authoring third-party TS plugins for the wasm tier.
|
||||
|
||||
## End-to-end test
|
||||
|
||||
- `tests/integration/api-plugin-example.test.ts` — compile the example plugin, instantiate via `api-sandbox-wasm-host-bridge` with `abiVariant: "wasi-stdio"`, deploy via `host.installPlugin`, exercise every declared route via a real HTTP request through `createTestHost`.
|
||||
|
||||
## What changes
|
||||
|
||||
### Compilation pipeline
|
||||
|
||||
```text
|
||||
pluginEntry ──[1]──> bundled ESM JS ──[2]──> ABI-shimmed JS ──[3]──> .wasm ──[4]──> validate ──[5]──> manifest sidecar
|
||||
Bun.build inject WASI-stdio loop javy build ABI check + Zod <basename>.wasm.json
|
||||
```
|
||||
|
||||
1. **`Bun.build({ entrypoints, target: "browser", format: "esm" })`** — tree-shake + inline. Reject `node:*` specifiers with `WASM_COMPILER.BROWSER_INCOMPAT_DEPENDENCY` + `context.specifier`.
|
||||
2. **Inject the WASI-stdio ABI shim** — prepend a generated header that:
|
||||
- Reads from `process.stdin` (Javy maps this to fd 0) in length-prefix-framed loop.
|
||||
- Validates each frame as `PluginEnvelopeIn` (Zod-validated runtime check, using the same Zod schemas exported by `@suaveplan/api-sandbox-wasm-host-bridge`).
|
||||
- Dispatches by `kind`: `routes` → write the plugin's `routes` array; `request` → call the plugin's `handleRequest(request)` and write the response; `start` → call optional `start()` then `{kind:"ok"}`; `dispose` → call optional `dispose()` then `{kind:"ok"}` and exit.
|
||||
- Writes length-prefixed `PluginEnvelopeOut` frames to `process.stdout` (Javy maps to fd 1).
|
||||
- Surfaces thrown `SuaveplanError`-shaped values as `{kind:"error", code, message}` envelopes.
|
||||
3. **`javy build <bundled.js> -o <output.wasm>`** — invokes the platform-appropriate `javy-cli-*` binary via `optionalDependencies` resolution. Non-zero exit → `WASM_COMPILER.JAVY_INVOCATION_FAILED` with `{exitCode, stderr (8 KiB trunc), command}`.
|
||||
4. **`WebAssembly.compile(bytes)`** + reflection → assert `memory` + `_start` + the required `wasi_snapshot_preview1.*` imports. Missing → `WASM_COMPILER.ABI_VALIDATION_FAILED` + `context.missing`. This matches the WASI-stdio bridge variant's import allowlist exactly.
|
||||
5. **Emit `<basename>.wasm.json` sidecar** — `{ routes, abiVersion, abiVariant: "wasi-stdio", javyVersion, compilerVersion, compiledAt (RFC 3339 UTC), sourceDigest (SHA-256 of pre-Javy bundle) }`. Empty `routes` → `WASM_COMPILER.MANIFEST_MISSING_ROUTES`. Routes are extracted at compile time by booting the WASM in a one-shot host-side WASI runner (`@bjorn3/browser_wasi_shim` or equivalent) and capturing the `{kind:"routes"}` reply.
|
||||
|
||||
### Package surface
|
||||
|
||||
```ts
|
||||
import { compilePluginToWasm } from "@suaveplan/wasm-compiler-javy";
|
||||
|
||||
const result = await compilePluginToWasm({
|
||||
pluginEntry: "/path/to/plugin/src/index.ts",
|
||||
outDir: "/tmp/plugin-wasm",
|
||||
optimizeFor: "size", // default "size" — passes -O to javy build
|
||||
sourcemaps: false, // default false
|
||||
javyVersion: "v8.1.1", // default catalog-pinned version
|
||||
});
|
||||
|
||||
// result.wasmPath: string
|
||||
// result.metadataPath: string
|
||||
// result.sizeBytes: number
|
||||
// result.compilationMs: number
|
||||
```
|
||||
|
||||
### Platform-wrapper packages
|
||||
|
||||
5 thin-wrapper packages under `packages/devtools/tooling/`:
|
||||
- `javy-cli-linux-x64`, `javy-cli-linux-arm64`, `javy-cli-darwin-x64`, `javy-cli-darwin-arm64`, `javy-cli-win32-x64`.
|
||||
|
||||
Each declares matching `os` + `cpu` in `package.json` so npm/bun skips installation on mismatched platforms. Binaries are fetched via a `postinstall` script from Bytecode Alliance's Javy GitHub releases at the catalog-pinned tag, with SHA-256 checksum verification of every download. Checksums are committed; binaries are not.
|
||||
|
||||
### CLI binary
|
||||
|
||||
`suaveplan-wasm-compile <entry> --out-dir <dir> [--optimize-for size|speed] [--sourcemaps]` — delegates to `compilePluginToWasm`. JSON result on stdout, structured error JSON on stderr, non-zero exit on any failure.
|
||||
|
||||
### OpenTelemetry
|
||||
|
||||
Meter `@suaveplan/wasm-compiler-javy`:
|
||||
- counter `wasm_compiler.compiles_total{result}` where `result ∈ {"ok","bundle_failed","javy_failed","abi_failed","manifest_failed"}`.
|
||||
- histogram `wasm_compiler.compile_duration_ms{result}`.
|
||||
- counter `wasm_compiler.abi_validation_failures_total{missing_export}`.
|
||||
|
||||
### Conformance harness
|
||||
|
||||
`runWasmCompilerConformance({ compileFn })` from `./testing` with four invariants:
|
||||
1. ABI completeness — every required WASI import + `_start` + `memory` present.
|
||||
2. Deterministic output — same input + pinned Javy version → identical SHA-256.
|
||||
3. Manifest fidelity — `routes` array matches the plugin's declared routes.
|
||||
4. Cross-bridge deployability — produced WASM passes `runWasmHostBridgeConformance({ buildBridge, abiVariant: "wasi-stdio" })`.
|
||||
|
||||
### Error codes
|
||||
|
||||
`WASM_COMPILER.{BUNDLE_FAILED, BROWSER_INCOMPAT_DEPENDENCY, JAVY_PLATFORM_MISMATCH, JAVY_INVOCATION_FAILED, ABI_VALIDATION_FAILED, MANIFEST_MISSING_ROUTES}` — 6 codes via `defineCodes`, error class with **public constructor** (rule §19).
|
||||
|
||||
## Out of scope
|
||||
|
||||
- Direct-i32-i64 ABI compilation. That variant remains supported by `api-sandbox-wasm-host-bridge` but is not what this compiler emits — and is not required by `api-plugin-example` or any planned consumer.
|
||||
- Component Model output. A future `wasm-compiler-component` package could target that surface; not in this scope.
|
||||
- Compilation of non-suaveplan plugin shapes. The ABI shim is hard-coded to the suaveplan WASI-stdio protocol.
|
||||
Reference in New Issue
Block a user