first commit
This commit is contained in:
219
plugins/library-navigator/skills/sp-error/SKILL.md
Normal file
219
plugins/library-navigator/skills/sp-error/SKILL.md
Normal 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.
|
||||
Reference in New Issue
Block a user