ts-procedures 6.0.1 → 6.1.0

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 (88) hide show
  1. package/agent_config/bin/setup.mjs +0 -0
  2. package/agent_config/claude-code/skills/ts-procedures/SKILL.md +1 -0
  3. package/agent_config/claude-code/skills/ts-procedures/api-reference.md +2 -0
  4. package/agent_config/claude-code/skills/ts-procedures-kotlin/SKILL.md +106 -0
  5. package/agent_config/copilot/copilot-instructions.md +2 -0
  6. package/agent_config/cursor/cursorrules +2 -0
  7. package/build/codegen/bin/cli.d.ts +25 -0
  8. package/build/codegen/bin/cli.js +88 -0
  9. package/build/codegen/bin/cli.js.map +1 -1
  10. package/build/codegen/bin/cli.test.js +180 -1
  11. package/build/codegen/bin/cli.test.js.map +1 -1
  12. package/build/codegen/index.d.ts +19 -0
  13. package/build/codegen/index.js +5 -0
  14. package/build/codegen/index.js.map +1 -1
  15. package/build/codegen/pipeline.d.ts +7 -0
  16. package/build/codegen/pipeline.js +57 -0
  17. package/build/codegen/pipeline.js.map +1 -1
  18. package/build/codegen/pipeline.test.js +162 -0
  19. package/build/codegen/pipeline.test.js.map +1 -1
  20. package/build/codegen/targets/kotlin/ajsc-adapter.d.ts +26 -0
  21. package/build/codegen/targets/kotlin/ajsc-adapter.js +38 -0
  22. package/build/codegen/targets/kotlin/ajsc-adapter.js.map +1 -0
  23. package/build/codegen/targets/kotlin/ajsc-adapter.test.d.ts +1 -0
  24. package/build/codegen/targets/kotlin/ajsc-adapter.test.js +37 -0
  25. package/build/codegen/targets/kotlin/ajsc-adapter.test.js.map +1 -0
  26. package/build/codegen/targets/kotlin/e2e-compile.test.d.ts +1 -0
  27. package/build/codegen/targets/kotlin/e2e-compile.test.js +75 -0
  28. package/build/codegen/targets/kotlin/e2e-compile.test.js.map +1 -0
  29. package/build/codegen/targets/kotlin/emit-route-kotlin.d.ts +15 -0
  30. package/build/codegen/targets/kotlin/emit-route-kotlin.js +80 -0
  31. package/build/codegen/targets/kotlin/emit-route-kotlin.js.map +1 -0
  32. package/build/codegen/targets/kotlin/emit-route-kotlin.test.d.ts +1 -0
  33. package/build/codegen/targets/kotlin/emit-route-kotlin.test.js +207 -0
  34. package/build/codegen/targets/kotlin/emit-route-kotlin.test.js.map +1 -0
  35. package/build/codegen/targets/kotlin/emit-scope-kotlin.d.ts +14 -0
  36. package/build/codegen/targets/kotlin/emit-scope-kotlin.js +40 -0
  37. package/build/codegen/targets/kotlin/emit-scope-kotlin.js.map +1 -0
  38. package/build/codegen/targets/kotlin/emit-scope-kotlin.test.d.ts +1 -0
  39. package/build/codegen/targets/kotlin/emit-scope-kotlin.test.js +91 -0
  40. package/build/codegen/targets/kotlin/emit-scope-kotlin.test.js.map +1 -0
  41. package/build/codegen/targets/kotlin/format-kotlin.d.ts +15 -0
  42. package/build/codegen/targets/kotlin/format-kotlin.js +40 -0
  43. package/build/codegen/targets/kotlin/format-kotlin.js.map +1 -0
  44. package/build/codegen/targets/kotlin/format-kotlin.test.d.ts +1 -0
  45. package/build/codegen/targets/kotlin/format-kotlin.test.js +50 -0
  46. package/build/codegen/targets/kotlin/format-kotlin.test.js.map +1 -0
  47. package/build/codegen/targets/kotlin/integration.test.d.ts +1 -0
  48. package/build/codegen/targets/kotlin/integration.test.js +51 -0
  49. package/build/codegen/targets/kotlin/integration.test.js.map +1 -0
  50. package/build/codegen/targets/kotlin/probe-unsupported-unions.test.d.ts +1 -0
  51. package/build/codegen/targets/kotlin/probe-unsupported-unions.test.js +50 -0
  52. package/build/codegen/targets/kotlin/probe-unsupported-unions.test.js.map +1 -0
  53. package/build/codegen/test-helpers/golden.d.ts +15 -0
  54. package/build/codegen/test-helpers/golden.js +30 -0
  55. package/build/codegen/test-helpers/golden.js.map +1 -0
  56. package/build/codegen/test-helpers/golden.test.d.ts +1 -0
  57. package/build/codegen/test-helpers/golden.test.js +76 -0
  58. package/build/codegen/test-helpers/golden.test.js.map +1 -0
  59. package/docs/codegen-kotlin.md +175 -0
  60. package/docs/http-integrations.md +32 -0
  61. package/docs/superpowers/plans/2026-04-24-kotlin-codegen-target.md +1265 -0
  62. package/docs/superpowers/plans/2026-04-25-ajsc-v7-kotlin-polish.md +1993 -0
  63. package/docs/superpowers/specs/2026-04-24-kotlin-swift-codegen-design.md +401 -0
  64. package/docs/superpowers/specs/2026-04-25-ajsc-v7-kotlin-polish-design.md +314 -0
  65. package/package.json +2 -2
  66. package/src/codegen/bin/cli.test.ts +200 -1
  67. package/src/codegen/bin/cli.ts +103 -0
  68. package/src/codegen/index.ts +27 -0
  69. package/src/codegen/pipeline.test.ts +175 -0
  70. package/src/codegen/pipeline.ts +79 -0
  71. package/src/codegen/targets/kotlin/__fixtures__/users-envelope.json +144 -0
  72. package/src/codegen/targets/kotlin/__fixtures__/users-golden.kt +121 -0
  73. package/src/codegen/targets/kotlin/__snapshots__/probe-unsupported-unions.test.ts.snap +27 -0
  74. package/src/codegen/targets/kotlin/ajsc-adapter.test.ts +47 -0
  75. package/src/codegen/targets/kotlin/ajsc-adapter.ts +66 -0
  76. package/src/codegen/targets/kotlin/e2e-compile.test.ts +86 -0
  77. package/src/codegen/targets/kotlin/emit-route-kotlin.test.ts +239 -0
  78. package/src/codegen/targets/kotlin/emit-route-kotlin.ts +109 -0
  79. package/src/codegen/targets/kotlin/emit-scope-kotlin.test.ts +112 -0
  80. package/src/codegen/targets/kotlin/emit-scope-kotlin.ts +65 -0
  81. package/src/codegen/targets/kotlin/format-kotlin.test.ts +70 -0
  82. package/src/codegen/targets/kotlin/format-kotlin.ts +45 -0
  83. package/src/codegen/targets/kotlin/integration.test.ts +77 -0
  84. package/src/codegen/targets/kotlin/probe-unsupported-unions.test.ts +64 -0
  85. package/src/codegen/test-helpers/golden.test.ts +80 -0
  86. package/src/codegen/test-helpers/golden.ts +34 -0
  87. package/src/implementations/http/README.md +2 -0
  88. package/src/implementations/http/hono-stream/README.md +15 -0
@@ -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;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,WAAW,CACzB,GAAM,EACN,IAAkB;IAElB,MAAM,GAAG,GAAwB,EAAE,CAAA;IACnC,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;QACvB,MAAM,KAAK,GAAG,GAAG,CAAC,GAAG,CAAC,CAAA;QACtB,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;YACxB,GAAG,CAAC,GAAG,CAAC,GAAG,KAAK,CAAA;QAClB,CAAC;IACH,CAAC;IACD,OAAO,GAAG,CAAA;AACZ,CAAC"}
@@ -0,0 +1,50 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { kotlinPackageDecl, kotlinSourceHashHeader, kotlinImports, indent, pickDefined, } 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
+ describe('pickDefined', () => {
25
+ it('returns only the keys whose values are not undefined', () => {
26
+ expect(pickDefined({ a: 1, b: undefined, c: 3 }, ['a', 'b', 'c']))
27
+ .toEqual({ a: 1, c: 3 });
28
+ });
29
+ it('preserves the literal `false` value for boolean opts', () => {
30
+ expect(pickDefined({ depluralize: false, x: undefined }, ['depluralize', 'x']))
31
+ .toEqual({ depluralize: false });
32
+ });
33
+ it('preserves the literal `false` for `string | false` opts (e.g. arrayItemNaming)', () => {
34
+ const result = pickDefined({ arrayItemNaming: false }, ['arrayItemNaming']);
35
+ expect(result).toEqual({ arrayItemNaming: false });
36
+ expect('arrayItemNaming' in result).toBe(true);
37
+ });
38
+ it('omits keys not in the keys list even if they are defined on src', () => {
39
+ expect(pickDefined({ a: 1, b: 2 }, ['a']))
40
+ .toEqual({ a: 1 });
41
+ });
42
+ it('returns an empty object when all keys are undefined', () => {
43
+ expect(pickDefined({ a: undefined, b: undefined }, ['a', 'b']))
44
+ .toEqual({});
45
+ });
46
+ it('returns an empty object when keys is empty', () => {
47
+ expect(pickDefined({ a: 1 }, [])).toEqual({});
48
+ });
49
+ });
50
+ //# 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":"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"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,51 @@
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 { assertGoldenOrUpdate } from '../../test-helpers/golden.js';
7
+ import { createStubKotlinEmitter } from './ajsc-adapter.js';
8
+ const __filename = fileURLToPath(import.meta.url);
9
+ const __dirname = dirname(__filename);
10
+ const ok = (code, rootTypeName, imports = ['kotlinx.serialization.Serializable']) => ({
11
+ code,
12
+ rootTypeName,
13
+ extractedTypeNames: [],
14
+ imports,
15
+ });
16
+ describe('kotlin codegen — integration', () => {
17
+ it('produces byte-identical output against the golden fixture', async () => {
18
+ const envelopePath = join(__dirname, '__fixtures__/users-envelope.json');
19
+ const goldenPath = join(__dirname, '__fixtures__/users-golden.kt');
20
+ const envelope = JSON.parse(await readFile(envelopePath, 'utf8'));
21
+ // Hand-authored slot outputs in the v7.2 nested-class shape (inlineTypes: true).
22
+ //
23
+ // The stub map is keyed on rootTypeName, NOT on (route, slot). Both GetUser
24
+ // and CreateUser have a slot named "Response" — they intentionally share the
25
+ // single stub entry below. The golden file therefore shows three identical
26
+ // Response data classes (one per route that has one). This is correct: the
27
+ // integration test pins our file-assembly logic, not real ajsc per-route
28
+ // output. The kotlinc E2E (Task 11) exercises real ajsc against the same
29
+ // fixture and would surface any incompatibility.
30
+ const emitter = createStubKotlinEmitter({
31
+ // GetUser
32
+ PathParams: ok('@Serializable\ndata class PathParams(\n val id: String,\n)', 'PathParams'),
33
+ Response: ok('@Serializable\ndata class Response(\n val id: String,\n val name: String,\n @SerialName("created-at") @Contextual val createdAt: java.time.Instant,\n val address: Address,\n) {\n @Serializable\n data class Address(\n val street: String,\n val city: String,\n )\n}', 'Response', ['kotlinx.serialization.Serializable', 'kotlinx.serialization.SerialName', 'kotlinx.serialization.Contextual']),
34
+ NotFound: ok('@Serializable\ndata class NotFound(\n val name: String = "NotFound",\n val message: String,\n)', 'NotFound'),
35
+ // CreateUser
36
+ Body: ok('@Serializable\n@JsonClassDiscriminator("kind")\nsealed interface Body {\n @Serializable\n @SerialName("guest")\n data class GuestBody(\n val displayName: String,\n ) : Body\n\n @Serializable\n @SerialName("registered")\n data class RegisteredBody(\n val email: String,\n val name: String,\n ) : Body\n}', 'Body', ['kotlinx.serialization.Serializable', 'kotlinx.serialization.SerialName', 'kotlinx.serialization.json.JsonClassDiscriminator']),
37
+ ValidationError: ok('@Serializable\ndata class ValidationError(\n val name: String = "ValidationError",\n val message: String,\n val field: String? = null,\n)', 'ValidationError'),
38
+ // ListUsers
39
+ Query: ok('@Serializable\ndata class Query(\n val status: Status? = null,\n val limit: Long? = null,\n) {\n @Serializable\n enum class Status {\n @SerialName("active") ACTIVE,\n @SerialName("inactive") INACTIVE,\n }\n}', 'Query', ['kotlinx.serialization.Serializable', 'kotlinx.serialization.SerialName']),
40
+ });
41
+ const files = await runPipeline({
42
+ envelope, outDir: 'out', dryRun: true,
43
+ target: 'kotlin', kotlinPackage: 'com.example.api',
44
+ kotlinEmitter: emitter,
45
+ });
46
+ expect(files).toHaveLength(1);
47
+ expect(files[0].path).toBe(join('out', 'Users.kt'));
48
+ await assertGoldenOrUpdate(files[0].code, goldenPath);
49
+ });
50
+ });
51
+ //# 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,oBAAoB,EAAE,MAAM,8BAA8B,CAAA;AACnE,OAAO,EAAE,uBAAuB,EAAyB,MAAM,mBAAmB,CAAA;AAElF,MAAM,UAAU,GAAG,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;AACjD,MAAM,SAAS,GAAG,OAAO,CAAC,UAAU,CAAC,CAAA;AAErC,MAAM,EAAE,GAAG,CAAC,IAAY,EAAE,YAAoB,EAAE,UAAoB,CAAC,oCAAoC,CAAC,EAAoB,EAAE,CAAC,CAAC;IAChI,IAAI;IACJ,YAAY;IACZ,kBAAkB,EAAE,EAAE;IACtB,OAAO;CACR,CAAC,CAAA;AAEF,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;QAEjE,iFAAiF;QACjF,EAAE;QACF,4EAA4E;QAC5E,6EAA6E;QAC7E,2EAA2E;QAC3E,2EAA2E;QAC3E,yEAAyE;QACzE,yEAAyE;QACzE,iDAAiD;QACjD,MAAM,OAAO,GAAG,uBAAuB,CAAC;YACtC,UAAU;YACV,UAAU,EAAE,EAAE,CAAC,+DAA+D,EAAE,YAAY,CAAC;YAC7F,QAAQ,EAAE,EAAE,CACV,4SAA4S,EAC5S,UAAU,EACV,CAAC,oCAAoC,EAAE,kCAAkC,EAAE,kCAAkC,CAAC,CAC/G;YACD,QAAQ,EAAE,EAAE,CACV,sGAAsG,EACtG,UAAU,CACX;YAED,aAAa;YACb,IAAI,EAAE,EAAE,CACN,6VAA6V,EAC7V,MAAM,EACN,CAAC,oCAAoC,EAAE,kCAAkC,EAAE,mDAAmD,CAAC,CAChI;YACD,eAAe,EAAE,EAAE,CACjB,oJAAoJ,EACpJ,iBAAiB,CAClB;YAED,YAAY;YACZ,KAAK,EAAE,EAAE,CACP,8OAA8O,EAC9O,OAAO,EACP,CAAC,oCAAoC,EAAE,kCAAkC,CAAC,CAC3E;SACF,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,MAAM,oBAAoB,CAAC,KAAK,CAAC,CAAC,CAAE,CAAC,IAAI,EAAE,UAAU,CAAC,CAAA;IACxD,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA"}
@@ -0,0 +1,50 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ let ajscResolvable = false;
3
+ let emitKotlinFn;
4
+ try {
5
+ const ajsc = await import('ajsc');
6
+ if (typeof ajsc.emitKotlin === 'function') {
7
+ ajscResolvable = true;
8
+ emitKotlinFn = ajsc.emitKotlin;
9
+ }
10
+ }
11
+ catch {
12
+ // ajsc not installed (e.g. npm install --omit=optional); test skips below.
13
+ }
14
+ describe('ajsc.emitKotlin — untagged oneOf behavior', () => {
15
+ it.skipIf(!ajscResolvable)('produces a deterministic fallback shape for an untagged oneOf', () => {
16
+ const schema = {
17
+ oneOf: [{ type: 'string' }, { type: 'integer' }],
18
+ };
19
+ const result = emitKotlinFn(schema, {
20
+ rootTypeName: 'Mixed',
21
+ inlineTypes: true,
22
+ unsupportedUnions: 'fallback',
23
+ });
24
+ // Snapshot pins the current ajsc behavior. If ajsc changes the fallback
25
+ // shape, this diff prompts an intentional review and a corresponding
26
+ // update to docs/codegen-kotlin.md.
27
+ expect({
28
+ code: result.code,
29
+ imports: result.imports.slice().sort(),
30
+ rootTypeName: result.rootTypeName,
31
+ extractedTypeNames: result.extractedTypeNames.slice().sort(),
32
+ }).toMatchSnapshot();
33
+ });
34
+ // Companion to test 1: confirms 'fallback' is not load-bearing — ajsc emits the
35
+ // same shape with or without it. If these snapshots ever diverge, that's a
36
+ // meaningful behavior change worth surfacing in docs/codegen-kotlin.md.
37
+ it.skipIf(!ajscResolvable)('silently falls back to empty data class when unsupportedUnions is not specified', () => {
38
+ const schema = { oneOf: [{ type: 'string' }, { type: 'integer' }] };
39
+ const result = emitKotlinFn(schema, { rootTypeName: 'Mixed', inlineTypes: true });
40
+ // When unsupportedUnions defaults (not explicitly set), ajsc does NOT throw.
41
+ // Instead it silently emits an empty data class.
42
+ expect({
43
+ code: result.code,
44
+ imports: result.imports.slice().sort(),
45
+ rootTypeName: result.rootTypeName,
46
+ extractedTypeNames: result.extractedTypeNames.slice().sort(),
47
+ }).toMatchSnapshot();
48
+ });
49
+ });
50
+ //# sourceMappingURL=probe-unsupported-unions.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"probe-unsupported-unions.test.js","sourceRoot":"","sources":["../../../../src/codegen/targets/kotlin/probe-unsupported-unions.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAA;AAE7C,IAAI,cAAc,GAAG,KAAK,CAAA;AAC1B,IAAI,YAKS,CAAA;AAEb,IAAI,CAAC;IACH,MAAM,IAAI,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC,CAAA;IACjC,IAAI,OAAQ,IAAiC,CAAC,UAAU,KAAK,UAAU,EAAE,CAAC;QACxE,cAAc,GAAG,IAAI,CAAA;QACrB,YAAY,GAAI,IAA4C,CAAC,UAAW,CAAA;IAC1E,CAAC;AACH,CAAC;AAAC,MAAM,CAAC;IACP,2EAA2E;AAC7E,CAAC;AAED,QAAQ,CAAC,2CAA2C,EAAE,GAAG,EAAE;IACzD,EAAE,CAAC,MAAM,CAAC,CAAC,cAAc,CAAC,CACxB,+DAA+D,EAC/D,GAAG,EAAE;QACH,MAAM,MAAM,GAAG;YACb,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC;SACjD,CAAA;QACD,MAAM,MAAM,GAAG,YAAa,CAAC,MAAM,EAAE;YACnC,YAAY,EAAE,OAAO;YACrB,WAAW,EAAE,IAAI;YACjB,iBAAiB,EAAE,UAAU;SAC9B,CAAC,CAAA;QAEF,wEAAwE;QACxE,qEAAqE;QACrE,oCAAoC;QACpC,MAAM,CAAC;YACL,IAAI,EAAE,MAAM,CAAC,IAAI;YACjB,OAAO,EAAE,MAAM,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC,IAAI,EAAE;YACtC,YAAY,EAAE,MAAM,CAAC,YAAY;YACjC,kBAAkB,EAAE,MAAM,CAAC,kBAAkB,CAAC,KAAK,EAAE,CAAC,IAAI,EAAE;SAC7D,CAAC,CAAC,eAAe,EAAE,CAAA;IACtB,CAAC,CACF,CAAA;IAED,gFAAgF;IAChF,2EAA2E;IAC3E,wEAAwE;IACxE,EAAE,CAAC,MAAM,CAAC,CAAC,cAAc,CAAC,CACxB,iFAAiF,EACjF,GAAG,EAAE;QACH,MAAM,MAAM,GAAG,EAAE,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC,EAAE,CAAA;QACnE,MAAM,MAAM,GAAG,YAAa,CAAC,MAAM,EAAE,EAAE,YAAY,EAAE,OAAO,EAAE,WAAW,EAAE,IAAI,EAAE,CAAC,CAAA;QAClF,6EAA6E;QAC7E,iDAAiD;QACjD,MAAM,CAAC;YACL,IAAI,EAAE,MAAM,CAAC,IAAI;YACjB,OAAO,EAAE,MAAM,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC,IAAI,EAAE;YACtC,YAAY,EAAE,MAAM,CAAC,YAAY;YACjC,kBAAkB,EAAE,MAAM,CAAC,kBAAkB,CAAC,KAAK,EAAE,CAAC,IAAI,EAAE;SAC7D,CAAC,CAAC,eAAe,EAAE,CAAA;IACtB,CAAC,CACF,CAAA;AACH,CAAC,CAAC,CAAA"}
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Compares produced source against a golden file, with optional regeneration.
3
+ *
4
+ * - Normal mode: reads `goldenPath`, splices the produced source-hash line into
5
+ * the golden's `<PLACEHOLDER>` slot, and asserts byte-equality.
6
+ * - Regen mode (`UPDATE_GOLDENS=1`): replaces the produced source-hash line with
7
+ * `<PLACEHOLDER>` and writes the result to `goldenPath`. The next normal run
8
+ * will assert against the new golden.
9
+ *
10
+ * The source-hash placeholder lets us check generated content into source
11
+ * control without coupling the golden to a specific envelope hash. Codegen
12
+ * outputs that don't include a `// Source hash:` line work too — the
13
+ * splice/replace is a no-op when no match is found.
14
+ */
15
+ export declare function assertGoldenOrUpdate(produced: string, goldenPath: string): Promise<void>;
@@ -0,0 +1,30 @@
1
+ import { expect } from 'vitest';
2
+ import { readFile, writeFile } from 'node:fs/promises';
3
+ /**
4
+ * Compares produced source against a golden file, with optional regeneration.
5
+ *
6
+ * - Normal mode: reads `goldenPath`, splices the produced source-hash line into
7
+ * the golden's `<PLACEHOLDER>` slot, and asserts byte-equality.
8
+ * - Regen mode (`UPDATE_GOLDENS=1`): replaces the produced source-hash line with
9
+ * `<PLACEHOLDER>` and writes the result to `goldenPath`. The next normal run
10
+ * will assert against the new golden.
11
+ *
12
+ * The source-hash placeholder lets us check generated content into source
13
+ * control without coupling the golden to a specific envelope hash. Codegen
14
+ * outputs that don't include a `// Source hash:` line work too — the
15
+ * splice/replace is a no-op when no match is found.
16
+ */
17
+ export async function assertGoldenOrUpdate(produced, goldenPath) {
18
+ if (process.env.UPDATE_GOLDENS === '1') {
19
+ const goldenContent = produced.replace(/^\/\/ Source hash: [a-f0-9]+$/m, '// Source hash: <PLACEHOLDER>');
20
+ await writeFile(goldenPath, goldenContent, 'utf-8');
21
+ // eslint-disable-next-line no-console
22
+ console.log(`[golden-test] Wrote golden: ${goldenPath}`);
23
+ return;
24
+ }
25
+ const goldenTemplate = await readFile(goldenPath, 'utf8');
26
+ const sourceHashLine = produced.split('\n').find((l) => l.startsWith('// Source hash:')) ?? '';
27
+ const goldenWithHash = goldenTemplate.replace('// Source hash: <PLACEHOLDER>', sourceHashLine);
28
+ expect(produced).toBe(goldenWithHash);
29
+ }
30
+ //# sourceMappingURL=golden.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"golden.js","sourceRoot":"","sources":["../../../src/codegen/test-helpers/golden.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAA;AAC/B,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAA;AAEtD;;;;;;;;;;;;;GAaG;AACH,MAAM,CAAC,KAAK,UAAU,oBAAoB,CAAC,QAAgB,EAAE,UAAkB;IAC7E,IAAI,OAAO,CAAC,GAAG,CAAC,cAAc,KAAK,GAAG,EAAE,CAAC;QACvC,MAAM,aAAa,GAAG,QAAQ,CAAC,OAAO,CACpC,gCAAgC,EAChC,+BAA+B,CAChC,CAAA;QACD,MAAM,SAAS,CAAC,UAAU,EAAE,aAAa,EAAE,OAAO,CAAC,CAAA;QACnD,sCAAsC;QACtC,OAAO,CAAC,GAAG,CAAC,+BAA+B,UAAU,EAAE,CAAC,CAAA;QACxD,OAAM;IACR,CAAC;IAED,MAAM,cAAc,GAAG,MAAM,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC,CAAA;IACzD,MAAM,cAAc,GAAG,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,UAAU,CAAC,iBAAiB,CAAC,CAAC,IAAI,EAAE,CAAA;IAC9F,MAAM,cAAc,GAAG,cAAc,CAAC,OAAO,CAAC,+BAA+B,EAAE,cAAc,CAAC,CAAA;IAC9F,MAAM,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC,cAAc,CAAC,CAAA;AACvC,CAAC"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,76 @@
1
+ import { describe, expect, it, afterEach } from 'vitest';
2
+ import { mkdtempSync, writeFileSync, readFileSync, rmSync } from 'node:fs';
3
+ import { tmpdir } from 'node:os';
4
+ import { join } from 'node:path';
5
+ import { assertGoldenOrUpdate } from './golden.js';
6
+ let tmpDir;
7
+ afterEach(() => {
8
+ if (tmpDir != null) {
9
+ rmSync(tmpDir, { recursive: true, force: true });
10
+ tmpDir = undefined;
11
+ }
12
+ });
13
+ function makeTmp() {
14
+ tmpDir = mkdtempSync(join(tmpdir(), 'tsp-golden-'));
15
+ return tmpDir;
16
+ }
17
+ describe('assertGoldenOrUpdate', () => {
18
+ it('asserts byte-equality against golden when produced matches (with hash splice)', async () => {
19
+ const dir = makeTmp();
20
+ const goldenPath = join(dir, 'expected.txt');
21
+ writeFileSync(goldenPath, 'hello\n// Source hash: <PLACEHOLDER>\nworld\n', 'utf-8');
22
+ const produced = 'hello\n// Source hash: abc123def456\nworld\n';
23
+ await expect(assertGoldenOrUpdate(produced, goldenPath)).resolves.toBeUndefined();
24
+ });
25
+ it('throws when produced does not match golden', async () => {
26
+ const dir = makeTmp();
27
+ const goldenPath = join(dir, 'expected.txt');
28
+ writeFileSync(goldenPath, 'expected content\n', 'utf-8');
29
+ const produced = 'different content\n';
30
+ await expect(assertGoldenOrUpdate(produced, goldenPath)).rejects.toThrow();
31
+ });
32
+ it('writes a portable golden when UPDATE_GOLDENS=1', async () => {
33
+ const original = process.env.UPDATE_GOLDENS;
34
+ process.env.UPDATE_GOLDENS = '1';
35
+ try {
36
+ const dir = makeTmp();
37
+ const goldenPath = join(dir, 'expected.txt');
38
+ const produced = 'hello\n// Source hash: deadbeef1234\nworld\n';
39
+ await assertGoldenOrUpdate(produced, goldenPath);
40
+ const written = readFileSync(goldenPath, 'utf-8');
41
+ expect(written).toBe('hello\n// Source hash: <PLACEHOLDER>\nworld\n');
42
+ }
43
+ finally {
44
+ if (original === undefined)
45
+ delete process.env.UPDATE_GOLDENS;
46
+ else
47
+ process.env.UPDATE_GOLDENS = original;
48
+ }
49
+ });
50
+ it('handles produced output without a source-hash line', async () => {
51
+ const dir = makeTmp();
52
+ const goldenPath = join(dir, 'expected.txt');
53
+ writeFileSync(goldenPath, 'plain content\n', 'utf-8');
54
+ // No source hash; splice is a no-op on both sides.
55
+ await expect(assertGoldenOrUpdate('plain content\n', goldenPath)).resolves.toBeUndefined();
56
+ });
57
+ it('regenerate mode handles produced output without a source-hash line', async () => {
58
+ const original = process.env.UPDATE_GOLDENS;
59
+ process.env.UPDATE_GOLDENS = '1';
60
+ try {
61
+ const dir = makeTmp();
62
+ const goldenPath = join(dir, 'expected.txt');
63
+ const produced = 'no hash line here\n';
64
+ await assertGoldenOrUpdate(produced, goldenPath);
65
+ const written = readFileSync(goldenPath, 'utf-8');
66
+ expect(written).toBe('no hash line here\n');
67
+ }
68
+ finally {
69
+ if (original === undefined)
70
+ delete process.env.UPDATE_GOLDENS;
71
+ else
72
+ process.env.UPDATE_GOLDENS = original;
73
+ }
74
+ });
75
+ });
76
+ //# sourceMappingURL=golden.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"golden.test.js","sourceRoot":"","sources":["../../../src/codegen/test-helpers/golden.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,175 @@
1
+ # Kotlin Codegen Setup Guide
2
+
3
+ Generated by `ts-procedures-codegen --target kotlin`. One `.kt` file per scope; types are nested under route objects (`Users.GetUser.Response`, `Users.GetUser.Body.Address`).
4
+
5
+ ## Quickstart
6
+
7
+ ```bash
8
+ npx ts-procedures-codegen \
9
+ --target kotlin \
10
+ --kotlin-package com.example.api \
11
+ --url https://api.example.com/_ts-procedures.json \
12
+ --out ./src/main/kotlin/com/example/api
13
+ ```
14
+
15
+ Each scope produces one file (e.g. `Users.kt`). Access generated types as `Users.GetUser.Response`, `Users.GetUser.Body.GuestBody`, `Users.GetUser.Errors.NotFound`.
16
+
17
+ ## Gradle setup
18
+
19
+ The default `--kotlin-serializer kotlinx` mode emits `@Serializable` data classes. Add the kotlinx-serialization plugin and runtime:
20
+
21
+ ```kotlin
22
+ // build.gradle.kts
23
+ plugins {
24
+ kotlin("jvm") version "<your-kotlin-version>"
25
+ kotlin("plugin.serialization") version "<your-kotlin-version>"
26
+ }
27
+
28
+ dependencies {
29
+ implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:<version>")
30
+ }
31
+ ```
32
+
33
+ (`kotlinx-serialization-core` is a transitive dependency of `-json`; you don't need to declare it explicitly.)
34
+
35
+ **JVM only.** Kotlin Multiplatform is not yet supported. If you need KMP-portable types, fall back to `--kotlin-serializer none` or post-process the emitted code.
36
+
37
+ ## Contextual serializers
38
+
39
+ `format: date-time`, `format: uuid`, `format: uri`, `format: date`, `format: time` map to JVM stdlib types (`java.time.Instant`, `java.util.UUID`, etc.) annotated with `@Contextual`. Kotlinx-serialization does not know how to serialize these natively — register contextual serializers in your `Json` configuration:
40
+
41
+ ```kotlin
42
+ import kotlinx.serialization.json.Json
43
+ import kotlinx.serialization.modules.SerializersModule
44
+ import kotlinx.serialization.modules.contextual
45
+
46
+ val json = Json {
47
+ serializersModule = SerializersModule {
48
+ contextual(java.time.Instant::class, InstantSerializer)
49
+ contextual(java.util.UUID::class, UUIDSerializer)
50
+ contextual(java.net.URI::class, URISerializer)
51
+ // ...etc for any format you use
52
+ }
53
+ }
54
+ ```
55
+
56
+ The serializers themselves are your responsibility (we don't ship them — the choice between ISO-8601 strings, epoch milliseconds, etc. is application-specific). Public Gist-quality implementations are widely available; we recommend ISO-8601 to match the server.
57
+
58
+ ## Discriminated unions
59
+
60
+ A schema with a `oneOf` whose variants share a const-valued discriminator (e.g. `kind: "guest" | "registered"`) emits as a sealed interface:
61
+
62
+ ```kotlin
63
+ @Serializable
64
+ @JsonClassDiscriminator("kind")
65
+ sealed interface Body {
66
+ @Serializable
67
+ @SerialName("guest")
68
+ data class GuestBody(val displayName: String) : Body
69
+
70
+ @Serializable
71
+ @SerialName("registered")
72
+ data class RegisteredBody(val email: String, val name: String) : Body
73
+ }
74
+ ```
75
+
76
+ The discriminator field (`kind`) is **erased** from each variant under `--kotlin-serializer kotlinx` — `@SerialName` carries it on the wire. With `--kotlin-serializer none` the discriminator field is retained.
77
+
78
+ `@JsonClassDiscriminator` is read automatically by the kotlinx `Json` instance — no extra config needed.
79
+
80
+ ## JSON-key sanitization
81
+
82
+ Kebab-case and snake-case JSON keys become camelCase property names with `@SerialName` auto-emitted so the wire format stays correct:
83
+
84
+ ```kotlin
85
+ @Serializable
86
+ data class Response(
87
+ @SerialName("created-at")
88
+ @Contextual
89
+ val createdAt: java.time.Instant,
90
+ )
91
+ ```
92
+
93
+ Reserved Kotlin words get a trailing underscore (`class` → `class_`).
94
+
95
+ ## Switching off kotlinx (Moshi, Gson, hand-written)
96
+
97
+ `--kotlin-serializer none` emits plain `data class` types with no `@Serializable` annotation. You're then responsible for adapter setup:
98
+
99
+ - **Reflection-based libraries** (Gson) work without further changes.
100
+ - **Codegen-based** (Moshi with codegen) need their own annotation layer added.
101
+ - Sealed interfaces still emit; the discriminator field is **retained** in variants under `none` mode.
102
+
103
+ ## Error types
104
+
105
+ Each route that declares errors gets a nested `Errors` object containing one `@Serializable data class` per error name:
106
+
107
+ ```kotlin
108
+ object Users {
109
+ object GetUser {
110
+ // ... method, path, types ...
111
+ object Errors {
112
+ @Serializable
113
+ data class NotFound(
114
+ val name: String = "NotFound",
115
+ val message: String,
116
+ )
117
+ }
118
+ }
119
+ }
120
+ ```
121
+
122
+ Access generated error types as `Users.GetUser.Errors.NotFound`.
123
+
124
+ **No runtime dispatch.** Unlike the TypeScript target, the Kotlin target ships **types only** — there is no error registry, no `instanceof`-style lookup, no `dispatchTypedError`. Mobile consumers catch HTTP failures themselves and inspect `body.name` (which is a regular `String` field, not a type-system discriminator) to decide which error data class to deserialize against:
125
+
126
+ ```kotlin
127
+ suspend fun loadUser(id: String): Result<Users.GetUser.Response> {
128
+ val response = httpClient.request(method = Users.GetUser.method, path = "...")
129
+ return when {
130
+ response.status == 200 -> Result.success(json.decodeFromString(response.body))
131
+ response.status == 404 -> {
132
+ val err = json.decodeFromString<Users.GetUser.Errors.NotFound>(response.body)
133
+ Result.failure(NotFoundException(err.message))
134
+ }
135
+ else -> Result.failure(IOException("HTTP ${response.status}"))
136
+ }
137
+ }
138
+ ```
139
+
140
+ Choosing the dispatch strategy (status-code based, body.name based, sealed-class hierarchy of your own, etc.) is intentionally left to consumers.
141
+
142
+ ## Untagged unions
143
+
144
+ ajsc v7.2's Kotlin emitter silently produces an empty `@Serializable data class` for any untagged `oneOf` schema, regardless of whether `--unsupported-unions fallback` is set. There is no "throws on default" mode for Kotlin (that's Swift behavior). The `--unsupported-unions` flag is currently a no-op for the Kotlin target.
145
+
146
+ For example, the schema `oneOf: [{ type: 'string' }, { type: 'integer' }]` with `rootTypeName: 'Mixed'` produces:
147
+
148
+ ```kotlin
149
+ @Serializable
150
+ data class Mixed()
151
+ ```
152
+
153
+ Imports: `kotlinx.serialization.Serializable`.
154
+
155
+ **Practical implication.** This empty data class won't round-trip your data — calling `Json.decodeFromString<Mixed>("\"hello\"")` will fail because `Mixed` has no payload. **Untagged unions in your schema cannot be consumed via the generated Kotlin types.** Workarounds:
156
+
157
+ 1. **Add a discriminator** to your server-side schema so it becomes a tagged union (sealed interface emission — see "Discriminated unions" section above).
158
+ 2. **Hand-write a `KSerializer<T>` `companion object`** in a sibling file that resolves the union at deserialization time.
159
+ 3. **Strip the union field at the codegen boundary** — pre-process the envelope to replace untagged `oneOf` with a single permissive type (e.g. `JsonElement`).
160
+
161
+ We track this as an ajsc upstream limitation; if it's resolved in a future ajsc release, this section will be updated.
162
+
163
+ ## Documented limitations
164
+
165
+ The following ajsc behaviors are intentional and documented; they are **not bugs**:
166
+
167
+ - `additionalProperties: { type: T }` is silently dropped with a `/** Note: schema permits additional keys of type T — not modeled. */` KDoc note. If your contract uses extra keys, add a sibling `Map<String, T>` field by hand or write a custom `KSerializer`.
168
+ - Tuples with 4 or more positional types throw (`Pair`/`Triple` only). Refactor to a struct schema upstream.
169
+ - Schema-level `examples` and `not` / `patternProperties` are not modeled (the latter throw with a path-bearing error).
170
+
171
+ ## Reference
172
+
173
+ - Spec: [`docs/superpowers/specs/2026-04-25-ajsc-v7-kotlin-polish-design.md`](./superpowers/specs/2026-04-25-ajsc-v7-kotlin-polish-design.md)
174
+ - ajsc README: `node_modules/ajsc/README.md` (or [npmjs.com/package/ajsc](https://www.npmjs.com/package/ajsc))
175
+ - ts-procedures-codegen CLI flags: see `CLAUDE.md` (search for "Kotlin target") and the spec linked above. (`--help` is not currently implemented; pass invalid/missing args to see error messages with usage hints.)
@@ -271,6 +271,38 @@ The observer is awaited before the response is sent, and any error it throws is
271
271
 
272
272
  Configuring only the taxonomy, only `onError`, both, or neither are all valid. When neither is configured the builder goes straight from step 1 to step 4.
273
273
 
274
+ ## One Hono Server, Multiple Builders
275
+
276
+ You don't need a separate Hono instance per builder. `HonoRPCAppBuilder`, `HonoAPIAppBuilder`, and `HonoStreamAppBuilder` each accept an optional `app?: Hono` in their config — pass the same instance and they all mount their routes onto the same server:
277
+
278
+ ```typescript
279
+ import { Hono } from 'hono'
280
+ import { HonoRPCAppBuilder } from 'ts-procedures/hono-rpc'
281
+ import { HonoAPIAppBuilder } from 'ts-procedures/hono-api'
282
+ import { HonoStreamAppBuilder } from 'ts-procedures/hono-stream'
283
+ import { DocRegistry } from 'ts-procedures/http-docs'
284
+
285
+ const app = new Hono()
286
+
287
+ const rpcBuilder = new HonoRPCAppBuilder({ app, pathPrefix: '/rpc' })
288
+ .register(RPC, ctxResolver)
289
+ const apiBuilder = new HonoAPIAppBuilder({ app, pathPrefix: '/api' })
290
+ .register(API, ctxResolver)
291
+ const streamBuilder = new HonoStreamAppBuilder({ app })
292
+ .register(Stream, ctxResolver)
293
+
294
+ rpcBuilder.build()
295
+ apiBuilder.build()
296
+ streamBuilder.build()
297
+
298
+ const docs = new DocRegistry().from(rpcBuilder).from(apiBuilder).from(streamBuilder)
299
+ app.get('/docs', (c) => c.json(docs.toJSON()))
300
+ ```
301
+
302
+ One server, one Hono instance, one `/docs` endpoint. The three builders are **registration scopes**, not separate servers — each owns the routes it mounts (RPC vs API vs Stream) but all of them coexist on the same listener. Mix in custom middleware (`app.use(...)`), health checks, or static routes on the shared `app` before or after `.build()`.
303
+
304
+ If you omit `app`, each builder constructs its own internal `Hono` instance — useful when you really do want isolated apps (multi-tenant routing, separate ports, etc.).
305
+
274
306
  ## DocRegistry — Composing Docs from Multiple Builders
275
307
 
276
308
  Use `DocRegistry` to compose route documentation from any combination of HTTP builders into a typed envelope: