3.9 KiB
OpenSpec Change: events-spec-tightening
- Change ID: events-spec-tightening
- Package:
@suaveplan/events - Status: PROPOSED
- Date: 2026-04-27
Summary
Tighten openspec/specs/events/spec.md to cover behaviors already shipped by @suaveplan/events: emitAsync timeout (EVENTS.EMIT.TIMED_OUT), abort (EVENTS.EMIT.ABORTED), envelope traceId plumbing, and the onEmitError observability hook. Resolve the createBus vs new EventBus API ambiguity (impl ships only the class) and add an explicit async listener-throw scenario. Pin every gap behind a *.spec-conformance.test.ts so future drift exits the 5-gate non-zero.
Current state
@suaveplan/events is fully implemented:
EventBus<TMap>exposesnew EventBus({ clock?, idGenerator? }); nocreateBusfactory.emitAsync(type, payload, { signal?, timeoutMs?, traceId? })—withTimeoutwraps the dispatch chain and rejects withEventsError("EVENTS.EMIT.TIMED_OUT")whentimeoutMselapses.- Per-listener
signal.abortedcheck throwsEventsError("EVENTS.EMIT.ABORTED")withcause === signal.reason. wrapaccepts and emitstraceIdonEventEnvelope.withObservability(bus, adapter)registers an interceptor that callsadapter.onEmit?/onEmitComplete?/onEmitError?.EVENTS_CODESno longer carriesSCHEMA.REJECTED(delegated to the futureevents-schemainterceptor package).
The spec, however, still says createBus({ clock?, idGenerator? }) (line 118) and has zero scenarios for timeout, abort, traceId, onEmitError, or async listener-throw. A consumer reading the spec gets a misleading picture of the public API surface, and conformance can silently regress without a single CI signal.
What changes
-
Modify Requirement: Deterministic id and clock sources — replace
createBus({ clock?, idGenerator? })withnew EventBus({ clock?, idGenerator? }). The class constructor is the canonical injection point. -
Modify Requirement: Structured errors — add a scenario covering async listener throw (parallel to the existing sync-emit scenario), since
dispatch()andemit()capture errors via independent paths. -
Modify Requirement: Passive observability adapter — add a scenario covering
adapter.onEmitError(envelope, error)firing when the dispatch chain rejects. -
ADD Requirement: emitAsync timeout — codify
timeoutMssemantics andEVENTS.EMIT.TIMED_OUT. -
ADD Requirement: emitAsync abort — codify
signalsemantics andEVENTS.EMIT.ABORTED(includingcause === signal.reason). -
ADD Requirement: Envelope traceId propagation — codify that
emit/emitAsyncaccepttraceIdand surface it onEventEnvelope.traceId. -
Add
src/conformance.spec-conformance.test.tspinning the new scenarios so the 5-gate publish flow catches drift. -
Add
**Verified by:**lines to every requirement (matching the conformance convention used bydatetime,i18n, and other recently-folded packages).
Why now
These are the five spec gaps surfaced during a focused enterprise-quality audit of openspec/archive/2026-04-19-enterprise-events/. The implementation has the behaviors today (and 100% coverage), but the spec is the contract — without scenarios, a future refactor can drop timeout handling and ship green. Tightening now is cheap (~30 minutes) and prevents both phantom enhancement requests and silent behavior regression in the substrate every domain-event-emitting package depends on.
Impact
@suaveplan/events— no runtime changes. Adds one*.spec-conformance.test.ts(~6 scenarios). Coverage stays at 100%.- Downstream
events-metrics,events-otel,events-schema,events-transport— none affected; their interceptor contract is unchanged. - Spec readers — get an accurate picture of the API including timeout / abort / traceId / onEmitError, which removes the most likely sources of misuse.