@arcforge/err 2.0.98
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +27 -0
- package/src/err.ts +132 -0
- package/src/index.ts +12 -0
- package/src/map.ts +1044 -0
- package/src/render.ts +102 -0
- package/src/sink.ts +37 -0
- package/src/stack.ts +119 -0
package/package.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@arcforge/err",
|
|
3
|
+
"version": "2.0.98",
|
|
4
|
+
"description": "Structured Axon errors — the code map every user-facing failure is rendered from.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"private": false,
|
|
7
|
+
"main": "./src/index.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": "./src/index.ts"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"src"
|
|
13
|
+
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"deploy": "bun publish --access public",
|
|
16
|
+
"deploy:patch": "npm version patch && bun publish --access public",
|
|
17
|
+
"deploy:minor": "npm version minor && bun publish --access public",
|
|
18
|
+
"deploy:major": "npm version major && bun publish --access public"
|
|
19
|
+
},
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"@arcforge/types": "2.0.97"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"@types/bun": "latest",
|
|
25
|
+
"typescript": "^5"
|
|
26
|
+
}
|
|
27
|
+
}
|
package/src/err.ts
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { errorMap, type AxonErrorCode, type AxonErrorMap } from "./map"
|
|
2
|
+
import { captureStack, firstRealFrame } from "./stack"
|
|
3
|
+
import { renderError } from "./render"
|
|
4
|
+
import { emitError } from "./sink"
|
|
5
|
+
import type { AxonError, AxonErrorContext, AxonErrorJSON, AxonErrorSeverity, AxonStackFrame } from "@arcforge/types"
|
|
6
|
+
|
|
7
|
+
// AxonError and its data shapes are the wire contract — they live in
|
|
8
|
+
// @arcforge/types. err() below is their runtime implementation. Re-exported
|
|
9
|
+
// so existing `import { AxonError } from "@arcforge/err"` call sites keep working.
|
|
10
|
+
export type { AxonError, AxonErrorContext, AxonErrorJSON } from "@arcforge/types"
|
|
11
|
+
|
|
12
|
+
/** Everything optional about a single err() call, collapsed into one object — one call shape, no positional slots to skip with `undefined`. */
|
|
13
|
+
export type AxonErrorOpts = {
|
|
14
|
+
/** The specific instance of this failure ("prompt \"greeting\" not found", not just "Prompt Not Found"). Omit when the map's title already says everything there is to say — render() only prints a message line when detail adds information the title doesn't already carry. */
|
|
15
|
+
detail?: string
|
|
16
|
+
context?: AxonErrorContext
|
|
17
|
+
severity?: AxonErrorSeverity
|
|
18
|
+
cause?: unknown
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Narrow an unknown catch value to an AxonError. */
|
|
22
|
+
export function isAxonError(value: unknown): value is AxonError {
|
|
23
|
+
return value instanceof Error && (value as AxonError).isAxonError === true
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* One constructor, one call shape, every failure site:
|
|
28
|
+
*
|
|
29
|
+
* err("PROMPT_NOT_FOUND", { context: { name } }) — you know exactly what this is; the map's identity contract applies
|
|
30
|
+
* err(cause) — a catch boundary caught something uninspected
|
|
31
|
+
*
|
|
32
|
+
* The second form is not an escape hatch around the map — it resolves to
|
|
33
|
+
* the map's own "UNKNOWN" entry, which exists for exactly this and is
|
|
34
|
+
* fully visible in the log (AX-UNKNOWN-001) same as any declared code. An
|
|
35
|
+
* already-constructed AxonError passed to the second form comes back
|
|
36
|
+
* untouched (re-wrapping a rethrow must never lose the original identity
|
|
37
|
+
* or stack) — and, since it already emitted once at its original throw
|
|
38
|
+
* site, is NOT re-emitted here.
|
|
39
|
+
*
|
|
40
|
+
* Transport: every fresh construction calls emitError(), which delivers to
|
|
41
|
+
* the current AsyncLocalStorage scope's sink (see sink.ts errScope) — the
|
|
42
|
+
* runtime establishes that scope at its well-defined entry points, so the
|
|
43
|
+
* error reaches THAT runtime's session and no other. This is the ONLY place
|
|
44
|
+
* emission fires — a catch boundary must never re-commit an error under its
|
|
45
|
+
* own event type; it rethrows, or if it must record lifecycle bookkeeping
|
|
46
|
+
* (a run "failed" vs "completed"), that record carries no error payload of
|
|
47
|
+
* its own. One error, one emission, one canonical "error" session event
|
|
48
|
+
* downstream.
|
|
49
|
+
*/
|
|
50
|
+
export function err(code: AxonErrorCode, opts?: AxonErrorOpts): AxonError
|
|
51
|
+
export function err(cause: unknown): AxonError
|
|
52
|
+
export function err(codeOrCause: AxonErrorCode | unknown, opts?: AxonErrorOpts): AxonError {
|
|
53
|
+
if (isAxonError(codeOrCause)) return codeOrCause
|
|
54
|
+
if (typeof codeOrCause !== "string" || !(codeOrCause in errorMap)) {
|
|
55
|
+
return fromUnknown(codeOrCause)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const code = codeOrCause as AxonErrorCode
|
|
59
|
+
const def = (errorMap as AxonErrorMap)[code]
|
|
60
|
+
|
|
61
|
+
const e = new Error(opts?.detail ?? def.title, { cause: opts?.cause }) as AxonError
|
|
62
|
+
|
|
63
|
+
e.code = def.code
|
|
64
|
+
e.title = def.title
|
|
65
|
+
e.description = def.description
|
|
66
|
+
e.source = def.source
|
|
67
|
+
e.severity = opts?.severity ?? def.severity
|
|
68
|
+
e.context = opts?.context
|
|
69
|
+
e.frames = captureStack(2) // drop captureStack's own frame + err()'s
|
|
70
|
+
e.isAxonError = true
|
|
71
|
+
|
|
72
|
+
e.render = () => renderError(e)
|
|
73
|
+
e.toJSON = () => ({
|
|
74
|
+
isAxonError: true,
|
|
75
|
+
code: e.code,
|
|
76
|
+
title: e.title,
|
|
77
|
+
description: e.description,
|
|
78
|
+
message: e.message,
|
|
79
|
+
source: e.source,
|
|
80
|
+
severity: e.severity,
|
|
81
|
+
...(e.context !== undefined ? { context: toJsonSafe(e.context) } : {}),
|
|
82
|
+
frames: e.frames,
|
|
83
|
+
...(e.stack !== undefined ? { stack: e.stack } : {}),
|
|
84
|
+
...(e.cause !== undefined ? { cause: causeToJSON(e.cause) } : {}),
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
emitError(e)
|
|
88
|
+
return e
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** The UNKNOWN path — a caught value that never went through err() at its origin. Still fully renderable, still logged, just honestly marked as unclassified. */
|
|
92
|
+
function fromUnknown(value: unknown): AxonError {
|
|
93
|
+
const message = value instanceof Error ? value.message : String(value)
|
|
94
|
+
const wrapped = err("UNKNOWN", { detail: message, cause: value, severity: "fatal" })
|
|
95
|
+
if (value instanceof Error && value.stack) wrapped.stack = value.stack
|
|
96
|
+
return wrapped
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Context is caller-supplied and not guaranteed JSON-safe (circular
|
|
101
|
+
* references, non-plain objects) — but toJSON()'s output gets serialized
|
|
102
|
+
* automatically (session append, bus relay), far from this call site.
|
|
103
|
+
* Guaranteeing safety HERE, at the one chokepoint every AxonError passes
|
|
104
|
+
* through on its way to disk/wire, means a bad context value degrades to a
|
|
105
|
+
* string instead of crashing the writer.
|
|
106
|
+
*/
|
|
107
|
+
function toJsonSafe(value: Record<string, unknown>): Record<string, unknown> {
|
|
108
|
+
try {
|
|
109
|
+
return JSON.parse(JSON.stringify(value)) as Record<string, unknown>
|
|
110
|
+
} catch {
|
|
111
|
+
return { unserializable: String(value) }
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
type CauseJSON = { message: string; stack?: string; frame?: AxonStackFrame | null } | string
|
|
116
|
+
|
|
117
|
+
/** cause is `unknown` by native Error.cause's own type — never assume it's JSON-safe (same reasoning as context). */
|
|
118
|
+
function causeToJSON(cause: unknown): CauseJSON {
|
|
119
|
+
if (cause instanceof Error) {
|
|
120
|
+
return {
|
|
121
|
+
message: cause.message,
|
|
122
|
+
...(cause.stack !== undefined ? { stack: cause.stack } : {}),
|
|
123
|
+
frame: firstRealFrame(cause.stack),
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
try {
|
|
127
|
+
JSON.stringify(cause)
|
|
128
|
+
return String(cause)
|
|
129
|
+
} catch {
|
|
130
|
+
return "[unserializable cause]"
|
|
131
|
+
}
|
|
132
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export { err, isAxonError, type AxonError, type AxonErrorContext, type AxonErrorOpts, type AxonErrorJSON } from "./err"
|
|
2
|
+
export { renderError, renderFrame, type AxonErrorLike } from "./render"
|
|
3
|
+
export { captureStack, parseStack, firstRealFrame, type AxonStackFrame } from "./stack"
|
|
4
|
+
export {
|
|
5
|
+
errorMap,
|
|
6
|
+
type AxonErrorCode,
|
|
7
|
+
type AxonErrorMap,
|
|
8
|
+
type AxonErrorMapEntry,
|
|
9
|
+
type AxonErrorSeverity,
|
|
10
|
+
type AxonErrorSource,
|
|
11
|
+
} from "./map"
|
|
12
|
+
export { errScope, type AxonErrorSink } from "./sink"
|