ts-procedures 6.0.0 → 6.0.1

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.
Files changed (30) hide show
  1. package/agent_config/claude-code/agents/ts-procedures-architect.md +1 -1
  2. package/agent_config/claude-code/skills/ts-procedures/SKILL.md +1 -1
  3. package/agent_config/claude-code/skills/ts-procedures/anti-patterns.md +2 -2
  4. package/agent_config/claude-code/skills/ts-procedures/api-reference.md +15 -27
  5. package/agent_config/claude-code/skills/ts-procedures/patterns.md +11 -4
  6. package/agent_config/claude-code/skills/ts-procedures-review/checklist.md +2 -2
  7. package/agent_config/copilot/copilot-instructions.md +3 -2
  8. package/agent_config/cursor/cursorrules +3 -2
  9. package/build/implementations/http/doc-registry.d.ts +14 -19
  10. package/build/implementations/http/doc-registry.js +41 -46
  11. package/build/implementations/http/doc-registry.js.map +1 -1
  12. package/build/implementations/http/doc-registry.test.js +141 -10
  13. package/build/implementations/http/doc-registry.test.js.map +1 -1
  14. package/build/implementations/http/error-taxonomy.d.ts +11 -2
  15. package/build/implementations/http/error-taxonomy.js +24 -2
  16. package/build/implementations/http/error-taxonomy.js.map +1 -1
  17. package/build/implementations/http/route-errors.test.js +5 -6
  18. package/build/implementations/http/route-errors.test.js.map +1 -1
  19. package/build/implementations/types.d.ts +13 -1
  20. package/docs/http-integrations.md +7 -5
  21. package/docs/superpowers/plans/2026-04-24-doc-registry-simplification.md +886 -0
  22. package/package.json +1 -1
  23. package/src/implementations/http/README.md +2 -3
  24. package/src/implementations/http/doc-registry.test.ts +154 -10
  25. package/src/implementations/http/doc-registry.ts +46 -53
  26. package/src/implementations/http/error-taxonomy.ts +26 -2
  27. package/src/implementations/http/express-rpc/README.md +2 -2
  28. package/src/implementations/http/hono-rpc/README.md +2 -2
  29. package/src/implementations/http/route-errors.test.ts +5 -6
  30. package/src/implementations/types.ts +13 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ts-procedures",
3
- "version": "6.0.0",
3
+ "version": "6.0.1",
4
4
  "description": "A TypeScript RPC framework that creates type-safe, schema-validated procedure calls with a single function definition. Define your procedures once and get full type inference, runtime validation, and framework integration hooks.",
5
5
  "main": "build/exports.js",
6
6
  "types": "build/exports.d.ts",
@@ -257,8 +257,7 @@ import { DocRegistry } from 'ts-procedures/http-docs'
257
257
 
258
258
  const docs = new DocRegistry({
259
259
  basePath: '/api',
260
- headers: [{ name: 'Authorization', description: 'Bearer token', required: false }],
261
- errors: DocRegistry.defaultErrors(),
260
+ errors: appErrors, // your ErrorTaxonomy framework defaults auto-merged and deduped
262
261
  })
263
262
  .from(rpcBuilder)
264
263
  .from(apiBuilder)
@@ -269,7 +268,7 @@ app.get('/docs', (c) => c.json(docs.toJSON()))
269
268
 
270
269
  - `from()` stores a reference — routes are read lazily at `toJSON()` time
271
270
  - `toJSON()` supports optional `filter` and `transform` options
272
- - `defaultErrors()` returns error schemas for all 4 procedure error types
271
+ - `errors` accepts an `ErrorTaxonomy` or raw `ErrorDoc[]`; framework defaults are auto-merged (opt out via `includeDefaults: false`)
273
272
  - All builders satisfy the `DocSource` interface (`{ readonly docs: AnyHttpRouteDoc[] }`)
274
273
 
275
274
  ### Client Code Generation
@@ -3,6 +3,7 @@ import { v } from 'suretype'
3
3
  import { Procedures } from '../../index.js'
4
4
  import { HonoRPCAppBuilder } from './hono-rpc/index.js'
5
5
  import { DocRegistry } from './doc-registry.js'
6
+ import { defineErrorTaxonomy } from './error-taxonomy.js'
6
7
  import type {
7
8
  AnyHttpRouteDoc,
8
9
  RPCHttpRouteDoc,
@@ -11,6 +12,7 @@ import type {
11
12
  StreamHttpRouteDoc,
12
13
  DocSource,
13
14
  DocEnvelope,
15
+ ErrorDoc,
14
16
  } from '../types.js'
15
17
 
16
18
  // ---------------------------------------------------------------------------
@@ -61,7 +63,7 @@ describe('DocRegistry', () => {
61
63
  // --------------------------------------------------------------------------
62
64
  describe('constructor', () => {
63
65
  test('uses defaults when no config provided', () => {
64
- const registry = new DocRegistry()
66
+ const registry = new DocRegistry({ includeDefaults: false })
65
67
  const out = registry.toJSON()
66
68
  expect(out.basePath).toBe('')
67
69
  expect(out.headers).toEqual([])
@@ -69,7 +71,7 @@ describe('DocRegistry', () => {
69
71
  })
70
72
 
71
73
  test('accepts partial config', () => {
72
- const registry = new DocRegistry({ basePath: '/v1' })
74
+ const registry = new DocRegistry({ basePath: '/v1', includeDefaults: false })
73
75
  const out = registry.toJSON()
74
76
  expect(out.basePath).toBe('/v1')
75
77
  expect(out.headers).toEqual([])
@@ -79,7 +81,7 @@ describe('DocRegistry', () => {
79
81
  test('accepts full config', () => {
80
82
  const headers = [{ name: 'Authorization', description: 'Bearer token', required: true }]
81
83
  const errors = [{ name: 'Unauthorized', statusCode: 401, description: 'Missing token' }]
82
- const registry = new DocRegistry({ basePath: '/api', headers, errors })
84
+ const registry = new DocRegistry({ basePath: '/api', headers, errors, includeDefaults: false })
83
85
  const out = registry.toJSON()
84
86
  expect(out.basePath).toBe('/api')
85
87
  expect(out.headers).toEqual(headers)
@@ -175,7 +177,7 @@ describe('DocRegistry', () => {
175
177
  test('headers and errors are copies', () => {
176
178
  const headers = [{ name: 'X-Custom' }]
177
179
  const errors = [{ name: 'E', statusCode: 500, description: 'd' }]
178
- const registry = new DocRegistry({ headers, errors })
180
+ const registry = new DocRegistry({ headers, errors, includeDefaults: false })
179
181
  const out = registry.toJSON()
180
182
  expect(out.headers).toEqual(headers)
181
183
  expect(out.headers).not.toBe(headers)
@@ -298,6 +300,153 @@ describe('DocRegistry', () => {
298
300
  })
299
301
  })
300
302
 
303
+ // --------------------------------------------------------------------------
304
+ // taxonomy input + auto-defaults + dedupe (v6.0.1 simplification)
305
+ // --------------------------------------------------------------------------
306
+ describe('errors: ErrorTaxonomy (polymorphic constructor input)', () => {
307
+ test('accepts an ErrorTaxonomy directly and converts to ErrorDoc[]', () => {
308
+ const taxonomy = defineErrorTaxonomy({
309
+ AuthError: {
310
+ class: class AuthError extends Error {},
311
+ statusCode: 401,
312
+ description: 'unauthenticated',
313
+ },
314
+ })
315
+ const envelope = new DocRegistry({ errors: taxonomy }).toJSON()
316
+ const auth = envelope.errors.find((e) => e.name === 'AuthError')
317
+ expect(auth).toBeDefined()
318
+ expect(auth?.statusCode).toBe(401)
319
+ expect(auth?.description).toBe('unauthenticated')
320
+ })
321
+
322
+ test('auto-includes framework defaults when errors is a taxonomy', () => {
323
+ const taxonomy = defineErrorTaxonomy({
324
+ AuthError: { class: class AuthError extends Error {}, statusCode: 401 },
325
+ })
326
+ const names = new DocRegistry({ errors: taxonomy }).toJSON().errors.map((e) => e.name)
327
+ expect(names).toContain('AuthError')
328
+ expect(names).toContain('ProcedureValidationError')
329
+ expect(names).toContain('ProcedureYieldValidationError')
330
+ expect(names).toContain('ProcedureError')
331
+ expect(names).toContain('ProcedureRegistrationError')
332
+ })
333
+
334
+ test('auto-includes framework defaults when errors is ErrorDoc[]', () => {
335
+ const custom: ErrorDoc = { name: 'CustomThing', statusCode: 418, description: 'teapot' }
336
+ const names = new DocRegistry({ errors: [custom] }).toJSON().errors.map((e) => e.name)
337
+ expect(names).toContain('CustomThing')
338
+ expect(names).toContain('ProcedureValidationError')
339
+ })
340
+
341
+ test('includeDefaults: false omits framework defaults', () => {
342
+ const taxonomy = defineErrorTaxonomy({
343
+ AuthError: { class: class AuthError extends Error {}, statusCode: 401 },
344
+ })
345
+ const names = new DocRegistry({ errors: taxonomy, includeDefaults: false })
346
+ .toJSON()
347
+ .errors.map((e) => e.name)
348
+ expect(names).toEqual(['AuthError'])
349
+ })
350
+
351
+ test('user taxonomy entry with same name as default overrides default (dedupe, user wins)', () => {
352
+ const taxonomy = defineErrorTaxonomy({
353
+ ProcedureError: {
354
+ class: Error,
355
+ statusCode: 418,
356
+ description: 'custom override',
357
+ },
358
+ })
359
+ const envelope = new DocRegistry({ errors: taxonomy }).toJSON()
360
+ const proc = envelope.errors.filter((e) => e.name === 'ProcedureError')
361
+ expect(proc).toHaveLength(1)
362
+ expect(proc[0]!.statusCode).toBe(418)
363
+ expect(proc[0]!.description).toBe('custom override')
364
+ })
365
+
366
+ test('user ErrorDoc with same name as default overrides default (dedupe, user wins)', () => {
367
+ const custom: ErrorDoc = {
368
+ name: 'ProcedureError',
369
+ statusCode: 418,
370
+ description: 'custom override',
371
+ }
372
+ const envelope = new DocRegistry({ errors: [custom] }).toJSON()
373
+ const proc = envelope.errors.filter((e) => e.name === 'ProcedureError')
374
+ expect(proc).toHaveLength(1)
375
+ expect(proc[0]!.statusCode).toBe(418)
376
+ expect(proc[0]!.description).toBe('custom override')
377
+ })
378
+
379
+ test('empty errors config still returns framework defaults', () => {
380
+ const names = new DocRegistry().toJSON().errors.map((e) => e.name)
381
+ expect(names).toContain('ProcedureError')
382
+ expect(names).toContain('ProcedureValidationError')
383
+ expect(names).toContain('ProcedureYieldValidationError')
384
+ expect(names).toContain('ProcedureRegistrationError')
385
+ })
386
+
387
+ test('includeDefaults: false with no errors produces empty error list', () => {
388
+ const envelope = new DocRegistry({ includeDefaults: false }).toJSON()
389
+ expect(envelope.errors).toEqual([])
390
+ })
391
+ })
392
+
393
+ // --------------------------------------------------------------------------
394
+ // .documentError() fluent extension
395
+ // --------------------------------------------------------------------------
396
+ describe('.documentError()', () => {
397
+ test('adds a single ErrorDoc to the envelope', () => {
398
+ const registry = new DocRegistry({ includeDefaults: false }).documentError({
399
+ name: 'RateLimitExceeded',
400
+ statusCode: 429,
401
+ description: 'too many requests',
402
+ })
403
+ const names = registry.toJSON().errors.map((e) => e.name)
404
+ expect(names).toEqual(['RateLimitExceeded'])
405
+ })
406
+
407
+ test('accepts multiple docs via variadic args', () => {
408
+ const registry = new DocRegistry({ includeDefaults: false }).documentError(
409
+ { name: 'A', statusCode: 400, description: 'a' },
410
+ { name: 'B', statusCode: 500, description: 'b' }
411
+ )
412
+ const names = registry.toJSON().errors.map((e) => e.name)
413
+ expect(names).toEqual(['A', 'B'])
414
+ })
415
+
416
+ test('returns this for chaining', () => {
417
+ const registry = new DocRegistry()
418
+ const returned = registry.documentError({
419
+ name: 'Foo',
420
+ statusCode: 500,
421
+ description: 'x',
422
+ })
423
+ expect(returned).toBe(registry)
424
+ })
425
+
426
+ test('composes with taxonomy input — extends docs without replacing', () => {
427
+ const taxonomy = defineErrorTaxonomy({
428
+ AuthError: { class: class AuthError extends Error {}, statusCode: 401 },
429
+ })
430
+ const envelope = new DocRegistry({ errors: taxonomy })
431
+ .documentError({ name: 'RateLimitExceeded', statusCode: 429, description: 'x' })
432
+ .toJSON()
433
+ const names = envelope.errors.map((e) => e.name)
434
+ expect(names).toContain('AuthError')
435
+ expect(names).toContain('RateLimitExceeded')
436
+ expect(names).toContain('ProcedureValidationError')
437
+ })
438
+
439
+ test('dedupes against existing errors (last write wins)', () => {
440
+ const registry = new DocRegistry({ includeDefaults: false })
441
+ .documentError({ name: 'Foo', statusCode: 400, description: 'first' })
442
+ .documentError({ name: 'Foo', statusCode: 500, description: 'second' })
443
+ const foo = registry.toJSON().errors.filter((e) => e.name === 'Foo')
444
+ expect(foo).toHaveLength(1)
445
+ expect(foo[0]!.statusCode).toBe(500)
446
+ expect(foo[0]!.description).toBe('second')
447
+ })
448
+ })
449
+
301
450
  // --------------------------------------------------------------------------
302
451
  // kind discriminant
303
452
  // --------------------------------------------------------------------------
@@ -333,7 +482,6 @@ describe('DocRegistry', () => {
333
482
  const registry = new DocRegistry({
334
483
  basePath: '/api',
335
484
  headers: [{ name: 'Authorization', description: 'Bearer token', required: false }],
336
- errors: DocRegistry.defaultErrors(),
337
485
  })
338
486
  .from(makeSource([rpcDoc]))
339
487
  .from(makeSource([apiDoc]))
@@ -347,10 +495,7 @@ describe('DocRegistry', () => {
347
495
  })
348
496
 
349
497
  test('filter + transform combined', () => {
350
- const registry = new DocRegistry({
351
- basePath: '/api',
352
- errors: DocRegistry.defaultErrors(),
353
- })
498
+ const registry = new DocRegistry({ basePath: '/api' })
354
499
  .from(makeSource([rpcDoc]))
355
500
  .from(makeSource([apiDoc]))
356
501
  .from(makeSource([streamDoc]))
@@ -372,7 +517,6 @@ describe('DocRegistry', () => {
372
517
  const registry = new DocRegistry({
373
518
  basePath: '/api',
374
519
  headers: [{ name: 'X-Request-Id', example: 'abc-123' }],
375
- errors: DocRegistry.defaultErrors(),
376
520
  })
377
521
  .from(makeSource([rpcDoc]))
378
522
  .from(makeSource([apiDoc]))
@@ -8,9 +8,10 @@ import type {
8
8
  HeaderDoc,
9
9
  } from '../types.js'
10
10
  import {
11
- ErrorTaxonomy,
11
+ PROCEDURE_REGISTRATION_ERROR_DOC,
12
12
  defaultErrorTaxonomy,
13
13
  taxonomyToErrorDocs,
14
+ type ErrorTaxonomy,
14
15
  } from './error-taxonomy.js'
15
16
 
16
17
  export type {
@@ -23,37 +24,44 @@ export type {
23
24
  HeaderDoc,
24
25
  } from '../types.js'
25
26
 
27
+ function isTaxonomy(input: ErrorTaxonomy | ErrorDoc[]): input is ErrorTaxonomy {
28
+ return !Array.isArray(input)
29
+ }
30
+
26
31
  /**
27
- * `ProcedureRegistrationError` is thrown at procedure-definition time (never at
28
- * request time), so it doesn't appear in the runtime taxonomy. It is documented
29
- * here so consumers still see it in the error catalog.
32
+ * Dedupes ErrorDocs by `name`, last occurrence wins. Map insertion order
33
+ * preserves the latest position of each name.
30
34
  */
31
- const PROCEDURE_REGISTRATION_ERROR_DOC: ErrorDoc = {
32
- name: 'ProcedureRegistrationError',
33
- statusCode: 500,
34
- description:
35
- 'An invalid schema or configuration was detected at procedure registration time.',
36
- schema: {
37
- type: 'object',
38
- properties: {
39
- name: { type: 'string', const: 'ProcedureRegistrationError' },
40
- procedureName: { type: 'string' },
41
- message: { type: 'string' },
42
- },
43
- required: ['name', 'procedureName', 'message'],
44
- },
35
+ function dedupeByName(docs: ErrorDoc[]): ErrorDoc[] {
36
+ const byName = new Map<string, ErrorDoc>()
37
+ for (const doc of docs) byName.set(doc.name, doc)
38
+ return Array.from(byName.values())
45
39
  }
46
40
 
47
41
  export class DocRegistry {
48
42
  private readonly basePath: string
49
43
  private readonly headers: HeaderDoc[]
50
- private readonly errors: ErrorDoc[]
44
+ private errors: ErrorDoc[]
51
45
  private readonly sources: DocSource<AnyHttpRouteDoc>[] = []
52
46
 
53
47
  constructor(config?: DocRegistryConfig) {
54
48
  this.basePath = config?.basePath ?? ''
55
49
  this.headers = config?.headers ?? []
56
- this.errors = config?.errors ?? []
50
+
51
+ const includeDefaults = config?.includeDefaults ?? true
52
+ const userErrors: ErrorDoc[] = config?.errors
53
+ ? isTaxonomy(config.errors)
54
+ ? taxonomyToErrorDocs(config.errors)
55
+ : config.errors
56
+ : []
57
+
58
+ // Precedence: defaults come first, user errors override via dedupe
59
+ // (last-write-wins). Matches runtime resolution order.
60
+ const merged = includeDefaults
61
+ ? [...DocRegistry.defaultErrors(), ...userErrors]
62
+ : userErrors
63
+
64
+ this.errors = dedupeByName(merged)
57
65
  }
58
66
 
59
67
  from(source: DocSource<AnyHttpRouteDoc>): this {
@@ -61,6 +69,18 @@ export class DocRegistry {
61
69
  return this
62
70
  }
63
71
 
72
+ /**
73
+ * Adds one or more {@link ErrorDoc} entries to the envelope. Use for errors
74
+ * outside your runtime taxonomy — middleware-level errors, infrastructure
75
+ * errors (502/503/504), or doc-only meta errors.
76
+ *
77
+ * Deduped by `name` — last write wins.
78
+ */
79
+ documentError(...docs: ErrorDoc[]): this {
80
+ this.errors = dedupeByName([...this.errors, ...docs])
81
+ return this
82
+ }
83
+
64
84
  toJSON<T = DocEnvelope>(options?: DocRegistryOutputOptions<T>): T {
65
85
  let routes = this.sources.flatMap((source) => source.docs)
66
86
 
@@ -83,11 +103,12 @@ export class DocRegistry {
83
103
  }
84
104
 
85
105
  /**
86
- * Framework error defaults for the DocEnvelope derived from
87
- * {@link defaultErrorTaxonomy} so the documented shape cannot drift from what
88
- * the HTTP builders actually emit at runtime. `ProcedureRegistrationError` is
89
- * appended because it's thrown at registration time (never at request time)
90
- * and therefore lives only in the catalog, not the runtime taxonomy.
106
+ * Framework error defaults derived from {@link defaultErrorTaxonomy} plus
107
+ * `ProcedureRegistrationError` (which is thrown only at registration time
108
+ * and therefore lives in the catalog, not the runtime taxonomy).
109
+ *
110
+ * Most consumers do not need to call this directly — the `DocRegistry`
111
+ * constructor auto-includes these unless `includeDefaults: false` is passed.
91
112
  */
92
113
  static defaultErrors(): ErrorDoc[] {
93
114
  return [
@@ -95,32 +116,4 @@ export class DocRegistry {
95
116
  PROCEDURE_REGISTRATION_ERROR_DOC,
96
117
  ]
97
118
  }
98
-
99
- /**
100
- * Convenience constructor that seeds `config.errors` from a taxonomy so the
101
- * DocEnvelope automatically documents every error class registered with the
102
- * HTTP builders. Framework defaults (including `ProcedureRegistrationError`)
103
- * are included unless `includeDefaults: false` is passed.
104
- *
105
- * @example
106
- * const registry = DocRegistry.fromTaxonomy(appErrors, { basePath: '/api' })
107
- * .from(apiApp)
108
- */
109
- static fromTaxonomy(
110
- taxonomy: ErrorTaxonomy,
111
- config?: Omit<DocRegistryConfig, 'errors'> & { includeDefaults?: boolean }
112
- ): DocRegistry {
113
- const { includeDefaults = true, ...rest } = config ?? {}
114
- const errors: ErrorDoc[] = [
115
- ...taxonomyToErrorDocs(taxonomy),
116
- ...(includeDefaults ? DocRegistry.defaultErrors() : []),
117
- ]
118
- // Dedupe by name — user entries take precedence over defaults with the
119
- // same key, matching runtime resolution order.
120
- const seen = new Set<string>()
121
- const deduped = errors.filter((e) =>
122
- seen.has(e.name) ? false : (seen.add(e.name), true)
123
- )
124
- return new DocRegistry({ ...rest, errors: deduped })
125
- }
126
119
  }
@@ -182,10 +182,34 @@ export const defaultErrorTaxonomy = defineErrorTaxonomy({
182
182
  },
183
183
  })
184
184
 
185
+ /**
186
+ * Doc-only entry for `ProcedureRegistrationError`, which is thrown at
187
+ * procedure-definition time (never at request time) and therefore doesn't
188
+ * appear in the runtime taxonomy. Consumers still see it in the error catalog
189
+ * via `DocRegistry.defaultErrors()`.
190
+ */
191
+ export const PROCEDURE_REGISTRATION_ERROR_DOC: ErrorDoc = {
192
+ name: 'ProcedureRegistrationError',
193
+ statusCode: 500,
194
+ description:
195
+ 'An invalid schema or configuration was detected at procedure registration time.',
196
+ schema: {
197
+ type: 'object',
198
+ properties: {
199
+ name: { type: 'string', const: 'ProcedureRegistrationError' },
200
+ procedureName: { type: 'string' },
201
+ message: { type: 'string' },
202
+ },
203
+ required: ['name', 'procedureName', 'message'],
204
+ },
205
+ }
206
+
185
207
  /**
186
208
  * Converts a taxonomy into {@link ErrorDoc} objects suitable for a DocEnvelope.
187
- * Single source of truth so the runtime mapping and the documented shape
188
- * cannot drift apart.
209
+ *
210
+ * @internal Used by `DocRegistry` to merge taxonomy entries into the envelope.
211
+ * Consumers should pass their taxonomy directly to `new DocRegistry({ errors: taxonomy })`
212
+ * rather than calling this helper — the constructor handles the conversion.
189
213
  */
190
214
  export function taxonomyToErrorDocs(taxonomy: ErrorTaxonomy): ErrorDoc[] {
191
215
  return Object.entries(taxonomy).map(([key, entry]) => ({
@@ -59,7 +59,7 @@ type ExpressRPCAppBuilderConfig = {
59
59
  onRequestStart?: (req: express.Request) => void
60
60
  onRequestEnd?: (req: express.Request, res: express.Response) => void
61
61
  onSuccess?: (procedure: TProcedureRegistration, req: express.Request, res: express.Response) => void
62
- error?: (procedure: TProcedureRegistration, req: express.Request, res: express.Response, error: Error) => void
62
+ onError?: (procedure: TProcedureRegistration, req: express.Request, res: express.Response, error: Error) => void
63
63
  }
64
64
  ```
65
65
 
@@ -70,7 +70,7 @@ type ExpressRPCAppBuilderConfig = {
70
70
  | `onRequestStart` | `(req) => void` | Called at start of each request |
71
71
  | `onRequestEnd` | `(req, res) => void` | Called after response finishes |
72
72
  | `onSuccess` | `(proc, req, res) => void` | Called on successful handler execution |
73
- | `error` | `(proc, req, res, err) => void` | Custom error handler |
73
+ | `onError` | `(proc, req, res, err) => void` | Imperative error handler (peer of `errors` taxonomy — see Error Handling) |
74
74
 
75
75
  ## Context Resolution
76
76
 
@@ -64,7 +64,7 @@ type HonoRPCAppBuilderConfig = {
64
64
  onRequestStart?: (c: Context) => void
65
65
  onRequestEnd?: (c: Context) => void
66
66
  onSuccess?: (procedure: TProcedureRegistration, c: Context) => void
67
- error?: (
67
+ onError?: (
68
68
  procedure: TProcedureRegistration,
69
69
  c: Context,
70
70
  error: Error
@@ -79,7 +79,7 @@ type HonoRPCAppBuilderConfig = {
79
79
  | `onRequestStart` | `(c) => void` | Called at start of each request |
80
80
  | `onRequestEnd` | `(c) => void` | Called after handler completes |
81
81
  | `onSuccess` | `(proc, c) => void` | Called on successful handler execution |
82
- | `error` | `(proc, c, err) => Response` | Custom error handler (must return Response) |
82
+ | `onError` | `(proc, c, err) => Response` | Imperative error handler (peer of `errors` taxonomy — see Error Handling) |
83
83
 
84
84
  ## Context Resolution
85
85
 
@@ -117,9 +117,9 @@ describe('per-route errors declaration', () => {
117
117
  })
118
118
  })
119
119
 
120
- describe('DocRegistry.fromTaxonomy', () => {
120
+ describe('DocRegistry with taxonomy input', () => {
121
121
  test('seeds envelope errors from the taxonomy + framework defaults', () => {
122
- const registry = DocRegistry.fromTaxonomy(appErrors, { basePath: '/api' })
122
+ const registry = new DocRegistry({ errors: appErrors, basePath: '/api' })
123
123
  const envelope = registry.toJSON()
124
124
  const names = envelope.errors.map((e) => e.name)
125
125
  expect(names).toContain('UseCaseError')
@@ -130,7 +130,7 @@ describe('DocRegistry.fromTaxonomy', () => {
130
130
  })
131
131
 
132
132
  test('includeDefaults: false omits framework entries', () => {
133
- const registry = DocRegistry.fromTaxonomy(appErrors, { includeDefaults: false })
133
+ const registry = new DocRegistry({ errors: appErrors, includeDefaults: false })
134
134
  const names = registry.toJSON().errors.map((e) => e.name)
135
135
  expect(names).toEqual(['UseCaseError', 'AuthError'])
136
136
  })
@@ -143,11 +143,10 @@ describe('DocRegistry.fromTaxonomy', () => {
143
143
  description: 'custom override',
144
144
  },
145
145
  })
146
- const envelope = DocRegistry.fromTaxonomy(overridden).toJSON()
146
+ const envelope = new DocRegistry({ errors: overridden }).toJSON()
147
147
  const proc = envelope.errors.find((e) => e.name === 'ProcedureError')
148
148
  expect(proc?.statusCode).toBe(418)
149
149
  expect(proc?.description).toBe('custom override')
150
- // no duplicates
151
150
  const count = envelope.errors.filter((e) => e.name === 'ProcedureError').length
152
151
  expect(count).toBe(1)
153
152
  })
@@ -170,7 +169,7 @@ describe('DocRegistry.fromTaxonomy', () => {
170
169
  const app = new HonoAPIAppBuilder().register(API, () => ({}))
171
170
  app.build()
172
171
 
173
- const envelope = DocRegistry.fromTaxonomy(appErrors).from(app).toJSON()
172
+ const envelope = new DocRegistry({ errors: appErrors }).from(app).toJSON()
174
173
  const route = envelope.routes.find((r) => r.kind === 'api' && r.name === 'GetUser')
175
174
  expect(route?.errors).toEqual(['UseCaseError'])
176
175
  })
@@ -1,4 +1,5 @@
1
1
  import { Procedures } from '../index.js'
2
+ import type { ErrorTaxonomy } from './http/error-taxonomy.js'
2
3
 
3
4
  /**
4
5
  * @typeParam TErrorKey - Union of valid taxonomy keys. Defaults to `string`
@@ -186,7 +187,18 @@ export interface ErrorDoc {
186
187
  export interface DocRegistryConfig {
187
188
  basePath?: string
188
189
  headers?: HeaderDoc[]
189
- errors?: ErrorDoc[]
190
+ /**
191
+ * Errors to document in the envelope. Accepts either your runtime
192
+ * {@link ErrorTaxonomy} (from `defineErrorTaxonomy`) — the common case — or
193
+ * a raw `ErrorDoc[]` for consumers who aren't using a taxonomy.
194
+ *
195
+ * Framework defaults (`ProcedureError`, `ProcedureValidationError`,
196
+ * `ProcedureYieldValidationError`, `ProcedureRegistrationError`) are merged
197
+ * in automatically and deduped. Opt out via `includeDefaults: false`.
198
+ */
199
+ errors?: ErrorTaxonomy | ErrorDoc[]
200
+ /** Whether to auto-merge framework error defaults. Defaults to `true`. */
201
+ includeDefaults?: boolean
190
202
  }
191
203
 
192
204
  export interface DocRegistryOutputOptions<TEnvelope = DocEnvelope> {