Files
2026-05-27 23:29:53 +01:00

83 lines
5.1 KiB
Markdown

# Capability Spec Delta: api-plugin-example (dual-shape)
**Package**: `@suaveplan/api-plugin-example`
**Status**: PROPOSED
## ADDED Requirements
### Requirement: The package SHALL expose a `./wasm` subpath surfacing a WASI-stdio-shaped plugin
`@suaveplan/api-plugin-example/wasm` SHALL export `const plugin` whose shape is `{ routes: readonly SerializedRoute[]; handleRequest(req: SerializedRequest): Promise<SerializedResponse>; start?(): void | Promise<void>; dispose?(): void | Promise<void> }`. The wire types are imported from `@suaveplan/api-types/sandbox-bridge`.
#### Scenario: wasm subpath imports cleanly under browser-target bundling
- **WHEN** `Bun.build({ entrypoints: ["packages/api-stack/api/api-plugin-example/src/wasm/index.ts"], target: "browser", format: "esm" })` runs
- **THEN** the build succeeds AND the produced ESM bundle imports no `node:*` specifier AND no Hono/OTel-SDK specifier
**Verified by:** `tests/wasm/browser-bundle.test.ts::"wasm subpath bundles for browser target"`
---
### Requirement: The `./wasm` subpath SHALL have a constrained dependency graph
A new lint script `bun scripts/lint/check-wasm-subpath-deps.ts` MUST walk every static import from `src/wasm/index.ts` transitively and reject any specifier matching the wasm-tier ban list: `@suaveplan/api-host`, `hono`, `@hono/*`, `@opentelemetry/sdk-metrics`, `@opentelemetry/sdk-trace-base`, `@opentelemetry/sdk-node`, any `node:*` specifier, and any package whose own dependency tree drags one of the above. The lint MUST be wired into `bunx turbo lint` so CI rejects accidental dependency creep.
#### Scenario: Adding `import "hono"` inside `src/wasm/` fails the lint
- **WHEN** a contributor adds `import { Hono } from "hono"` to any file reachable from `src/wasm/index.ts`
- **THEN** `bun scripts/lint/check-wasm-subpath-deps.ts` exits non-zero with a message naming `hono` and the importing file
**Verified by:** `scripts/lint/check-wasm-subpath-deps.test.ts::"rejects banned specifier in wasm subpath"`
---
### Requirement: Trusted-tier and wasm-tier surfaces SHALL share a single domain core
The package SHALL contain a `src/domain/` directory with pure (no IO, no Hono, no OTel SDK, no Request/Response) modules: `invoice.types.ts`, `invoice-rules.ts`, `in-memory-store.ts`. Both the trusted-tier routes (`src/trusted/`) AND the wasm-tier handlers (`src/wasm/`) MUST call the same rule functions from `src/domain/invoice-rules.ts` for any business decision (state transitions, validation, etc.).
#### Scenario: Trusted and wasm handlers produce identical bodies for the same input
- **WHEN** the same `POST /invoices` payload is dispatched through the trusted handler AND through `plugin.handleRequest(...)`
- **THEN** both produce the same HTTP status AND the same JSON response body
**Verified by:** `tests/wasm/shape-conformance.test.ts::"trusted vs wasm parity for every shared route"`
---
### Requirement: The existing root export surface SHALL remain unchanged
The shipped `@suaveplan/api-plugin-example` exports (`examplePluginModule`, `buildRoutes`, `EXAMPLE_CODES`, error class) MUST keep their existing types and runtime semantics after the refactor. The internal `git mv` from `src/{routes,plugin,install,events}/` to `src/trusted/{routes,plugin,install,events}/` MUST be invisible to consumers — the root barrel re-exports the same names from the new locations.
#### Scenario: api-host's existing consumer of api-plugin-example still passes
- **WHEN** `bunx --bun turbo test:coverage --filter=@suaveplan/api-host` runs after the refactor
- **THEN** every test that imports `@suaveplan/api-plugin-example` still passes AND no test file in `api-host/` requires source modification
**Verified by:** `tests/trusted/root-export-shape.test.ts::"root exports preserve existing types and names"`
---
### Requirement: The wasm subpath SHALL omit endpoints that cannot run under the sandbox tier
The wasm-tier `routes` array MUST NOT include endpoints that require streaming responses, server-sent events, or any other transport pattern incompatible with the synchronous `(SerializedRequest) => SerializedResponse` contract. The trusted-tier `streamLargeReport` endpoint is the canonical example — it stays in the trusted-tier exports and is documented in `src/wasm/index.md` as a deliberate omission.
#### Scenario: Wasm routes contain no streaming endpoint
- **WHEN** the wasm `routes` array is inspected
- **THEN** no entry's `path` matches `/stream/*` or any documented streaming pattern
**Verified by:** `src/wasm/routes.test.ts::"wasm routes omit streaming endpoints"`
---
### Requirement: Lifecycle hooks SHALL initialise and tear down the in-memory store deterministically
`plugin.start()` SHALL prime the module-singleton `InMemoryStore` with the same seed data the trusted-tier `install` hook uses. `plugin.dispose()` SHALL clear the store. Repeated calls to either MUST be idempotent (the second call MUST NOT throw).
#### Scenario: Dispose then start re-primes the store
- **WHEN** `plugin.dispose()` runs, then `plugin.start()` runs
- **THEN** the wasm-tier `GET /invoices` route returns the seeded list of invoices
**Verified by:** `src/wasm/start.test.ts::"start re-primes after dispose"`