Initial Commit
This commit is contained in:
86
openspec/changes/api-host-pipeline-events/proposal.md
Normal file
86
openspec/changes/api-host-pipeline-events/proposal.md
Normal file
@@ -0,0 +1,86 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user