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

3.7 KiB

OpenSpec Change: retry-spec-conformance

  • Change ID: retry-spec-conformance
  • Package: @suaveplan/retry
  • Status: PROPOSED
  • Date: 2026-04-26

Summary

Add **Verified by:** lines to every requirement in openspec/specs/retry/spec.md, fill four implicit-only test scenarios (RetryConfig generic threading, deterministic jitter under an injected Rng, per-attempt AbortSignal forwarding to fn, mid-backoff abort yielding RetryAbortedError), pin the full circuit-breaker FSM walk in a single test, and round-trip every published Zod schema (RT-09) so bun scripts/openspec-conformance.ts @suaveplan/retry exits 0. No public-API changes.

Current state

@suaveplan/retry is structurally solid: retry/withRetry/withTimeout/sleep engine, three backoff factories (constant/linear/exponential) with four jitter modes, CircuitBreaker FSM with EventBus-emitted stateChange, four structured errors (RetryExhaustedError, RetryAbortedError, AttemptTimeoutError, CircuitOpenError), OTel recorder helpers, eight Zod schemas, fast-check property tests, 100% coverage. Conformance gaps are documentary plus four un-pinned scenarios:

  • 11/11 requirements lack **Verified by:** lines (cf. openspec/AGENTS.md §13.1).
  • "RetryConfig generic threading" — type-level scenario with no expectTypeOf assertion.
  • "Deterministic jitter under injected Rng" — covered indirectly by single-shot rng tests but no two-strategy cross-check.
  • "Signal forwarded to fn on every attempt" — covered for one attempt only.
  • "Mid-backoff abort yields RetryAbortedError, not RetryExhaustedError" — implicit in aborts between attempts but no explicit type check.
  • "Full FSM walk closed→open→half-open→closed→open→half-open→open in one trace" — separate transitions covered, full path not pinned.
  • Zod schemas (RT-09) are exported but never round-tripped in tests.

Spec drift noted (no code change)

The withTimeout signature in the implementation is (fn, ms, attemptNumber, signal?) (4 args), while the spec previously read (fn, ms, signal?). The implementation is the more complete shape — attemptNumber is required to populate AttemptTimeoutError.attemptNumber. This delta updates the spec language to match the shipped signature; no code change.

What changes

  1. Add **Verified by:** line under every ### RT-NN — block in openspec/specs/retry/spec.md.
  2. Update RT-05 spec language to match the shipped 4-arg withTimeout signature.
  3. Update RT-10 spec language so it reflects the opt-in helper-based emission model (no auto-instrumentation in retry() / CircuitBreaker.call()).
  4. Add src/retry/retry.spec-conformance.test.ts covering: retry<T> generic narrowing (expectTypeOf), deterministic seeded-Rng jitter sequence, signal-forwarded-to-fn-on-every-attempt, mid-backoff abort instanceof RetryAbortedError not RetryExhaustedError, backoff factory purity, RetryExhaustedError cause chain.
  5. Add src/circuit-breaker/circuit-breaker.spec-conformance.test.ts covering: full closed→open→half-open→closed→open→half-open→open trace in one assertion, open-state never invokes fn, CircuitOpenError.context.openedAt carries the injected-clock timestamp.
  6. Add src/types/retry.schemas.spec-conformance.test.ts covering: round-trip parse + reject for every published schema (JitterMode, ConstantBackoffOptions, LinearBackoffOptions, ExponentialBackoffOptions, RetryConfig, CircuitBreakerConfig).
  7. Re-run bun scripts/openspec-conformance.ts @suaveplan/retry and confirm exit 0.

Impact

  • Public API unchanged.
  • Three new test files (~280 lines combined).
  • openspec/specs/retry/spec.md gains 11 **Verified by:** lines and two language clarifications (RT-05, RT-10).