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

4.8 KiB

OpenSpec Change: error-spec-tightening

  • Change ID: error-spec-tightening
  • Package: @suaveplan/error
  • Status: PROPOSED
  • Date: 2026-04-27

Summary

Reconcile openspec/specs/error/spec.md with the actual @suaveplan/error implementation. Three real defects + several spec gaps:

  1. Spec ↔ impl runtime-dep violation — spec mandates runtime deps = { tslib, zod } and forbids any @suaveplan/*. Impl carries @suaveplan/types for MessageResolver. Resolution: amend the rule to allow @suaveplan/types exclusively (types-only, zero-runtime, byte-size guarded — same carve-out logic as testing-of-testing).
  2. fromJSON cannot satisfy spec scenario — SuaveplanError is abstract, so fromJSON correctly returns a structurally-shaped POJO carrying the __suaveplanError discriminator. The spec scenario asserting instanceof SuaveplanError is wrong (impossible to satisfy by design). Resolution: rewrite the scenario around the discriminator + structural equality.
  3. Redaction was shallow — redactContext only walked one level via Object.entries. Nested Sensitive<T> inside arrays / sub-objects passed through unredacted, leaking PII on the wire. FIXED in this change: redactValue now recursively walks objects and arrays with a cycle-safe WeakSet.

Plus three previously-undocumented behaviors that are pinned: TruncatedCause sentinel emitted by toJSON on cycle/depth-cap, WIRE_VERSION discriminator semantics, and **Verified by:** lines on every requirement.

Current state

  • redactContext (now redactValue, recursive) replaces Sensitive<T> with REDACTED_LABEL at any depth.
  • toJSON emits { code: "ERROR.CAUSE.TRUNCATED", name: "TruncatedCause", message: "[cause chain truncated]", timestamp: 0 } when maxDepth is exceeded or a cycle is detected — undocumented in spec.
  • fromJSON returns ReconstructedError (POJO with __suaveplanError: 1 discriminator). Cannot return a real SuaveplanError instance because the base class is abstract; reconstructing subclass identity from the wire is unsafe (consumer-defined classes aren't on the wire).
  • package.json dependencies carries @suaveplan/types for the MessageResolver type. @suaveplan/types ships zero runtime code (verified by byte-size test in the types package).

What changes

  1. Modify Requirement: Zero workspace and runtime dependencies — narrow the prohibition: forbid @suaveplan/* packages EXCEPT @suaveplan/types (types-only, zero runtime). Same carve-out logic as the testing-infrastructure rule.
  2. Modify Requirement: Lossless wire serialization — rewrite the JSON round-trip scenario around structural equality + __suaveplanError discriminator (drop the impossible instanceof SuaveplanError claim).
  3. Modify Requirement: Cause-chain traversal — add a scenario covering cycle detection during toJSON (parallel to the existing walkCauses cycle scenario).
  4. Modify Requirement: Redaction at the serialization boundary — codify recursive redaction: Sensitive<T> is replaced with [REDACTED] at any depth, in arrays and sub-objects, with cycle safety.
  5. ADD Requirement: TruncatedCause sentinel — codify the sentinel shape and the conditions under which it is emitted (cycle OR depth-cap during toJSON).
  6. ADD Requirement: Wire format version discriminator — codify __suaveplanError: 1 as the wire version field, what WIRE_VERSION represents, and the forward-compat policy (rejects unknown versions via Zod).
  7. Add src/conformance.spec-conformance.test.ts pinning recursive redaction, TruncatedCause emission, and the discriminator round-trip.
  8. Add **Verified by:** lines to every requirement in the live spec.

Why now

@suaveplan/error is the substrate every other @suaveplan/* package extends. Three of the issues here are real defects (PII leak via shallow redaction, an impossible spec assertion, and a self-violated dependency rule) — the others are drift that will silently regress under future refactoring without **Verified by:** lines pinning behavior.

Impact

  • @suaveplan/error — one behavioral fix (recursive redaction). Adds one *.spec-conformance.test.ts. Coverage stays at 100%.
  • All downstream packages — none affected; SuaveplanError API surface unchanged.
  • @suaveplan/types runtime dep is now contractually permitted (it was always present; spec just lied about it).

Deferred (not in scope)

  • WIRE_VERSION migration / negotiation policy beyond rejection of unknowns (can land when v2 is contemplated).
  • Deprecation marker support in defineCodes (currently spec mandates removal-via-@deprecated, but impl has no support — defer; track separately).
  • Per-subclass ErrorContext generic narrowing (would help downstream typing but is a bigger surface change).