ts-procedures 6.0.0 → 6.0.2

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 (63) 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/codegen/targets/kotlin/ajsc-adapter.d.ts +24 -0
  10. package/build/codegen/targets/kotlin/ajsc-adapter.js +33 -0
  11. package/build/codegen/targets/kotlin/ajsc-adapter.js.map +1 -0
  12. package/build/codegen/targets/kotlin/ajsc-adapter.test.d.ts +1 -0
  13. package/build/codegen/targets/kotlin/ajsc-adapter.test.js +19 -0
  14. package/build/codegen/targets/kotlin/ajsc-adapter.test.js.map +1 -0
  15. package/build/codegen/targets/kotlin/e2e-compile.test.d.ts +1 -0
  16. package/build/codegen/targets/kotlin/e2e-compile.test.js +43 -0
  17. package/build/codegen/targets/kotlin/e2e-compile.test.js.map +1 -0
  18. package/build/codegen/targets/kotlin/emit-route-kotlin.d.ts +11 -0
  19. package/build/codegen/targets/kotlin/emit-route-kotlin.js +73 -0
  20. package/build/codegen/targets/kotlin/emit-route-kotlin.js.map +1 -0
  21. package/build/codegen/targets/kotlin/emit-route-kotlin.test.d.ts +1 -0
  22. package/build/codegen/targets/kotlin/emit-route-kotlin.test.js +88 -0
  23. package/build/codegen/targets/kotlin/emit-route-kotlin.test.js.map +1 -0
  24. package/build/codegen/targets/kotlin/emit-scope-kotlin.d.ts +11 -0
  25. package/build/codegen/targets/kotlin/emit-scope-kotlin.js +35 -0
  26. package/build/codegen/targets/kotlin/emit-scope-kotlin.js.map +1 -0
  27. package/build/codegen/targets/kotlin/emit-scope-kotlin.test.d.ts +1 -0
  28. package/build/codegen/targets/kotlin/emit-scope-kotlin.test.js +52 -0
  29. package/build/codegen/targets/kotlin/emit-scope-kotlin.test.js.map +1 -0
  30. package/build/codegen/targets/kotlin/format-kotlin.d.ts +4 -0
  31. package/build/codegen/targets/kotlin/format-kotlin.js +20 -0
  32. package/build/codegen/targets/kotlin/format-kotlin.js.map +1 -0
  33. package/build/codegen/targets/kotlin/format-kotlin.test.d.ts +1 -0
  34. package/build/codegen/targets/kotlin/format-kotlin.test.js +24 -0
  35. package/build/codegen/targets/kotlin/format-kotlin.test.js.map +1 -0
  36. package/build/codegen/targets/kotlin/integration.test.d.ts +1 -0
  37. package/build/codegen/targets/kotlin/integration.test.js +34 -0
  38. package/build/codegen/targets/kotlin/integration.test.js.map +1 -0
  39. package/build/implementations/http/doc-registry.d.ts +14 -19
  40. package/build/implementations/http/doc-registry.js +41 -46
  41. package/build/implementations/http/doc-registry.js.map +1 -1
  42. package/build/implementations/http/doc-registry.test.js +141 -10
  43. package/build/implementations/http/doc-registry.test.js.map +1 -1
  44. package/build/implementations/http/error-taxonomy.d.ts +11 -2
  45. package/build/implementations/http/error-taxonomy.js +24 -2
  46. package/build/implementations/http/error-taxonomy.js.map +1 -1
  47. package/build/implementations/http/route-errors.test.js +5 -6
  48. package/build/implementations/http/route-errors.test.js.map +1 -1
  49. package/build/implementations/types.d.ts +13 -1
  50. package/docs/http-integrations.md +39 -5
  51. package/docs/superpowers/plans/2026-04-24-doc-registry-simplification.md +886 -0
  52. package/docs/superpowers/plans/2026-04-24-kotlin-codegen-target.md +1265 -0
  53. package/docs/superpowers/specs/2026-04-24-kotlin-swift-codegen-design.md +401 -0
  54. package/package.json +1 -1
  55. package/src/implementations/http/README.md +4 -3
  56. package/src/implementations/http/doc-registry.test.ts +154 -10
  57. package/src/implementations/http/doc-registry.ts +46 -53
  58. package/src/implementations/http/error-taxonomy.ts +26 -2
  59. package/src/implementations/http/express-rpc/README.md +2 -2
  60. package/src/implementations/http/hono-rpc/README.md +2 -2
  61. package/src/implementations/http/hono-stream/README.md +15 -0
  62. package/src/implementations/http/route-errors.test.ts +5 -6
  63. package/src/implementations/types.ts +13 -1
@@ -0,0 +1,52 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { emitKotlinScope } from './emit-scope-kotlin.js';
3
+ import { createStubKotlinEmitter } from './ajsc-adapter.js';
4
+ const ok = (code, rootTypeName) => ({
5
+ code,
6
+ rootTypeName,
7
+ extractedTypeNames: [],
8
+ imports: ['kotlinx.serialization.Serializable'],
9
+ });
10
+ describe('emitKotlinScope', () => {
11
+ it('produces a complete kotlin source file for a single-route scope', () => {
12
+ const route = {
13
+ kind: 'api',
14
+ name: 'GetUser',
15
+ method: 'GET',
16
+ fullPath: '/users/:id',
17
+ schema: { input: { pathParams: { type: 'object' } }, returnType: { type: 'object' } },
18
+ errors: [],
19
+ };
20
+ const group = { scopeKey: 'users', camelCase: 'users', routes: [route] };
21
+ const emitter = createStubKotlinEmitter({
22
+ PathParams: ok('@Serializable data class PathParams(val id: String)', 'PathParams'),
23
+ Response: ok('@Serializable data class Response(val id: String)', 'Response'),
24
+ });
25
+ const file = emitKotlinScope(group, { kotlinPackage: 'com.example.api', sourceHash: 'abc123' }, emitter, new Map());
26
+ expect(file.filename).toBe('Users.kt');
27
+ expect(file.code).toContain('package com.example.api');
28
+ expect(file.code).toContain('// Source hash: abc123');
29
+ expect(file.code).toContain('import kotlinx.serialization.Serializable');
30
+ expect(file.code).toContain('object Users {');
31
+ expect(file.code).toContain('object GetUser {');
32
+ expect(file.code).toContain('const val method = "GET"');
33
+ });
34
+ it('joins multiple routes inside one scope object', () => {
35
+ const route1 = { kind: 'api', name: 'GetUser', method: 'GET', fullPath: '/users/:id', schema: {}, errors: [] };
36
+ const route2 = { kind: 'api', name: 'CreateUser', method: 'POST', fullPath: '/users', schema: {}, errors: [] };
37
+ const group = { scopeKey: 'users', camelCase: 'users', routes: [route1, route2] };
38
+ const emitter = createStubKotlinEmitter({});
39
+ const file = emitKotlinScope(group, { kotlinPackage: 'com.example.api', sourceHash: 'h' }, emitter, new Map());
40
+ expect(file.code).toContain('object GetUser {');
41
+ expect(file.code).toContain('object CreateUser {');
42
+ // exactly one outer scope object
43
+ expect((file.code.match(/^object Users \{/gm) ?? []).length).toBe(1);
44
+ });
45
+ it('uses PascalCase scope name for the filename and outer object', () => {
46
+ const group = { scopeKey: 'admin-users', camelCase: 'adminUsers', routes: [] };
47
+ const file = emitKotlinScope(group, { kotlinPackage: 'p', sourceHash: 'h' }, createStubKotlinEmitter({}), new Map());
48
+ expect(file.filename).toBe('AdminUsers.kt');
49
+ expect(file.code).toContain('object AdminUsers {');
50
+ });
51
+ });
52
+ //# sourceMappingURL=emit-scope-kotlin.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"emit-scope-kotlin.test.js","sourceRoot":"","sources":["../../../../src/codegen/targets/kotlin/emit-scope-kotlin.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,4 @@
1
+ export declare function kotlinPackageDecl(pkg: string): string;
2
+ export declare function kotlinSourceHashHeader(hash: string): string;
3
+ export declare function kotlinImports(imports: string[]): string;
4
+ export declare function indent(text: string, level: number): string;
@@ -0,0 +1,20 @@
1
+ export function kotlinPackageDecl(pkg) {
2
+ return `package ${pkg}`;
3
+ }
4
+ export function kotlinSourceHashHeader(hash) {
5
+ return `// Source hash: ${hash}`;
6
+ }
7
+ export function kotlinImports(imports) {
8
+ if (imports.length === 0)
9
+ return '';
10
+ const unique = Array.from(new Set(imports)).sort();
11
+ return unique.map((i) => `import ${i}`).join('\n');
12
+ }
13
+ export function indent(text, level) {
14
+ const prefix = ' '.repeat(level);
15
+ return text
16
+ .split('\n')
17
+ .map((line) => (line.length === 0 ? line : `${prefix}${line}`))
18
+ .join('\n');
19
+ }
20
+ //# sourceMappingURL=format-kotlin.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"format-kotlin.js","sourceRoot":"","sources":["../../../../src/codegen/targets/kotlin/format-kotlin.ts"],"names":[],"mappings":"AAAA,MAAM,UAAU,iBAAiB,CAAC,GAAW;IAC3C,OAAO,WAAW,GAAG,EAAE,CAAA;AACzB,CAAC;AAED,MAAM,UAAU,sBAAsB,CAAC,IAAY;IACjD,OAAO,mBAAmB,IAAI,EAAE,CAAA;AAClC,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,OAAiB;IAC7C,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,CAAA;IACnC,MAAM,MAAM,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,EAAE,CAAA;IAClD,OAAO,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,UAAU,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;AACpD,CAAC;AAED,MAAM,UAAU,MAAM,CAAC,IAAY,EAAE,KAAa;IAChD,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;IACnC,OAAO,IAAI;SACR,KAAK,CAAC,IAAI,CAAC;SACX,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,MAAM,GAAG,IAAI,EAAE,CAAC,CAAC;SAC9D,IAAI,CAAC,IAAI,CAAC,CAAA;AACf,CAAC"}
@@ -0,0 +1,24 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { kotlinPackageDecl, kotlinSourceHashHeader, kotlinImports, indent, } from './format-kotlin.js';
3
+ describe('format-kotlin', () => {
4
+ it('emits a package declaration', () => {
5
+ expect(kotlinPackageDecl('com.example.api')).toBe('package com.example.api');
6
+ });
7
+ it('emits a source-hash header line', () => {
8
+ expect(kotlinSourceHashHeader('abc123')).toBe('// Source hash: abc123');
9
+ });
10
+ it('dedupes and sorts imports', () => {
11
+ expect(kotlinImports(['kotlinx.serialization.Serializable', 'kotlinx.serialization.SerialName', 'kotlinx.serialization.Serializable'])).toBe('import kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable');
12
+ });
13
+ it('returns empty string when no imports', () => {
14
+ expect(kotlinImports([])).toBe('');
15
+ });
16
+ it('indents every line by 4 spaces per level', () => {
17
+ expect(indent('a\nb', 1)).toBe(' a\n b');
18
+ expect(indent('a', 2)).toBe(' a');
19
+ });
20
+ it('preserves blank lines without trailing whitespace when indenting', () => {
21
+ expect(indent('a\n\nb', 1)).toBe(' a\n\n b');
22
+ });
23
+ });
24
+ //# sourceMappingURL=format-kotlin.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"format-kotlin.test.js","sourceRoot":"","sources":["../../../../src/codegen/targets/kotlin/format-kotlin.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAA;AAC7C,OAAO,EACL,iBAAiB,EACjB,sBAAsB,EACtB,aAAa,EACb,MAAM,GACP,MAAM,oBAAoB,CAAA;AAE3B,QAAQ,CAAC,eAAe,EAAE,GAAG,EAAE;IAC7B,EAAE,CAAC,6BAA6B,EAAE,GAAG,EAAE;QACrC,MAAM,CAAC,iBAAiB,CAAC,iBAAiB,CAAC,CAAC,CAAC,IAAI,CAAC,yBAAyB,CAAC,CAAA;IAC9E,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,iCAAiC,EAAE,GAAG,EAAE;QACzC,MAAM,CAAC,sBAAsB,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,wBAAwB,CAAC,CAAA;IACzE,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,2BAA2B,EAAE,GAAG,EAAE;QACnC,MAAM,CAAC,aAAa,CAAC,CAAC,oCAAoC,EAAE,kCAAkC,EAAE,oCAAoC,CAAC,CAAC,CAAC,CAAC,IAAI,CAC1I,oFAAoF,CACrF,CAAA;IACH,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,sCAAsC,EAAE,GAAG,EAAE;QAC9C,MAAM,CAAC,aAAa,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;IACpC,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,0CAA0C,EAAE,GAAG,EAAE;QAClD,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,cAAc,CAAC,CAAA;QAC9C,MAAM,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,WAAW,CAAC,CAAA;IAC1C,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,kEAAkE,EAAE,GAAG,EAAE;QAC1E,MAAM,CAAC,MAAM,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,gBAAgB,CAAC,CAAA;IACpD,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,34 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { readFile } from 'node:fs/promises';
3
+ import { dirname, join } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { runPipeline } from '../../pipeline.js';
6
+ import { createStubKotlinEmitter } from './ajsc-adapter.js';
7
+ const __filename = fileURLToPath(import.meta.url);
8
+ const __dirname = dirname(__filename);
9
+ describe('kotlin codegen — integration', () => {
10
+ it('produces byte-identical output against the golden fixture', async () => {
11
+ const envelopePath = join(__dirname, '__fixtures__/users-envelope.json');
12
+ const goldenPath = join(__dirname, '__fixtures__/users-golden.kt');
13
+ const envelope = JSON.parse(await readFile(envelopePath, 'utf8'));
14
+ const goldenTemplate = await readFile(goldenPath, 'utf8');
15
+ const emitter = createStubKotlinEmitter({
16
+ PathParams: { code: '@Serializable\ndata class PathParams(val id: String)', rootTypeName: 'PathParams', extractedTypeNames: [], imports: ['kotlinx.serialization.Serializable'] },
17
+ Response: { code: '@Serializable\ndata class Response(val id: String, val name: String)', rootTypeName: 'Response', extractedTypeNames: [], imports: ['kotlinx.serialization.Serializable'] },
18
+ Body: { code: '@Serializable\ndata class Body(val name: String, val email: String)', rootTypeName: 'Body', extractedTypeNames: [], imports: ['kotlinx.serialization.Serializable'] },
19
+ NotFound: { code: '@Serializable\ndata class NotFound(val name: String = "NotFound", val message: String)', rootTypeName: 'NotFound', extractedTypeNames: [], imports: ['kotlinx.serialization.Serializable'] },
20
+ });
21
+ const files = await runPipeline({
22
+ envelope, outDir: 'out', dryRun: true,
23
+ target: 'kotlin', kotlinPackage: 'com.example.api',
24
+ kotlinEmitter: emitter,
25
+ });
26
+ expect(files).toHaveLength(1);
27
+ expect(files[0].path).toBe(join('out', 'Users.kt'));
28
+ // Hash is deterministic given envelope contents; splice into golden template.
29
+ const sourceHashLine = files[0].code.split('\n').find((l) => l.startsWith('// Source hash:')) ?? '';
30
+ const goldenWithHash = goldenTemplate.replace('// Source hash: <PLACEHOLDER>', sourceHashLine);
31
+ expect(files[0].code).toBe(goldenWithHash);
32
+ });
33
+ });
34
+ //# sourceMappingURL=integration.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"integration.test.js","sourceRoot":"","sources":["../../../../src/codegen/targets/kotlin/integration.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAA;AAC7C,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAA;AAC3C,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;AACzC,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAA;AACxC,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAC/C,OAAO,EAAE,uBAAuB,EAAE,MAAM,mBAAmB,CAAA;AAE3D,MAAM,UAAU,GAAG,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;AACjD,MAAM,SAAS,GAAG,OAAO,CAAC,UAAU,CAAC,CAAA;AAErC,QAAQ,CAAC,8BAA8B,EAAE,GAAG,EAAE;IAC5C,EAAE,CAAC,2DAA2D,EAAE,KAAK,IAAI,EAAE;QACzE,MAAM,YAAY,GAAG,IAAI,CAAC,SAAS,EAAE,kCAAkC,CAAC,CAAA;QACxE,MAAM,UAAU,GAAG,IAAI,CAAC,SAAS,EAAE,8BAA8B,CAAC,CAAA;QAClE,MAAM,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC,CAAC,CAAA;QACjE,MAAM,cAAc,GAAG,MAAM,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC,CAAA;QAEzD,MAAM,OAAO,GAAG,uBAAuB,CAAC;YACtC,UAAU,EAAE,EAAE,IAAI,EAAE,sDAAsD,EAAE,YAAY,EAAE,YAAY,EAAE,kBAAkB,EAAE,EAAE,EAAE,OAAO,EAAE,CAAC,oCAAoC,CAAC,EAAE;YACjL,QAAQ,EAAE,EAAE,IAAI,EAAE,sEAAsE,EAAE,YAAY,EAAE,UAAU,EAAE,kBAAkB,EAAE,EAAE,EAAE,OAAO,EAAE,CAAC,oCAAoC,CAAC,EAAE;YAC7L,IAAI,EAAE,EAAE,IAAI,EAAE,qEAAqE,EAAE,YAAY,EAAE,MAAM,EAAE,kBAAkB,EAAE,EAAE,EAAE,OAAO,EAAE,CAAC,oCAAoC,CAAC,EAAE;YACpL,QAAQ,EAAE,EAAE,IAAI,EAAE,wFAAwF,EAAE,YAAY,EAAE,UAAU,EAAE,kBAAkB,EAAE,EAAE,EAAE,OAAO,EAAE,CAAC,oCAAoC,CAAC,EAAE;SAChN,CAAC,CAAA;QAEF,MAAM,KAAK,GAAG,MAAM,WAAW,CAAC;YAC9B,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI;YACrC,MAAM,EAAE,QAAQ,EAAE,aAAa,EAAE,iBAAiB;YAClD,aAAa,EAAE,OAAO;SACvB,CAAC,CAAA;QAEF,MAAM,CAAC,KAAK,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAA;QAC7B,MAAM,CAAC,KAAK,CAAC,CAAC,CAAE,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,UAAU,CAAC,CAAC,CAAA;QAEpD,8EAA8E;QAC9E,MAAM,cAAc,GAAG,KAAK,CAAC,CAAC,CAAE,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,UAAU,CAAC,iBAAiB,CAAC,CAAC,IAAI,EAAE,CAAA;QACpG,MAAM,cAAc,GAAG,cAAc,CAAC,OAAO,CAAC,+BAA+B,EAAE,cAAc,CAAC,CAAA;QAC9F,MAAM,CAAC,KAAK,CAAC,CAAC,CAAE,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,cAAc,CAAC,CAAA;IAC7C,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA"}
@@ -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]));