Move docs/ package mirror into document-navigator plugin

Relocate the ~6k-file docs tree from the repo root into
plugins/document-navigator/docs/ so the plugin is self-contained and
portable. docs/superpowers/ (specs and plans) stays at the repo root.

Moved via cp+rm rather than git mv because directory rename() fails with
EPERM on this WSL /mnt DrvFs mount; git rename-detection pairs the
identical-content files at commit time.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-30 01:10:00 +01:00
parent fe3fedf42e
commit 9bf56a806b
6022 changed files with 12437 additions and 0 deletions

View File

@@ -0,0 +1,47 @@
# `buildGlobalsPolyfill` — Javy guest polyfills
## Purpose
`buildGlobalsPolyfill` returns the hand-rolled JavaScript that installs `URL`, `URLSearchParams`, `atob`, and `btoa` onto the Javy v8.1.1 guest's `globalThis`. Javy ships QuickJS plus the minimal `TextEncoder`/`TextDecoder`/`console`/`Promise` surface its `-J` flags enable; the wider Web Platform globals every suaveplan plugin's `handleRequest` reaches for are absent. Rather than ask plugin authors to dodge those primitives, the shim installs minimum-viable polyfills before the bundle runs.
The polyfill source is plain literal string — byte-stable across every compilation of every plugin — so it never perturbs the determinism cache the orchestrator maintains.
## Coverage
- `globalThis.btoa(input: string): string` — base64 encoder for ASCII / latin-1 input bytes (matches WHATWG semantics for the common case).
- `globalThis.atob(input: string): string` — base64 decoder, tolerant of `=` padding.
- `globalThis.URL(input: string, base?: string)` — parses absolute URLs, exposes `href`/`protocol`/`host`/`hostname`/`port`/`pathname`/`search`/`hash`/`origin`/`searchParams` plus `toString()`.
- `globalThis.URLSearchParams(initial: string)` — `get`/`getAll`/`has` (read-only subset; the trusted-tier consumers do not mutate the search params).
## Out of scope
- WHATWG-spec normalisation (IDN punycode, percent-encoding round-trips, default-port stripping). The polyfill targets the pragmatic 95% case suaveplan plugins exercise — `new URL(req.url).pathname` and `url.searchParams.get(...)`.
- `Request`/`Response`/`fetch` — those would pull in a network primitive the WASI-stdio variant deliberately blocks. Plugins needing outbound HTTP route through the host's capability ABI, not a `fetch` call.
- `crypto.getRandomValues` — the suaveplan plugins this targets do not generate randomness inside the sandbox; the trusted tier owns nonce/secret generation.
## Usage: Basic
```ts
import { buildGlobalsPolyfill, composeAbiShimmedSource } from "@suaveplan/wasm-compiler-javy";
const polyfill = buildGlobalsPolyfill();
const shimmed = composeAbiShimmedSource({ bundledJs: yourBundle });
// `polyfill` is automatically included inside `composeAbiShimmedSource`;
// importing it directly is only useful for tests.
```
## Usage: Advanced
```ts
const src = buildGlobalsPolyfill();
console.log(src.includes("globalThis.URL")); // true
console.log(src.includes("globalThis.atob")); // true
```
## API Reference
- `buildGlobalsPolyfill(): string` — returns the polyfill body as a literal string.
## Implementation Notes
Polyfills are installed under `if (typeof globalThis.X !== 'function')` guards so a future Javy release that natively provides the global silently wins. The URL parser uses one composed RegExp constructor rather than a literal because the shim's source is loaded into Javy and a literal `/.../` could perturb Bun's bundler's source-map plumbing under certain `target: "browser"` paths. The `URLSearchParams` polyfill exposes a deliberately tight read-only surface — enough for typical handlers that decode the query string but not so much that it ever satisfies the assumption a plugin author MAY mutate the params. Plugins needing iteration over query params would need to walk an array snapshot themselves; this is a documented limitation rather than a defect.

View File

@@ -0,0 +1,78 @@
# `composeAbiShimmedSource` — WASI-stdio ABI shim generator
## Purpose
`composeAbiShimmedSource` is step 2 of the TS->WASM compilation pipeline: it concatenates the bundled plugin ESM source with a hand-written WASI-stdio ABI shim. Together they form the complete JavaScript module Javy compiles into the `.wasm` artifact. The shim turns the plugin's `{ routes, handleRequest, start, dispose }` surface into a WASI-stdio guest that reads exactly one length-prefixed JSON envelope from fd 0 per `_start` invocation and writes exactly one length-prefixed reply to fd 1.
The shim is appended (not prepended) so the bundle's top-level `var plugin = ...; export { plugin };` declaration evaluates first, leaving `plugin` reachable as an ordinary lexical binding inside the shim's IIFE. The whole composition is byte-stable across compilations of the same bundle so the determinism invariant in the spec (`tests/integration/determinism.test.ts`) holds.
## Wire format
Every envelope is a length-prefixed JSON frame matching `@suaveplan/api-types/sandbox-bridge` exactly:
- 4-byte **big-endian** unsigned integer header carrying the JSON byte length.
- UTF-8 JSON payload of that length.
The shim pins to `WASI_STDIO_PROTOCOL_VERSION` (currently `1`) — a future breaking change to the envelope shape bumps that constant and forces this shim to be regenerated.
## Hand-rolled validators
The shim cannot import Zod (the full schema would inflate the bundle by ~70 KiB of QuickJS bytecode). Instead, `validateIn` performs a hand-written narrowing on the incoming envelope:
- `routes` / `start` / `dispose` — single-field shape; the validator passes the discriminator through unchanged.
- `request` — verifies a nested `request` field exists as an object; the host bridge has already Zod-validated the inner `SerializedRequest`, so the shim only needs to confirm presence.
A malformed envelope throws an `Error`; the dispatcher's `try { ... } catch` then emits `{kind:"error", code:"WASM_PLUGIN.UNCAUGHT", message}` so the host sees a structured failure rather than a hang.
## Dispatch cases
| Envelope | Plugin call | Reply |
| ------------------- | ------------------------------------------------ | ---------------------------------------------------- |
| `{kind:"routes"}` | none (reads `plugin.routes`) | `{kind:"routes", routes: plugin.routes}` |
| `{kind:"start"}` | `plugin.start?.()` (awaited if a Promise) | `{kind:"ok"}` |
| `{kind:"dispose"}` | `plugin.dispose?.()` (awaited if a Promise) | `{kind:"ok"}` |
| `{kind:"request"}` | `await plugin.handleRequest(env.request)` | `{kind:"response", response: <SerializedResponse>}` |
Any thrown value carrying a `code: string` + `message: string` (the structural shape of a `SuaveplanError`) is converted to `{kind:"error", code, message}` so host-side `interpretReply` can re-throw a faithful host-tier `SuaveplanError`.
## Features
- Synchronous-binary stdin reads via `Javy.IO.readSync(0, buf)` with retry on short reads.
- Synchronous-binary stdout writes via `Javy.IO.writeSync(1, buf)` issued atomically (header + payload in two back-to-back calls).
- `try`-wrapped dispatcher so a thrown `Error` becomes an `{kind:"error"}` reply rather than a `_start` trap.
- Top-level `await` (Javy `-J event-loop=y`) so async `handleRequest`/`start`/`dispose` resolve before the reply frame is written.
- Header comment pins the protocol version and the framing layout for forensic inspection of an emitted artifact.
## Usage: Basic
```ts
import { composeAbiShimmedSource } from "@suaveplan/wasm-compiler-javy";
const { js } = composeAbiShimmedSource({
bundledJs: 'var plugin = { routes: [], handleRequest: async () => ({ status:200, headers:[], bodyBase64:"" }) }; export { plugin };',
});
console.log(js); // bundle + shim, ready for Javy
```
## Usage: Advanced
```ts
import { bundleTs, composeAbiShimmedSource } from "@suaveplan/wasm-compiler-javy";
const { js: bundledJs } = await bundleTs({ entry: "/abs/src/plugin/index.ts" });
const { js: shimmedJs } = composeAbiShimmedSource({ bundledJs });
import { writeFileSync } from "node:fs";
writeFileSync("/tmp/plugin-shimmed.js", shimmedJs);
// Now `javy build /tmp/plugin-shimmed.js -o plugin.wasm -J javy-stream-io=y -J event-loop=y -J text-encoding=y -C deterministic=y`
```
## API Reference
- `composeAbiShimmedSource(options: AbiShimOptions): AbiShimResult` — entrypoint.
- `AbiShimOptions: { bundledJs: string }`.
- `AbiShimResult: { js: string }`.
## Implementation Notes
The shim is intentionally written as a single hand-rolled string array (`buildShimSuffix`) rather than read from a `.js` template file. Two reasons: (1) the package's bundler does not load `.js` as resources, and (2) the byte-stable invariant is easier to enforce when the shim source is in the same compilation unit as the determinism test. The validator inlines its checks rather than building a structural type schema — every branch is one strict-equality compare on `env.kind`. The `asErrorEnvelope` helper degrades gracefully when the throw is not a `SuaveplanError` (an internal `Error`, a string, or anything else) so the host always sees a well-formed `{kind:"error"}` envelope.

View File

@@ -0,0 +1,57 @@
# `suaveplan-wasm-compile` — CLI binary
## Purpose
`suaveplan-wasm-compile` is the package's command-line entry. It parses an argv tail into a `CliInvocation`, calls `compilePluginToWasm`, and emits the result as a single-line JSON record on stdout (success) or as a structured error envelope on stderr (failure). The contract is machine-friendly so CI workflows can pipe the success line into downstream tools (`jq`, sandbox install scripts) and surface failures with `code`/`message`/`context` triplets that operators can grep against.
The CLI is intentionally thin — every argument has a one-to-one mapping to a `compilePluginToWasm` option, and there is no second-tier configuration (env vars, config files). This keeps the CLI's behaviour identical to programmatic callers; a `suaveplan-wasm-compile` failure reproduces by calling `compilePluginToWasm` with the same options.
## Features
- `--out-dir <dir>` mandatory.
- `--optimize-for size|speed` defaulting to `size`.
- `--sourcemaps` boolean toggle.
- `-h`/`--help` prints the usage block and exits 0.
- Structured JSON IO on every code path.
- `CliDeps` injection for tests so stdout / stderr / exit can be captured without the test runner observing the actual process streams.
## Usage: Basic
```sh
bunx suaveplan-wasm-compile ./src/plugin/index.ts --out-dir /tmp/out
# stdout (one line):
# {"wasmPath":"/tmp/out/index.wasm","metadataPath":"/tmp/out/index.wasm.json","sizeBytes":1250000,"compilationMs":312,"routes":2}
```
## Usage: Advanced
```sh
bunx suaveplan-wasm-compile ./src/plugin/index.ts \
--out-dir ./build/wasm \
--optimize-for speed \
--sourcemaps
```
```ts
import { runCli, parseCliArgs } from "@suaveplan/wasm-compiler-javy";
const lines: string[] = [];
await runCli(["./entry.ts", "--out-dir", "./out"], {
stdout: (line) => lines.push(line),
stderr: (line) => lines.push("ERR " + line),
exit: (code) => { if (code !== 0) throw new Error("nonzero"); },
});
```
## API Reference
- `runCli(argv, depsOverride?): Promise<void>` — programmatic entry.
- `parseCliArgs(argv): CliInvocation` — exported for tests.
- `isBinInvocation(argv1): boolean` — pure predicate the module-load guard delegates to; matches `cli.js` or `suaveplan-wasm-compile` suffixes.
- `bootstrap(argv1, argvRest, depsOverride?): Promise<void> | null` — module-load entry. Returns the in-flight `runCli` promise when invoked as the bin script, or `null` when imported as a library.
- `CliInvocation: { entry, outDir, optimizeFor, sourcemaps, help }`.
- `CliDeps: { stdout, stderr, exit, compile? }`.
## Implementation Notes
The argv loop is intentionally hand-rolled rather than using a library (`commander`, `yargs`) so the CLI carries zero runtime deps beyond what the compiler itself needs. Each flag handler advances the cursor explicitly to avoid the off-by-one bugs typical of greedy parsers. The CLI does NOT swallow unknown flags — they raise `INVALID_ARGS` with exit code 2 (distinct from `1` reserved for pipeline failures). The bin guard at the bottom of the file checks `process.argv[1]` so importing the module for tests does not auto-execute `runCli` against the test runner's argv. Errors thrown by `compilePluginToWasm` are caught and serialised with their full `context` payload (sanitised by the framework) so operators can reproduce the failure from the JSON alone.

View File

@@ -0,0 +1,56 @@
# `bundleTs` — Bun.build wrapper
## Purpose
`bundleTs` is step 1 of the TS->WASM compilation pipeline: it consumes the plugin's TypeScript entry, runs `Bun.build({ target: "browser", format: "esm" })`, and returns the bundled ESM JavaScript source as a string. The browser target is deliberate — it surfaces any transitive `node:*` import that the WASI-stdio ABI shim cannot polyfill (the shim only patches `process.stdin`/`process.stdout`/`crypto.getRandomValues`/`TextEncoder`/`TextDecoder`) as a fail-fast `BROWSER_INCOMPAT_DEPENDENCY` error rather than letting it leak through to Javy.
The function is independent of the surrounding pipeline so plugin authors can call it directly when they want a bundled-but-not-yet-shimmed view of their entry — useful for diffing the bundle digest across refactors without rerunning Javy.
## Features
- Wraps `Bun.build` with the WASM-tier-correct config (browser target, ESM format, no minify, optional inline sourcemap).
- Detects banned `node:*` specifiers in both thrown errors AND non-empty `logs[]` arrays — the more-specific `BROWSER_INCOMPAT_DEPENDENCY` always wins over the generic `BUNDLE_FAILED`.
- Exports `extractBannedSpecifier` for the test suite to assert the substring matcher's coverage.
- Fails fast with `BUNDLE_FAILED` + `context.reason="bun_runtime_unavailable"` when invoked outside Bun (Node-only CI environments).
## Usage: Basic
```ts
import { bundleTs } from "@suaveplan/wasm-compiler-javy";
const { js } = await bundleTs({ entry: "/abs/src/plugin/index.ts" });
console.log(js.length, "bytes of bundled ESM");
```
## Usage: Advanced
```ts
import { bundleTs, WasmCompilerError } from "@suaveplan/wasm-compiler-javy";
try {
const result = await bundleTs({
entry: "/abs/src/plugin/index.ts",
sourcemaps: true,
});
console.log(result.js);
} catch (err) {
if (
err instanceof WasmCompilerError &&
err.code === "WASM_COMPILER.BROWSER_INCOMPAT_DEPENDENCY"
) {
console.error("offending import:", err.context.specifier);
}
throw err;
}
```
## API Reference
- `bundleTs(options: BundleTsOptions): Promise<BundleTsResult>` — entrypoint.
- `BundleTsOptions: { entry: string; sourcemaps?: boolean }`.
- `BundleTsResult: { js: string; sourceMap?: string }`.
- `extractBannedSpecifier(message: string): string | null` — exported for tests.
## Implementation Notes
The function reaches into `globalThis.Bun` rather than importing `bun` so the package builds cleanly under Node — at runtime the absence of `Bun` is itself a `BUNDLE_FAILED` outcome. The bundler is configured with `external: []` to force every transitive import to be inlined; this is critical because Javy only consumes self-contained JS — anything left as a runtime import would resolve against the (non-existent) module graph inside the WASM. `extractBannedSpecifier` walks the alphanumeric/dash/slash/underscore token after the `node:` prefix so the matcher captures bare specifiers (`node:fs`) without including trailing punctuation from the diagnostic. The shim assumes browser-safe globals (no `Buffer`, no `URL.fileURLToPath`), so any plugin authoring violating that constraint will trip the bundle step before Javy is even spawned.

View File

@@ -0,0 +1,66 @@
# `compilePluginToWasm` — top-level pipeline orchestrator
## Purpose
`compilePluginToWasm` is the single public entrypoint the package exposes — every consumer (the CLI binary, plugin authors, and CI gates) calls it. It composes the seven internal steps (bundle, shim, locate Javy, invoke Javy, validate ABI, extract routes, write manifest) into one linear async function and reports the result as a `CompilePluginToWasmResult`.
The orchestrator owns the result-label taxonomy used by the OTel instruments: which step failed determines the `result` label stamped on every metric emission. Operators can then split the compile failure rate by step in a single dashboard without re-reading the source. Every other failure mode (a missing file, a malformed envelope, a Javy crash) maps to one of the six `WASM_COMPILER.*` codes.
## Features
- One-call pipeline — no setup required beyond a plugin entry path and an output directory.
- Idempotent: the output directory is created with `mkdir -p` semantics.
- Bun's `Bun.build` is the bundle backbone — TypeScript is parsed natively, no extra TS toolchain required.
- The intermediate `<basename>.shimmed.js` is left on disk so operators can inspect exactly what Javy compiled.
- `compiledAt` override for reproducible-build CI.
## Usage: Basic
```ts
import { compilePluginToWasm } from "@suaveplan/wasm-compiler-javy";
const result = await compilePluginToWasm({
pluginEntry: "/abs/src/plugin/index.ts",
outDir: "/tmp/plugin-wasm",
});
console.log(result.wasmPath, "bytes:", result.sizeBytes);
```
## Usage: Advanced
```ts
import {
compilePluginToWasm,
createWasmCompilerInstruments,
} from "@suaveplan/wasm-compiler-javy";
const instruments = createWasmCompilerInstruments();
const result = await compilePluginToWasm({
pluginEntry: "/abs/src/plugin/index.ts",
outDir: "/tmp/plugin-wasm",
optimizeFor: "speed",
sourcemaps: true,
pluginName: "@suaveplan/my-plugin",
instruments,
compiledAt: "2026-05-27T00:00:00.000Z",
});
console.log(result.manifest.routes.length, "routes compiled in", result.compilationMs, "ms");
```
## API Reference
- `compilePluginToWasm(options: CompilePluginToWasmOptions): Promise<CompilePluginToWasmResult>` — entrypoint.
- `CompilePluginToWasmOptions: { pluginEntry, outDir, optimizeFor?, sourcemaps?, javyVersion?, pluginName?, instruments?, compiledAt? }`.
- `CompilePluginToWasmResult: { wasmPath, metadataPath, sizeBytes, compilationMs, manifest }`.
- `WASM_COMPILER_VERSION` — the version string the manifest pins.
## Determinism cache
Javy v8.1.1 with `-J event-loop=y` (required for top-level `await` in our ABI shim) is **not** byte-deterministic across invocations — even with `-C deterministic=y -C source=omitted`, two consecutive `javy build` runs over identical input produce WASM that differs in ~2 bytes inside a QuickJS atom-hash table. The official `-C deterministic=y` flag only stabilises the pre-init clock / RNG, not the JS engine's atom hashing.
To satisfy the spec's determinism invariant without working around Javy internals, the orchestrator **content-addresses Javy's output**: every compile keys on `SHA-256(shimmed_js) + "-" + optimizeFor` and stores the WASM at `<os.tmpdir>/suaveplan-wasm-compile/<key>/<key>.wasm`. A second compile with identical inputs short-circuits to the cached bytes. Two consecutive calls in the same job always agree; cross-job runs on the same machine also agree until the system tmpdir is pruned. CI workers that prune `/tmp` between jobs will recompile (Javy may yield a different artifact then), but each run's *self-consistency* — what the spec actually requires for the determinism scenario — holds unconditionally.
## Implementation Notes
The result-label flow uses a single `resultLabel` variable updated in each step's `catch`; the `finally` block emits both counter (`recordCompile`) and histogram (`recordDuration`) so a successful compile produces exactly one `ok`-labelled increment in each instrument. The pipeline writes `<basename>.shimmed.js` next to the `.wasm` so reproducing a failed compile only requires re-running `javy build <basename>.shimmed.js`. Reading the WASM back from disk (rather than capturing the bytes Javy wrote) lets validators run against the on-disk representation, catching the rare case where Javy's output buffer differs from what the OS-level write actually persisted. The `pluginName` defaults to the entry file's basename without extension — adequate for tooling but the CLI / advanced caller should pass the manifest-declared name.

View File

@@ -0,0 +1,65 @@
# `WASM_COMPILER` error codes
## Purpose
This module defines the six stable error codes raised by the TS->WASM compiler and the concrete `WasmCompilerError` class every production path throws. The codes name the failure modes exhaustively: the bundle step (`BUNDLE_FAILED`), bundler input rejections (`BROWSER_INCOMPAT_DEPENDENCY`), the platform-locate step (`JAVY_PLATFORM_MISMATCH`), the Javy spawn step (`JAVY_INVOCATION_FAILED`), the post-Javy ABI reflection (`ABI_VALIDATION_FAILED`), and the route-extraction probe (`MANIFEST_MISSING_ROUTES`).
Stability matters because callers (`@suaveplan/api-host` operators, CI gates, plugin operators) branch on `error.code` rather than parsing free-form messages. The code registry is built via `@suaveplan/error`'s `defineCodes` so each code is a frozen `"WASM_COMPILER.<NAME>"` string literal, and TypeScript can narrow `error.code` to the `WasmCompilerCode` union for exhaustive switches.
## Features
- `WASM_COMPILER_CODES` — frozen `defineCodes` registry of all six codes.
- `WasmCompilerCode` — exhaustive union type.
- `WasmCompilerError` — `SuaveplanError` subclass with a **public** constructor (rule §19), carrying the namespaced code and i18n message key.
- `WASM_COMPILER_ERROR_NAME` — stable `error.name` (`"WasmCompilerError"`) for cross-realm `instanceof` fallbacks.
## Usage: Basic
```ts
import { WASM_COMPILER_CODES, WasmCompilerError } from "@suaveplan/wasm-compiler-javy";
throw new WasmCompilerError(
WASM_COMPILER_CODES.JAVY_PLATFORM_MISMATCH,
"error.wasm_compiler.javy_platform_mismatch",
{ context: { requested: "freebsd-x64" } }
);
```
## Usage: Advanced
```ts
import { WasmCompilerError } from "@suaveplan/wasm-compiler-javy";
try {
await compilePluginToWasm({ pluginEntry, outDir });
} catch (err) {
if (err instanceof WasmCompilerError) {
switch (err.code) {
case "WASM_COMPILER.BUNDLE_FAILED":
case "WASM_COMPILER.BROWSER_INCOMPAT_DEPENDENCY":
console.error("bundle:", err.message);
break;
case "WASM_COMPILER.JAVY_PLATFORM_MISMATCH":
case "WASM_COMPILER.JAVY_INVOCATION_FAILED":
console.error("javy:", err.message);
break;
case "WASM_COMPILER.ABI_VALIDATION_FAILED":
case "WASM_COMPILER.MANIFEST_MISSING_ROUTES":
console.error("post-compile:", err.message);
break;
}
}
throw err;
}
```
## API Reference
- `WASM_COMPILER_CODES: Readonly<Record<"BUNDLE_FAILED" | "BROWSER_INCOMPAT_DEPENDENCY" | "JAVY_PLATFORM_MISMATCH" | "JAVY_INVOCATION_FAILED" | "ABI_VALIDATION_FAILED" | "MANIFEST_MISSING_ROUTES", string>>`.
- `type WasmCompilerCode = (typeof WASM_COMPILER_CODES)[keyof typeof WASM_COMPILER_CODES]`.
- `class WasmCompilerError extends SuaveplanError<WasmCompilerCode>` — public constructor taking `(code, messageKey, options?)`.
- `const WASM_COMPILER_ERROR_NAME = "WasmCompilerError"`.
## Implementation Notes
The class extends the framework's `SuaveplanError<TCode>`, providing the namespace-qualified `code`, an i18n `messageKey`, and the `cause`/`context` plumbing every `@suaveplan` package shares. The public constructor satisfies rule §19 (catches stale `// biome-ignore` suppressions where subclasses redeclared protected constructors as private). Every throw site in this package allocates one of these — no naked `Error`s — so `instanceof WasmCompilerError` is sufficient to distinguish compiler failures from unrelated runtime errors at the boundary.

View File

@@ -0,0 +1,58 @@
# OTel instruments — `@suaveplan/wasm-compiler-javy`
## Purpose
This module publishes the three OpenTelemetry instruments the compiler emits on every production path. Operators correlate compiler health with downstream sandbox/host metrics by joining on the same trace/resource attributes — the compiler intentionally exposes its outcome as a `result` label (`ok` / `bundle_failed` / `javy_failed` / `abi_failed` / `manifest_failed`) so a single PromQL/MetricsQL query can split successful artifact emissions from each failure mode.
The factory `createWasmCompilerInstruments` resolves a `Meter` from either a caller-supplied `MeterProvider` (production hosts) or `@opentelemetry/api#metrics` (CI gates and tests). All three instruments are created eagerly on construction so the first compile observed by an exporter does not race factory initialisation.
## Features
- Counter `wasm_compiler.compiles_total{result}` — every compile attempt, labelled by outcome.
- Histogram `wasm_compiler.compile_duration_ms{result}` — wall-clock compile latency in milliseconds, split by outcome.
- Counter `wasm_compiler.abi_validation_failures_total{missing_export}` — bumped per missing required export discovered by the ABI validator.
## Usage: Basic
```ts
import { createWasmCompilerInstruments } from "@suaveplan/wasm-compiler-javy";
const instr = createWasmCompilerInstruments();
instr.recordCompile("ok");
instr.recordDuration("ok", 412);
```
## Usage: Advanced
```ts
import { MeterProvider } from "@opentelemetry/sdk-metrics";
import { createWasmCompilerInstruments } from "@suaveplan/wasm-compiler-javy";
const provider = new MeterProvider();
const instr = createWasmCompilerInstruments({ meterProvider: provider });
try {
await compilePluginToWasm({ pluginEntry, outDir });
instr.recordCompile("ok");
} catch (err) {
if (err.code === "WASM_COMPILER.ABI_VALIDATION_FAILED") {
for (const exp of err.context.missing as string[]) {
instr.recordAbiFailure(exp);
}
instr.recordCompile("abi_failed");
} else {
instr.recordCompile("javy_failed");
}
}
```
## API Reference
- `WASM_COMPILER_COUNTER_NAMES` — frozen tuple of the two counter names.
- `WASM_COMPILER_HISTOGRAM_NAMES` — frozen tuple containing `"wasm_compiler.compile_duration_ms"`.
- `type WasmCompilerResult = "ok" | "bundle_failed" | "javy_failed" | "abi_failed" | "manifest_failed"`.
- `createWasmCompilerInstruments(config?): WasmCompilerInstruments` — returns `{ meter, recordCompile, recordDuration, recordAbiFailure }`.
## Implementation Notes
The factory resolves a `Meter` for the package-named meter (`@suaveplan/wasm-compiler-javy`) and creates one `Counter` per counter name and one `Histogram` for the duration metric. Every recording method threads through OTel's `add` / `record` APIs with an attributes object — no SDK-specific allocation occurs in the hot path. Histogram unit is fixed at `"ms"` to keep dashboards aligned across the sandbox and bridge metrics that share the same unit. The compiler's `compile.ts` orchestrator owns the per-step `recordCompile` / `recordDuration` calls; the validator owns `recordAbiFailure`. This split keeps the instruments stateless and the orchestrator's outcome-classification logic in one place.

View File

@@ -0,0 +1,57 @@
# `invokeJavy` — synchronous Javy CLI spawner
## Purpose
`invokeJavy` is step 4 of the TS->WASM compilation pipeline: a thin wrapper around `child_process.spawnSync` that runs `javy build <input.js> -o <output.wasm>` with the exact runtime flag combination the WASI-stdio ABI shim relies on. The function is synchronous because the rest of the pipeline is already sequential and `spawnSync` is the simplest correct way to bound the child's lifetime — there is no benefit to making the spawn itself async.
The flag combination is non-negotiable: `-J javy-stream-io=y` enables the `Javy.IO.readSync`/`writeSync` synchronous-binary stdio API the shim's framers use; `-J event-loop=y` enables top-level `await` so `handleRequest`/`start`/`dispose` async hooks resolve before the reply frame is written; `-J text-encoding=y` exposes `TextEncoder`/`TextDecoder`; and `-C deterministic=y` is what makes the determinism invariant possible (Javy uses fixed clocks + zero-filled RNG during pre-initialisation).
## Features
- Synchronous spawn — the orchestrator's `compile.ts` stays linear and easy to reason about.
- Deterministic environment: `LANG=C`, `LC_ALL=C`, `TZ=UTC` so Javy's log timestamps and locale-sensitive diagnostics stay stable across machines.
- Failure-mode classification: any non-zero exit OR spawn error becomes `WASM_COMPILER.JAVY_INVOCATION_FAILED` with `context.exitCode`, `context.stderr` (truncated to 8 KiB), and `context.command` (full argv).
- `spawn` injection point so tests assert command-line composition without a real child process.
- Speed profile turns on `-J simd-json-builtins=y` for plugins whose hot path is `JSON.parse`/`JSON.stringify`.
## Usage: Basic
```ts
import { invokeJavy, locateJavyBinary } from "@suaveplan/wasm-compiler-javy";
const { binary } = await locateJavyBinary();
const { command, stdout, stderr } = invokeJavy({
binary,
inputPath: "/tmp/plugin-shimmed.js",
outputPath: "/tmp/plugin.wasm",
});
console.log("ran:", command.join(" "));
```
## Usage: Advanced
```ts
import { invokeJavy } from "@suaveplan/wasm-compiler-javy";
import { spawnSync } from "node:child_process";
// Wrap the spawn to add a timeout the default API does not expose.
const result = invokeJavy({
binary: "/abs/javy",
inputPath: "/tmp/in.js",
outputPath: "/tmp/out.wasm",
optimizeFor: "speed",
spawn: (cmd, args, opts) =>
spawnSync(cmd, args, { ...opts, timeout: 30_000 }),
});
```
## API Reference
- `invokeJavy(options: InvokeJavyOptions): InvokeJavyResult` — entrypoint.
- `InvokeJavyOptions: { binary, inputPath, outputPath, optimizeFor?, spawn? }`.
- `InvokeJavyResult: { command, stdout, stderr }`.
- `truncate(input: string, limit: number): string` — exported for tests.
## Implementation Notes
The function rejects spawn errors (Node `EACCES`/`ENOENT`/etc.) AND non-zero exits with the same error code so callers do not need to branch — they can read `context.exitCode` (a number) or check for `cause` (Node's spawn `Error`). The captured stderr is bounded at 8 KiB; Javy emits a single short error line on most failures so the cap rarely trips. Determinism note: the env vars matter — without them, Javy's logger embeds local time/locale into its (rare) diagnostic output, which would in turn poison the bundle's source-digest sidecar. The `optimizeFor` knob is currently a 2-way switch; future Javy releases may add a `none`/`debug` profile and this enum will extend with backward-compat default `"size"`.

View File

@@ -0,0 +1,55 @@
# `locateJavyBinary` — platform-aware Javy CLI resolver
## Purpose
`locateJavyBinary` is step 3 of the TS->WASM compilation pipeline. Given the current process's platform-arch pair, it resolves the matching `@suaveplan/javy-cli-*` wrapper package — one of five (`linux-x64`, `linux-arm64`, `darwin-x64`, `darwin-arm64`, `win32-x64`) — and calls its `getJavyBinary()` helper to retrieve the absolute path of the platform-appropriate Javy binary. The result is a `JavyLocation` record the orchestrator hands to `child_process.spawnSync` in `javy-invoke.ts`.
The locator is the single source of truth for "what platforms do we support?". Adding a sixth wrapper means adding an entry to `JAVY_WRAPPER_PACKAGES` and an arm in `platformKey`; the rest of the pipeline picks it up automatically. Unsupported platform-arch pairs fail fast with `WASM_COMPILER.JAVY_PLATFORM_MISMATCH` so operators get a structured signal rather than a missing-binary spawn error.
## Features
- Pure mapping from `(platform, arch)` to wrapper package — testable without spawning anything.
- Dynamic `import(...)` resolution keeps wrapper packages out of the static module graph, so a CI environment missing four of the five wrappers still imports cleanly.
- Test-friendly injection: callers may pass `{ platform, arch, resolver }` overrides so `javy-locate.test.ts` exercises every branch without actually swapping the binary.
- Three failure sub-modes (`wrapper_not_installed`, `wrapper_missing_export`, `binary_lookup_threw`) are surfaced via `context.reason` so operators can distinguish a misconfigured workspace from a corrupt download.
## Usage: Basic
```ts
import { locateJavyBinary } from "@suaveplan/wasm-compiler-javy";
const { binary, platform } = await locateJavyBinary();
console.log("javy at", binary, "for", platform);
```
## Usage: Advanced
```ts
import { locateJavyBinary, platformKey, JAVY_WRAPPER_PACKAGES } from "@suaveplan/wasm-compiler-javy";
// Sanity check before invoking — useful in cross-build CI workflows.
const expected = JAVY_WRAPPER_PACKAGES[platformKey(process.platform, process.arch) ?? "linux-x64"];
const location = await locateJavyBinary({
platform: "linux",
arch: "x64",
resolver: async (spec) => {
if (spec === expected) {
return { getJavyBinary: () => "/opt/javy-cached/javy" };
}
throw new Error("unexpected resolver call: " + spec);
},
});
```
## API Reference
- `locateJavyBinary(options?: LocateJavyOptions): Promise<JavyLocation>` — entrypoint.
- `LocateJavyOptions: { platform?, arch?, resolver? }` — all fields optional.
- `JavyLocation: { binary: string; platform: JavyPlatformKey; wrapperPackage: string }`.
- `platformKey(platform, arch): JavyPlatformKey | null` — exported for tests.
- `JAVY_WRAPPER_PACKAGES: Readonly<Record<JavyPlatformKey, string>>` — registry.
## Implementation Notes
The dynamic-import resolver is the only piece of the locator that needs runtime privileges; everything else is a pure data lookup. The resolver default uses the standard `import(specifier)` form, which both Node and Bun resolve against the workspace's installed dependencies. When a wrapper is missing (the common case: a Linux x64 dev box that never installed the macOS-arm64 wrapper), the import rejection becomes `JAVY_PLATFORM_MISMATCH` with `cause` pointing at the original `ModuleNotFoundError`. Tests override `resolver` rather than spawning child processes — this is the rule §2 exception called out in tasks 5.5: we're testing platform branch logic, not a service.

View File

@@ -0,0 +1,58 @@
# `writeManifest` — `<basename>.wasm.json` sidecar emitter
## Purpose
`writeManifest` is step 7 of the TS->WASM compilation pipeline: it serialises the captured `routes`, the ABI variant pin, the Javy version, the compiler version, the RFC 3339 UTC compile timestamp, and the SHA-256 of the pre-Javy bundle into a `<basename>.wasm.json` sidecar next to the `.wasm` artifact. Hosts (and CI gates) read this sidecar to verify the artifact's provenance and to register routes against their pipeline router without booting the WASM.
Refusing empty `routes` is intentional — a routeless plugin cannot route a single request through the bridge, so shipping it would surface as a deploy-time failure rather than a build-time one. The sidecar is the build-time guard.
## Features
- SHA-256 source digest fingerprints the *pre-Javy* bundle so two artifacts with the same digest are byte-identical at the source level even if the WASM bytes diverge across Javy upgrades.
- RFC 3339 UTC timestamps via `new Date().toISOString()`; callers can pass `compiledAt` to lock the timestamp for reproducible-build CI.
- `abiVariant: "wasi-stdio"` constant — the type system narrows this so downstream consumers can branch on the variant without a runtime string compare.
- Refuses empty `routes` via `WASM_COMPILER.MANIFEST_MISSING_ROUTES`.
- `fsWrite` injection lets tests assert payload composition without touching disk.
## Usage: Basic
```ts
import { writeManifest } from "@suaveplan/wasm-compiler-javy";
const manifest = await writeManifest({
manifestPath: "/tmp/plugin.wasm.json",
routes,
bundledJs,
javyVersion: "v8.1.1",
compilerVersion: "0.1.0",
});
```
## Usage: Advanced
```ts
import { writeManifest } from "@suaveplan/wasm-compiler-javy";
const captured: Array<[string, string]> = [];
const manifest = await writeManifest({
manifestPath: "/tmp/plugin.wasm.json",
routes,
bundledJs,
javyVersion: "v8.1.1",
compilerVersion: "0.1.0",
compiledAt: "2026-05-27T00:00:00.000Z", // pin for reproducible build
fsWrite: async (path, data) => { captured.push([path, data]); },
});
console.log(captured[0][1]); // raw manifest JSON
```
## API Reference
- `writeManifest(options: WriteManifestOptions): Promise<CompiledPluginManifest>` — entrypoint.
- `WriteManifestOptions: { manifestPath, routes, bundledJs, javyVersion, compilerVersion, compiledAt?, fsWrite? }`.
- `CompiledPluginManifest: { routes, abiVersion, abiVariant:"wasi-stdio", javyVersion, compilerVersion, compiledAt, sourceDigest }`.
- `COMPILER_ABI_VERSION: "1.0.0"` — the compiler ABI version constant pinned in every manifest.
## Implementation Notes
The SHA-256 is computed over the pre-Javy bundle rather than the post-Javy WASM because (a) the bundle is what the source-of-truth content really is — Javy's output can change across upstream versions for the same input — and (b) the bundle digest is stable across compiler refactors so CI cache keys built on it stay valid. The manifest is written with 2-space JSON indentation for human inspection; the host parses with `JSON.parse` so the formatting does not affect downstream consumers. Empty-routes guarding is the only validation here — the orchestrator's `compile.ts` re-asserts after writing too, so a mis-wired call path cannot leak through.

View File

@@ -0,0 +1,49 @@
# `extractRoutes` — host-side WASI route probe
## Purpose
`extractRoutes` is step 6 of the TS->WASM compilation pipeline: it boots the produced `.wasm` artifact under a host-side WASI runner (the same `createWasiStdioRunner` `@suaveplan/api-sandbox-wasm-host-bridge` uses at deploy time), sends a single `{kind:"routes"}` envelope, and captures the `{kind:"routes", routes}` reply. The captured `routes: readonly SerializedRoute[]` array feeds the manifest step.
Reusing the bridge's runner is deliberate. If the compiler probed routes via a bespoke WASI snapshot, a guest that booted under the probe but tripped on the bridge's snapshot (or vice-versa) would slip through. By going through the exact code path the host will run, the compile-time probe exercises the same instantiation, memory binding, stdio framing, and decode logic — eliminating "compiles but won't deploy" failure modes at the source.
## Features
- Single-envelope round-trip — `runner.exchange({kind:"routes"}, "routes")` returns synchronously.
- `pluginName` threads through into bridge error context so a malformed routes reply surfaces with the plugin's name attached.
- `stdioTimeoutMs` knob bumps the per-step deadline for plugins that need to do heavy module-scope init before the routes reply.
- Guaranteed cleanup: `runner.dispose()` runs in a `finally` block whether the exchange succeeded or threw.
## Usage: Basic
```ts
import { extractRoutes, validateAbi } from "@suaveplan/wasm-compiler-javy";
const { module } = await validateAbi({ wasmBytes });
const { routes } = extractRoutes({ module, pluginName: "my-plugin" });
console.log(routes); // [{ method:"GET", path:"/hello" }, ...]
```
## Usage: Advanced
```ts
import { extractRoutes } from "@suaveplan/wasm-compiler-javy";
const { routes } = extractRoutes({
module,
pluginName: "my-plugin",
stdioTimeoutMs: 30_000, // give a slow plugin 30s of init headroom
});
if (routes.length === 0) {
throw new Error("plugin declares no routes — refusing to ship");
}
```
## API Reference
- `extractRoutes(options: ExtractRoutesOptions): ExtractRoutesResult` — entrypoint.
- `ExtractRoutesOptions: { module: WebAssembly.Module; pluginName: string; stdioTimeoutMs?: number }`.
- `ExtractRoutesResult: { routes: readonly SerializedRoute[] }`.
## Implementation Notes
The runner expects a `WasmSandboxHandle` for dispose chaining but accepts an `undefined` sandbox — the compile-time probe has no sandbox to chain against, so the field is omitted. Should the reply envelope come back with an unexpected `kind` (e.g. an `{kind:"error"}` from the plugin choking on its own `routes` getter), the function raises `ABI_VALIDATION_FAILED` with `context.reason="routes_reply_kind"` and the received kind. Empty `routes` arrays are deliberately NOT raised here — the manifest step owns that check via `WASM_COMPILER.MANIFEST_MISSING_ROUTES` so this function remains a pure probe.

View File

@@ -0,0 +1,66 @@
# `runWasmCompilerConformance` — alternate-backend contract harness
## Purpose
`runWasmCompilerConformance` is the conformance harness any alternate compiler backend (a future Component-Model emitter, a hypothetical wasm-tool-chain rewrite) MUST pass to substitute for the default Javy-backed pipeline. It encodes the four invariants the spec demands as a `describe` block that asserts the implementation produces an artifact compatible with both the ABI validator and the host bridge.
The harness is intentionally compiler-agnostic — it takes a `compileFn` plus a fixture-entry path and exercises the implementation through its public surface. The four invariants (ABI completeness, deterministic output, manifest fidelity, cross-bridge deployability) cover the contract the host depends on.
## Features
- Pure `vitest`-style asserter — uses `@suaveplan/testing/runner` so it composes with every existing test config.
- ABI invariant assertions against the same `REQUIRED_EXPORTS` / `REQUIRED_WASI_IMPORTS` lists the production validator uses.
- Determinism assertion: compile twice, SHA-256 must match.
- Manifest fidelity: assert the manifest is a superset of the caller-supplied `expectedRoutes`.
- Optional bridge-deploy probe — when supplied, exercises the host-bridge install path against the artifact for end-to-end coverage.
## Usage: Basic
```ts
import { runWasmCompilerConformance, compilePluginToWasm } from "@suaveplan/wasm-compiler-javy/testing";
runWasmCompilerConformance({
compileFn: compilePluginToWasm,
fixtureEntry: "/abs/tests/fixtures/tiny-plugin/src/index.ts",
outDir: "/tmp/conformance-out",
expectedRoutes: [
{ method: "GET", path: "/hello" },
{ method: "POST", path: "/echo" },
],
});
```
## Usage: Advanced
```ts
import { runWasmCompilerConformance } from "@suaveplan/wasm-compiler-javy/testing";
import { createSandboxedWasmPluginModule } from "@suaveplan/api-sandbox-wasm-host-bridge";
import { readFile } from "node:fs/promises";
runWasmCompilerConformance({
compileFn: myAlternativeCompiler,
fixtureEntry,
outDir,
expectedRoutes,
bridgeDeploy: async (wasmPath) => {
const bytes = await readFile(wasmPath);
const wasmModule = await WebAssembly.compile(bytes);
createSandboxedWasmPluginModule({
abiVariant: "wasi-stdio",
sandbox: builtSandbox,
manifest,
wasmModule,
});
},
});
```
## API Reference
- `runWasmCompilerConformance(options: RunWasmCompilerConformanceOptions): void` — invokes a `describe`+`it` block.
- `RunWasmCompilerConformanceOptions: { compileFn, fixtureEntry, outDir, expectedRoutes, bridgeDeploy? }`.
- `ConformanceCompileFn` — the compiler shape accepted by the harness.
## Implementation Notes
The harness is structured as a free-standing function the caller invokes inside its own test file — this keeps it composable with other suites the package may add (instrument observation, route-extract failure modes). Each invariant is a separate `it` so a partial pass surfaces precisely which contract broke. Determinism is checked end-to-end (compile twice via `compileFn`, SHA-256 the artifact) so the harness validates the full pipeline's determinism, not just Javy's. The `bridgeDeploy` callback receives only the absolute wasm path because the bridge's install API depends on additional sandbox + manifest plumbing the harness has no opinion on.

View File

@@ -0,0 +1,50 @@
# `validateAbi` — post-Javy module reflection
## Purpose
`validateAbi` is step 5 of the TS->WASM compilation pipeline: it runs `WebAssembly.compile(bytes)` against Javy's output and reflects the resulting module to assert the WASI-stdio variant's surface contract. The contract has two halves: required *exports* (`memory` + `_start`) and required *imports* (`wasi_snapshot_preview1.{fd_read, fd_write, environ_get, environ_sizes_get, proc_exit}`).
Mirroring the host bridge's `REQUIRED_WASI_STDIO_IMPORTS` list intentionally: the bridge instantiates the module against its own snapshot of those imports, and any future divergence between the lists would cause one half of the system to ship validations the other cannot satisfy. Single-sourcing the list in spec ensures the bridge and compiler stay in lock-step; this module hard-codes its own copy because cycling the import via `@suaveplan/api-sandbox-wasm-host-bridge` would pull a 30+ file graph into the compiler's static surface.
## Features
- One-shot `WebAssembly.compile` returns both the validation verdict AND a reusable `Module` — the orchestrator threads the same `Module` into the route-extract step so we never compile twice.
- Failure mode `compile_failed` (`WebAssembly.compile` threw) is separated from `ABI_VALIDATION_FAILED` (compile succeeded, exports/imports incomplete) via `context.reason`.
- Per-missing-item `recordAbiFailure` increments thread through the optional `instruments` so dashboards can split the failure rate by which symbol vanished — useful when a Javy upgrade silently drops `clock_time_get` or similar.
- Missing list is sorted for diagnostic stability.
## Usage: Basic
```ts
import { validateAbi } from "@suaveplan/wasm-compiler-javy";
import { readFile } from "node:fs/promises";
const bytes = await readFile("/tmp/plugin.wasm");
const { module } = await validateAbi({ wasmBytes: bytes });
```
## Usage: Advanced
```ts
import { validateAbi, createWasmCompilerInstruments } from "@suaveplan/wasm-compiler-javy";
const instr = createWasmCompilerInstruments();
try {
const { module } = await validateAbi({ wasmBytes, instruments: instr });
// forward `module` to route-extract
} catch (err) {
// `recordAbiFailure` has already counted the missing exports for the dashboard.
throw err;
}
```
## API Reference
- `validateAbi(options: ValidateAbiOptions): Promise<ValidateAbiResult>` — entrypoint.
- `ValidateAbiOptions: { wasmBytes: Uint8Array; instruments?: WasmCompilerInstruments }`.
- `ValidateAbiResult: { module: WebAssembly.Module }`.
- `REQUIRED_WASI_IMPORTS` / `REQUIRED_EXPORTS` — frozen string-array registries.
## Implementation Notes
`WebAssembly.compile` is the only piece of the validator that crosses the JS/WASM boundary. The reflection methods `WebAssembly.Module.imports(module)` and `WebAssembly.Module.exports(module)` are pure JS — they walk parsed metadata without touching memory or invoking the runtime. The compiled `Module` is opaque to the validator but the orchestrator hands it to the route-extract step, which instantiates it against a host-built WASI snapshot to fish the routes out. The implementation deliberately does NOT cycle through `@suaveplan/api-sandbox-wasm-host-bridge` so the compiler's static graph stays compact; the bridge re-validates anyway when its `buildWasiStdioInstall` runs, so a one-pass divergence cannot leak past the host's installPlugin call.