@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 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"