Files
suaveplan/openspec/archive/2026-05-26-api-router-pipeline-extraction/proposal.md
2026-05-27 23:29:53 +01:00

79 lines
5.6 KiB
Markdown

# Extract PipelineRouter Contract to api-router
**Packages**: `@suaveplan/api-router`, `@suaveplan/api-host`, `@suaveplan/api-router-in-memory`, `@suaveplan/api-router-hono`
**Status**: PROPOSED
**Type**: architectural-refactor
## Why
`@suaveplan/api-host` owns the abstract `PipelineRouter` interface AND its conformance harness `runPipelineRouterConformance`. Every concrete router adapter that satisfies the §6 pipeline (currently `api-router-in-memory` and `api-router-hono`'s pipeline facet, eventually every Stage-7 engine) must declare a build dep on `@suaveplan/api-host` to import the harness.
`@suaveplan/api-host`, in turn, declares a `peerDependency` + `devDependency` edge on `@suaveplan/api-router-in-memory` (the testing kit's default in-memory adapter, dynamically imported by `createTestHost`). Turborepo refuses to schedule the resulting cycle:
```text
Cyclic dependency detected:
@suaveplan/api-host#build, @suaveplan/api-router-in-memory#build
```
`bunx turbo test`, `bunx turbo lint typecheck test`, and `bunx turbo build` repo-wide are all blocked. Per-package gates pass via direct `bun run` invocation, but the orchestrator-facing pre-publish gate documented in `CLAUDE.md` is unusable. This has been worked around in every recent agent dispatch — it is now blocking enough that fixing it correctly is a prerequisite to further stage work.
The root cause is misplaced ownership. Per `openspec/AGENTS.md §16` (contract + conformance harness colocate with the contract package), `PipelineRouter` belongs in `@suaveplan/api-router` — the existing abstract router-contract package every adapter already depends on. `api-host` is a consumer of that contract, not the contract owner.
## What changes
### `@suaveplan/api-router` — contract owner (new subpaths)
- New feature folder `src/pipeline/`:
- `pipeline.types.ts` exporting `PipelineRoute`, `PipelineRouteOptions`, `PipelineRouteRegistration`, `PipelineRouter` (relocated verbatim from `api-host/src/request-pipeline/request-pipeline.types.ts`).
- `pipeline.md` + `index.ts` barrel (named exports, rule 10).
- `pipeline.test.ts` covering the type-shape invariants the harness asserts on.
- New feature folder `src/testing/pipeline-router-conformance/`:
- `pipeline-router-conformance.ts` — `runPipelineRouterConformance({ label, build })` (relocated verbatim from `api-host/src/testing/pipeline-router-conformance/`).
- `pipeline-router-conformance.md`, `pipeline-router-conformance.types.ts`, `pipeline-router-conformance.test.ts`, `index.ts`.
- `package.json` subpaths added: `./pipeline`, `./testing/pipeline-router-conformance`. The existing `./testing` barrel re-exports the harness for ergonomic single-subpath consumption.
### `@suaveplan/api-host` — consumer (de-owned)
- `src/request-pipeline/request-pipeline.types.ts` re-exports the 4 contract symbols from `@suaveplan/api-router/pipeline` (single source of truth) — every internal consumer keeps working without code changes.
- `src/testing/pipeline-router-conformance/` is **deleted** in favour of the relocated harness. `src/testing/testing.ts` re-exports `runPipelineRouterConformance` from `@suaveplan/api-router/testing/pipeline-router-conformance` to preserve the back-compat path `@suaveplan/api-host/testing` already in use.
- `dependencies` adds `@suaveplan/api-router`.
- `peerDependencies` + `peerDependenciesMeta` + `devDependencies`: remove the `@suaveplan/api-router-in-memory` entry. The dynamic-import pattern in `createTestHost` remains — no package-graph edge is needed for runtime resolution.
### `@suaveplan/api-router-in-memory` — adapter
- `tests/conformance/api-host.test.ts` renamed → `tests/conformance/pipeline-router.test.ts`; import path updates to `@suaveplan/api-router/testing/pipeline-router-conformance` (or `@suaveplan/api-router/testing`).
- `package.json` `dependencies`: replace `@suaveplan/api-host` with `@suaveplan/api-router` (already present — drop the api-host entry).
- `vite.config.ts` `external` list updates (`@suaveplan/api-host` → `@suaveplan/api-router`).
- Docstring mentions of `@suaveplan/api-host`'s `PipelineRouter` rewritten to `@suaveplan/api-router/pipeline`'s `PipelineRouter`.
### `@suaveplan/api-router-hono` — adapter
- `src/pipeline/pipeline.conformance.test.ts` import path updates to `@suaveplan/api-router/testing/pipeline-router-conformance`.
- `package.json` `dependencies` swap if api-host was listed — replace with `@suaveplan/api-router`.
## Cycle audit (acceptance criterion)
After the refactor, `bunx turbo run build --dry=json | jq '...'` MUST show:
- `@suaveplan/api-host` build edges: `api-router`, `api-db-adapter`, `api-lifecycle`, `api-loader`, `api-manifest`, `api-secrets`, `api-types`, `error`, `events`, `opentelemetry`, `rate-limiter`, `retry`. NO `api-router-in-memory`.
- `@suaveplan/api-router-in-memory` build edges: `api-router`, `api-types`, `error`. NO `api-host`.
- `bunx turbo test --output-logs=errors-only` exit code 0 (no `Cyclic dependency detected` warning).
## Back-compat
Every existing import path remains valid:
- `import { PipelineRouter } from "@suaveplan/api-host/request-pipeline"` → works (re-export).
- `import { runPipelineRouterConformance } from "@suaveplan/api-host/testing"` → works (re-export).
New canonical imports:
- `import { PipelineRouter } from "@suaveplan/api-router/pipeline"` (preferred).
- `import { runPipelineRouterConformance } from "@suaveplan/api-router/testing/pipeline-router-conformance"` (preferred).
## Out of scope
- Adding a second `PipelineRouter` impl beyond what already exists.
- Changing the harness's invariants.
- Stage-7 engine work — that's gate 2.7, not this change.