Files
suaveplan/openspec/archive/2026-04-19-enterprise-error/proposal.md
2026-05-27 23:29:53 +01:00

38 lines
3.2 KiB
Markdown

# Change: enterprise-error
## Why
`@suaveplan/error` is the **foundation error substrate** — every other package in the monorepo extends `SuaveplanError` and uses its stable code registry. On the genesis branch it does not yet exist. It must land **before** any other Layer 1 package (events, crypto, bitpacker, timing, datetime, serialize, mime, env, opentelemetry) because each of them declares its own error subclass against this base.
Per the project rule for foundation packages (`memory: foundation-100-coverage`), this ships at **100%** coverage (statements / branches / functions / lines) — not the 95% floor. Anything less puts a long-term ceiling on every consumer.
## What Changes
### New package: `packages/universal/error`
Ships a self-contained, zero-dependency (no `@suaveplan/*`, no runtime browser/Node/React APIs) TypeScript library:
- **`SuaveplanError`** abstract base class with `{ code, message, cause?, context?, timestamp, traceId? }` shape
- **Stable code registry** — namespaced `PACKAGE.DOMAIN.REASON` strings, typed as a const record; each package adds its own leaf registry that extends this one without renumbering
- **Typed error discriminants** — generics propagate the code union through `instanceof` checks so consumers get exhaustive switch coverage without casts
- **Cause-chain traversal** — `walkCauses(err)` generator, cycle-safe, depth-bounded
- **Wire format** — Zod schema for JSON and MessagePack (MessagePack encoder ships as a pure function, no external `msgpack` dep; binary format is documented and versioned)
- **Redaction** — `@sensitive` tag on context fields, applied at the serialization boundary; redacted fields become `"[REDACTED]"` in the wire payload but remain accessible on the in-memory instance
- **Injectable clock** — `createError({ clock })` takes an optional clock so timestamps are deterministic under test; the module-level default uses `Date.now()` but is never touched at import time
- **OTel span event adapter** — `toSpanEvent(err)` returns the payload an OTel span recorder expects (`exception.type`, `exception.message`, `exception.stacktrace`, all attribute values serialized)
- **i18n-ready** — every user-visible string is keyed, never literal; the default locale is pass-through (returns the key)
### Why this stays foundation-pure
The enterprise brief calls for `@suaveplan/schema-registry`, `@suaveplan/logger`, `@suaveplan/i18n`, and `@suaveplan/opentelemetry` integration. Those are **adapters**, not dependencies — this package exposes the hooks (`toSpanEvent`, i18n-key strings, Zod schemas, logger-compatible `toJSON`) but does not import any of them. Wiring happens in the dependent packages.
### Coverage gate
Vitest thresholds set to 100/100/100/100 from day one. No `istanbul ignore` pragmas (empirically stripped by the esbuild transform). Dead defensive branches are either removed or upgraded to `as` assertions with invariant comments.
## Impact
- Unblocks every other Layer 1 package (events, crypto, bitpacker, timing, datetime, serialize, mime, env, opentelemetry) which extend `SuaveplanError`
- Adds one new spec capability: `error`
- No existing code paths affected (genesis branch has no consumers yet)