Files
2026-05-27 23:29:53 +01:00

5.6 KiB

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:

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.