Initial Commit
This commit is contained in:
@@ -0,0 +1,154 @@
|
||||
# Capability Spec Delta: wasm-compiler-javy
|
||||
|
||||
**Package**: `@suaveplan/wasm-compiler-javy`
|
||||
**Status**: PROPOSED (rewritten 2026-05-27 — WASI-stdio variant)
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: `compilePluginToWasm` produces a WASI-stdio WASM artifact
|
||||
|
||||
The package SHALL export `compilePluginToWasm({ pluginEntry, outDir, optimizeFor?, sourcemaps?, javyVersion? })` returning `{ wasmPath, metadataPath, sizeBytes, compilationMs }`. The produced `.wasm` MUST:
|
||||
1. Export `memory` and `_start`.
|
||||
2. Import every entry in the WASI snapshot-preview1 allowlist (`fd_read`, `fd_write`, `environ_get`, `environ_sizes_get`, `proc_exit`).
|
||||
3. Speak the WASI-stdio framing protocol defined in `@suaveplan/api-types/sandbox-bridge` (length-prefixed JSON envelopes on fd 0 / fd 1).
|
||||
|
||||
#### Scenario: Compiled api-plugin-example deploys via the WASI-stdio bridge variant
|
||||
|
||||
- **WHEN** `compilePluginToWasm({ pluginEntry: "<api-plugin-example>/src/wasm/index.ts", outDir })` is called
|
||||
- **AND** `createSandboxedWasmPluginModule({ sandbox: <api-sandbox-wasm built from the artifact>, manifest, abiVariant: "wasi-stdio" })` is invoked
|
||||
- **THEN** ABI validation passes, route discovery returns every route declared in the plugin's manifest, AND a real HTTP request through `host.installPlugin` reaches a handler that returns the expected response
|
||||
|
||||
**Verified by:** `tests/integration/api-plugin-example.test.ts::"compiled WASM deploys via wasi-stdio bridge"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Bundle step uses Bun.build with browser target
|
||||
|
||||
The compiler MUST invoke `Bun.build({ entrypoints: [pluginEntry], target: "browser", format: "esm" })` as step 1. Any transitive dependency requiring a Node-only API not polyfilled by the ABI shim MUST fail fast with `WASM_COMPILER.BROWSER_INCOMPAT_DEPENDENCY` and `context.specifier` naming the offending import.
|
||||
|
||||
#### Scenario: Plugin imports `node:fs` and is rejected
|
||||
|
||||
- **WHEN** `compilePluginToWasm` is called on a plugin whose dependency tree imports `node:fs`
|
||||
- **THEN** the call rejects with `code === "WASM_COMPILER.BROWSER_INCOMPAT_DEPENDENCY"` AND `context.specifier === "node:fs"`
|
||||
|
||||
**Verified by:** `src/bundle/bundle.test.ts::"rejects node:fs in dependency tree"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: ABI shim wraps the bundle with the WASI-stdio request loop
|
||||
|
||||
After bundling, the compiler MUST prepend a generated ABI-shim header that implements: (a) length-prefix framed read from fd 0, (b) Zod-equivalent validation of every incoming envelope as `PluginEnvelopeIn`, (c) `kind`-dispatch to the plugin's `routes` / `handleRequest` / `start` / `dispose` exports, (d) length-prefix framed write to fd 1, (e) `SuaveplanError` catch → `{kind:"error", code, message}` envelope. The shim MUST be byte-stable across compilations of the same plugin.
|
||||
|
||||
#### Scenario: Two compilations of the same input produce byte-identical WASM
|
||||
|
||||
- **WHEN** `compilePluginToWasm` is invoked twice with identical inputs and a pinned `javyVersion`
|
||||
- **THEN** the two `.wasm` artifacts have identical SHA-256 digests
|
||||
|
||||
**Verified by:** `tests/integration/determinism.test.ts::"compilation is deterministic"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Javy binary located via platform-specific optional dependency
|
||||
|
||||
The compiler MUST locate the Javy CLI via the `javy-cli-{linux-x64,linux-arm64,darwin-x64,darwin-arm64,win32-x64}` optional-dependency wrappers, selecting by `process.platform` + `process.arch`. If no matching wrapper is installed, the call MUST fail with `WASM_COMPILER.JAVY_PLATFORM_MISMATCH` and `context.requested` naming the platform-arch pair.
|
||||
|
||||
#### Scenario: Unsupported platform fails fast
|
||||
|
||||
- **WHEN** `compilePluginToWasm` runs on a platform without a matching `javy-cli-*` package
|
||||
- **THEN** the call rejects with `code === "WASM_COMPILER.JAVY_PLATFORM_MISMATCH"`
|
||||
|
||||
**Verified by:** `src/javy/javy-locate.test.ts::"raises JAVY_PLATFORM_MISMATCH for unsupported platform"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Javy invocation failures surface as `JAVY_INVOCATION_FAILED`
|
||||
|
||||
Non-zero exit OR fatal stderr from the spawned Javy process MUST raise `WASM_COMPILER.JAVY_INVOCATION_FAILED` with `context.exitCode`, `context.stderr` (truncated to 8 KiB), AND `context.command`.
|
||||
|
||||
#### Scenario: Malformed JS input fails with structured error
|
||||
|
||||
- **WHEN** Javy is invoked on intentionally malformed input
|
||||
- **THEN** the call rejects with `code === "WASM_COMPILER.JAVY_INVOCATION_FAILED"` AND `context.exitCode !== 0` AND `context.stderr` is non-empty
|
||||
|
||||
**Verified by:** `src/javy/javy-invoke.test.ts::"non-zero Javy exit surfaces structured error"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: ABI validation runs after Javy emits the WASM
|
||||
|
||||
The compiler MUST `WebAssembly.compile` the bytes and reflect the module to assert: `memory` export, `_start` export, AND every required WASI snapshot-preview1 import. Missing any → `WASM_COMPILER.ABI_VALIDATION_FAILED` with `context.missing: string[]`.
|
||||
|
||||
#### Scenario: Missing `_start` rejected
|
||||
|
||||
- **WHEN** Javy produces a WASM that somehow lacks `_start`
|
||||
- **THEN** the compile rejects with `code === "WASM_COMPILER.ABI_VALIDATION_FAILED"` AND `context.missing` contains `"_start"`
|
||||
|
||||
**Verified by:** `src/validate/abi-validate.test.ts::"missing _start raises ABI_VALIDATION_FAILED"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Route extraction by booting the produced WASM under a host-side WASI runner
|
||||
|
||||
Before emitting the manifest, the compiler MUST boot the produced `.wasm` under a host-side WASI runner, send a `{kind:"routes"}` envelope, and capture the `{kind:"routes", routes}` reply. The captured `routes: readonly SerializedRoute[]` becomes the manifest's `routes` field. Empty list → `WASM_COMPILER.MANIFEST_MISSING_ROUTES`.
|
||||
|
||||
#### Scenario: Compiled api-plugin-example yields 11+ routes
|
||||
|
||||
- **WHEN** route extraction runs against the example plugin's compiled WASM
|
||||
- **THEN** the captured `routes` array length matches the plugin's source-declared route count
|
||||
|
||||
**Verified by:** `src/route-extract/route-extract.test.ts::"route extraction matches source declarations"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Metadata sidecar emitted alongside `.wasm`
|
||||
|
||||
The compiler MUST emit `<basename>.wasm.json` containing: `routes`, `abiVersion` (semver), `abiVariant: "wasi-stdio"`, `javyVersion`, `compilerVersion`, `compiledAt` (RFC 3339 UTC), AND `sourceDigest` (SHA-256 of the bundled JS pre-Javy).
|
||||
|
||||
#### Scenario: Manifest mirrors the plugin's declared routes
|
||||
|
||||
- **WHEN** `compilePluginToWasm` runs against `api-plugin-example`
|
||||
- **THEN** the emitted manifest's `routes` array equals the plugin's source-declared routes AND `abiVariant === "wasi-stdio"`
|
||||
|
||||
**Verified by:** `src/manifest/manifest.test.ts::"manifest mirrors plugin routes and pins abiVariant"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: CLI binary `suaveplan-wasm-compile`
|
||||
|
||||
The package SHALL ship `suaveplan-wasm-compile <entry> --out-dir <dir> [--optimize-for size|speed] [--sourcemaps]` that delegates to `compilePluginToWasm` and prints the result as JSON on stdout. Non-zero exit on any error with the structured error JSON on stderr.
|
||||
|
||||
#### Scenario: CLI compiles api-plugin-example end-to-end
|
||||
|
||||
- **WHEN** `bunx suaveplan-wasm-compile <api-plugin-example/src/wasm/index.ts> --out-dir <tmp>` runs
|
||||
- **THEN** exit code is 0 AND stdout JSON contains `wasmPath` AND that path passes the WASI-stdio bridge ABI validation
|
||||
|
||||
**Verified by:** `src/bin/cli.test.ts::"CLI compiles api-plugin-example"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: OpenTelemetry instruments published
|
||||
|
||||
The compiler MUST publish three OTel instruments on 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}`.
|
||||
|
||||
#### Scenario: Successful compile increments `compiles_total{result="ok"}` exactly once
|
||||
|
||||
**Verified by:** `src/instruments/instruments.test.ts::"successful compile emits ok counter"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Conformance harness `runWasmCompilerConformance`
|
||||
|
||||
The package SHALL ship `runWasmCompilerConformance({ compileFn })` from `@suaveplan/wasm-compiler-javy/testing` with four invariants:
|
||||
1. ABI completeness — `memory` + `_start` + every required WASI import present.
|
||||
2. Deterministic output — same input → same SHA-256.
|
||||
3. Manifest fidelity — `routes` array equals the plugin's declared routes.
|
||||
4. Cross-bridge deployability — produced WASM passes `runWasmHostBridgeConformance({ buildBridge, abiVariant: "wasi-stdio" })`.
|
||||
|
||||
Any alternate compiler implementation MUST pass all four invariants to substitute for the default Javy backend.
|
||||
|
||||
#### Scenario: Harness rejects a compiler that strips `_start`
|
||||
|
||||
**Verified by:** `src/testing/wasm-compiler-conformance/wasm-compiler-conformance.test.ts::"harness rejects compiler missing _start"`
|
||||
Reference in New Issue
Block a user