# Host Pipeline Per-Stage Events **Package**: `@suaveplan/api-host` (extend) + `@suaveplan/api-types` (contract) **Status**: PROPOSED **Type**: capability-extension (coherence-fix) ## Why The §6 11-step request pipeline in `api-host`'s `RequestPipeline` is an opaque sequential function that returns a `Response`. Internal stages — request-id assignment, auth resolution, tenant resolution, capability check, rate-limit consumption, idempotency check, CSRF check, routing, handler dispatch, response validation, audit emission — are private state with no public observable surface. The foundation umbrella's gate 2.8 demands "exercises the full §6 11-step pipeline end-to-end" and the foundation-acceptance §3.1 demands "Reference plugin's full integration test (design doc §13.5) green in CI". Neither can be satisfied without per-stage observability: - A closure smoke that asserts "the request reached stage 4 (capability check)" cannot do so without a public signal. - An operator running the host in production has no way to see which stage rejected a malformed request. - The audit sink today emits ONE record per request — fine for accounting, useless for debugging cross-stage failures. OTel spans MAY be emitted today but the contract doesn't promise it. Tests can't depend on what isn't contractually emitted. ## Concrete consumer - `@suaveplan/e2e-foundation-stack-full` (Stage-8 closure smoke) — asserts each of the 11 stages by reading the PipelineEvents bus. - Any operator-built dashboard wanting per-stage latency / rejection-rate metrics. - `@suaveplan/api-plugin-example` `full-flow.integration.test.ts` — can replace string-parsing of audit records with structured event assertions. ## End-to-end test - `e2e/foundation-stack-full/tests/pipeline-stages.spec.ts` — drives one request through the host, subscribes to PipelineEvents, asserts the 11 events fired in order with the expected payload shape per stage. ## What changes ### `@suaveplan/api-types` — new `pipeline-events` subpath ```ts // packages/api-stack/api/api-types/src/pipeline-events/pipeline-events.types.ts export type PipelineStage = | "request-id" | "auth" | "tenant" | "capability" | "rate-limit" | "idempotency" | "csrf" | "route-match" | "handler" | "response-validate" | "audit"; export interface PipelineStageEvent { readonly requestId: string; readonly stage: PipelineStage; readonly outcome: "entered" | "completed" | "rejected" | "skipped"; readonly durationMs?: number; readonly cause?: { readonly code: string; readonly message: string }; readonly at: number; } export interface PipelineEventsBus { emit(event: PipelineStageEvent): void; subscribe(listener: (event: PipelineStageEvent) => void): () => void; } ``` Zero-runtime. Lives in `api-types/pipeline-events`. ### `@suaveplan/api-host` — emit events at every stage boundary `request-pipeline.ts` gains a `pipelineEvents: PipelineEventsBus` adapter slot on `HostAdapters`. Every stage emits `entered` on entry, `completed`/`rejected`/`skipped` on exit. Default is `NoopPipelineEventsBus` (zero-cost when not observed); operators wire `InMemoryPipelineEventsBus` (test fixture) or a custom OTel-bridging implementation in production. The `api-host/testing/createTestHost` factory wires the in-memory bus by default so smokes can assert without setup. ### Conformance harness `@suaveplan/api-host/testing/pipeline-events-conformance` — `runPipelineEventsConformance(host)`: - 11 invariants, one per stage: a request that reaches stage N MUST emit `entered` AND (`completed` OR `rejected`) for stage N. - Order invariant: stages emit in §6-spec order; no stage emits before its predecessor's `completed`. - Skip invariant: when a stage is conditionally skipped (e.g. unauthenticated request bypasses idempotency-by-principal), `skipped` MUST fire (not silently omitted). - Rejection invariant: when a stage rejects, no subsequent stage emits `entered`. ## Why this is not a "nice-to-have" Stage 4's foundation-fix-gap-1..5 surfaced because Stage 4 stage boundaries weren't observable. Same root failure mode the umbrella's gate 2.8 will hit at Stage 8 — already did when the closure-smoke agent reported the spec was unwriteable without per-stage observability. §19 (the new rule) explicitly forbids shipping a primitive without its host-integration; this proposal IS the host-integration for the §6 pipeline. ## Out of scope - Per-stage OTel span emission. PipelineEvents is the structured-event contract; OTel spans can be a bridge in a follow-up `api-host-otel-pipeline` change. - Wire-format serialisation of events (JSON-line audit sink format). Audit sink stays as-is; PipelineEvents is in-process observability only.