ts-procedures 8.2.1 → 8.4.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 (146) hide show
  1. package/agent_config/claude-code/skills/ts-procedures/SKILL.md +31 -9
  2. package/agent_config/claude-code/skills/ts-procedures/api-reference.md +3 -1
  3. package/agent_config/claude-code/skills/ts-procedures/patterns.md +30 -6
  4. package/agent_config/claude-code/skills/ts-procedures/templates/client.md +3 -3
  5. package/agent_config/claude-code/skills/ts-procedures/templates/hono.md +3 -3
  6. package/agent_config/claude-code/skills/ts-procedures/templates/procedure.md +3 -3
  7. package/agent_config/claude-code/skills/ts-procedures/templates/stream-procedure.md +3 -3
  8. package/agent_config/copilot/copilot-instructions.md +10 -6
  9. package/agent_config/cursor/cursorrules +10 -6
  10. package/build/client/call.js +1 -1
  11. package/build/client/call.js.map +1 -1
  12. package/build/client/index.d.ts +1 -1
  13. package/build/client/index.js +23 -1
  14. package/build/client/index.js.map +1 -1
  15. package/build/client/index.test.js +87 -0
  16. package/build/client/index.test.js.map +1 -1
  17. package/build/client/resolve-options.d.ts +5 -4
  18. package/build/client/resolve-options.js +18 -7
  19. package/build/client/resolve-options.js.map +1 -1
  20. package/build/client/resolve-options.test.js +53 -24
  21. package/build/client/resolve-options.test.js.map +1 -1
  22. package/build/client/stream.js +1 -1
  23. package/build/client/stream.js.map +1 -1
  24. package/build/client/types.d.ts +31 -3
  25. package/build/codegen/__fixtures__/make-envelope.d.ts +41 -0
  26. package/build/codegen/__fixtures__/make-envelope.js +38 -0
  27. package/build/codegen/__fixtures__/make-envelope.js.map +1 -0
  28. package/build/codegen/bin/cli.d.ts +11 -0
  29. package/build/codegen/bin/cli.js +30 -21
  30. package/build/codegen/bin/cli.js.map +1 -1
  31. package/build/codegen/bin/cli.test.js +36 -1
  32. package/build/codegen/bin/cli.test.js.map +1 -1
  33. package/build/codegen/bin/flag-specs.d.ts +10 -0
  34. package/build/codegen/bin/flag-specs.js +60 -0
  35. package/build/codegen/bin/flag-specs.js.map +1 -0
  36. package/build/codegen/bin/flag-specs.test.d.ts +1 -0
  37. package/build/codegen/bin/flag-specs.test.js +26 -0
  38. package/build/codegen/bin/flag-specs.test.js.map +1 -0
  39. package/build/codegen/collect-models.d.ts +37 -0
  40. package/build/codegen/collect-models.js +74 -0
  41. package/build/codegen/collect-models.js.map +1 -0
  42. package/build/codegen/collect-models.test.d.ts +1 -0
  43. package/build/codegen/collect-models.test.js +40 -0
  44. package/build/codegen/collect-models.test.js.map +1 -0
  45. package/build/codegen/emit-client-runtime.js +1 -0
  46. package/build/codegen/emit-client-runtime.js.map +1 -1
  47. package/build/codegen/emit-errors.integration.test.js +22 -0
  48. package/build/codegen/emit-errors.integration.test.js.map +1 -1
  49. package/build/codegen/emit-models.d.ts +26 -0
  50. package/build/codegen/emit-models.js +53 -0
  51. package/build/codegen/emit-models.js.map +1 -0
  52. package/build/codegen/emit-models.test.d.ts +1 -0
  53. package/build/codegen/emit-models.test.js +42 -0
  54. package/build/codegen/emit-models.test.js.map +1 -0
  55. package/build/codegen/emit-scope.d.ts +10 -0
  56. package/build/codegen/emit-scope.js +119 -34
  57. package/build/codegen/emit-scope.js.map +1 -1
  58. package/build/codegen/emit-types.d.ts +26 -1
  59. package/build/codegen/emit-types.js +27 -5
  60. package/build/codegen/emit-types.js.map +1 -1
  61. package/build/codegen/index.d.ts +5 -0
  62. package/build/codegen/index.js +2 -0
  63. package/build/codegen/index.js.map +1 -1
  64. package/build/codegen/model-refs.d.ts +27 -0
  65. package/build/codegen/model-refs.js +49 -0
  66. package/build/codegen/model-refs.js.map +1 -0
  67. package/build/codegen/model-refs.test.d.ts +1 -0
  68. package/build/codegen/model-refs.test.js +33 -0
  69. package/build/codegen/model-refs.test.js.map +1 -0
  70. package/build/codegen/pipeline.d.ts +3 -0
  71. package/build/codegen/pipeline.js +3 -1
  72. package/build/codegen/pipeline.js.map +1 -1
  73. package/build/codegen/schema-walk.d.ts +13 -0
  74. package/build/codegen/schema-walk.js +26 -0
  75. package/build/codegen/schema-walk.js.map +1 -0
  76. package/build/codegen/schema-walk.test.d.ts +1 -0
  77. package/build/codegen/schema-walk.test.js +35 -0
  78. package/build/codegen/schema-walk.test.js.map +1 -0
  79. package/build/codegen/targets/_shared/target-run.d.ts +5 -0
  80. package/build/codegen/targets/ts/run.js +28 -1
  81. package/build/codegen/targets/ts/run.js.map +1 -1
  82. package/build/codegen/targets/ts/shared-models.test.d.ts +1 -0
  83. package/build/codegen/targets/ts/shared-models.test.js +258 -0
  84. package/build/codegen/targets/ts/shared-models.test.js.map +1 -0
  85. package/build/doc-envelope.d.ts +13 -0
  86. package/build/doc-envelope.js +23 -0
  87. package/build/doc-envelope.js.map +1 -0
  88. package/build/doc-envelope.test.d.ts +1 -0
  89. package/build/doc-envelope.test.js +31 -0
  90. package/build/doc-envelope.test.js.map +1 -0
  91. package/build/exports.d.ts +2 -0
  92. package/build/exports.js +1 -0
  93. package/build/exports.js.map +1 -1
  94. package/build/implementations/http/error-taxonomy.d.ts +40 -0
  95. package/build/implementations/http/error-taxonomy.js +57 -5
  96. package/build/implementations/http/error-taxonomy.js.map +1 -1
  97. package/build/implementations/http/error-taxonomy.test.js +95 -1
  98. package/build/implementations/http/error-taxonomy.test.js.map +1 -1
  99. package/build/implementations/http/hono/handlers/http.js +19 -24
  100. package/build/implementations/http/hono/handlers/http.js.map +1 -1
  101. package/build/implementations/http/hono/handlers/http.test.js +64 -1
  102. package/build/implementations/http/hono/handlers/http.test.js.map +1 -1
  103. package/docs/client-and-codegen.md +109 -0
  104. package/docs/core.md +2 -0
  105. package/docs/handoffs/ajsc-named-type-collision.md +134 -0
  106. package/docs/handoffs/ajsc-named-type-support.md +181 -0
  107. package/docs/http-integrations.md +4 -0
  108. package/docs/superpowers/plans/2026-06-05-dx-feedback-round.md +1292 -0
  109. package/docs/superpowers/specs/2026-06-05-dx-feedback-round-design.md +285 -0
  110. package/package.json +2 -2
  111. package/src/client/call.ts +1 -1
  112. package/src/client/index.test.ts +98 -0
  113. package/src/client/index.ts +32 -1
  114. package/src/client/resolve-options.test.ts +73 -26
  115. package/src/client/resolve-options.ts +23 -9
  116. package/src/client/stream.ts +1 -1
  117. package/src/client/types.ts +34 -3
  118. package/src/codegen/__fixtures__/make-envelope.ts +89 -0
  119. package/src/codegen/bin/cli.test.ts +38 -1
  120. package/src/codegen/bin/cli.ts +33 -22
  121. package/src/codegen/bin/flag-specs.test.ts +27 -0
  122. package/src/codegen/bin/flag-specs.ts +69 -0
  123. package/src/codegen/collect-models.test.ts +46 -0
  124. package/src/codegen/collect-models.ts +108 -0
  125. package/src/codegen/emit-client-runtime.ts +1 -0
  126. package/src/codegen/emit-errors.integration.test.ts +26 -0
  127. package/src/codegen/emit-models.test.ts +48 -0
  128. package/src/codegen/emit-models.ts +63 -0
  129. package/src/codegen/emit-scope.ts +145 -33
  130. package/src/codegen/emit-types.ts +48 -7
  131. package/src/codegen/index.ts +7 -0
  132. package/src/codegen/model-refs.test.ts +37 -0
  133. package/src/codegen/model-refs.ts +57 -0
  134. package/src/codegen/pipeline.ts +6 -1
  135. package/src/codegen/schema-walk.test.ts +37 -0
  136. package/src/codegen/schema-walk.ts +23 -0
  137. package/src/codegen/targets/_shared/target-run.ts +5 -0
  138. package/src/codegen/targets/ts/run.ts +33 -0
  139. package/src/codegen/targets/ts/shared-models.test.ts +283 -0
  140. package/src/doc-envelope.test.ts +35 -0
  141. package/src/doc-envelope.ts +30 -0
  142. package/src/exports.ts +2 -0
  143. package/src/implementations/http/error-taxonomy.test.ts +111 -0
  144. package/src/implementations/http/error-taxonomy.ts +60 -5
  145. package/src/implementations/http/hono/handlers/http.test.ts +69 -1
  146. package/src/implementations/http/hono/handlers/http.ts +19 -21
@@ -0,0 +1,181 @@
1
+ # Handoff: named-type / verbatim-type support in ajsc
2
+
3
+ > **STATUS: Shipped in ajsc 7.3.0 and adopted in ts-procedures (this cutover).** The placeholder-token workaround described below has been deleted; `substituteModelRefs` now emits `{ 'x-named-type': '<Name>' }` and the codegen reads the converter's `referencedNamedTypes`. The rest of this doc is retained for historical context.
4
+
5
+ **To:** ajsc maintainer
6
+ **From:** ts-procedures codegen (consumer of ajsc as the JSON-Schema → TS/Kotlin/Swift emitter)
7
+ **Date:** 2026-06-05
8
+ **ajsc version inspected:** 7.2.0
9
+ **Priority:** medium — unblocks removing a workaround in ts-procedures, no current breakage
10
+
11
+ ---
12
+
13
+ ## TL;DR
14
+
15
+ ts-procedures needs a way to tell ajsc: *"this subschema is an already-defined named type called `Message` — emit a reference to it (`Message`), don't convert/inline it, and tell me you referenced it so I can add an import."*
16
+
17
+ ajsc 7.2.0 has no such mechanism: a `$ref` to a `$defs` entry is **inlined and re-extracted under the property name** (with no dedup), and there is no verbatim-type escape hatch. We've worked around this downstream with a placeholder-token hack (described below) that we'd like to delete once ajsc supports this directly.
18
+
19
+ The ask is small and **additive** (a new opt-in schema keyword + one new field on the emit result). Nothing changes when the keyword is absent.
20
+
21
+ ---
22
+
23
+ ## Why we need it
24
+
25
+ ts-procedures generates a typed API client from a server's schema. The same domain entity (e.g. `Message`) appears in many route schemas. Today ajsc inlines a fresh structural literal at every site, so `Message` is emitted ~4–6 times under different names in one file, disconnected from each other and from the author's `Message` type. We want **one** `Message` type that every route references.
26
+
27
+ We already know the identity: each such schema carries a stable `$id` (e.g. `urn:msg`) and a `title` (`Message`). So at codegen time we hoist every `$id`-bearing schema into a shared `_models.ts` (emitted via `ajsc.emitTypescript(messageSchema, { rootTypeName: 'Message' })` — which works great), and we want each *route* schema to **reference** `Message` rather than inline it.
28
+
29
+ ## What ajsc 7.2.0 does today (the blocker)
30
+
31
+ Based on inspecting the installed `dist/`:
32
+
33
+ 1. **`$ref` is inlined, not referenced.** The IR layer (`ir/JSONSchemaConverter`) resolves `$ref` against `$defs`/`definitions` by inlining, and rejects non-local refs (`"Only local references are supported"`). With `inlineTypes: false`, the inlined object is then re-extracted **named after the property** — so a schema with `author`, `lastReply`, and `all` all `$ref`-ing the same `Message` emits three duplicate types `Author`, `LastReply`, `All`, with **no deduplication** and no way to make them one `Message`.
34
+
35
+ ```jsonc
36
+ // input
37
+ { "type": "object", "properties": {
38
+ "author": { "$ref": "#/$defs/Message" },
39
+ "lastReply": { "$ref": "#/$defs/Message" } },
40
+ "$defs": { "Message": { "type": "object", "title": "Message",
41
+ "properties": { "id": { "type": "string" } } } } }
42
+ ```
43
+ ```ts
44
+ // ajsc 7.2.0 output (inlineTypes:false) — duplicated, property-named, no dedup
45
+ export type Author = { id?: string };
46
+ export type LastReply = { id?: string };
47
+ export type Root = { author?: Author; lastReply?: LastReply };
48
+ ```
49
+
50
+ 2. **No verbatim-type escape hatch.** Grepping `dist/` for `tsType | x-ts | verbatim | existingType | customType | rawType` returns nothing. We tried `{ tsType: 'Message' }`, `{ "x-tsType": 'Message' }`, `{ $ref: 'Message' }` (bare), `{ type:'object', tsType:'Message' }` — all are ignored (emit `any` / `{ [key:string]: unknown }`) or throw.
51
+
52
+ 3. **`EmitResult.imports` is empty for TypeScript.** The function-form `emitTypescript(schema, opts)` returns `{ code, rootTypeName, extractedTypeNames, imports }`, but `imports` is documented (README ~line 110) as Kotlin/Swift-only — there's no channel to surface "this TS output references external type `Message`."
53
+
54
+ ### Our current workaround (what we want to delete)
55
+
56
+ Because ajsc can't reference a named type, ts-procedures smuggles a sentinel through ajsc and scrapes it back out:
57
+
58
+ 1. Before calling ajsc, replace each `$id`-bearing subschema with `{ const: '__MODELREF__Message__' }`. ajsc emits that verbatim as a string-literal type (`author?: "__MODELREF__Message__"`), and — usefully — never extracts it as a sub-type, and survives inside `Array<…>`.
59
+ 2. After ajsc, a global regex `/["']__MODELREF__([A-Za-z_$][\w$]*)__["']/g` rewrites the tokens to bare names (`author?: Message`) and collects the referenced names so we can emit `import type { Message } from './_models'`.
60
+
61
+ It works and is well-tested, but it encodes a *type identity* as schema *data*, runs it through a code generator, and recovers it from generated *text* with a regex. We'd much rather express the intent directly to ajsc.
62
+
63
+ ---
64
+
65
+ ## Requested feature
66
+
67
+ A new **opt-in schema keyword** that marks a subschema as an already-defined named type. When present, ajsc:
68
+
69
+ 1. emits a **reference** to that name (does not convert/inline the subschema, does not extract a sub-type), and
70
+ 2. **reports** the referenced name on the emit result so the caller can wire an import.
71
+
72
+ ### Keyword name & shape
73
+
74
+ ajsc is multi-target (TS/Kotlin/Swift), so prefer a target-agnostic keyword over a TS-specific one. Suggested:
75
+
76
+ ```jsonc
77
+ { "x-named-type": "Message" }
78
+ ```
79
+
80
+ `x-`-prefixed so it's unambiguously an annotation and ignored by standard JSON-Schema validators. (If you'd rather not use `x-`, `namedType` or `typeRef` are fine — your call on convention.)
81
+
82
+ - **Value = string:** the identifier emitted verbatim in **every** target (`Message` in TS, Kotlin, and Swift). This matches our usage — we use the same PascalCase name across targets.
83
+ - **Optional future extension:** allow an object for per-language names, e.g. `{ "x-named-type": { "ts": "Message", "kotlin": "Message", "swift": "Message" } }`. Not needed by us now; mentioning so the string form can widen later without a breaking change.
84
+
85
+ ### Semantics
86
+
87
+ - The keyword **short-circuits conversion** of that subschema node: emit the bare identifier as the type, in all the positions ajsc already handles a leaf type (direct property, `Array<…>` items, union member, map value, etc.).
88
+ - With `inlineTypes: false`, the named type is **not** added to `extractedTypeNames` (it's external — defined elsewhere).
89
+ - The node is **not** recursed into (its `properties`/`items` are irrelevant once it's a named reference). A `type`/`properties` sitting alongside `x-named-type` should be ignored (or, if you prefer strictness, validated to match — but ignore is simpler and matches our need; we strip the body anyway).
90
+ - **Absent keyword ⇒ zero behaviour change.** Fully backward compatible.
91
+
92
+ ### Reporting referenced names
93
+
94
+ Add referenced external names to the emit result so the caller can build imports. Two acceptable shapes:
95
+
96
+ - **Preferred:** a new field `referencedNamedTypes: string[]` on `EmitResult` (deduped, sorted or insertion-order — either is fine; we sort downstream).
97
+ - **Or:** populate the existing (currently-empty-for-TS) `imports` field with the bare names.
98
+
99
+ A new dedicated field is cleaner than overloading `imports` (which means module paths for Kotlin/Swift).
100
+
101
+ ### Worked example
102
+
103
+ ```jsonc
104
+ // input
105
+ { "type": "object", "properties": {
106
+ "author": { "x-named-type": "Message" },
107
+ "replies": { "type": "array", "items": { "x-named-type": "Message" } },
108
+ "id": { "type": "string" } } }
109
+ ```
110
+
111
+ ```ts
112
+ // desired TS output (inlineTypes:false)
113
+ export type Root = { author?: Message; replies?: Array<Message>; id?: string };
114
+ // emitResult.referencedNamedTypes === ['Message'] // Root is NOT polluted with a Message decl
115
+ ```
116
+
117
+ ```kotlin
118
+ // desired Kotlin output
119
+ data class Root(val author: Message? = null, val replies: List<Message>? = null, val id: String? = null)
120
+ // referencedNamedTypes === ['Message']
121
+ ```
122
+
123
+ ```swift
124
+ // desired Swift output
125
+ struct Root: Codable { let author: Message?; let replies: [Message]?; let id: String? }
126
+ // referencedNamedTypes === ['Message']
127
+ ```
128
+
129
+ In every case ajsc does **not** define `Message` — the caller (ts-procedures) defines it once elsewhere and adds the import/reference.
130
+
131
+ ---
132
+
133
+ ## Where this likely lives in ajsc
134
+
135
+ From the `dist/` surface (you know the real source layout):
136
+
137
+ - **IR conversion** (`ir/JSONSchemaConverter` or equivalent): detect `x-named-type` on a node early and lower it to a new IR node kind — e.g. `NamedTypeRef(name)` — instead of converting the object. This is the one place that needs to intercept before the existing `$ref`-inline / object-extract logic runs.
138
+ - **Each language emitter** (`typescript/`, `kotlin/`, `swift/`): render `NamedTypeRef(name)` as the bare identifier wherever a leaf type is rendered, and **collect** the name into the result.
139
+ - **`EmitResult` type** (`index.d.ts`): add `referencedNamedTypes?: string[]` (or document `imports` carrying them for TS).
140
+ - **`BaseConverterOpts`:** no new option needed — the keyword lives in the schema, not the options. (You *could* add a `validateNamedTypes` strictness flag later; not required.)
141
+
142
+ ---
143
+
144
+ ## Tests worth adding (mirror our spike findings)
145
+
146
+ 1. `x-named-type` on a direct property ⇒ `prop?: Message`, `referencedNamedTypes` includes `Message`, no `Message` extracted.
147
+ 2. Inside `array.items` ⇒ `Array<Message>` (TS) / `List<Message>` (Kotlin) / `[Message]` (Swift).
148
+ 3. Same name referenced by N properties ⇒ deduped to a single `Message` reference, `referencedNamedTypes === ['Message']` (the dedup we can't get today).
149
+ 4. As a whole root schema (`{ "x-named-type": "Message" }` at top level) ⇒ output is just `Message` (our route-response-is-exactly-a-model case).
150
+ 5. Both `inlineTypes: true` and `inlineTypes: false` behave identically for the referenced name (it's a leaf either way).
151
+ 6. Absent keyword ⇒ byte-identical to current output (regression guard).
152
+ 7. Coexists with genuine sibling extraction: a schema with one `x-named-type` property and one ordinary nested object still extracts the ordinary object normally under `inlineTypes:false`.
153
+
154
+ ---
155
+
156
+ ## How ts-procedures will consume it (integration contract)
157
+
158
+ So you can sanity-check the design against the real caller:
159
+
160
+ 1. We already collect every `$id`-bearing subschema into a registry `{ $id, name, schema }` and emit each one standalone via `emitTypescript(schema, { rootTypeName: name })` into `_models.ts`. **No change needed there.**
161
+ 2. For each *route* schema, instead of our placeholder-token substitution, we'll walk the schema and replace each `$id`-bearing subschema with `{ "x-named-type": <name> }` before calling ajsc.
162
+ 3. We read `emitResult.referencedNamedTypes` to emit `import type { Message, … } from './_models'` per scope file.
163
+ 4. We delete `model-refs.ts` (the placeholder/regex module) entirely and the two wrapper functions in `emit-scope.ts`.
164
+
165
+ The contract we depend on: **(a)** a referenced `x-named-type` is never inlined and never extracted, and **(b)** its name is reported back. Those two guarantees are the whole feature.
166
+
167
+ ---
168
+
169
+ ## Versioning
170
+
171
+ Additive and opt-in ⇒ a **minor** bump (e.g. 7.3.0). No migration for existing consumers. ts-procedures will gate on the ajsc version that introduces `referencedNamedTypes` and keep the placeholder-token path as a fallback for one release if you'd like a soft cutover, or hard-switch if you prefer.
172
+
173
+ ---
174
+
175
+ ## Contact / context
176
+
177
+ The placeholder-token workaround, the design rationale, and the empirical ajsc probes live in the ts-procedures repo:
178
+ - `src/codegen/model-refs.ts` — the current substitution we want to remove
179
+ - `src/codegen/emit-models.ts`, `collect-models.ts` — the model collection/emission (unaffected)
180
+ - `docs/superpowers/specs/2026-06-05-dx-feedback-round-design.md` — design (see finding #3)
181
+ - commit `483185e` — the spike that established ajsc's inlining behaviour and chose the workaround
@@ -134,6 +134,10 @@ new HonoAppBuilder({
134
134
 
135
135
  For streaming procedures the taxonomy covers the pre-stream path only; mid-stream errors are handled via `stream.onMidStreamError`.
136
136
 
137
+ > **Typed client classes — when you get them for free.** A taxonomy entry declared with just `{ class, statusCode }` is self-describing: its default body is `{ name, message }`, so codegen emits a typed client error class and registry entry automatically — `catch (e) { if (e instanceof ApiErrors.NotFound) ... }` works with zero extra ceremony. The framework can only do this when it knows the wire shape. Two cases where it can't, and the client falls back to the untyped `ClientHttpError` until you help it:
138
+ > - **Custom `toResponse` without a `schema`.** Once you shape the body yourself, the framework won't guess it. Add an explicit `schema` matching your `toResponse` to restore the typed class.
139
+ > - **Raw `ErrorDoc`s** added via `DocRegistry.documentError(...)` or a `config.errors` array (rather than a taxonomy). These carry no body contract — give them a `schema` to make them typed on the client.
140
+
137
141
  ### Imperative — the `onError` callback
138
142
 
139
143
  For apps that don't need typed client dispatch or declarative docs, configure `onError` directly and handle every error in one place: