51 lines
4.8 KiB
Markdown
51 lines
4.8 KiB
Markdown
# 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).
|