first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,219 @@
---
name: sp-error
description: Structured error hierarchy with typed codes, context redaction, cause chaining, RFC 7807 Problem Details, OTel attributes, and neverthrow Result wrappers.
---
# Error for Suaveplan
Universal structured error system for the Suaveplan ecosystem. Provides typed error classes extending native `Error`, machine-readable codes, structured context with PII redaction, observability fields, retry hints, stable fingerprints, and interceptor pipelines for observability bridges.
## Architecture
| Component | Responsibility |
|-----------|---------------|
| `BaseError` | Foundation class — `code`, `context`, `severity`, `cause`, `timestamp`, `toJSON()`, `toProblemJson()`, `toOTelAttributes()`, `withContext()`, `hasCode()`, static `wrap()`, static `is()` |
| `NetworkError` | Network failures — `retryable`, `url`, `statusCode`. Factories: `timeout()`, `connectionRefused()`, `dnsFailure()`, `offline()` |
| `AuthError` | Authentication/authorization — `userId`, `requiredPermission`. Factories: `invalidCredentials()`, `tokenExpired()`, `forbidden()`, `sessionExpired()`, `mfaRequired()` |
| `ValidationError` | Field-level validation — `fields`, `addFieldError()`, `getFieldErrors()`, `hasFieldErrors`. Factories: `fromFields()`, `required()` |
| `HttpError` | HTTP-specific — `statusCode`, `isClientError`, `isServerError`, `retryable`. Factories: `fromStatus()`, `badRequest()`, `notFound()`, `conflict()`, `tooManyRequests()`, `internal()` |
| `TimeoutError` | Timeouts — `durationMs`, `limitMs`, `operation`. Factory: `for()` |
| `AbortError` | Cancellation — Factory: `fromSignal()` |
| `RateLimitError` | Rate limiting — `limit`, `remaining`, `resetAt`, `scope`. Factory: `exceeded()` |
| `ConfigError` | Configuration failures — Fatal + non-retryable by default. Factories: `missing()`, `invalidType()` |
| `AggregateError` | Bundles multiple `BaseError[]` — `filter()`, `byCode()`, `first`, `worstSeverity`. Factories: `from()`, `fromSettled()` |
| `ErrorCodes` | Process-wide singleton `CodeMetadataRegistry` — maps codes → HTTP/gRPC status, severity, retry, i18n |
| `errorRegistry` | Shared singleton registry — deserializes typed errors from JSON with pre-registered built-in classes |
| `redactContext` | Recursive PII redaction engine — cycles safe, depth bounded |
| `tryBase` / `tryBaseAsync` | neverthrow wrappers returning `Result<T, BaseError>` |
| `registerErrorInterceptor` | Callback invoked once per `BaseError` construction via `queueMicrotask` |
## File Structure
```
foundation/core/error/
src/base-error/base-error.ts # BaseError class
src/network-error/network-error.ts # NetworkError + factories
src/auth-error/auth-error.ts # AuthError + factories
src/validation-error/validation-error.ts # ValidationError + fluent field API
src/http-error/http-error.ts # HttpError + factories
src/timeout-error/timeout-error.ts # TimeoutError + factory
src/abort-error/abort-error.ts # AbortError + factory
src/rate-limit-error/rate-limit-error.ts # RateLimitError + factory
src/config-error/config-error.ts # ConfigError + factories
src/aggregate-error/aggregate-error.ts # AggregateError + factories
src/error-registry/error-registry.ts # ErrorRegistry + shared singleton
src/error-formatter/error-formatter.ts # formatErrorAsString, formatErrorAsJson, etc.
src/interceptors/interceptors.ts # registerErrorInterceptor, clearErrorInterceptors
src/result/result.ts # tryBase, tryBaseAsync, aggregate + neverthrow re-exports
src/utils/redaction.ts # redactContext, DEFAULT_REDACTION_PATTERNS
src/utils/fingerprint.ts # computeFingerprint, pickTopFrame
src/utils/clean-stack.ts # cleanStack
src/index.ts # Barrel re-export
```
Sibling packages:
- `@suaveplan/config` — Uses `ConfigError` for configuration failures.
- `@suaveplan/validation` — Uses `ValidationError` for schema validation failures.
- `neverthrow` — Runtime dependency for `Result` / `ResultAsync` wrappers.
## Patterns
### Quick Start
```typescript
import { BaseError } from "@suaveplan/error";
const err = new BaseError({ message: "User not found", code: "USER_NOT_FOUND" });
console.log(err.code); // "USER_NOT_FOUND"
console.log(err.toJSON()); // serialized with redacted context
```
### Factory Methods
```typescript
import { NetworkError, AuthError, HttpError, ValidationError } from "@suaveplan/error";
const timeout = NetworkError.timeout("https://api.io/users", 5000);
const forbidden = AuthError.forbidden("/admin", "admin:write", "user-42");
const notFound = HttpError.notFound("User 42");
const invalid = ValidationError.required("email", "User");
```
### Type Guards
```typescript
import { BaseError, NetworkError } from "@suaveplan/error";
if (BaseError.is(err, "APP_ERROR")) { /* narrowed to BaseError */ }
if (NetworkError.isNetworkError(timeout)) { /* narrowed to NetworkError */ }
```
### Context Augmentation (Immutable)
```typescript
const enriched = err.withContext({ retryCount: 3 });
// original err is unchanged
```
### Serialization with Redaction Control
```typescript
// Default: redacts password, token, apiKey, etc.
err.toJSON();
// Skip redaction for server-side internal logs
err.toJSON({ redact: false });
// Keep specific keys unredacted
new BaseError({
code: "X",
message: "m",
context: { apiKey: "k" },
safeContextKeys: ["apiKey"],
});
```
### RFC 7807 Problem Details
```typescript
import { BaseError, ErrorCodes } from "@suaveplan/error";
ErrorCodes.register("PJ_BAD", { httpStatus: 400 });
const e = new BaseError({ code: "PJ_BAD", message: "internal", userMessage: "Bad request" });
const pj = e.toProblemJson({ typeBase: "https://x/errors" });
// => { type: "https://x/errors/PJ_BAD", title: "BaseError", status: 400, detail: "Bad request", ... }
```
### Go-Style tryCatch
```typescript
import { tryCatch } from "@suaveplan/error";
const [result, err] = tryCatch(() => JSON.parse(input), "PARSE_ERROR");
if (err) {
console.error(err.code, err.message);
}
```
### neverthrow Result Helpers
```typescript
import { tryBase, aggregate } from "@suaveplan/error";
const r = tryBase(() => JSON.parse(input), "PARSE_FAILED");
if (r.isErr()) console.error(r.error.code);
const results = await Promise.all([
tryBaseAsync(() => loadUser(id1), "LOAD_FAILED"),
tryBaseAsync(() => loadUser(id2), "LOAD_FAILED"),
]);
const all = aggregate(results);
if (all.isErr()) throw all.error; // AggregateError
```
### AggregateError from Promise.allSettled
```typescript
import { AggregateError } from "@suaveplan/error";
const settled = await Promise.allSettled([saveA(), saveB(), saveC()]);
const agg = AggregateError.fromSettled(settled);
if (agg.errors.length > 0) throw agg;
```
### ValidationError Fluent API
```typescript
const err = new ValidationError({ message: "invalid" })
.addFieldError("name", "is required", "required")
.addFieldError("age", "must be positive", "min", -1);
```
### Interceptors
```typescript
import { registerErrorInterceptor } from "@suaveplan/error";
const unregister = registerErrorInterceptor((e) => {
logger.error(e.code, e.message);
});
// dispatched once per BaseError instance via queueMicrotask
```
### Custom Redaction Defaults
```typescript
import { setRedactionDefaults, DEFAULT_REDACTION_PATTERNS } from "@suaveplan/error";
setRedactionDefaults({
patterns: [...DEFAULT_REDACTION_PATTERNS, /internal_id/i],
replacement: "***",
maxDepth: 6,
});
```
## Constraints
- **Redaction is default-on in `toJSON()`** — Use `{ redact: false }` to opt out, or `safeContextKeys` to protect specific keys.
- **Interceptor dispatch is async** — Happens on `queueMicrotask`, so subclass fields are visible but dispatch is not synchronous with construction.
- **At-most-once interceptor dispatch** — Tracked via `WeakSet`; manual calls to `dispatchInterceptors` for the same instance are no-ops.
- **Interceptor exceptions are swallowed** — A misbehaving interceptor logs a warning but does not break others.
- **Fingerprint auto-derivation depends on stack trace** — If `Error.captureStackTrace` is unavailable, the top-frame portion may resolve to `"unknown"`.
- **Cause chain bounded by `maxCauseDepth`** (default 10) — Deep chains are truncated.
- **Redaction depth bounded by `maxDepth`** (default 4) — Deeply nested objects beyond this get `[TRUNCATED]`.
- **Cycle-safe redaction** — Circular references in context are replaced with `[CYCLE]`.
- **AggregateError severity escalation** — Computed as the worst severity among children (`fatal` > `error` > `warning` > `info`).
- **Suaveplan `AggregateError` shadows global** — Intentional; it extends `BaseError` rather than native `AggregateError`.
- **Tree-shaking requires subpath imports** — Full import pulls everything. Use `@suaveplan/error/base-error/base-error` etc. for granular bundles.
## Checklist
1. Always use `BaseError` (or a subclass) instead of plain `Error` for programmatic error matching via `err.code`.
2. Use factory methods (`NetworkError.timeout()`, `HttpError.notFound()`, etc.) for common error patterns.
3. Use `BaseError.is(err, "CODE")` for type-safe narrowing instead of `instanceof`.
4. Use `withContext()` to enrich errors immutably — never mutate `err.context` directly.
5. Use `toJSON()` for safe serialization (redaction on by default); use `toJSON({ redact: false })` for internal logging.
6. Use `tryBase()` / `tryBaseAsync()` for neverthrow-style Result handling.
7. Use `AggregateError.fromSettled()` to collect failures from `Promise.allSettled`.
8. Register error codes with `ErrorCodes.register()` to attach HTTP status, severity, and retry metadata.
9. Use `registerErrorInterceptor()` for observability hooks (logging, metrics, tracing) without coupling.