# Capability Spec Delta: api-host — Pipeline per-stage observability **Package**: `@suaveplan/api-host` **Status**: PROPOSED ## ADDED Requirements ### Requirement: Host emits a structured event at every §6 pipeline stage boundary `@suaveplan/api-host`'s `RequestPipeline` SHALL accept a `pipelineEvents: PipelineEventsBus` adapter slot on `HostAdapters` (from `@suaveplan/api-types/pipeline-events`). For every request processed, the pipeline MUST emit `PipelineStageEvent { stage, outcome: "entered", requestId, at }` on entry to each of the 11 stages defined in §6 of the foundation design doc, and MUST emit a matching `completed` / `rejected` / `skipped` event on exit. #### Scenario: a successful request emits 22 events (entered + completed for each of 11 stages) - **WHEN** a request that satisfies every stage's gates is processed - **THEN** `pipelineEvents.subscribe` observes exactly 22 events: `entered` followed by `completed` for each of the 11 stages in §6 order #### Scenario: a stage rejection halts subsequent stages - **WHEN** the auth stage rejects a request (`outcome: "rejected"`) - **THEN** no subsequent stage emits `entered` for that request id #### Scenario: conditionally-skipped stages emit `skipped`, not silent omission - **WHEN** an anonymous request bypasses idempotency-by-principal (no principal to key by) - **THEN** the idempotency stage emits `{ stage: "idempotency", outcome: "skipped", requestId }` (NOT silent omission) **Verified by:** `src/request-pipeline/request-pipeline.test.ts::"successful request emits entered+completed for all 11 stages in order"` AND `src/request-pipeline/request-pipeline.test.ts::"rejected stage halts subsequent stage emission"` AND `src/request-pipeline/request-pipeline.test.ts::"skipped stage emits skipped outcome explicitly"` --- ### Requirement: Default `pipelineEvents` is a zero-cost noop bus When no `pipelineEvents` adapter is configured, the host MUST install a `NoopPipelineEventsBus` whose `emit` and `subscribe` methods are zero-cost (no allocation per call, no listener-list iteration). Production deployments that don't need per-stage observability incur no overhead. #### Scenario: NoopBus emit allocates no objects - **WHEN** a request flows through a host configured with NoopBus - **THEN** `emit` returns immediately AND a microbench (`bench:emit-noop`) confirms zero allocation per call #### Scenario: NoopBus subscribe is a no-op returning an unsubscribe noop - **WHEN** a consumer calls `noopBus.subscribe(listener)` - **THEN** the listener is never invoked AND the returned unsubscribe function is callable without effect **Verified by:** `src/pipeline-events-bus/noop-bus.test.ts::"emit on noop bus is a no-op"` AND `src/pipeline-events-bus/noop-bus.test.ts::"subscribe on noop bus returns a no-op unsubscribe"` AND `src/pipeline-events-bus/noop-bus.bench.ts::"emit-noop zero-allocation"` --- ### Requirement: `createTestHost` wires an `InMemoryPipelineEventsBus` by default `@suaveplan/api-host/testing.createTestHost()` MUST default `pipelineEvents` to an `InMemoryPipelineEventsBus` (the canonical fixture). The bus is exposed on `host.adapters.pipelineEvents` for direct assertion in smoke tests. #### Scenario: createTestHost exposes the bus for direct assertion - **WHEN** a test calls `await createTestHost({ ... })` - **THEN** `host.adapters.pipelineEvents` is an `InMemoryPipelineEventsBus` instance whose `events` array can be inspected #### Scenario: explicit override is honoured - **WHEN** a test calls `createTestHost({ adapters: { pipelineEvents: customBus } })` - **THEN** `host.adapters.pipelineEvents === customBus` **Verified by:** `src/testing/create-test-host/create-test-host.test.ts::"defaults pipelineEvents to InMemoryPipelineEventsBus"` AND `src/testing/create-test-host/create-test-host.test.ts::"explicit pipelineEvents override is honoured"` --- ### Requirement: Conformance harness `runPipelineEventsConformance` The host SHALL ship `runPipelineEventsConformance(host)` from `@suaveplan/api-host/testing/pipeline-events-conformance`. The harness MUST assert four invariants: 1. **Per-stage emit** — every of the 11 stages emits `entered` AND (`completed` OR `rejected` OR `skipped`). 2. **Order** — stages emit in §6 order. 3. **Skip** — conditionally-skipped stages emit `skipped` (not silent omission). 4. **Rejection** — when a stage rejects, no subsequent stage emits `entered`. #### Scenario: harness rejects a host whose pipeline omits a stage - **WHEN** `runPipelineEventsConformance` runs against a stub host whose pipeline skips audit emission silently - **THEN** the harness reports the "Per-stage emit" invariant failure pointing to the audit stage **Verified by:** `src/testing/pipeline-events-conformance/pipeline-events-conformance.test.ts::"harness rejects host with missing stage emission"`