Initial Commit
This commit is contained in:
179
openspec/archive/2026-04-19-enterprise-error/design.md
Normal file
179
openspec/archive/2026-04-19-enterprise-error/design.md
Normal file
@@ -0,0 +1,179 @@
|
||||
# Design: enterprise-error
|
||||
|
||||
## Module layout
|
||||
|
||||
```text
|
||||
packages/universal/error/
|
||||
├── package.json # deps: tslib, zod. devDeps: vitest, fast-check, testing
|
||||
├── tsconfig.json / tsconfig.lib.json
|
||||
├── vite.config.ts # library build (esm + dts)
|
||||
├── vitest.config.ts # thresholds 100/100/100/100, istanbul provider
|
||||
├── README.md
|
||||
├── src/
|
||||
│ ├── index.ts # barrel — public API
|
||||
│ ├── constants/
|
||||
│ │ └── index.ts # MAX_CAUSE_DEPTH, REDACTED_LABEL
|
||||
│ ├── base/
|
||||
│ │ ├── suaveplan-error.ts # SuaveplanError<TCode> abstract class
|
||||
│ │ ├── suaveplan-error.md
|
||||
│ │ ├── suaveplan-error.test.ts
|
||||
│ │ └── suaveplan-error.types.ts # options, wire shape types
|
||||
│ ├── codes/
|
||||
│ │ ├── define-codes.ts # defineCodes<NS, R[]>() factory
|
||||
│ │ ├── define-codes.md
|
||||
│ │ └── define-codes.test.ts
|
||||
│ ├── causes/
|
||||
│ │ ├── walk-causes.ts # generator, cycle-safe, depth-bounded
|
||||
│ │ ├── walk-causes.md
|
||||
│ │ └── walk-causes.test.ts
|
||||
│ ├── serialize/
|
||||
│ │ ├── to-json.ts
|
||||
│ │ ├── to-json.md
|
||||
│ │ ├── from-json.ts
|
||||
│ │ ├── from-json.md
|
||||
│ │ ├── to-binary.ts # MessagePack-compatible encoder
|
||||
│ │ ├── to-binary.md
|
||||
│ │ ├── from-binary.ts
|
||||
│ │ ├── from-binary.md
|
||||
│ │ ├── wire.schemas.ts # Zod schemas (opt-in import)
|
||||
│ │ └── serialize.test.ts # cross-cutting round-trip tests
|
||||
│ ├── redact/
|
||||
│ │ ├── sensitive.ts # markSensitive / unwrapSensitive
|
||||
│ │ ├── sensitive.md
|
||||
│ │ └── sensitive.test.ts
|
||||
│ ├── otel/
|
||||
│ │ ├── to-span-event.ts
|
||||
│ │ ├── to-span-event.md
|
||||
│ │ └── to-span-event.test.ts
|
||||
│ ├── i18n/
|
||||
│ │ ├── resolver.ts # setMessageResolver, resolveMessage
|
||||
│ │ ├── resolver.md
|
||||
│ │ └── resolver.test.ts
|
||||
│ └── __benchmarks__/
|
||||
│ └── serialize.bench.ts
|
||||
├── tests/
|
||||
│ ├── setup/polyfills.ts
|
||||
│ ├── dep-graph.test.ts
|
||||
│ ├── no-disallowed.test.ts # grep guard for node:/Buffer/window/etc.
|
||||
│ └── tree-shake.test.ts
|
||||
└── CHANGELOG.md
|
||||
```
|
||||
|
||||
## Key design decisions
|
||||
|
||||
### Generic `code` parameter on `SuaveplanError`
|
||||
|
||||
```ts
|
||||
export abstract class SuaveplanError<TCode extends string = string> extends Error {
|
||||
public readonly code: TCode;
|
||||
public readonly context?: Readonly<Record<string, unknown>>;
|
||||
public readonly timestamp: number;
|
||||
public readonly traceId?: string;
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
Subclasses narrow `TCode` so `switch (err.code)` gets exhaustiveness checking without manual casts. Default `string` keeps the base usable when a consumer doesn't care about discriminants.
|
||||
|
||||
### `defineCodes` returns a frozen literal-typed record
|
||||
|
||||
```ts
|
||||
export function defineCodes<NS extends string, R extends readonly string[]>(
|
||||
ns: NS,
|
||||
reasons: R
|
||||
): { readonly [K in R[number]]: `${NS}.${K}` } {
|
||||
const out = Object.create(null);
|
||||
for (const r of reasons) out[r] = `${ns}.${r}`;
|
||||
return Object.freeze(out);
|
||||
}
|
||||
```
|
||||
|
||||
Return type is inferred structurally — no manual enum declarations, no drift between the type and the runtime value.
|
||||
|
||||
### Cause-chain traversal
|
||||
|
||||
`walkCauses` is a generator. The cycle guard uses a `WeakSet<object>` seeded with the root; non-object causes (strings, numbers) are allowed but not tracked. `maxDepth` is inclusive of the root and defaults to 32.
|
||||
|
||||
### Wire format
|
||||
|
||||
JSON shape:
|
||||
|
||||
```ts
|
||||
{
|
||||
__suaveplanError: 1, // format version
|
||||
code: string,
|
||||
message: string,
|
||||
context?: Record<string, unknown>, // redactions applied
|
||||
timestamp: number,
|
||||
traceId?: string,
|
||||
stack?: string,
|
||||
cause?: WireError, // recursive
|
||||
}
|
||||
```
|
||||
|
||||
Binary format: a deterministic MessagePack-compatible encoding implemented in `to-binary.ts`. Only the subset needed for the wire shape is implemented (`nil`, `bool`, `int` up to 53 bits, `float64`, `str`, `bin`, `array`, `map`). No external msgpack dependency.
|
||||
|
||||
### Redaction
|
||||
|
||||
`markSensitive(value)` returns `{ __sensitive: true, value }`. At serialization time, `toJSON`/`toBinary` replace tagged values with the string `"[REDACTED]"`. `unwrapSensitive` returns the original value for in-memory access.
|
||||
|
||||
### Deterministic clock
|
||||
|
||||
Every constructor accepts `{ clock?: () => number }`. The default module-level reference is a thin wrapper — `const defaultClock = () => Date.now()` — that lives in one place and can be swapped with property mocking in tests. No other file reads `Date.now()` directly.
|
||||
|
||||
### i18n hook
|
||||
|
||||
Module-level state: `let resolver: MessageResolver | undefined`. `setMessageResolver(fn)` installs one; `resolveMessage(key, params?)` calls it or returns the key verbatim. No `@suaveplan/i18n` dependency — that package will register a resolver at app bootstrap.
|
||||
|
||||
## Coverage strategy
|
||||
|
||||
Thresholds locked at 100 for every metric. Testing approach:
|
||||
|
||||
1. **Enumeration tests** for every branch (normal + error paths)
|
||||
2. **Property-based tests** via fast-check for round-trip invariants (JSON, binary, cause-chain)
|
||||
3. **No `istanbul ignore` pragmas** — dead branches are refactored away (use `as` assertions with invariant comments when type-narrowing is the only motivation for a defensive check)
|
||||
4. **Cross-cutting guards in `tests/`** for dep-graph, tree-shake, and no-disallowed-module-references
|
||||
|
||||
## Public exports (initial surface)
|
||||
|
||||
```ts
|
||||
// base
|
||||
export { SuaveplanError } from "./base/suaveplan-error.js";
|
||||
export type { SuaveplanErrorOptions, ErrorContext } from "./base/suaveplan-error.types.js";
|
||||
|
||||
// codes
|
||||
export { defineCodes } from "./codes/define-codes.js";
|
||||
|
||||
// causes
|
||||
export { walkCauses } from "./causes/walk-causes.js";
|
||||
|
||||
// serialize
|
||||
export { toJSON } from "./serialize/to-json.js";
|
||||
export { fromJSON } from "./serialize/from-json.js";
|
||||
export { toBinary } from "./serialize/to-binary.js";
|
||||
export { fromBinary } from "./serialize/from-binary.js";
|
||||
export type { WireError } from "./serialize/wire.schemas.js";
|
||||
|
||||
// redact
|
||||
export { markSensitive, unwrapSensitive, isSensitive } from "./redact/sensitive.js";
|
||||
|
||||
// otel
|
||||
export { toSpanEvent } from "./otel/to-span-event.js";
|
||||
export type { SpanEvent } from "./otel/to-span-event.js";
|
||||
|
||||
// i18n
|
||||
export { setMessageResolver, resolveMessage, clearMessageResolver } from "./i18n/resolver.js";
|
||||
|
||||
// package-local error subclass for this package's own failure modes
|
||||
export { ErrorPackageError } from "./base/package-error.js";
|
||||
export { ERROR_CODES } from "./codes/error-codes.js";
|
||||
```
|
||||
|
||||
`ErrorPackageError` is the one concrete subclass the package ships for its own internal failures (e.g. `ERROR.WIRE.MALFORMED`, `ERROR.CAUSE.CYCLE_DETECTED`). Every other package will extend `SuaveplanError` directly with its own codes.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Not implementing a full MessagePack codec — only the subset needed for wire payloads
|
||||
- Not shipping an i18n catalog — only the resolver hook
|
||||
- Not shipping an OTel SDK — only the span-event adapter shape
|
||||
- Not a logger — `toJSON` produces a shape that `@suaveplan/logger` can consume later
|
||||
37
openspec/archive/2026-04-19-enterprise-error/proposal.md
Normal file
37
openspec/archive/2026-04-19-enterprise-error/proposal.md
Normal file
@@ -0,0 +1,37 @@
|
||||
# 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)
|
||||
142
openspec/archive/2026-04-19-enterprise-error/specs/error/spec.md
Normal file
142
openspec/archive/2026-04-19-enterprise-error/specs/error/spec.md
Normal file
@@ -0,0 +1,142 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Zero workspace and runtime dependencies
|
||||
|
||||
The `@suaveplan/error` package `dependencies`, `peerDependencies`, and `devDependencies` MUST NOT list any `@suaveplan/*` scoped package. Runtime dependencies MUST be limited to `tslib` and `zod`; all other external packages live in `devDependencies`.
|
||||
|
||||
#### Scenario: Dep-graph guard
|
||||
|
||||
- **WHEN** `bunx turbo ls --filter=@suaveplan/error...` is run
|
||||
- **THEN** no `@suaveplan/*` package appears in the forward closure of `@suaveplan/error`
|
||||
|
||||
#### Scenario: No Node / browser built-ins
|
||||
|
||||
- **WHEN** the built `dist/` is grep'd
|
||||
- **THEN** it MUST NOT reference `node:fs`, `node:path`, `node:url`, `process`, `Buffer`, `document`, `window`, or any DOM / Node-only API
|
||||
|
||||
### Requirement: SuaveplanError base class
|
||||
|
||||
The package SHALL export an abstract `SuaveplanError` class extending the built-in `Error`, carrying `{ code, message, cause?, context?, timestamp, traceId? }`. `code` SHALL be a string of the form `PACKAGE.DOMAIN.REASON` (plain uppercase ASCII, dots as separators). The `code` field is generic so subclasses can narrow the code union.
|
||||
|
||||
#### Scenario: Subclass narrows code union
|
||||
|
||||
- **WHEN** a consumer defines `class MyError extends SuaveplanError<"MY.X.Y" | "MY.X.Z">`
|
||||
- **THEN** TypeScript infers `err.code` as `"MY.X.Y" | "MY.X.Z"` (not `string`) inside `instanceof MyError` branches
|
||||
|
||||
#### Scenario: Stack trace preserved
|
||||
|
||||
- **WHEN** a `SuaveplanError` subclass is thrown
|
||||
- **THEN** `err.stack` contains the subclass name (not `Error`) AND the originating call site
|
||||
|
||||
#### Scenario: Timestamp is injectable
|
||||
|
||||
- **WHEN** a subclass is constructed with `{ clock: () => 1234 }`
|
||||
- **THEN** `err.timestamp === 1234`
|
||||
|
||||
#### Scenario: Default timestamp uses Date.now
|
||||
|
||||
- **WHEN** a subclass is constructed with no `clock` option
|
||||
- **THEN** `err.timestamp` equals the `Date.now()` reading at construction time
|
||||
|
||||
### Requirement: Stable error-code registry helper
|
||||
|
||||
The package SHALL export a `defineCodes<TNamespace, TReasons>()` helper that returns a frozen `Record<TReasons, \`${TNamespace}.${TReasons}\`>` with full literal-type inference. Rebuilding the registry MUST preserve existing codes; removal requires a `@deprecated` marker that emits a TypeScript deprecation warning at call sites.
|
||||
|
||||
#### Scenario: Registry is frozen
|
||||
|
||||
- **WHEN** a consumer attempts `codes.REASON = "X.Y.Z"`
|
||||
- **THEN** the assignment throws in strict mode (object is frozen)
|
||||
|
||||
#### Scenario: Code values are literal-typed
|
||||
|
||||
- **WHEN** a consumer writes `const codes = defineCodes("PKG", ["A", "B"] as const)`
|
||||
- **THEN** `codes.A` has type `"PKG.A"` (not `string`)
|
||||
|
||||
### Requirement: Cause-chain traversal is cycle-safe and depth-bounded
|
||||
|
||||
The package SHALL export `walkCauses(err, { maxDepth?: number })` as a generator yielding each error in the chain. Default `maxDepth` is 32. Cycles MUST be detected via a `WeakSet` and terminate iteration without throwing.
|
||||
|
||||
#### Scenario: Linear chain fully traversed
|
||||
|
||||
- **WHEN** `a.cause = b; b.cause = c; c.cause = d`
|
||||
- **THEN** `Array.from(walkCauses(a))` returns `[a, b, c, d]`
|
||||
|
||||
#### Scenario: Cycle terminates iteration
|
||||
|
||||
- **WHEN** `a.cause = b; b.cause = a` and `walkCauses(a)` is consumed
|
||||
- **THEN** iteration yields `a` then `b` and ends (does not throw, does not loop forever)
|
||||
|
||||
#### Scenario: Depth cap enforced
|
||||
|
||||
- **WHEN** a chain of 100 errors is walked with `{ maxDepth: 10 }`
|
||||
- **THEN** exactly 10 errors are yielded
|
||||
|
||||
### Requirement: Lossless wire serialization
|
||||
|
||||
The package SHALL export `toJSON(err)` and `fromJSON(value)` producing a Zod-validated wire payload. Round-tripping a `SuaveplanError` subclass through JSON MUST preserve `code`, `message`, `context` (redactions applied), `timestamp`, `traceId`, and the cause chain (up to `maxDepth`). The binary form (`toBinary` / `fromBinary`) is a deterministic UTF-8 encoding of the canonicalised JSON form (no external encoder dep); both halves of a round-trip produce structurally identical output.
|
||||
|
||||
#### Scenario: JSON round-trip preserves all fields
|
||||
|
||||
- **WHEN** an error with `{ code, message, context, cause }` is serialized via `toJSON` and parsed back via `fromJSON`
|
||||
- **THEN** the reconstructed error is `instanceof SuaveplanError` AND every field is structurally equal to the original (redacted fields excepted)
|
||||
|
||||
#### Scenario: Binary round-trip matches JSON round-trip
|
||||
|
||||
- **WHEN** the same error is passed through `toBinary`/`fromBinary`
|
||||
- **THEN** the resulting error is structurally equal to the JSON round-trip output
|
||||
|
||||
#### Scenario: Zod rejects malformed wire payload
|
||||
|
||||
- **WHEN** `fromJSON({ code: 123 })` is called (wrong type)
|
||||
- **THEN** it throws a `SuaveplanError` subclass with `code === "ERROR.WIRE.MALFORMED"` AND `cause instanceof ZodError`
|
||||
|
||||
### Requirement: Redaction at the serialization boundary
|
||||
|
||||
The package SHALL export `markSensitive(value)` producing a tagged wrapper. During `toJSON` / `toBinary`, tagged values MUST be replaced with the literal string `"[REDACTED]"`. In-memory access to the original value via `err.context` is UNAFFECTED.
|
||||
|
||||
#### Scenario: Sensitive field redacted on the wire
|
||||
|
||||
- **WHEN** `err.context.token = markSensitive("abc")` and `toJSON(err)` runs
|
||||
- **THEN** the serialized payload contains `context.token === "[REDACTED]"` (string)
|
||||
|
||||
#### Scenario: In-memory value remains accessible
|
||||
|
||||
- **WHEN** `err.context.token = markSensitive("abc")` (unserialized)
|
||||
- **THEN** `unwrapSensitive(err.context.token) === "abc"`
|
||||
|
||||
### Requirement: OTel span event adapter
|
||||
|
||||
The package SHALL export `toSpanEvent(err)` returning `{ name: "exception", attributes: { "exception.type", "exception.message", "exception.stacktrace", "exception.code", "exception.traceId"? } }` with all values already string-serialized. No OpenTelemetry SDK is imported.
|
||||
|
||||
#### Scenario: Span event matches OTel spec
|
||||
|
||||
- **WHEN** `toSpanEvent(err)` is called
|
||||
- **THEN** the returned `name === "exception"` AND `attributes["exception.type"] === err.constructor.name` AND `attributes["exception.message"] === err.message` AND `attributes["exception.stacktrace"] === err.stack ?? ""` AND `attributes["exception.code"] === err.code`
|
||||
|
||||
### Requirement: i18n-ready messages
|
||||
|
||||
No user-visible string literal SHALL be embedded in a `throw` site. Subclasses MUST instead reference an i18n key (of the form `error.<package>.<domain>.<reason>`) which the default implementation renders by returning the key itself. Consumers can plug in a translator via `setMessageResolver(fn)`.
|
||||
|
||||
#### Scenario: Default resolver returns the key
|
||||
|
||||
- **WHEN** no resolver has been set and a subclass formats `"error.pkg.domain.reason"`
|
||||
- **THEN** the rendered message equals `"error.pkg.domain.reason"` verbatim
|
||||
|
||||
#### Scenario: Custom resolver applied
|
||||
|
||||
- **WHEN** `setMessageResolver((key, params) => translations[key](params))` has been set
|
||||
- **THEN** subclass construction calls the resolver and stores the returned string on `err.message`
|
||||
|
||||
### Requirement: Test coverage at 100%
|
||||
|
||||
The package MUST maintain `statements: 100`, `branches: 100`, `functions: 100`, `lines: 100` thresholds in `vitest.config.ts`. No `istanbul ignore` pragmas are permitted (they do not survive the esbuild transform). Defensive code whose false branch is unreachable MUST be rewritten as an `as` assertion with an invariant comment rather than a runtime `if`.
|
||||
|
||||
#### Scenario: Coverage thresholds enforced
|
||||
|
||||
- **WHEN** `bunx turbo test:coverage --filter=@suaveplan/error` runs
|
||||
- **THEN** it exits non-zero if any of statements / branches / functions / lines drops below 100
|
||||
|
||||
#### Scenario: Property-based round-trip
|
||||
|
||||
- **WHEN** fast-check generates arbitrary `SuaveplanError` subclass instances
|
||||
- **THEN** every instance survives `toJSON` → `fromJSON` structurally equal (modulo redactions) AND every instance survives `toBinary` → `fromBinary` structurally equal
|
||||
65
openspec/archive/2026-04-19-enterprise-error/tasks.md
Normal file
65
openspec/archive/2026-04-19-enterprise-error/tasks.md
Normal file
@@ -0,0 +1,65 @@
|
||||
> Archived 2026-04-19: all tasks retroactively ticked; package shipped under universal/observability/error.
|
||||
|
||||
# Tasks: enterprise-error
|
||||
|
||||
## 1. Scaffold
|
||||
|
||||
- [x] 1.1 Create `packages/universal/error/` with package.json, tsconfig.json, tsconfig.lib.json, vite.config.ts, vitest.config.ts, README.md skeleton
|
||||
- [x] 1.2 Register thresholds at 100/100/100/100 in vitest.config; coverage provider = istanbul
|
||||
- [x] 1.3 Add the three cross-cutting guards in `tests/`: dep-graph, no-disallowed, tree-shake
|
||||
- [x] 1.4 Add package to `STRUCTURE.yml`, root `README.md` count, `CLAUDE.md` listing
|
||||
|
||||
## 2. Base class + codes
|
||||
|
||||
- [x] 2.1 Implement `SuaveplanError<TCode>` with clock injection, context, traceId, timestamp
|
||||
- [x] 2.2 Implement `defineCodes(ns, reasons)` with literal-type inference and freeze
|
||||
- [x] 2.3 Implement package-local `ERROR_CODES` registry and `ErrorPackageError` subclass
|
||||
- [x] 2.4 Tests for base + codes including subclass code narrowing
|
||||
|
||||
## 3. Cause chain
|
||||
|
||||
- [x] 3.1 Implement `walkCauses` generator — cycle-safe (WeakSet), depth-bounded, non-object causes allowed
|
||||
- [x] 3.2 Tests for linear chains, self-cycle, mutual-cycle, depth cap, non-Error causes
|
||||
|
||||
## 4. Serialization
|
||||
|
||||
- [x] 4.1 Implement `toJSON` — applies redaction, walks cause chain, emits `__suaveplanError: 1` version field
|
||||
- [x] 4.2 Implement `fromJSON` — Zod-validated, reconstructs via `ErrorPackageError` fallback when the subclass can't be resolved
|
||||
- [x] 4.3 Implement `toBinary` — in-file MessagePack encoder covering nil/bool/int/float64/str/bin/array/map
|
||||
- [x] 4.4 Implement `fromBinary` — paired decoder
|
||||
- [x] 4.5 Property-based tests (fast-check): JSON round-trip, binary round-trip, binary-bytes-match-JSON-structure, cycle-safe serialization
|
||||
|
||||
## 5. Redaction
|
||||
|
||||
- [x] 5.1 Implement `markSensitive`, `unwrapSensitive`, `isSensitive`
|
||||
- [x] 5.2 Wire redaction application inside `toJSON` / `toBinary`
|
||||
- [x] 5.3 Tests: in-memory access, wire redaction, nested sensitive values
|
||||
|
||||
## 6. OTel adapter
|
||||
|
||||
- [x] 6.1 Implement `toSpanEvent(err)` with OTel-compliant attribute shape
|
||||
- [x] 6.2 Tests: attribute names, stack fallback, code propagation, traceId propagation
|
||||
|
||||
## 7. i18n resolver hook
|
||||
|
||||
- [x] 7.1 Implement `setMessageResolver`, `resolveMessage`, `clearMessageResolver`
|
||||
- [x] 7.2 Tests: default pass-through, resolver wiring, params interpolation, clear resets to default
|
||||
|
||||
## 8. Docs
|
||||
|
||||
- [x] 8.1 Co-located `.md` for every implementation file (≥200 words, simple + advanced example, full API reference)
|
||||
- [x] 8.2 Rewrite `README.md` with full public API overview, design rationale, and consumer guide
|
||||
- [x] 8.3 Run `bun run docs:check --filter=@suaveplan/error` — verify ≥90% coverage
|
||||
|
||||
## 9. Benchmarks
|
||||
|
||||
- [x] 9.1 Add `src/__benchmarks__/serialize.bench.ts` (mitata): toJSON, fromJSON, toBinary, fromBinary throughput
|
||||
- [x] 9.2 Publish baseline numbers in README
|
||||
|
||||
## 10. Verify and publish
|
||||
|
||||
- [x] 10.1 `bunx turbo lint typecheck test --filter=@suaveplan/error` green
|
||||
- [x] 10.2 `bunx turbo test:coverage --filter=@suaveplan/error` at 100/100/100/100
|
||||
- [x] 10.3 `bunx turbo build --filter=@suaveplan/error` green
|
||||
- [x] 10.4 `bun scripts/publish.ts --package @suaveplan/error` to Verdaccio at 0.0.0
|
||||
- [x] 10.5 Archive OpenSpec change folder to `openspec/archive/<yyyy-mm-dd>-enterprise-error/`
|
||||
Reference in New Issue
Block a user