Files
suaveplan/openspec/changes/api-plugin-example-dual-shape/tasks.md
2026-05-27 23:29:53 +01:00

5.7 KiB

Tasks — Dual-shape api-plugin-example

0. Pre-flight (non-negotiable)

  • 0.1 Clean working tree; account for every .claude/worktrees/agent-*
  • 0.2 MANIFEST.md package count matches find packages -maxdepth 3 -name package.json
  • 0.3 §0a Sync: worktree-isolated agent fast-forwards to orchestrator-supplied SHA

1. Extract the pure-domain core

  • 1.1 Author src/domain/invoice.types.ts + .md — pure type definitions (Invoice, InvoiceState, InvoiceLine, etc.) extracted from current schemas/ / routes/ / events/. Zero deps beyond @suaveplan/api-types, @suaveplan/error.
  • 1.2 Author src/domain/invoice-rules.ts + .md + .test.ts — state-transition machine, validation, plus any pure business rules the existing routes perform inline. NO Hono, NO OTel SDK, NO Request/Response.
  • 1.3 Author src/domain/in-memory-store.ts + .md + .test.ts — deterministic Map-backed store with create / get / list / setState / delete. Used by the wasm subpath.
  • 1.4 src/domain/index.ts — explicit named re-exports (rule 10 — no export *).
  • 1.5 100% istanbul coverage on every domain module.

2. Move existing source under trusted/

  • 2.1 git mv every existing src/routes/, src/plugin/, src/install/ (if present), src/events/ directory under src/trusted/. Preserve git history via git mv.
  • 2.2 Update all internal imports in those files to use the new paths.
  • 2.3 Update existing route handlers to delegate to src/domain/invoice-rules.ts for business rules — they SHOULD call the same rule functions the wasm tier will call. This is the contract that makes shape-conformance pass.
  • 2.4 src/trusted/index.ts — internal barrel; re-exports examplePluginModule + buildRoutes for the root src/index.ts to forward.
  • 2.5 Existing src/error-codes/, src/instruments/, src/schemas/ stay at the package root (shared between tiers). Existing exports remain unchanged.

3. Author the wasm-tier surface

  • 3.1 src/wasm/wasm-state.ts + .md + .test.ts — module-singleton InMemoryStore instance; resetState() helper for test isolation.
  • 3.2 src/wasm/routes.ts + .md + .test.ts — export const routes: readonly SerializedRoute[] listing every endpoint the wasm tier serves (excluding streamLargeReport).
  • 3.3 src/wasm/handle-request.ts + .md + .test.ts — (req: SerializedRequest) => Promise<SerializedResponse>. Routing is a simple method + pathname lookup against routes. Handlers delegate to src/domain/invoice-rules.ts. Body decoded from bodyBase64; response body re-encoded to bodyBase64. Headers shaped as ReadonlyArray<[string, string]>. SuaveplanError-shaped throws are NOT caught here — the WASI-stdio ABI shim (in wasm-compiler-javy) catches them and emits {kind:"error"} envelopes.
  • 3.4 src/wasm/start.ts + .md + .test.ts — primes the in-memory store with the same seed data the trusted-tier install hook uses.
  • 3.5 src/wasm/dispose.ts + .md + .test.ts — clears the in-memory store.
  • 3.6 src/wasm/index.ts — export const plugin = { routes, handleRequest, start, dispose } as const. Explicit named re-exports (no export *).

4. Subpath exports + dep-graph lint

  • 4.1 package.json — add ./wasm AND ./domain subpath exports per the proposal.
  • 4.2 tsconfig.lib.json — confirm new subpaths build into dist/{wasm,domain}/.
  • 4.3 scripts/lint/check-wasm-subpath-deps.ts (NEW) — walks every static import from src/wasm/index.ts transitively; rejects any specifier matching the wasm-tier ban list (@suaveplan/api-host, hono, @hono/*, @opentelemetry/sdk-*, node:*, plus any package whose graph includes those). Wired into bunx turbo lint as a new lint:wasm-subpath-deps task. Add a corresponding task entry in turbo.json and a top-level package.json script (lint:wasm-subpath-deps).
  • 4.4 Run the new lint script — must pass against the new layout.

5. Conformance test

  • 5.1 tests/wasm/shape-conformance.test.ts — for every route shared between tiers, assert that:
    • await handleTrustedRequest(new Request(...)) returns the same status + body bytes as
    • await plugin.handleRequest({ ...serializedRequest equivalent ... }).
    • The streamLargeReport endpoint is excluded with a documented it.skip and an explanatory comment.
  • 5.2 100% istanbul coverage on every wasm/* file.
  • 5.3 Public-constructor error class preserved; zero // biome-ignore; every test imports from @suaveplan/testing/runner.

6. Cross-package check

  • 6.1 The existing root exports (@suaveplan/api-plugin-example → trusted-tier exports) MUST remain byte-compatible. Run bunx --bun turbo lint typecheck test:coverage --filter=@suaveplan/api-host --filter=@suaveplan/api-plugin-example and confirm api-host's consumers of api-plugin-example still pass.
  • 6.2 Author tests/trusted/root-export-shape.test.ts — snapshot assertion that the root barrel still exports examplePluginModule, buildRoutes, EXAMPLE_CODES, ExamplePluginError with their existing types. Pinned guard against accidental signature drift during the refactor.

7. Gate

  • 7.1 bunx --bun turbo lint typecheck test:coverage --filter=@suaveplan/api-plugin-example --filter=@suaveplan/api-host --filter=@suaveplan/api-types --filter=@suaveplan/api-sandbox-wasm-host-bridge --concurrency=4 green
  • 7.2 bun scripts/folder-reorg/generate-manifest.ts — package count unchanged (same package, additive subpath).

8. Archive (orchestrator step)

  • 8.1 Fold delta into openspec/specs/api-plugin-example/spec.md (create if not yet present).
  • 8.2 Move folder → openspec/archive/2026-05-27-api-plugin-example-dual-shape/.
  • 8.3 Refresh MANIFEST.md.