4.8 KiB
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:
- Spec ↔ impl runtime-dep violation — spec mandates runtime deps =
{ tslib, zod }and forbids any@suaveplan/*. Impl carries@suaveplan/typesforMessageResolver. Resolution: amend the rule to allow@suaveplan/typesexclusively (types-only, zero-runtime, byte-size guarded — same carve-out logic as testing-of-testing). fromJSONcannot satisfy spec scenario —SuaveplanErrorisabstract, sofromJSONcorrectly returns a structurally-shaped POJO carrying the__suaveplanErrordiscriminator. The spec scenario assertinginstanceof SuaveplanErroris wrong (impossible to satisfy by design). Resolution: rewrite the scenario around the discriminator + structural equality.- Redaction was shallow —
redactContextonly walked one level viaObject.entries. NestedSensitive<T>inside arrays / sub-objects passed through unredacted, leaking PII on the wire. FIXED in this change:redactValuenow recursively walks objects and arrays with a cycle-safeWeakSet.
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(nowredactValue, recursive) replacesSensitive<T>withREDACTED_LABELat any depth.toJSONemits{ code: "ERROR.CAUSE.TRUNCATED", name: "TruncatedCause", message: "[cause chain truncated]", timestamp: 0 }whenmaxDepthis exceeded or a cycle is detected — undocumented in spec.fromJSONreturnsReconstructedError(POJO with__suaveplanError: 1discriminator). Cannot return a realSuaveplanErrorinstance because the base class is abstract; reconstructing subclass identity from the wire is unsafe (consumer-defined classes aren't on the wire).package.jsondependenciescarries@suaveplan/typesfor theMessageResolvertype.@suaveplan/typesships zero runtime code (verified by byte-size test in the types package).
What changes
- 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. - Modify Requirement: Lossless wire serialization — rewrite the JSON round-trip scenario around structural equality +
__suaveplanErrordiscriminator (drop the impossibleinstanceof SuaveplanErrorclaim). - Modify Requirement: Cause-chain traversal — add a scenario covering cycle detection during
toJSON(parallel to the existingwalkCausescycle scenario). - 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. - ADD Requirement: TruncatedCause sentinel — codify the sentinel shape and the conditions under which it is emitted (cycle OR depth-cap during
toJSON). - ADD Requirement: Wire format version discriminator — codify
__suaveplanError: 1as the wire version field, whatWIRE_VERSIONrepresents, and the forward-compat policy (rejects unknown versions via Zod). - Add
src/conformance.spec-conformance.test.tspinning recursive redaction, TruncatedCause emission, and the discriminator round-trip. - 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;
SuaveplanErrorAPI surface unchanged. @suaveplan/typesruntime dep is now contractually permitted (it was always present; spec just lied about it).
Deferred (not in scope)
WIRE_VERSIONmigration / 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
ErrorContextgeneric narrowing (would help downstream typing but is a bigger surface change).