Initial Commit
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
# foundation-fix-gap-1-pipeline-router
|
||||
|
||||
## Summary
|
||||
|
||||
Close foundation-coherence Gap #1: `@suaveplan/api-router-hono`'s
|
||||
`HonoRouter` did not satisfy `@suaveplan/api-host`'s `PipelineRouter`
|
||||
contract. The two `match()` shapes diverged (`RouteMatch` vs
|
||||
`PipelineRoute`), forcing every host-pipeline consumer to hand-roll a
|
||||
shim — the E2E `foundation-stack` server's `createFixtureRouter()` was
|
||||
the canonical example. This change adds a second, additive facet to
|
||||
`HonoRouter` (`registerPipelineRoute` / `matchPipeline` /
|
||||
`unregisterPipelinePlugin` / `listPipelineRoutes`) that produces values
|
||||
structurally assignable to the host's `PipelineRoute` interface, and
|
||||
ships a canonical `runPipelineRouterConformance(factory)` test harness
|
||||
in `@suaveplan/api-host/testing` so every concrete adapter has a single
|
||||
contract to pass.
|
||||
|
||||
## Motivation
|
||||
|
||||
Empirically the gap caused two failure modes:
|
||||
|
||||
1. **Hand-rolled shims** in every consumer — the E2E `foundation-stack`
|
||||
server's `createFixtureRouter()` was 80 lines of router logic that
|
||||
should have lived inside `api-router-hono`. Every new host-pipeline
|
||||
consumer was on track to write the same shim.
|
||||
2. **No conformance suite** — `api-router-hono` passes
|
||||
`runRouterConformance` against the abstract `Router`, but no harness
|
||||
existed for the host's `PipelineRouter` surface. Adapter drift would
|
||||
only surface during a real E2E run.
|
||||
|
||||
The fix is additive — the existing abstract-`Router` surface is
|
||||
preserved verbatim; the pipeline facet lives next to it as a parallel
|
||||
metadata sidecar. No breaking changes to existing consumers.
|
||||
|
||||
## Scope
|
||||
|
||||
- `@suaveplan/api-router-hono`:
|
||||
- new `src/pipeline/` feature module (`PipelineRouteTable`,
|
||||
`HonoPipelineRoute`, `HonoPipelineRegistration`,
|
||||
`HonoPipelineRouteOptions`, `HonoPipelineHandler`).
|
||||
- `HonoRouter` gains `registerPipelineRoute`,
|
||||
`unregisterPipelineRoute`, `unregisterPipelinePlugin`,
|
||||
`listPipelineRoutes`, `matchPipeline`.
|
||||
- new error code `ROUTER_HONO.PIPELINE_ROUTE_COLLISION`.
|
||||
- new subpath export `./pipeline`.
|
||||
- dev-dep on `@suaveplan/api-host` (testing-only).
|
||||
- vite config emits `index.js` per subpath (closes a pre-existing
|
||||
`./adapter` resolution bug).
|
||||
- `@suaveplan/api-host`:
|
||||
- `./testing` subpath populated with
|
||||
`runPipelineRouterConformance(factory)` +
|
||||
`PipelineRouterConformanceFactory` /
|
||||
`PipelineRouteRegistration` types.
|
||||
- vite config switched to multi-entry so the dist actually emits
|
||||
`testing/testing.js` (closes a pre-existing dead-export bug).
|
||||
- `e2e/foundation-stack`:
|
||||
- drop hand-rolled `createFixtureRouter` body; replace with the real
|
||||
`HonoRouter` pipeline facet.
|
||||
|
||||
## Verification
|
||||
|
||||
- Unit + integration: `bunx turbo lint typecheck test build
|
||||
--filter=@suaveplan/api-router-hono --filter=@suaveplan/api-host` →
|
||||
green; 75 unit tests across `api-router-hono`, 252 across `api-host`.
|
||||
- Coverage: `api-router-hono` 100/100/100/100 on every touched module
|
||||
(`adapter`, `pipeline`, `error-codes`).
|
||||
- E2E: `cd e2e/foundation-stack && bunx playwright test` — all 10
|
||||
test groups pass (`livez`/`readyz`, plugin routes, tenant resolver,
|
||||
domain error envelope, 404 envelope, secrets, admin plugins/details,
|
||||
admin capabilities, rate-limit, idempotency).
|
||||
|
||||
## Out of scope
|
||||
|
||||
- Migrating `api-router-worker-bridge` to the pipeline facet — that
|
||||
adapter will adopt the same harness in a follow-up.
|
||||
- Wiring the `HonoRouter` pipeline facet into the host's
|
||||
`LifecycleDriver.mount-routes` op — currently consumers register
|
||||
manually; threading through the lifecycle is a follow-up change.
|
||||
@@ -0,0 +1,30 @@
|
||||
<!-- markdownlint-disable MD024 -->
|
||||
|
||||
# Spec delta: api-host
|
||||
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: `./testing` subpath ships the PipelineRouter conformance harness
|
||||
|
||||
The package SHALL declare `./testing` as a subpath export AND ship `runPipelineRouterConformance(factory)` plus the supporting `PipelineRouterConformanceFactory` and `PipelineRouteRegistration` types. The harness SHALL exercise the host's `PipelineRouter` contract against any adapter via a factory-supplied `(router, register, reload?)` triple, covering: register → match round-trip, `null` on miss, `statusMap` propagation, plugin ownership across reload, `:param` parsing, and collision detection.
|
||||
|
||||
The placeholder `STAGE_2_RESERVED` sentinel SHALL remain exported for back-compat so existing detection logic (`Object.keys(mod).length > 0` style) continues to discriminate "is the harness wired?" from "is the subpath reserved?".
|
||||
|
||||
The package's vite build SHALL emit `dist/testing/testing.js` AND `dist/testing/pipeline-router-conformance/pipeline-router-conformance.js` so the subpath actually resolves at runtime — these files were previously absent because the testing module was tree-shaken out of the single-entry build.
|
||||
|
||||
#### Scenario: harness drives a reference PipelineRouter through every required case
|
||||
|
||||
- **WHEN** a test calls `runPipelineRouterConformance({ label, build: () => ({ router, register, reload? }) })`
|
||||
- **THEN** `describe("PipelineRouter conformance — ${label}", ...)` registers six `it()` cases AND all of them pass against any adapter whose `register` populates a backing table consulted by `router.match`
|
||||
|
||||
#### Scenario: harness asserts collision detection throws
|
||||
|
||||
- **WHEN** the harness invokes `register({ plugin: "p", method: "GET", path: "/x" })` followed by `register({ plugin: "q", method: "GET", path: "/x" })`
|
||||
- **THEN** the second call SHALL throw — the harness uses `expect(...).toThrow()` without prescribing the error class so any adapter-specific typed error satisfies the contract
|
||||
|
||||
#### Scenario: harness asserts plugin ownership survives reload
|
||||
|
||||
- **WHEN** the factory exposes a `reload()` hook, the harness registers a route, calls `reload()`, then calls `router.match(...)`
|
||||
- **THEN** the matched route's `plugin` AND `params` survive the reload cycle
|
||||
|
||||
**Verified by:** `src/testing/pipeline-router-conformance/pipeline-router-conformance.test.ts` AND (downstream) `@suaveplan/api-router-hono`'s `src/pipeline/pipeline.conformance.test.ts`
|
||||
@@ -0,0 +1,49 @@
|
||||
<!-- markdownlint-disable MD024 -->
|
||||
|
||||
# Spec delta: api-router-hono
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: HonoRouter pipeline facet satisfies the host's PipelineRouter contract
|
||||
|
||||
The package SHALL extend `HonoRouter` with a second, additive facet that produces values structurally assignable to `@suaveplan/api-host`'s `PipelineRoute` interface. The facet SHALL expose:
|
||||
|
||||
- `registerPipelineRoute(reg: HonoPipelineRegistration): void` — stores `(plugin, method, path, handler, options?, statusMap?)`.
|
||||
- `unregisterPipelineRoute(method: string, path: string): void` — no-op when absent.
|
||||
- `unregisterPipelinePlugin(plugin: string): void` — removes every entry owned by `plugin` (hot-reload teardown).
|
||||
- `listPipelineRoutes(): readonly HonoPipelineRoute[]` — snapshot for admin tooling.
|
||||
- `matchPipeline(method: string, url: string): HonoPipelineRoute | null` — resolves `(method, url)` to a `HonoPipelineRoute` carrying `plugin`, `path`, `options`, `handler`, `params`, `statusMap`.
|
||||
|
||||
The facet SHALL preserve the existing abstract `Router` surface verbatim. Registering the same `(method, path-shape)` under a DIFFERENT plugin SHALL throw `ROUTER_HONO.PIPELINE_ROUTE_COLLISION`. Re-registering the same `(plugin, method, path)` SHALL overwrite. Parameter names SHALL be erased in the collision key so `:id` and `:other` clash.
|
||||
|
||||
#### Scenario: facet satisfies the host's PipelineRouter conformance suite
|
||||
|
||||
- **WHEN** `runPipelineRouterConformance` (from `@suaveplan/api-host/testing`) is invoked with a factory that returns `{ router: { match: (m, u) => r.matchPipeline(m, u) }, register: (reg) => r.registerPipelineRoute(reg) }`
|
||||
- **THEN** every harness case passes (register → match round-trip, 404, statusMap propagation, plugin ownership across reload, param parsing, collision detection)
|
||||
|
||||
#### Scenario: collision detection throws ROUTER_HONO.PIPELINE_ROUTE_COLLISION
|
||||
|
||||
- **WHEN** two plugins register the same `(method, path-shape)` on one `HonoRouter`
|
||||
- **THEN** the second call throws `RouterHonoError` with `code === "ROUTER_HONO.PIPELINE_ROUTE_COLLISION"`
|
||||
|
||||
#### Scenario: hot-reload teardown removes plugin-owned entries
|
||||
|
||||
- **WHEN** `unregisterPipelinePlugin("billing")` is called after `billing` registered two pipeline routes
|
||||
- **THEN** subsequent `matchPipeline(...)` calls against either route return `null` AND `listPipelineRoutes()` no longer contains a `"billing"`-owned entry
|
||||
|
||||
**Verified by:** `src/pipeline/pipeline.test.ts` AND `src/pipeline/pipeline.conformance.test.ts` AND `src/adapter/adapter.test.ts::"pipeline facet — register/match/list/unregister/unregisterPlugin"`
|
||||
|
||||
---
|
||||
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: ROUTER_HONO error-code registry
|
||||
|
||||
The package SHALL expose `ROUTER_HONO_CODES` (`defineCodes("ROUTER_HONO", [...])`) and a `RouterHonoError` extending `SuaveplanError`. The registry MUST contain `HONO_INTERNAL_ERROR`, `INVALID_HONO_PEER`, AND `PIPELINE_ROUTE_COLLISION`. Every code MUST be thrown by at least one production path in this package.
|
||||
|
||||
#### Scenario: registry shape
|
||||
|
||||
- **WHEN** `Object.keys(ROUTER_HONO_CODES)` is inspected
|
||||
- **THEN** the keys are exactly `["HONO_INTERNAL_ERROR", "INVALID_HONO_PEER", "PIPELINE_ROUTE_COLLISION"]` AND each value is `ROUTER_HONO.<name>`
|
||||
|
||||
**Verified by:** `src/error-codes/error-codes.test.ts::"enumerates the three declared codes"` AND `src/error-codes/error-codes.test.ts::"namespaces each entry under ROUTER_HONO.*"`
|
||||
@@ -0,0 +1,23 @@
|
||||
# Tasks
|
||||
|
||||
- [x] `api-router-hono`: add `src/pipeline/pipeline.ts` (`PipelineRouteTable`).
|
||||
- [x] `api-router-hono`: add `src/pipeline/pipeline.types.ts` (`HonoPipelineRoute` + bag types).
|
||||
- [x] `api-router-hono`: add `src/pipeline/index.ts` barrel.
|
||||
- [x] `api-router-hono`: add `src/pipeline/pipeline.md` (≥200 words, basic + advanced examples).
|
||||
- [x] `api-router-hono`: add `src/pipeline/pipeline.test.ts` (full coverage on `PipelineRouteTable`).
|
||||
- [x] `api-router-hono`: extend `HonoRouter` with pipeline-facet methods.
|
||||
- [x] `api-router-hono`: add `ROUTER_HONO.PIPELINE_ROUTE_COLLISION` error code.
|
||||
- [x] `api-router-hono`: add `./pipeline` subpath + `PipelineRouteTable` to top-level barrel.
|
||||
- [x] `api-router-hono`: vite config — multi-entry so subpath `index.js` files emit.
|
||||
- [x] `api-router-hono`: add `@suaveplan/api-host` as dev dep.
|
||||
- [x] `api-host/testing`: add `pipeline-router-conformance/` feature module with `runPipelineRouterConformance(factory)`.
|
||||
- [x] `api-host/testing`: self-test the harness via a reference in-memory `PipelineRouter`.
|
||||
- [x] `api-host/testing`: re-export the harness from `testing.ts` and `testing/index.ts`.
|
||||
- [x] `api-host`: vite config — emit `testing/testing.js` and `testing/pipeline-router-conformance/pipeline-router-conformance.js`.
|
||||
- [x] `api-router-hono`: add `pipeline.conformance.test.ts` wiring the harness against `HonoRouter`.
|
||||
- [x] `e2e/foundation-stack/src/fixture-plugin.ts`: drop `createFixtureRouter` body; use real `HonoRouter` pipeline facet.
|
||||
- [x] `e2e/foundation-stack/src/server.ts`: refresh the wiring-deviations comment.
|
||||
- [x] Run `bunx turbo lint typecheck test build --filter=@suaveplan/api-router-hono --filter=@suaveplan/api-host` → green.
|
||||
- [x] Run `bunx turbo test:coverage --filter=@suaveplan/api-router-hono` → 100/100/100/100.
|
||||
- [x] Run `cd e2e/foundation-stack && bunx playwright test` → all 10 tests pass.
|
||||
- [x] Fold deltas + archive change folder.
|
||||
Reference in New Issue
Block a user