# Capability Spec Delta: api-router-hono (api-router-hono) **Package**: `@suaveplan/api-router-hono` **Status**: PROPOSED > Every requirement carries a `**Verified by:**` line at authoring time. (See `openspec/AGENTS.md` Section 13.) ## ADDED Requirements ### Requirement: Direct surface implements the Router contract The package SHALL export `createHonoRouter(opts)` returning a `Router` (from `@suaveplan/api-router`) backed by a Hono instance. The returned router MUST pass `runRouterConformance` from `@suaveplan/api-router/testing`. #### Scenario: factory returns a conformant Router - **WHEN** `createHonoRouter({ basePath: "/" })` is called - **THEN** the result satisfies the `Router` type AND `runRouterConformance("hono-direct", () => createHonoRouter(...))` reports zero failures #### Scenario: route registration is order-independent for static paths - **WHEN** `/a/b` is registered after `/a/:p` - **THEN** a request to `/a/b` matches the static route AND a request to `/a/c` matches the param route **Verified by:** `src/factory/create-hono-router.spec-conformance.test.ts::"hono-direct conforms to Router contract"` AND `src/factory/create-hono-router.test.ts::"static routes win over param routes when registered in either order"` --- ### Requirement: Worker-proxy surface implements the Router contract The package SHALL export `createHonoRouterWorkerProxy(port, opts)` returning a `Router` whose calls are marshalled over a `MessagePort` via `@suaveplan/api-worker-rpc`. The proxy MUST pass the same `runRouterConformance` suite as the direct surface — no invariant may regress under marshalling. #### Scenario: proxy passes every conformance invariant - **WHEN** `runRouterConformance("hono-worker-proxy", () => createHonoRouterWorkerProxy(channel.port2, ...))` runs against a real `MessageChannel` bridged to a direct router on `channel.port1` - **THEN** every invariant passes AND no invariant is skipped #### Scenario: AbortSignal propagates across the proxy - **WHEN** a route handler is invoked through the proxy AND the caller aborts via `AbortSignal` - **THEN** the in-flight handler on the host side observes `signal.aborted === true` within one event-loop tick **Verified by:** `src/proxy/create-hono-router-worker-proxy.spec-conformance.test.ts::"hono-worker-proxy conforms to Router contract"` AND `src/proxy/create-hono-router-worker-proxy.test.ts::"AbortSignal aborts the host-side handler"` --- ### Requirement: Dual-surface conformance gate The adapter SHALL pass its capability's conformance suite under BOTH the direct surface AND the worker-proxy surface in the same test run. Failing either surface MUST fail the package's pre-publish gate. #### Scenario: both surfaces run side by side - **WHEN** the conformance suite executes - **THEN** two `describe` blocks emit results — one labelled `hono-direct`, one labelled `hono-worker-proxy` — AND both report zero failures **Verified by:** `src/factory/create-hono-router.spec-conformance.test.ts::"hono-direct conforms to Router contract"` AND `src/proxy/create-hono-router-worker-proxy.spec-conformance.test.ts::"hono-worker-proxy conforms to Router contract"` --- ### Requirement: Blue-green slot management The package SHALL expose `HonoBlueGreenSlot` implementing the `BlueGreenSlot` contract from `api-router`. It maintains two Hono instances and an atomic active-slot pointer. New requests route to the active slot; in-flight requests on the inactive slot complete normally. A `swap()` call MUST be gated on a health-probe result when `strategy === "swap-on-health"`. #### Scenario: in-flight requests complete on the old slot - **WHEN** a request is in-flight on the blue slot AND `swap()` cuts over to green - **THEN** the in-flight blue request completes its response AND any new request after `swap()` lands on green #### Scenario: health-gated swap rolls back on failure - **WHEN** the new slot's health probe throws OR returns false - **THEN** the pointer remains on the old slot AND `ROUTER_HONO_BLUE_GREEN_SWAP_FAILED` is thrown to the caller **Verified by:** `src/blue-green/hono-blue-green-slot.test.ts::"in-flight requests complete on the old slot after swap"` AND `src/blue-green/hono-blue-green-slot.test.ts::"swap rolls back when health probe fails"` --- ### Requirement: Streaming routes bypass buffering Routes registered with `streaming: true` SHALL receive the raw `ReadableStream` body and SHALL write responses via a streaming bridge that emits chunks to the underlying socket without intermediate buffering. #### Scenario: streaming handler writes chunks incrementally - **WHEN** a streaming handler emits 3 chunks separated by event-loop ticks - **THEN** the client observes 3 distinct read events AND no buffering across the whole response **Verified by:** `src/streaming/streaming-bridge.test.ts::"streaming handler emits chunks incrementally without buffering"` --- ### Requirement: Options are Zod-validated `HonoRouterOptions` SHALL be parsed through a Zod schema. Invalid options (negative `maxBodyBytes`, non-positive `streamingTimeoutMs`, unknown `strategy`) MUST throw at factory construction. #### Scenario: invalid options throw at construction - **WHEN** `createHonoRouter({ maxBodyBytes: -1 } as never)` is called - **THEN** a `ZodError`-wrapped `SuaveplanError` with code `ROUTER_HONO_INVALID_OPTIONS` is thrown synchronously **Verified by:** `src/options/hono-router-options.test.ts::"invalid options throw at factory construction"` --- ### Requirement: Telemetry instruments emitted The adapter SHALL emit `api.router.match_duration_ms` (histogram, tagged with `route`, `method`, `outcome`) and `api.router.blue_green_swaps_total` (counter) on every match and every successful swap respectively. #### Scenario: histogram records on every route match - **WHEN** 5 requests are dispatched through `createHonoRouter` - **THEN** the in-memory `MeterProvider` records exactly 5 observations on `api.router.match_duration_ms` **Verified by:** `src/factory/create-hono-router.spec-conformance.test.ts::"emits api.router.match_duration_ms on every match"`