# 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` 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` 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` 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).