ts-procedures 5.16.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 (147) hide show
  1. package/README.md +2 -0
  2. package/agent_config/claude-code/agents/ts-procedures-architect.md +13 -6
  3. package/agent_config/claude-code/skills/ts-procedures/SKILL.md +26 -4
  4. package/agent_config/claude-code/skills/ts-procedures/anti-patterns.md +87 -19
  5. package/agent_config/claude-code/skills/ts-procedures/api-reference.md +162 -16
  6. package/agent_config/claude-code/skills/ts-procedures/patterns.md +179 -16
  7. package/agent_config/claude-code/skills/ts-procedures-review/SKILL.md +1 -1
  8. package/agent_config/claude-code/skills/ts-procedures-review/checklist.md +20 -12
  9. package/agent_config/claude-code/skills/ts-procedures-scaffold/SKILL.md +2 -1
  10. package/agent_config/claude-code/skills/ts-procedures-scaffold/templates/client.md +22 -15
  11. package/agent_config/claude-code/skills/ts-procedures-scaffold/templates/express-rpc.md +20 -17
  12. package/agent_config/claude-code/skills/ts-procedures-scaffold/templates/hono-api.md +20 -16
  13. package/agent_config/claude-code/skills/ts-procedures-scaffold/templates/hono-rpc.md +20 -17
  14. package/agent_config/claude-code/skills/ts-procedures-scaffold/templates/hono-stream.md +16 -3
  15. package/agent_config/copilot/copilot-instructions.md +78 -12
  16. package/agent_config/cursor/cursorrules +78 -12
  17. package/build/client/call.d.ts +2 -1
  18. package/build/client/call.js +9 -1
  19. package/build/client/call.js.map +1 -1
  20. package/build/client/error-dispatch.d.ts +13 -0
  21. package/build/client/error-dispatch.js +26 -0
  22. package/build/client/error-dispatch.js.map +1 -0
  23. package/build/client/error-dispatch.test.d.ts +1 -0
  24. package/build/client/error-dispatch.test.js +56 -0
  25. package/build/client/error-dispatch.test.js.map +1 -0
  26. package/build/client/fetch-adapter.js +10 -4
  27. package/build/client/fetch-adapter.js.map +1 -1
  28. package/build/client/index.d.ts +2 -1
  29. package/build/client/index.js +5 -1
  30. package/build/client/index.js.map +1 -1
  31. package/build/client/stream.d.ts +2 -1
  32. package/build/client/stream.js +13 -3
  33. package/build/client/stream.js.map +1 -1
  34. package/build/client/typed-error-dispatch.test.d.ts +1 -0
  35. package/build/client/typed-error-dispatch.test.js +168 -0
  36. package/build/client/typed-error-dispatch.test.js.map +1 -0
  37. package/build/client/types.d.ts +37 -0
  38. package/build/codegen/e2e.test.js +9 -4
  39. package/build/codegen/e2e.test.js.map +1 -1
  40. package/build/codegen/emit-client-runtime.js +4 -0
  41. package/build/codegen/emit-client-runtime.js.map +1 -1
  42. package/build/codegen/emit-errors.d.ts +17 -6
  43. package/build/codegen/emit-errors.integration.test.d.ts +1 -0
  44. package/build/codegen/emit-errors.integration.test.js +162 -0
  45. package/build/codegen/emit-errors.integration.test.js.map +1 -0
  46. package/build/codegen/emit-errors.js +50 -39
  47. package/build/codegen/emit-errors.js.map +1 -1
  48. package/build/codegen/emit-errors.test.js +75 -78
  49. package/build/codegen/emit-errors.test.js.map +1 -1
  50. package/build/codegen/emit-index.d.ts +7 -0
  51. package/build/codegen/emit-index.js +26 -4
  52. package/build/codegen/emit-index.js.map +1 -1
  53. package/build/codegen/emit-index.test.js +55 -23
  54. package/build/codegen/emit-index.test.js.map +1 -1
  55. package/build/codegen/emit-scope.d.ts +8 -0
  56. package/build/codegen/emit-scope.js +82 -7
  57. package/build/codegen/emit-scope.js.map +1 -1
  58. package/build/codegen/pipeline.js +22 -2
  59. package/build/codegen/pipeline.js.map +1 -1
  60. package/build/implementations/http/doc-registry.d.ts +17 -1
  61. package/build/implementations/http/doc-registry.js +47 -79
  62. package/build/implementations/http/doc-registry.js.map +1 -1
  63. package/build/implementations/http/doc-registry.test.js +149 -16
  64. package/build/implementations/http/doc-registry.test.js.map +1 -1
  65. package/build/implementations/http/error-taxonomy.d.ts +249 -0
  66. package/build/implementations/http/error-taxonomy.js +252 -0
  67. package/build/implementations/http/error-taxonomy.js.map +1 -0
  68. package/build/implementations/http/error-taxonomy.test.d.ts +1 -0
  69. package/build/implementations/http/error-taxonomy.test.js +399 -0
  70. package/build/implementations/http/error-taxonomy.test.js.map +1 -0
  71. package/build/implementations/http/express-rpc/error-taxonomy.test.d.ts +1 -0
  72. package/build/implementations/http/express-rpc/error-taxonomy.test.js +83 -0
  73. package/build/implementations/http/express-rpc/error-taxonomy.test.js.map +1 -0
  74. package/build/implementations/http/express-rpc/index.d.ts +39 -8
  75. package/build/implementations/http/express-rpc/index.js +39 -8
  76. package/build/implementations/http/express-rpc/index.js.map +1 -1
  77. package/build/implementations/http/hono-api/error-taxonomy.test.d.ts +1 -0
  78. package/build/implementations/http/hono-api/error-taxonomy.test.js +137 -0
  79. package/build/implementations/http/hono-api/error-taxonomy.test.js.map +1 -0
  80. package/build/implementations/http/hono-api/index.d.ts +38 -1
  81. package/build/implementations/http/hono-api/index.js +32 -0
  82. package/build/implementations/http/hono-api/index.js.map +1 -1
  83. package/build/implementations/http/hono-rpc/error-taxonomy.test.d.ts +1 -0
  84. package/build/implementations/http/hono-rpc/error-taxonomy.test.js +64 -0
  85. package/build/implementations/http/hono-rpc/error-taxonomy.test.js.map +1 -0
  86. package/build/implementations/http/hono-rpc/index.d.ts +34 -7
  87. package/build/implementations/http/hono-rpc/index.js +31 -4
  88. package/build/implementations/http/hono-rpc/index.js.map +1 -1
  89. package/build/implementations/http/hono-stream/error-taxonomy.test.d.ts +1 -0
  90. package/build/implementations/http/hono-stream/error-taxonomy.test.js +87 -0
  91. package/build/implementations/http/hono-stream/error-taxonomy.test.js.map +1 -0
  92. package/build/implementations/http/hono-stream/index.d.ts +40 -3
  93. package/build/implementations/http/hono-stream/index.js +37 -10
  94. package/build/implementations/http/hono-stream/index.js.map +1 -1
  95. package/build/implementations/http/hono-stream/index.test.js +45 -18
  96. package/build/implementations/http/hono-stream/index.test.js.map +1 -1
  97. package/build/implementations/http/on-request-error.test.d.ts +1 -0
  98. package/build/implementations/http/on-request-error.test.js +173 -0
  99. package/build/implementations/http/on-request-error.test.js.map +1 -0
  100. package/build/implementations/http/route-errors.test.d.ts +1 -0
  101. package/build/implementations/http/route-errors.test.js +139 -0
  102. package/build/implementations/http/route-errors.test.js.map +1 -0
  103. package/build/implementations/types.d.ts +43 -3
  104. package/docs/client-and-codegen.md +105 -12
  105. package/docs/core.md +14 -5
  106. package/docs/http-integrations.md +138 -5
  107. package/docs/streaming.md +3 -1
  108. package/docs/superpowers/plans/2026-04-24-doc-registry-simplification.md +886 -0
  109. package/package.json +7 -2
  110. package/src/client/call.ts +10 -1
  111. package/src/client/error-dispatch.test.ts +72 -0
  112. package/src/client/error-dispatch.ts +27 -0
  113. package/src/client/fetch-adapter.ts +11 -5
  114. package/src/client/index.ts +9 -0
  115. package/src/client/stream.ts +14 -3
  116. package/src/client/typed-error-dispatch.test.ts +211 -0
  117. package/src/client/types.ts +42 -0
  118. package/src/codegen/e2e.test.ts +9 -4
  119. package/src/codegen/emit-client-runtime.ts +4 -0
  120. package/src/codegen/emit-errors.integration.test.ts +183 -0
  121. package/src/codegen/emit-errors.test.ts +91 -87
  122. package/src/codegen/emit-errors.ts +123 -41
  123. package/src/codegen/emit-index.test.ts +68 -24
  124. package/src/codegen/emit-index.ts +66 -4
  125. package/src/codegen/emit-scope.ts +124 -7
  126. package/src/codegen/pipeline.ts +25 -2
  127. package/src/implementations/http/README.md +21 -7
  128. package/src/implementations/http/doc-registry.test.ts +164 -16
  129. package/src/implementations/http/doc-registry.ts +58 -82
  130. package/src/implementations/http/error-taxonomy.test.ts +438 -0
  131. package/src/implementations/http/error-taxonomy.ts +361 -0
  132. package/src/implementations/http/express-rpc/README.md +23 -24
  133. package/src/implementations/http/express-rpc/error-taxonomy.test.ts +103 -0
  134. package/src/implementations/http/express-rpc/index.ts +75 -14
  135. package/src/implementations/http/hono-api/README.md +284 -0
  136. package/src/implementations/http/hono-api/error-taxonomy.test.ts +179 -0
  137. package/src/implementations/http/hono-api/index.ts +76 -1
  138. package/src/implementations/http/hono-rpc/README.md +20 -21
  139. package/src/implementations/http/hono-rpc/error-taxonomy.test.ts +82 -0
  140. package/src/implementations/http/hono-rpc/index.ts +65 -9
  141. package/src/implementations/http/hono-stream/README.md +44 -25
  142. package/src/implementations/http/hono-stream/error-taxonomy.test.ts +98 -0
  143. package/src/implementations/http/hono-stream/index.test.ts +54 -18
  144. package/src/implementations/http/hono-stream/index.ts +83 -13
  145. package/src/implementations/http/on-request-error.test.ts +201 -0
  146. package/src/implementations/http/route-errors.test.ts +176 -0
  147. package/src/implementations/types.ts +43 -3
@@ -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)
@@ -263,20 +265,24 @@ describe('DocRegistry', () => {
263
265
 
264
266
  test('has correct error names', () => {
265
267
  const names = DocRegistry.defaultErrors().map((e) => e.name)
268
+ // Runtime errors come from the taxonomy (topologically sorted —
269
+ // subclasses first), followed by the doc-only registration error.
266
270
  expect(names).toEqual([
267
- 'ProcedureError',
268
271
  'ProcedureValidationError',
269
272
  'ProcedureYieldValidationError',
273
+ 'ProcedureError',
270
274
  'ProcedureRegistrationError',
271
275
  ])
272
276
  })
273
277
 
274
278
  test('has correct status codes', () => {
275
- const errors = DocRegistry.defaultErrors()
276
- expect(errors[0]!.statusCode).toBe(500) // ProcedureError
277
- expect(errors[1]!.statusCode).toBe(400) // ProcedureValidationError
278
- expect(errors[2]!.statusCode).toBe(500) // ProcedureYieldValidationError
279
- expect(errors[3]!.statusCode).toBe(500) // ProcedureRegistrationError
279
+ const byName = Object.fromEntries(
280
+ DocRegistry.defaultErrors().map((e) => [e.name, e.statusCode])
281
+ )
282
+ expect(byName.ProcedureError).toBe(500)
283
+ expect(byName.ProcedureValidationError).toBe(400)
284
+ expect(byName.ProcedureYieldValidationError).toBe(500)
285
+ expect(byName.ProcedureRegistrationError).toBe(500)
280
286
  })
281
287
 
282
288
  test('each entry has schema', () => {
@@ -294,6 +300,153 @@ describe('DocRegistry', () => {
294
300
  })
295
301
  })
296
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
+
297
450
  // --------------------------------------------------------------------------
298
451
  // kind discriminant
299
452
  // --------------------------------------------------------------------------
@@ -329,7 +482,6 @@ describe('DocRegistry', () => {
329
482
  const registry = new DocRegistry({
330
483
  basePath: '/api',
331
484
  headers: [{ name: 'Authorization', description: 'Bearer token', required: false }],
332
- errors: DocRegistry.defaultErrors(),
333
485
  })
334
486
  .from(makeSource([rpcDoc]))
335
487
  .from(makeSource([apiDoc]))
@@ -343,10 +495,7 @@ describe('DocRegistry', () => {
343
495
  })
344
496
 
345
497
  test('filter + transform combined', () => {
346
- const registry = new DocRegistry({
347
- basePath: '/api',
348
- errors: DocRegistry.defaultErrors(),
349
- })
498
+ const registry = new DocRegistry({ basePath: '/api' })
350
499
  .from(makeSource([rpcDoc]))
351
500
  .from(makeSource([apiDoc]))
352
501
  .from(makeSource([streamDoc]))
@@ -368,7 +517,6 @@ describe('DocRegistry', () => {
368
517
  const registry = new DocRegistry({
369
518
  basePath: '/api',
370
519
  headers: [{ name: 'X-Request-Id', example: 'abc-123' }],
371
- errors: DocRegistry.defaultErrors(),
372
520
  })
373
521
  .from(makeSource([rpcDoc]))
374
522
  .from(makeSource([apiDoc]))
@@ -7,6 +7,12 @@ import type {
7
7
  ErrorDoc,
8
8
  HeaderDoc,
9
9
  } from '../types.js'
10
+ import {
11
+ PROCEDURE_REGISTRATION_ERROR_DOC,
12
+ defaultErrorTaxonomy,
13
+ taxonomyToErrorDocs,
14
+ type ErrorTaxonomy,
15
+ } from './error-taxonomy.js'
10
16
 
11
17
  export type {
12
18
  AnyHttpRouteDoc,
@@ -18,16 +24,44 @@ export type {
18
24
  HeaderDoc,
19
25
  } from '../types.js'
20
26
 
27
+ function isTaxonomy(input: ErrorTaxonomy | ErrorDoc[]): input is ErrorTaxonomy {
28
+ return !Array.isArray(input)
29
+ }
30
+
31
+ /**
32
+ * Dedupes ErrorDocs by `name`, last occurrence wins. Map insertion order
33
+ * preserves the latest position of each name.
34
+ */
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())
39
+ }
40
+
21
41
  export class DocRegistry {
22
42
  private readonly basePath: string
23
43
  private readonly headers: HeaderDoc[]
24
- private readonly errors: ErrorDoc[]
44
+ private errors: ErrorDoc[]
25
45
  private readonly sources: DocSource<AnyHttpRouteDoc>[] = []
26
46
 
27
47
  constructor(config?: DocRegistryConfig) {
28
48
  this.basePath = config?.basePath ?? ''
29
49
  this.headers = config?.headers ?? []
30
- 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)
31
65
  }
32
66
 
33
67
  from(source: DocSource<AnyHttpRouteDoc>): this {
@@ -35,6 +69,18 @@ export class DocRegistry {
35
69
  return this
36
70
  }
37
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
+
38
84
  toJSON<T = DocEnvelope>(options?: DocRegistryOutputOptions<T>): T {
39
85
  let routes = this.sources.flatMap((source) => source.docs)
40
86
 
@@ -56,88 +102,18 @@ export class DocRegistry {
56
102
  return envelope as T
57
103
  }
58
104
 
59
- // Keep in sync with src/errors.ts
105
+ /**
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.
112
+ */
60
113
  static defaultErrors(): ErrorDoc[] {
61
114
  return [
62
- {
63
- name: 'ProcedureError',
64
- statusCode: 500,
65
- description: 'An error thrown from within a procedure handler via ctx.error().',
66
- schema: {
67
- type: 'object',
68
- properties: {
69
- name: { type: 'string', const: 'ProcedureError' },
70
- procedureName: { type: 'string' },
71
- message: { type: 'string' },
72
- meta: { type: 'object' },
73
- },
74
- required: ['name', 'procedureName', 'message'],
75
- },
76
- },
77
- {
78
- name: 'ProcedureValidationError',
79
- statusCode: 400,
80
- description: 'Schema validation failed for the procedure input parameters.',
81
- schema: {
82
- type: 'object',
83
- properties: {
84
- name: { type: 'string', const: 'ProcedureValidationError' },
85
- procedureName: { type: 'string' },
86
- message: { type: 'string' },
87
- errors: {
88
- type: 'array',
89
- items: {
90
- type: 'object',
91
- properties: {
92
- instancePath: { type: 'string' },
93
- message: { type: 'string' },
94
- },
95
- },
96
- },
97
- },
98
- required: ['name', 'procedureName', 'message'],
99
- },
100
- },
101
- {
102
- name: 'ProcedureYieldValidationError',
103
- statusCode: 500,
104
- description:
105
- 'Schema validation failed for a yielded value in a streaming procedure.',
106
- schema: {
107
- type: 'object',
108
- properties: {
109
- name: { type: 'string', const: 'ProcedureYieldValidationError' },
110
- procedureName: { type: 'string' },
111
- message: { type: 'string' },
112
- errors: {
113
- type: 'array',
114
- items: {
115
- type: 'object',
116
- properties: {
117
- instancePath: { type: 'string' },
118
- message: { type: 'string' },
119
- },
120
- },
121
- },
122
- },
123
- required: ['name', 'procedureName', 'message'],
124
- },
125
- },
126
- {
127
- name: 'ProcedureRegistrationError',
128
- statusCode: 500,
129
- description:
130
- 'An invalid schema or configuration was detected at procedure registration time.',
131
- schema: {
132
- type: 'object',
133
- properties: {
134
- name: { type: 'string', const: 'ProcedureRegistrationError' },
135
- procedureName: { type: 'string' },
136
- message: { type: 'string' },
137
- },
138
- required: ['name', 'procedureName', 'message'],
139
- },
140
- },
115
+ ...taxonomyToErrorDocs(defaultErrorTaxonomy),
116
+ PROCEDURE_REGISTRATION_ERROR_DOC,
141
117
  ]
142
118
  }
143
119
  }