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
@@ -37,7 +37,7 @@ When asked to plan an API or procedure set:
37
37
  - AJV is configured with `allErrors: true`, `coerceTypes: true`, `removeAdditional: true`.
38
38
  - `schema.params` and `schema.input` are mutually exclusive — defining both throws `ProcedureRegistrationError`.
39
39
  - Path param names in route template (`:id`) must match `schema.input.pathParams` property names.
40
- - Use `DocRegistry` to compose route docs from multiple builders — never manually wire `/docs` endpoints. Use `DocRegistry.fromTaxonomy(appErrors)` to seed envelope errors from your taxonomy + framework defaults in one call.
40
+ - Use `DocRegistry` to compose route docs from multiple builders — never manually wire `/docs` endpoints. Pass your taxonomy directly: `new DocRegistry({ errors: appErrors })` framework defaults are auto-merged and deduped. For errors outside your taxonomy (middleware, infrastructure, doc-only), chain `.documentError(...docs)`.
41
41
  - Two first-class peer error-handling modes: **declarative taxonomy** (`defineErrorTaxonomy` + `errors` config) OR **imperative callback** (`onError`). Neither is deprecated. Pick the taxonomy for structured apps with typed client dispatch; pick `onError` for simple apps or full response control. Mixing both is allowed — the taxonomy handles what it covers, `onError` handles the tail. The anti-pattern is `instanceof` ladders inside `onError` (see anti-pattern #20 in the skill reference) — that's exactly what the taxonomy expresses declaratively.
42
42
  - Per-route `errors: ['UseCaseError', ...]` narrows typed errors on the generated client. Declare via `APIConfig<keyof typeof appErrors & string>` / `RPCConfig<keyof typeof appErrors & string>` for compile-time typo protection.
43
43
  - Generated `_errors.ts` emits real runtime classes extending `${Service}ProcedureError` — consumers catch with `instanceof ${Service}Errors.${Name}` and access `err.body`, `err.status`, `err.procedureName`, `err.scope`. Use the generated `create${Service}Client(config)` factory to wire the error registry automatically.
@@ -171,7 +171,7 @@ The npm package ships user-facing documentation with narrative explanations and
171
171
  |------|--------|
172
172
  | `docs/core.md` | Procedures factory, Create, CreateStream, schema.input, error handling |
173
173
  | `docs/streaming.md` | Streaming procedures, AbortSignal, SSE patterns |
174
- | `docs/http-integrations.md` | Express RPC, Hono RPC/Stream/API builders, **error taxonomy (canonical)**, DocRegistry + `fromTaxonomy` |
174
+ | `docs/http-integrations.md` | Express RPC, Hono RPC/Stream/API builders, **error taxonomy (canonical)**, DocRegistry (unified constructor, `.documentError()`) |
175
175
  | `docs/client-and-codegen.md` | Client code generation, `createApiClient`/`createClient`, typed error dispatch, per-route `Errors` unions, per-call options, client-level defaults, typed RequestMeta augmentation, CLI options |
176
176
  | `CHANGELOG.md` | Release notes — see `[6.0.0]` for the current peer error-handling model (taxonomy + `onError` + `onRequestError`), per-route errors, and client runtime error classes. |
177
177
 
@@ -476,13 +476,13 @@ import { DocRegistry } from 'ts-procedures/http-docs'
476
476
  const docs = new DocRegistry({
477
477
  basePath: '/api',
478
478
  headers: [{ name: 'Authorization', description: 'Bearer token' }],
479
- errors: DocRegistry.defaultErrors(),
479
+ errors: appErrors, // your ErrorTaxonomy — framework defaults auto-merged
480
480
  }).from(rpcBuilder).from(apiBuilder).from(streamBuilder)
481
481
 
482
482
  app.get('/docs', (c) => c.json(docs.toJSON()))
483
483
  ```
484
484
 
485
- **Why:** `DocRegistry` provides a typed `DocEnvelope`, includes `defaultErrors()` with JSON Schemas for all 4 procedure error types, reads docs lazily (order-independent), and supports filtering and transformation via `toJSON()` options.
485
+ **Why:** `DocRegistry` provides a typed `DocEnvelope`, auto-merges framework error defaults (`ProcedureError`, `ProcedureValidationError`, `ProcedureYieldValidationError`, `ProcedureRegistrationError`) with JSON Schemas, reads docs lazily (order-independent), and supports filtering and transformation via `toJSON()` options.
486
486
 
487
487
  ---
488
488
 
@@ -295,13 +295,7 @@ When `toResponse` is omitted, the body defaults to `{ name: key, message: err.me
295
295
 
296
296
  Pre-built taxonomy covering `ProcedureValidationError` (400), `ProcedureYieldValidationError` (500), and direct `ProcedureError` (500, unwrapped throws only — wrapped ones fall through so user taxonomy / `unknownError` sees the real error). Applied as a fallback after the user taxonomy unless `includeDefaults: false`.
297
297
 
298
- ### taxonomyToErrorDocs(taxonomy)
299
-
300
- ```typescript
301
- function taxonomyToErrorDocs(taxonomy: ErrorTaxonomy): ErrorDoc[]
302
- ```
303
-
304
- Converts a taxonomy to the `ErrorDoc[]` format consumed by `DocRegistry.errors` — single source of truth so the documented shape cannot drift from runtime behavior.
298
+ Taxonomy-to-doc conversion is handled automatically by `DocRegistry` when you pass your taxonomy to the constructor. There is no public helper.
305
299
 
306
300
  ### resolveErrorResponse(params)
307
301
 
@@ -795,30 +789,24 @@ import type { DocEnvelope, DocRegistryConfig, DocRegistryOutputOptions, HeaderDo
795
789
  ```
796
790
 
797
791
  ```typescript
792
+ import { DocRegistry } from 'ts-procedures/http-docs'
793
+ import type { DocEnvelope, DocRegistryConfig, DocRegistryOutputOptions, HeaderDoc, ErrorDoc, DocSource } from 'ts-procedures/http-docs'
794
+
795
+ interface DocRegistryConfig {
796
+ basePath?: string
797
+ headers?: HeaderDoc[]
798
+ errors?: ErrorTaxonomy | ErrorDoc[] // polymorphic — pass taxonomy or raw docs
799
+ includeDefaults?: boolean // default: true (auto-merges framework defaults)
800
+ }
801
+
798
802
  class DocRegistry {
799
- constructor(config?: {
800
- basePath?: string
801
- headers?: HeaderDoc[]
802
- errors?: ErrorDoc[]
803
- })
803
+ constructor(config?: DocRegistryConfig)
804
804
 
805
805
  from(source: DocSource<AnyHttpRouteDoc>): this
806
+ documentError(...docs: ErrorDoc[]): this
807
+ toJSON<T = DocEnvelope>(options?: DocRegistryOutputOptions<T>): T
806
808
 
807
- toJSON<T = DocEnvelope>(options?: {
808
- filter?: (route: AnyHttpRouteDoc) => boolean
809
- transform?: (envelope: DocEnvelope) => T
810
- }): T
811
-
812
- // Framework error defaults derived from defaultErrorTaxonomy — single source
813
- // of truth so runtime and documented shapes cannot drift.
814
809
  static defaultErrors(): ErrorDoc[]
815
-
816
- // Convenience: seed envelope errors from a taxonomy + framework defaults
817
- // in one call (deduped — user entries win when keys overlap).
818
- static fromTaxonomy(
819
- taxonomy: ErrorTaxonomy,
820
- config?: Omit<DocRegistryConfig, 'errors'> & { includeDefaults?: boolean }
821
- ): DocRegistry
822
810
  }
823
811
  ```
824
812
 
@@ -875,7 +863,7 @@ type AnyHttpRouteDoc = RPCHttpRouteDoc | APIHttpRouteDoc | StreamHttpRouteDoc
875
863
 
876
864
  - `from()` stores a **reference** to the builder — routes are read lazily at `toJSON()` time, so builders can be registered before or after `.build()`
877
865
  - `toJSON()` collects routes via `flatMap(source => source.docs)`, applies optional `filter`, builds envelope, then applies optional `transform`
878
- - `defaultErrors()` returns 4 `ErrorDoc` entries describing `ProcedureError` (500), `ProcedureValidationError` (400), `ProcedureYieldValidationError` (500), `ProcedureRegistrationError` (500) — each with a JSON Schema for the error response body shape
866
+ - `defaultErrors()` returns 4 `ErrorDoc` entries describing `ProcedureError` (500), `ProcedureValidationError` (400), `ProcedureYieldValidationError` (500), `ProcedureRegistrationError` (500) — each with a JSON Schema for the error response body shape. Most consumers do not need to call this directly; the `DocRegistry` constructor auto-merges these unless `includeDefaults: false` is passed.
879
867
  - `JSON.stringify(registry)` works directly (calls `toJSON()` implicitly)
880
868
 
881
869
  ---
@@ -186,7 +186,7 @@ Seed the DocEnvelope with both taxonomy errors and framework defaults in one cal
186
186
  ```typescript
187
187
  import { DocRegistry } from 'ts-procedures/http-docs'
188
188
 
189
- const envelope = DocRegistry.fromTaxonomy(appErrors, { basePath: '/api' })
189
+ const envelope = new DocRegistry({ errors: appErrors, basePath: '/api' })
190
190
  .from(apiApp)
191
191
  .from(rpcApp)
192
192
  .toJSON()
@@ -194,6 +194,13 @@ const envelope = DocRegistry.fromTaxonomy(appErrors, { basePath: '/api' })
194
194
  // envelope.routes[i].errors: per-route subset declared on config.errors
195
195
  ```
196
196
 
197
+ ```typescript
198
+ // For errors outside your taxonomy (middleware, infrastructure, doc-only):
199
+ const docsWithExtras = new DocRegistry({ errors: appErrors, basePath: '/api' })
200
+ .from(apiBuilder)
201
+ .documentError({ name: 'RateLimitExceeded', statusCode: 429, description: 'too many requests' })
202
+ ```
203
+
197
204
  ### Typed catch blocks on the client (via codegen)
198
205
 
199
206
  The codegen emits runtime error classes (not just types) and a registry object. `createApiClient` wires the registry automatically so non-2xx responses arrive as typed class instances.
@@ -805,7 +812,7 @@ import { DocRegistry } from 'ts-procedures/http-docs'
805
812
  const docs = new DocRegistry({
806
813
  basePath: '/api',
807
814
  headers: [{ name: 'Authorization', description: 'Bearer token', required: false }],
808
- errors: DocRegistry.defaultErrors(),
815
+ errors: appErrors, // your ErrorTaxonomy — framework defaults auto-merged and deduped
809
816
  })
810
817
  .from(rpcBuilder)
811
818
  .from(apiBuilder)
@@ -824,7 +831,7 @@ app.get('/docs', (c) => c.json(docs.toJSON({
824
831
  **Key points:**
825
832
  - `from()` stores a reference — register builders before or after `.build()`
826
833
  - `toJSON()` reads docs lazily, so late-registered procedures are included
827
- - `DocRegistry.defaultErrors()` provides error schemas for all 4 procedure error types
834
+ - Pass your `ErrorTaxonomy` directly to `errors` the constructor auto-merges framework defaults and dedupes; opt out with `includeDefaults: false`
828
835
  - Accepts any object satisfying `{ readonly docs: AnyHttpRouteDoc[] }` — not limited to built-in builders
829
836
 
830
837
  ---
@@ -926,7 +933,7 @@ import { DocRegistry } from 'ts-procedures/http-docs'
926
933
  const docs = new DocRegistry({
927
934
  basePath: '/api',
928
935
  headers: [{ name: 'Authorization', description: 'Bearer token' }],
929
- errors: DocRegistry.defaultErrors(),
936
+ errors: appErrors, // your ErrorTaxonomy — framework defaults auto-merged
930
937
  })
931
938
  .from(rpcBuilder)
932
939
  .from(apiBuilder)
@@ -74,7 +74,7 @@
74
74
 
75
75
  ### SUGGESTION
76
76
  - [ ] Route documentation accessed via `builder.docs` for OpenAPI generation
77
- - [ ] Uses `DocRegistry.fromTaxonomy(appErrors)` (or plain `DocRegistry` with explicit `errors`) to compose docs from multiple builders
77
+ - [ ] Uses `new DocRegistry({ errors: appErrors })` to compose docs from multiple builders
78
78
  - [ ] Lifecycle hooks used for observability (logging, metrics)
79
79
  - [ ] Per-route `errors: [...]` declared so generated clients can narrow `catch` types
80
80
 
@@ -115,7 +115,7 @@
115
115
 
116
116
  ### SUGGESTION
117
117
  - [ ] Route documentation accessed via `builder.docs` for OpenAPI generation
118
- - [ ] Uses `DocRegistry.fromTaxonomy(appErrors)` to compose docs across builders instead of manual assembly
118
+ - [ ] Uses `new DocRegistry({ errors: appErrors })` to compose docs across builders instead of manual assembly
119
119
  - [ ] Custom `queryParser` provided if complex query string formats needed
120
120
  - [ ] Lifecycle hooks used for observability (logging, metrics)
121
121
 
@@ -218,7 +218,8 @@ class UseCaseError extends Error {
218
218
  }
219
219
 
220
220
  const appErrors = defineErrorTaxonomy({
221
- // First-match wins declare subclasses before base classes
221
+ // class: entries are topologically sorted (subclasses checked first) automatically.
222
+ // Predicate (match:) entries keep declared order — put narrower predicates first.
222
223
  UseCaseError: {
223
224
  class: UseCaseError,
224
225
  statusCode: 422,
@@ -254,7 +255,7 @@ const API = Procedures<Ctx, MyAPIConfig>()
254
255
  API.Create('GetUser', { path: '/users/:id', method: 'get', errors: ['UseCaseError'], /* ... */ }, ...)
255
256
 
256
257
  // Seed envelope errors from the taxonomy + framework defaults in one call
257
- const envelope = DocRegistry.fromTaxonomy(appErrors, { basePath: '/api' }).from(apiApp).toJSON()
258
+ const envelope = new DocRegistry({ errors: appErrors, basePath: '/api' }).from(apiApp).toJSON()
258
259
  ```
259
260
 
260
261
  ### Client-side typed catch blocks (via codegen)
@@ -218,7 +218,8 @@ class UseCaseError extends Error {
218
218
  }
219
219
 
220
220
  const appErrors = defineErrorTaxonomy({
221
- // First-match wins declare subclasses before base classes
221
+ // class: entries are topologically sorted (subclasses checked first) automatically.
222
+ // Predicate (match:) entries keep declared order — put narrower predicates first.
222
223
  UseCaseError: {
223
224
  class: UseCaseError,
224
225
  statusCode: 422,
@@ -254,7 +255,7 @@ const API = Procedures<Ctx, MyAPIConfig>()
254
255
  API.Create('GetUser', { path: '/users/:id', method: 'get', errors: ['UseCaseError'], /* ... */ }, ...)
255
256
 
256
257
  // Seed envelope errors from the taxonomy + framework defaults in one call
257
- const envelope = DocRegistry.fromTaxonomy(appErrors, { basePath: '/api' }).from(apiApp).toJSON()
258
+ const envelope = new DocRegistry({ errors: appErrors, basePath: '/api' }).from(apiApp).toJSON()
258
259
  ```
259
260
 
260
261
  ### Client-side typed catch blocks (via codegen)
@@ -1,33 +1,28 @@
1
1
  import type { AnyHttpRouteDoc, DocEnvelope, DocRegistryConfig, DocRegistryOutputOptions, DocSource, ErrorDoc } from '../types.js';
2
- import { ErrorTaxonomy } from './error-taxonomy.js';
3
2
  export type { AnyHttpRouteDoc, DocEnvelope, DocRegistryConfig, DocRegistryOutputOptions, DocSource, ErrorDoc, HeaderDoc, } from '../types.js';
4
3
  export declare class DocRegistry {
5
4
  private readonly basePath;
6
5
  private readonly headers;
7
- private readonly errors;
6
+ private errors;
8
7
  private readonly sources;
9
8
  constructor(config?: DocRegistryConfig);
10
9
  from(source: DocSource<AnyHttpRouteDoc>): this;
11
- toJSON<T = DocEnvelope>(options?: DocRegistryOutputOptions<T>): T;
12
10
  /**
13
- * Framework error defaults for the DocEnvelope derived from
14
- * {@link defaultErrorTaxonomy} so the documented shape cannot drift from what
15
- * the HTTP builders actually emit at runtime. `ProcedureRegistrationError` is
16
- * appended because it's thrown at registration time (never at request time)
17
- * and therefore lives only in the catalog, not the runtime taxonomy.
11
+ * Adds one or more {@link ErrorDoc} entries to the envelope. Use for errors
12
+ * outside your runtime taxonomy middleware-level errors, infrastructure
13
+ * errors (502/503/504), or doc-only meta errors.
14
+ *
15
+ * Deduped by `name` last write wins.
18
16
  */
19
- static defaultErrors(): ErrorDoc[];
17
+ documentError(...docs: ErrorDoc[]): this;
18
+ toJSON<T = DocEnvelope>(options?: DocRegistryOutputOptions<T>): T;
20
19
  /**
21
- * Convenience constructor that seeds `config.errors` from a taxonomy so the
22
- * DocEnvelope automatically documents every error class registered with the
23
- * HTTP builders. Framework defaults (including `ProcedureRegistrationError`)
24
- * are included unless `includeDefaults: false` is passed.
20
+ * Framework error defaults derived from {@link defaultErrorTaxonomy} plus
21
+ * `ProcedureRegistrationError` (which is thrown only at registration time
22
+ * and therefore lives in the catalog, not the runtime taxonomy).
25
23
  *
26
- * @example
27
- * const registry = DocRegistry.fromTaxonomy(appErrors, { basePath: '/api' })
28
- * .from(apiApp)
24
+ * Most consumers do not need to call this directly — the `DocRegistry`
25
+ * constructor auto-includes these unless `includeDefaults: false` is passed.
29
26
  */
30
- static fromTaxonomy(taxonomy: ErrorTaxonomy, config?: Omit<DocRegistryConfig, 'errors'> & {
31
- includeDefaults?: boolean;
32
- }): DocRegistry;
27
+ static defaultErrors(): ErrorDoc[];
33
28
  }
@@ -1,23 +1,17 @@
1
- import { defaultErrorTaxonomy, taxonomyToErrorDocs, } from './error-taxonomy.js';
1
+ import { PROCEDURE_REGISTRATION_ERROR_DOC, defaultErrorTaxonomy, taxonomyToErrorDocs, } from './error-taxonomy.js';
2
+ function isTaxonomy(input) {
3
+ return !Array.isArray(input);
4
+ }
2
5
  /**
3
- * `ProcedureRegistrationError` is thrown at procedure-definition time (never at
4
- * request time), so it doesn't appear in the runtime taxonomy. It is documented
5
- * here so consumers still see it in the error catalog.
6
+ * Dedupes ErrorDocs by `name`, last occurrence wins. Map insertion order
7
+ * preserves the latest position of each name.
6
8
  */
7
- const PROCEDURE_REGISTRATION_ERROR_DOC = {
8
- name: 'ProcedureRegistrationError',
9
- statusCode: 500,
10
- description: 'An invalid schema or configuration was detected at procedure registration time.',
11
- schema: {
12
- type: 'object',
13
- properties: {
14
- name: { type: 'string', const: 'ProcedureRegistrationError' },
15
- procedureName: { type: 'string' },
16
- message: { type: 'string' },
17
- },
18
- required: ['name', 'procedureName', 'message'],
19
- },
20
- };
9
+ function dedupeByName(docs) {
10
+ const byName = new Map();
11
+ for (const doc of docs)
12
+ byName.set(doc.name, doc);
13
+ return Array.from(byName.values());
14
+ }
21
15
  export class DocRegistry {
22
16
  basePath;
23
17
  headers;
@@ -26,12 +20,34 @@ export class DocRegistry {
26
20
  constructor(config) {
27
21
  this.basePath = config?.basePath ?? '';
28
22
  this.headers = config?.headers ?? [];
29
- this.errors = config?.errors ?? [];
23
+ const includeDefaults = config?.includeDefaults ?? true;
24
+ const userErrors = config?.errors
25
+ ? isTaxonomy(config.errors)
26
+ ? taxonomyToErrorDocs(config.errors)
27
+ : config.errors
28
+ : [];
29
+ // Precedence: defaults come first, user errors override via dedupe
30
+ // (last-write-wins). Matches runtime resolution order.
31
+ const merged = includeDefaults
32
+ ? [...DocRegistry.defaultErrors(), ...userErrors]
33
+ : userErrors;
34
+ this.errors = dedupeByName(merged);
30
35
  }
31
36
  from(source) {
32
37
  this.sources.push(source);
33
38
  return this;
34
39
  }
40
+ /**
41
+ * Adds one or more {@link ErrorDoc} entries to the envelope. Use for errors
42
+ * outside your runtime taxonomy — middleware-level errors, infrastructure
43
+ * errors (502/503/504), or doc-only meta errors.
44
+ *
45
+ * Deduped by `name` — last write wins.
46
+ */
47
+ documentError(...docs) {
48
+ this.errors = dedupeByName([...this.errors, ...docs]);
49
+ return this;
50
+ }
35
51
  toJSON(options) {
36
52
  let routes = this.sources.flatMap((source) => source.docs);
37
53
  if (options?.filter) {
@@ -49,11 +65,12 @@ export class DocRegistry {
49
65
  return envelope;
50
66
  }
51
67
  /**
52
- * Framework error defaults for the DocEnvelope derived from
53
- * {@link defaultErrorTaxonomy} so the documented shape cannot drift from what
54
- * the HTTP builders actually emit at runtime. `ProcedureRegistrationError` is
55
- * appended because it's thrown at registration time (never at request time)
56
- * and therefore lives only in the catalog, not the runtime taxonomy.
68
+ * Framework error defaults derived from {@link defaultErrorTaxonomy} plus
69
+ * `ProcedureRegistrationError` (which is thrown only at registration time
70
+ * and therefore lives in the catalog, not the runtime taxonomy).
71
+ *
72
+ * Most consumers do not need to call this directly — the `DocRegistry`
73
+ * constructor auto-includes these unless `includeDefaults: false` is passed.
57
74
  */
58
75
  static defaultErrors() {
59
76
  return [
@@ -61,27 +78,5 @@ export class DocRegistry {
61
78
  PROCEDURE_REGISTRATION_ERROR_DOC,
62
79
  ];
63
80
  }
64
- /**
65
- * Convenience constructor that seeds `config.errors` from a taxonomy so the
66
- * DocEnvelope automatically documents every error class registered with the
67
- * HTTP builders. Framework defaults (including `ProcedureRegistrationError`)
68
- * are included unless `includeDefaults: false` is passed.
69
- *
70
- * @example
71
- * const registry = DocRegistry.fromTaxonomy(appErrors, { basePath: '/api' })
72
- * .from(apiApp)
73
- */
74
- static fromTaxonomy(taxonomy, config) {
75
- const { includeDefaults = true, ...rest } = config ?? {};
76
- const errors = [
77
- ...taxonomyToErrorDocs(taxonomy),
78
- ...(includeDefaults ? DocRegistry.defaultErrors() : []),
79
- ];
80
- // Dedupe by name — user entries take precedence over defaults with the
81
- // same key, matching runtime resolution order.
82
- const seen = new Set();
83
- const deduped = errors.filter((e) => seen.has(e.name) ? false : (seen.add(e.name), true));
84
- return new DocRegistry({ ...rest, errors: deduped });
85
- }
86
81
  }
87
82
  //# sourceMappingURL=doc-registry.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"doc-registry.js","sourceRoot":"","sources":["../../../src/implementations/http/doc-registry.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"doc-registry.js","sourceRoot":"","sources":["../../../src/implementations/http/doc-registry.ts"],"names":[],"mappings":"AASA,OAAO,EACL,gCAAgC,EAChC,oBAAoB,EACpB,mBAAmB,GAEpB,MAAM,qBAAqB,CAAA;AAY5B,SAAS,UAAU,CAAC,KAAiC;IACnD,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAA;AAC9B,CAAC;AAED;;;GAGG;AACH,SAAS,YAAY,CAAC,IAAgB;IACpC,MAAM,MAAM,GAAG,IAAI,GAAG,EAAoB,CAAA;IAC1C,KAAK,MAAM,GAAG,IAAI,IAAI;QAAE,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,EAAE,GAAG,CAAC,CAAA;IACjD,OAAO,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,CAAA;AACpC,CAAC;AAED,MAAM,OAAO,WAAW;IACL,QAAQ,CAAQ;IAChB,OAAO,CAAa;IAC7B,MAAM,CAAY;IACT,OAAO,GAAiC,EAAE,CAAA;IAE3D,YAAY,MAA0B;QACpC,IAAI,CAAC,QAAQ,GAAG,MAAM,EAAE,QAAQ,IAAI,EAAE,CAAA;QACtC,IAAI,CAAC,OAAO,GAAG,MAAM,EAAE,OAAO,IAAI,EAAE,CAAA;QAEpC,MAAM,eAAe,GAAG,MAAM,EAAE,eAAe,IAAI,IAAI,CAAA;QACvD,MAAM,UAAU,GAAe,MAAM,EAAE,MAAM;YAC3C,CAAC,CAAC,UAAU,CAAC,MAAM,CAAC,MAAM,CAAC;gBACzB,CAAC,CAAC,mBAAmB,CAAC,MAAM,CAAC,MAAM,CAAC;gBACpC,CAAC,CAAC,MAAM,CAAC,MAAM;YACjB,CAAC,CAAC,EAAE,CAAA;QAEN,mEAAmE;QACnE,uDAAuD;QACvD,MAAM,MAAM,GAAG,eAAe;YAC5B,CAAC,CAAC,CAAC,GAAG,WAAW,CAAC,aAAa,EAAE,EAAE,GAAG,UAAU,CAAC;YACjD,CAAC,CAAC,UAAU,CAAA;QAEd,IAAI,CAAC,MAAM,GAAG,YAAY,CAAC,MAAM,CAAC,CAAA;IACpC,CAAC;IAED,IAAI,CAAC,MAAkC;QACrC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,CAAA;QACzB,OAAO,IAAI,CAAA;IACb,CAAC;IAED;;;;;;OAMG;IACH,aAAa,CAAC,GAAG,IAAgB;QAC/B,IAAI,CAAC,MAAM,GAAG,YAAY,CAAC,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,GAAG,IAAI,CAAC,CAAC,CAAA;QACrD,OAAO,IAAI,CAAA;IACb,CAAC;IAED,MAAM,CAAkB,OAAqC;QAC3D,IAAI,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;QAE1D,IAAI,OAAO,EAAE,MAAM,EAAE,CAAC;YACpB,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,CAAA;QACxC,CAAC;QAED,MAAM,QAAQ,GAAgB;YAC5B,QAAQ,EAAE,IAAI,CAAC,QAAQ;YACvB,OAAO,EAAE,CAAC,GAAG,IAAI,CAAC,OAAO,CAAC;YAC1B,MAAM,EAAE,CAAC,GAAG,IAAI,CAAC,MAAM,CAAC;YACxB,MAAM;SACP,CAAA;QAED,IAAI,OAAO,EAAE,SAAS,EAAE,CAAC;YACvB,OAAO,OAAO,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAA;QACpC,CAAC;QAED,OAAO,QAAa,CAAA;IACtB,CAAC;IAED;;;;;;;OAOG;IACH,MAAM,CAAC,aAAa;QAClB,OAAO;YACL,GAAG,mBAAmB,CAAC,oBAAoB,CAAC;YAC5C,gCAAgC;SACjC,CAAA;IACH,CAAC;CACF"}
@@ -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
  // ---------------------------------------------------------------------------
7
8
  // Helpers — minimal doc fixtures
8
9
  // ---------------------------------------------------------------------------
@@ -45,14 +46,14 @@ describe('DocRegistry', () => {
45
46
  // --------------------------------------------------------------------------
46
47
  describe('constructor', () => {
47
48
  test('uses defaults when no config provided', () => {
48
- const registry = new DocRegistry();
49
+ const registry = new DocRegistry({ includeDefaults: false });
49
50
  const out = registry.toJSON();
50
51
  expect(out.basePath).toBe('');
51
52
  expect(out.headers).toEqual([]);
52
53
  expect(out.errors).toEqual([]);
53
54
  });
54
55
  test('accepts partial config', () => {
55
- const registry = new DocRegistry({ basePath: '/v1' });
56
+ const registry = new DocRegistry({ basePath: '/v1', includeDefaults: false });
56
57
  const out = registry.toJSON();
57
58
  expect(out.basePath).toBe('/v1');
58
59
  expect(out.headers).toEqual([]);
@@ -61,7 +62,7 @@ describe('DocRegistry', () => {
61
62
  test('accepts full config', () => {
62
63
  const headers = [{ name: 'Authorization', description: 'Bearer token', required: true }];
63
64
  const errors = [{ name: 'Unauthorized', statusCode: 401, description: 'Missing token' }];
64
- const registry = new DocRegistry({ basePath: '/api', headers, errors });
65
+ const registry = new DocRegistry({ basePath: '/api', headers, errors, includeDefaults: false });
65
66
  const out = registry.toJSON();
66
67
  expect(out.basePath).toBe('/api');
67
68
  expect(out.headers).toEqual(headers);
@@ -142,7 +143,7 @@ describe('DocRegistry', () => {
142
143
  test('headers and errors are copies', () => {
143
144
  const headers = [{ name: 'X-Custom' }];
144
145
  const errors = [{ name: 'E', statusCode: 500, description: 'd' }];
145
- const registry = new DocRegistry({ headers, errors });
146
+ const registry = new DocRegistry({ headers, errors, includeDefaults: false });
146
147
  const out = registry.toJSON();
147
148
  expect(out.headers).toEqual(headers);
148
149
  expect(out.headers).not.toBe(headers);
@@ -249,6 +250,141 @@ describe('DocRegistry', () => {
249
250
  });
250
251
  });
251
252
  // --------------------------------------------------------------------------
253
+ // taxonomy input + auto-defaults + dedupe (v6.0.1 simplification)
254
+ // --------------------------------------------------------------------------
255
+ describe('errors: ErrorTaxonomy (polymorphic constructor input)', () => {
256
+ test('accepts an ErrorTaxonomy directly and converts to ErrorDoc[]', () => {
257
+ const taxonomy = defineErrorTaxonomy({
258
+ AuthError: {
259
+ class: class AuthError extends Error {
260
+ },
261
+ statusCode: 401,
262
+ description: 'unauthenticated',
263
+ },
264
+ });
265
+ const envelope = new DocRegistry({ errors: taxonomy }).toJSON();
266
+ const auth = envelope.errors.find((e) => e.name === 'AuthError');
267
+ expect(auth).toBeDefined();
268
+ expect(auth?.statusCode).toBe(401);
269
+ expect(auth?.description).toBe('unauthenticated');
270
+ });
271
+ test('auto-includes framework defaults when errors is a taxonomy', () => {
272
+ const taxonomy = defineErrorTaxonomy({
273
+ AuthError: { class: class AuthError extends Error {
274
+ }, statusCode: 401 },
275
+ });
276
+ const names = new DocRegistry({ errors: taxonomy }).toJSON().errors.map((e) => e.name);
277
+ expect(names).toContain('AuthError');
278
+ expect(names).toContain('ProcedureValidationError');
279
+ expect(names).toContain('ProcedureYieldValidationError');
280
+ expect(names).toContain('ProcedureError');
281
+ expect(names).toContain('ProcedureRegistrationError');
282
+ });
283
+ test('auto-includes framework defaults when errors is ErrorDoc[]', () => {
284
+ const custom = { name: 'CustomThing', statusCode: 418, description: 'teapot' };
285
+ const names = new DocRegistry({ errors: [custom] }).toJSON().errors.map((e) => e.name);
286
+ expect(names).toContain('CustomThing');
287
+ expect(names).toContain('ProcedureValidationError');
288
+ });
289
+ test('includeDefaults: false omits framework defaults', () => {
290
+ const taxonomy = defineErrorTaxonomy({
291
+ AuthError: { class: class AuthError extends Error {
292
+ }, statusCode: 401 },
293
+ });
294
+ const names = new DocRegistry({ errors: taxonomy, includeDefaults: false })
295
+ .toJSON()
296
+ .errors.map((e) => e.name);
297
+ expect(names).toEqual(['AuthError']);
298
+ });
299
+ test('user taxonomy entry with same name as default overrides default (dedupe, user wins)', () => {
300
+ const taxonomy = defineErrorTaxonomy({
301
+ ProcedureError: {
302
+ class: Error,
303
+ statusCode: 418,
304
+ description: 'custom override',
305
+ },
306
+ });
307
+ const envelope = new DocRegistry({ errors: taxonomy }).toJSON();
308
+ const proc = envelope.errors.filter((e) => e.name === 'ProcedureError');
309
+ expect(proc).toHaveLength(1);
310
+ expect(proc[0].statusCode).toBe(418);
311
+ expect(proc[0].description).toBe('custom override');
312
+ });
313
+ test('user ErrorDoc with same name as default overrides default (dedupe, user wins)', () => {
314
+ const custom = {
315
+ name: 'ProcedureError',
316
+ statusCode: 418,
317
+ description: 'custom override',
318
+ };
319
+ const envelope = new DocRegistry({ errors: [custom] }).toJSON();
320
+ const proc = envelope.errors.filter((e) => e.name === 'ProcedureError');
321
+ expect(proc).toHaveLength(1);
322
+ expect(proc[0].statusCode).toBe(418);
323
+ expect(proc[0].description).toBe('custom override');
324
+ });
325
+ test('empty errors config still returns framework defaults', () => {
326
+ const names = new DocRegistry().toJSON().errors.map((e) => e.name);
327
+ expect(names).toContain('ProcedureError');
328
+ expect(names).toContain('ProcedureValidationError');
329
+ expect(names).toContain('ProcedureYieldValidationError');
330
+ expect(names).toContain('ProcedureRegistrationError');
331
+ });
332
+ test('includeDefaults: false with no errors produces empty error list', () => {
333
+ const envelope = new DocRegistry({ includeDefaults: false }).toJSON();
334
+ expect(envelope.errors).toEqual([]);
335
+ });
336
+ });
337
+ // --------------------------------------------------------------------------
338
+ // .documentError() fluent extension
339
+ // --------------------------------------------------------------------------
340
+ describe('.documentError()', () => {
341
+ test('adds a single ErrorDoc to the envelope', () => {
342
+ const registry = new DocRegistry({ includeDefaults: false }).documentError({
343
+ name: 'RateLimitExceeded',
344
+ statusCode: 429,
345
+ description: 'too many requests',
346
+ });
347
+ const names = registry.toJSON().errors.map((e) => e.name);
348
+ expect(names).toEqual(['RateLimitExceeded']);
349
+ });
350
+ test('accepts multiple docs via variadic args', () => {
351
+ const registry = new DocRegistry({ includeDefaults: false }).documentError({ name: 'A', statusCode: 400, description: 'a' }, { name: 'B', statusCode: 500, description: 'b' });
352
+ const names = registry.toJSON().errors.map((e) => e.name);
353
+ expect(names).toEqual(['A', 'B']);
354
+ });
355
+ test('returns this for chaining', () => {
356
+ const registry = new DocRegistry();
357
+ const returned = registry.documentError({
358
+ name: 'Foo',
359
+ statusCode: 500,
360
+ description: 'x',
361
+ });
362
+ expect(returned).toBe(registry);
363
+ });
364
+ test('composes with taxonomy input — extends docs without replacing', () => {
365
+ const taxonomy = defineErrorTaxonomy({
366
+ AuthError: { class: class AuthError extends Error {
367
+ }, statusCode: 401 },
368
+ });
369
+ const envelope = new DocRegistry({ errors: taxonomy })
370
+ .documentError({ name: 'RateLimitExceeded', statusCode: 429, description: 'x' })
371
+ .toJSON();
372
+ const names = envelope.errors.map((e) => e.name);
373
+ expect(names).toContain('AuthError');
374
+ expect(names).toContain('RateLimitExceeded');
375
+ expect(names).toContain('ProcedureValidationError');
376
+ });
377
+ test('dedupes against existing errors (last write wins)', () => {
378
+ const registry = new DocRegistry({ includeDefaults: false })
379
+ .documentError({ name: 'Foo', statusCode: 400, description: 'first' })
380
+ .documentError({ name: 'Foo', statusCode: 500, description: 'second' });
381
+ const foo = registry.toJSON().errors.filter((e) => e.name === 'Foo');
382
+ expect(foo).toHaveLength(1);
383
+ expect(foo[0].statusCode).toBe(500);
384
+ expect(foo[0].description).toBe('second');
385
+ });
386
+ });
387
+ // --------------------------------------------------------------------------
252
388
  // kind discriminant
253
389
  // --------------------------------------------------------------------------
254
390
  describe('kind discriminant', () => {
@@ -279,7 +415,6 @@ describe('DocRegistry', () => {
279
415
  const registry = new DocRegistry({
280
416
  basePath: '/api',
281
417
  headers: [{ name: 'Authorization', description: 'Bearer token', required: false }],
282
- errors: DocRegistry.defaultErrors(),
283
418
  })
284
419
  .from(makeSource([rpcDoc]))
285
420
  .from(makeSource([apiDoc]))
@@ -291,10 +426,7 @@ describe('DocRegistry', () => {
291
426
  expect(out.routes).toHaveLength(3);
292
427
  });
293
428
  test('filter + transform combined', () => {
294
- const registry = new DocRegistry({
295
- basePath: '/api',
296
- errors: DocRegistry.defaultErrors(),
297
- })
429
+ const registry = new DocRegistry({ basePath: '/api' })
298
430
  .from(makeSource([rpcDoc]))
299
431
  .from(makeSource([apiDoc]))
300
432
  .from(makeSource([streamDoc]));
@@ -313,7 +445,6 @@ describe('DocRegistry', () => {
313
445
  const registry = new DocRegistry({
314
446
  basePath: '/api',
315
447
  headers: [{ name: 'X-Request-Id', example: 'abc-123' }],
316
- errors: DocRegistry.defaultErrors(),
317
448
  })
318
449
  .from(makeSource([rpcDoc]))
319
450
  .from(makeSource([apiDoc]));