# 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.