Initial Commit

This commit is contained in:
2026-05-27 23:29:53 +01:00
commit fbc70f96e6
10000 changed files with 1102152 additions and 0 deletions

View File

@@ -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"`