ts-procedures 8.6.0 → 9.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 (630) hide show
  1. package/CHANGELOG.md +540 -0
  2. package/README.md +166 -101
  3. package/agent_config/claude-code/.claude-plugin/plugin.json +1 -1
  4. package/agent_config/claude-code/agents/ts-procedures-architect.md +11 -10
  5. package/agent_config/claude-code/skills/ts-procedures/SKILL.md +25 -12
  6. package/agent_config/claude-code/skills/ts-procedures/anti-patterns.md +10 -12
  7. package/agent_config/claude-code/skills/ts-procedures/api-reference.md +141 -45
  8. package/agent_config/claude-code/skills/ts-procedures/checklist.md +7 -6
  9. package/agent_config/claude-code/skills/ts-procedures/patterns.md +45 -6
  10. package/agent_config/claude-code/skills/ts-procedures/templates/client.md +1 -1
  11. package/agent_config/claude-code/skills/ts-procedures/templates/hono.md +1 -1
  12. package/agent_config/copilot/copilot-instructions.md +50 -33
  13. package/agent_config/cursor/cursorrules +50 -33
  14. package/build/adapters/astro/astro-context.js.map +1 -0
  15. package/build/adapters/astro/create-handler.js.map +1 -0
  16. package/build/adapters/astro/index.js.map +1 -0
  17. package/build/{implementations/http → adapters}/astro/index.test.js +1 -1
  18. package/build/adapters/astro/index.test.js.map +1 -0
  19. package/build/adapters/astro/rewrite-request.js.map +1 -0
  20. package/build/adapters/hono/envelope-parity.test.js +98 -0
  21. package/build/adapters/hono/envelope-parity.test.js.map +1 -0
  22. package/build/{implementations/http → adapters}/hono/handlers/http-stream.d.ts +1 -1
  23. package/build/adapters/hono/handlers/http-stream.js +55 -0
  24. package/build/adapters/hono/handlers/http-stream.js.map +1 -0
  25. package/build/{implementations/http → adapters}/hono/handlers/http-stream.test.js +1 -1
  26. package/build/adapters/hono/handlers/http-stream.test.js.map +1 -0
  27. package/build/{implementations/http → adapters}/hono/handlers/http.d.ts +1 -1
  28. package/build/adapters/hono/handlers/http.js +50 -0
  29. package/build/adapters/hono/handlers/http.js.map +1 -0
  30. package/build/{implementations/http → adapters}/hono/handlers/http.test.js +1 -1
  31. package/build/adapters/hono/handlers/http.test.js.map +1 -0
  32. package/build/{implementations/http → adapters}/hono/handlers/rpc.d.ts +2 -2
  33. package/build/adapters/hono/handlers/rpc.js +23 -0
  34. package/build/adapters/hono/handlers/rpc.js.map +1 -0
  35. package/build/{implementations/http → adapters}/hono/handlers/rpc.test.js +1 -1
  36. package/build/adapters/hono/handlers/rpc.test.js.map +1 -0
  37. package/build/adapters/hono/handlers/stream.d.ts +12 -0
  38. package/build/adapters/hono/handlers/stream.js +89 -0
  39. package/build/adapters/hono/handlers/stream.js.map +1 -0
  40. package/build/{implementations/http → adapters}/hono/handlers/stream.test.js +3 -2
  41. package/build/adapters/hono/handlers/stream.test.js.map +1 -0
  42. package/build/{implementations/http → adapters}/hono/index.d.ts +24 -12
  43. package/build/{implementations/http → adapters}/hono/index.js +19 -8
  44. package/build/adapters/hono/index.js.map +1 -0
  45. package/build/{implementations/http → adapters}/hono/index.test.js +2 -4
  46. package/build/adapters/hono/index.test.js.map +1 -0
  47. package/build/{implementations/http → adapters/hono}/on-request-error.test.js +2 -2
  48. package/build/adapters/hono/on-request-error.test.js.map +1 -0
  49. package/build/adapters/hono/request.d.ts +7 -0
  50. package/build/adapters/hono/request.js +22 -0
  51. package/build/adapters/hono/request.js.map +1 -0
  52. package/build/{implementations/http → adapters/hono}/route-errors.test.js +4 -4
  53. package/build/adapters/hono/route-errors.test.js.map +1 -0
  54. package/build/adapters/hono/types.d.ts +55 -0
  55. package/build/adapters/hono/types.js +19 -0
  56. package/build/adapters/hono/types.js.map +1 -0
  57. package/build/client/freeze.test.js +39 -0
  58. package/build/client/freeze.test.js.map +1 -0
  59. package/build/client/typed-error-dispatch.test.js +2 -2
  60. package/build/client/typed-error-dispatch.test.js.map +1 -1
  61. package/build/codegen/__fixtures__/make-envelope.d.ts +1 -1
  62. package/build/codegen/bin/cli.d.ts +5 -0
  63. package/build/codegen/bin/cli.js +139 -182
  64. package/build/codegen/bin/cli.js.map +1 -1
  65. package/build/codegen/bin/cli.test.js +12 -2
  66. package/build/codegen/bin/cli.test.js.map +1 -1
  67. package/build/codegen/bin/flag-specs.d.ts +9 -0
  68. package/build/codegen/bin/flag-specs.js +33 -31
  69. package/build/codegen/bin/flag-specs.js.map +1 -1
  70. package/build/codegen/bin/flag-specs.test.js +14 -1
  71. package/build/codegen/bin/flag-specs.test.js.map +1 -1
  72. package/build/codegen/collect-models.d.ts +1 -1
  73. package/build/codegen/emit/api-route.d.ts +8 -0
  74. package/build/codegen/emit/api-route.js +156 -0
  75. package/build/codegen/emit/api-route.js.map +1 -0
  76. package/build/codegen/emit/context.d.ts +30 -0
  77. package/build/codegen/emit/context.js +2 -0
  78. package/build/codegen/emit/context.js.map +1 -0
  79. package/build/codegen/emit/declarations.d.ts +24 -0
  80. package/build/codegen/emit/declarations.js +48 -0
  81. package/build/codegen/emit/declarations.js.map +1 -0
  82. package/build/codegen/emit/format-types.d.ts +61 -0
  83. package/build/codegen/emit/format-types.js +188 -0
  84. package/build/codegen/emit/format-types.js.map +1 -0
  85. package/build/codegen/emit/http-stream-route.d.ts +7 -0
  86. package/build/codegen/emit/http-stream-route.js +138 -0
  87. package/build/codegen/emit/http-stream-route.js.map +1 -0
  88. package/build/codegen/emit/route-shared.d.ts +37 -0
  89. package/build/codegen/emit/route-shared.js +88 -0
  90. package/build/codegen/emit/route-shared.js.map +1 -0
  91. package/build/codegen/emit/rpc-route.d.ts +7 -0
  92. package/build/codegen/emit/rpc-route.js +37 -0
  93. package/build/codegen/emit/rpc-route.js.map +1 -0
  94. package/build/codegen/emit/scope-file.d.ts +39 -0
  95. package/build/codegen/emit/scope-file.js +166 -0
  96. package/build/codegen/emit/scope-file.js.map +1 -0
  97. package/build/codegen/emit/stream-route.d.ts +7 -0
  98. package/build/codegen/emit/stream-route.js +62 -0
  99. package/build/codegen/emit/stream-route.js.map +1 -0
  100. package/build/codegen/emit-errors.d.ts +1 -1
  101. package/build/codegen/emit-errors.integration.test.js +1 -1
  102. package/build/codegen/emit-errors.integration.test.js.map +1 -1
  103. package/build/codegen/emit-scope.d.ts +13 -30
  104. package/build/codegen/emit-scope.js +15 -844
  105. package/build/codegen/emit-scope.js.map +1 -1
  106. package/build/codegen/emit-scope.test.js +67 -0
  107. package/build/codegen/emit-scope.test.js.map +1 -1
  108. package/build/codegen/goldens.test.js +69 -0
  109. package/build/codegen/goldens.test.js.map +1 -0
  110. package/build/codegen/group-routes.d.ts +1 -1
  111. package/build/codegen/pipeline.d.ts +1 -1
  112. package/build/codegen/resolve-envelope.d.ts +1 -1
  113. package/build/codegen/targets/_shared/error-schemas.d.ts +1 -1
  114. package/build/codegen/targets/_shared/route-slots.d.ts +1 -1
  115. package/build/codegen/targets/_shared/target-run.d.ts +1 -1
  116. package/build/codegen/targets/kotlin/emit-route-kotlin.d.ts +1 -1
  117. package/build/codegen/targets/swift/emit-route-swift.d.ts +1 -1
  118. package/build/core/create-http-stream.d.ts +50 -0
  119. package/build/core/create-http-stream.js +108 -0
  120. package/build/core/create-http-stream.js.map +1 -0
  121. package/build/{create-http-stream.test.js → core/create-http-stream.test.js} +1 -1
  122. package/build/core/create-http-stream.test.js.map +1 -0
  123. package/build/core/create-http.d.ts +51 -0
  124. package/build/core/create-http.js +65 -0
  125. package/build/core/create-http.js.map +1 -0
  126. package/build/{create-http.test.js → core/create-http.test.js} +27 -4
  127. package/build/core/create-http.test.js.map +1 -0
  128. package/build/core/create-stream.d.ts +26 -0
  129. package/build/core/create-stream.js +80 -0
  130. package/build/core/create-stream.js.map +1 -0
  131. package/build/{create-stream.test.js → core/create-stream.test.js} +23 -28
  132. package/build/core/create-stream.test.js.map +1 -0
  133. package/build/core/create.d.ts +22 -0
  134. package/build/core/create.js +71 -0
  135. package/build/core/create.js.map +1 -0
  136. package/build/{create.test.js → core/create.test.js} +25 -46
  137. package/build/core/create.test.js.map +1 -0
  138. package/build/core/definition-site.d.ts +24 -0
  139. package/build/{stack-utils.js → core/definition-site.js} +20 -20
  140. package/build/core/definition-site.js.map +1 -0
  141. package/build/{stack-utils.test.js → core/definition-site.test.js} +12 -3
  142. package/build/core/definition-site.test.js.map +1 -0
  143. package/build/{errors.d.ts → core/errors.d.ts} +19 -8
  144. package/build/{errors.js → core/errors.js} +21 -26
  145. package/build/core/errors.js.map +1 -0
  146. package/build/core/errors.test.js.map +1 -0
  147. package/build/core/factory-options.test.js +82 -0
  148. package/build/core/factory-options.test.js.map +1 -0
  149. package/build/core/http-route.d.ts +13 -0
  150. package/build/core/http-route.js +54 -0
  151. package/build/core/http-route.js.map +1 -0
  152. package/build/core/internal.d.ts +72 -0
  153. package/build/core/internal.js +128 -0
  154. package/build/core/internal.js.map +1 -0
  155. package/build/{migration.test.js → core/migration.test.js} +17 -1
  156. package/build/core/migration.test.js.map +1 -0
  157. package/build/core/procedures.d.ts +143 -0
  158. package/build/core/procedures.js +64 -0
  159. package/build/core/procedures.js.map +1 -0
  160. package/build/{index.test.js → core/procedures.test.js} +14 -11
  161. package/build/core/procedures.test.js.map +1 -0
  162. package/build/core/types.d.ts +183 -0
  163. package/build/{schema → core}/types.js.map +1 -1
  164. package/build/exports.d.ts +31 -11
  165. package/build/exports.js +23 -8
  166. package/build/exports.js.map +1 -1
  167. package/build/schema/adapter.d.ts +35 -0
  168. package/build/schema/adapter.js +13 -0
  169. package/build/schema/adapter.js.map +1 -0
  170. package/build/schema/adapter.test.js +53 -0
  171. package/build/schema/adapter.test.js.map +1 -0
  172. package/build/schema/compile.d.ts +37 -0
  173. package/build/schema/compile.js +38 -0
  174. package/build/schema/compile.js.map +1 -0
  175. package/build/schema/compile.test.js +78 -0
  176. package/build/schema/compile.test.js.map +1 -0
  177. package/build/schema/compute-schema.d.ts +47 -37
  178. package/build/schema/compute-schema.js +86 -29
  179. package/build/schema/compute-schema.js.map +1 -1
  180. package/build/schema/compute-schema.test.js +158 -40
  181. package/build/schema/compute-schema.test.js.map +1 -1
  182. package/build/schema/json-schema.d.ts +17 -0
  183. package/build/schema/json-schema.js +2 -0
  184. package/build/schema/json-schema.js.map +1 -0
  185. package/build/schema/typebox.d.ts +11 -0
  186. package/build/schema/typebox.js +24 -0
  187. package/build/schema/typebox.js.map +1 -0
  188. package/build/schema/typebox.test.js +34 -0
  189. package/build/schema/typebox.test.js.map +1 -0
  190. package/build/server/context.d.ts +8 -0
  191. package/build/server/context.js +7 -0
  192. package/build/server/context.js.map +1 -0
  193. package/build/server/context.test.js +16 -0
  194. package/build/server/context.test.js.map +1 -0
  195. package/build/{doc-envelope.d.ts → server/doc-envelope.d.ts} +1 -1
  196. package/build/server/doc-envelope.js.map +1 -0
  197. package/build/server/doc-envelope.test.d.ts +1 -0
  198. package/build/server/doc-envelope.test.js.map +1 -0
  199. package/build/{implementations/http → server}/doc-registry.d.ts +7 -2
  200. package/build/{implementations/http → server}/doc-registry.js +9 -5
  201. package/build/server/doc-registry.js.map +1 -0
  202. package/build/server/doc-registry.test.d.ts +1 -0
  203. package/build/{implementations/http → server}/doc-registry.test.js +27 -24
  204. package/build/server/doc-registry.test.js.map +1 -0
  205. package/build/server/docs/docs.test.d.ts +1 -0
  206. package/build/server/docs/docs.test.js +237 -0
  207. package/build/server/docs/docs.test.js.map +1 -0
  208. package/build/{implementations/http/hono → server}/docs/http-doc.d.ts +2 -2
  209. package/build/{implementations/http/hono → server}/docs/http-doc.js +1 -1
  210. package/build/server/docs/http-doc.js.map +1 -0
  211. package/build/{implementations/http/hono → server}/docs/http-stream-doc.d.ts +2 -2
  212. package/build/{implementations/http/hono → server}/docs/http-stream-doc.js +1 -1
  213. package/build/server/docs/http-stream-doc.js.map +1 -0
  214. package/build/{implementations/http/hono → server}/docs/rpc-doc.d.ts +2 -2
  215. package/build/{implementations/http/hono → server}/docs/rpc-doc.js +1 -1
  216. package/build/server/docs/rpc-doc.js.map +1 -0
  217. package/build/{implementations/http/hono → server}/docs/stream-doc.d.ts +2 -2
  218. package/build/{implementations/http/hono → server}/docs/stream-doc.js +1 -1
  219. package/build/server/docs/stream-doc.js.map +1 -0
  220. package/build/server/errors/dispatch.d.ts +96 -0
  221. package/build/{implementations/http/error-dispatch.js → server/errors/dispatch.js} +20 -10
  222. package/build/server/errors/dispatch.js.map +1 -0
  223. package/build/server/errors/dispatch.test.d.ts +1 -0
  224. package/build/server/errors/dispatch.test.js +418 -0
  225. package/build/server/errors/dispatch.test.js.map +1 -0
  226. package/build/{implementations/http/error-taxonomy.d.ts → server/errors/taxonomy.d.ts} +8 -17
  227. package/build/{implementations/http/error-taxonomy.js → server/errors/taxonomy.js} +6 -15
  228. package/build/server/errors/taxonomy.js.map +1 -0
  229. package/build/server/errors/taxonomy.test.d.ts +1 -0
  230. package/build/{implementations/http/error-taxonomy.test.js → server/errors/taxonomy.test.js} +45 -39
  231. package/build/server/errors/taxonomy.test.js.map +1 -0
  232. package/build/server/index.d.ts +29 -0
  233. package/build/server/index.js +27 -0
  234. package/build/server/index.js.map +1 -0
  235. package/build/server/no-framework-imports.test.d.ts +1 -0
  236. package/build/server/no-framework-imports.test.js +40 -0
  237. package/build/server/no-framework-imports.test.js.map +1 -0
  238. package/build/{implementations/http/hono/path.d.ts → server/paths.d.ts} +2 -3
  239. package/build/{implementations/http/hono/path.js → server/paths.js} +1 -1
  240. package/build/server/paths.js.map +1 -0
  241. package/build/server/paths.test.d.ts +1 -0
  242. package/build/server/paths.test.js +111 -0
  243. package/build/server/paths.test.js.map +1 -0
  244. package/build/server/request/params.d.ts +29 -0
  245. package/build/server/request/params.js +43 -0
  246. package/build/server/request/params.js.map +1 -0
  247. package/build/server/request/params.test.d.ts +1 -0
  248. package/build/server/request/params.test.js +91 -0
  249. package/build/server/request/params.test.js.map +1 -0
  250. package/build/server/request/query.d.ts +9 -0
  251. package/build/server/request/query.js +22 -0
  252. package/build/server/request/query.js.map +1 -0
  253. package/build/server/request/query.test.d.ts +1 -0
  254. package/build/server/request/query.test.js +60 -0
  255. package/build/server/request/query.test.js.map +1 -0
  256. package/build/server/sse.d.ts +70 -0
  257. package/build/server/sse.js +94 -0
  258. package/build/server/sse.js.map +1 -0
  259. package/build/server/sse.test.d.ts +1 -0
  260. package/build/server/sse.test.js +98 -0
  261. package/build/server/sse.test.js.map +1 -0
  262. package/build/{implementations → server}/types.d.ts +17 -15
  263. package/build/{implementations → server}/types.js.map +1 -1
  264. package/docs/astro-adapter.md +8 -9
  265. package/docs/client-and-codegen.md +10 -4
  266. package/docs/client-error-handling.md +5 -5
  267. package/docs/codegen-kotlin.md +2 -3
  268. package/docs/codegen-swift.md +1 -2
  269. package/docs/core.md +135 -54
  270. package/docs/http-integrations.md +58 -6
  271. package/docs/migration-v8-to-v9.md +200 -0
  272. package/docs/plans/2026-06-09-v9-rewrite.md +130 -0
  273. package/docs/specs/2026-06-09-v9-rewrite-design.md +221 -0
  274. package/docs/streaming.md +12 -0
  275. package/package.json +25 -48
  276. package/src/{implementations/http → adapters}/astro/index.test.ts +2 -2
  277. package/src/adapters/hono/__fixtures__/parity-envelope.json +389 -0
  278. package/src/adapters/hono/envelope-parity.test.ts +126 -0
  279. package/src/{implementations/http → adapters}/hono/handlers/http-stream.test.ts +1 -1
  280. package/src/adapters/hono/handlers/http-stream.ts +73 -0
  281. package/src/{implementations/http → adapters}/hono/handlers/http.test.ts +1 -1
  282. package/src/adapters/hono/handlers/http.ts +70 -0
  283. package/src/{implementations/http → adapters}/hono/handlers/rpc.test.ts +2 -2
  284. package/src/adapters/hono/handlers/rpc.ts +39 -0
  285. package/src/{implementations/http → adapters}/hono/handlers/stream.test.ts +4 -3
  286. package/src/{implementations/http → adapters}/hono/handlers/stream.ts +19 -92
  287. package/src/{implementations/http → adapters}/hono/index.test.ts +14 -16
  288. package/src/{implementations/http → adapters}/hono/index.ts +35 -30
  289. package/src/{implementations/http → adapters/hono}/on-request-error.test.ts +3 -3
  290. package/src/adapters/hono/request.ts +28 -0
  291. package/src/{implementations/http → adapters/hono}/route-errors.test.ts +5 -5
  292. package/src/{implementations/http → adapters}/hono/types.ts +43 -20
  293. package/src/client/freeze.test.ts +41 -0
  294. package/src/client/typed-error-dispatch.test.ts +3 -3
  295. package/src/codegen/__fixtures__/make-envelope.ts +1 -1
  296. package/src/codegen/__fixtures__/models-envelope.json +310 -0
  297. package/src/codegen/__goldens__/MANIFEST.json +85 -0
  298. package/src/codegen/__goldens__/kotlin-default--models/Billing.kt +112 -0
  299. package/src/codegen/__goldens__/kotlin-default--models/BillingReports.kt +26 -0
  300. package/src/codegen/__goldens__/kotlin-default--models/Orders.kt +88 -0
  301. package/src/codegen/__goldens__/kotlin-default--users/Users.kt +189 -0
  302. package/src/codegen/__goldens__/swift-default--models/Billing.swift +97 -0
  303. package/src/codegen/__goldens__/swift-default--models/BillingReports.swift +20 -0
  304. package/src/codegen/__goldens__/swift-default--models/Orders.swift +81 -0
  305. package/src/codegen/__goldens__/swift-default--users/Users.swift +204 -0
  306. package/src/codegen/__goldens__/ts-default--models/_client.ts +1319 -0
  307. package/src/codegen/__goldens__/ts-default--models/_errors.ts +90 -0
  308. package/src/codegen/__goldens__/ts-default--models/_models.ts +10 -0
  309. package/src/codegen/__goldens__/ts-default--models/_types.ts +502 -0
  310. package/src/codegen/__goldens__/ts-default--models/billing-reports.ts +29 -0
  311. package/src/codegen/__goldens__/ts-default--models/billing.ts +67 -0
  312. package/src/codegen/__goldens__/ts-default--models/index.ts +48 -0
  313. package/src/codegen/__goldens__/ts-default--models/orders.ts +80 -0
  314. package/src/codegen/__goldens__/ts-default--users/_client.ts +1319 -0
  315. package/src/codegen/__goldens__/ts-default--users/_errors.ts +90 -0
  316. package/src/codegen/__goldens__/ts-default--users/_types.ts +502 -0
  317. package/src/codegen/__goldens__/ts-default--users/index.ts +38 -0
  318. package/src/codegen/__goldens__/ts-default--users/users.ts +169 -0
  319. package/src/codegen/__goldens__/ts-external-runtime--models/_errors.ts +90 -0
  320. package/src/codegen/__goldens__/ts-external-runtime--models/_models.ts +10 -0
  321. package/src/codegen/__goldens__/ts-external-runtime--models/billing-reports.ts +29 -0
  322. package/src/codegen/__goldens__/ts-external-runtime--models/billing.ts +67 -0
  323. package/src/codegen/__goldens__/ts-external-runtime--models/index.ts +48 -0
  324. package/src/codegen/__goldens__/ts-external-runtime--models/orders.ts +80 -0
  325. package/src/codegen/__goldens__/ts-external-runtime--users/_errors.ts +90 -0
  326. package/src/codegen/__goldens__/ts-external-runtime--users/index.ts +38 -0
  327. package/src/codegen/__goldens__/ts-external-runtime--users/users.ts +169 -0
  328. package/src/codegen/__goldens__/ts-flat--models/_client.ts +1319 -0
  329. package/src/codegen/__goldens__/ts-flat--models/_errors.ts +87 -0
  330. package/src/codegen/__goldens__/ts-flat--models/_models.ts +10 -0
  331. package/src/codegen/__goldens__/ts-flat--models/_types.ts +502 -0
  332. package/src/codegen/__goldens__/ts-flat--models/billing-reports.ts +28 -0
  333. package/src/codegen/__goldens__/ts-flat--models/billing.ts +51 -0
  334. package/src/codegen/__goldens__/ts-flat--models/index.ts +42 -0
  335. package/src/codegen/__goldens__/ts-flat--models/orders.ts +73 -0
  336. package/src/codegen/__goldens__/ts-flat--users/_client.ts +1319 -0
  337. package/src/codegen/__goldens__/ts-flat--users/_errors.ts +87 -0
  338. package/src/codegen/__goldens__/ts-flat--users/_types.ts +502 -0
  339. package/src/codegen/__goldens__/ts-flat--users/index.ts +34 -0
  340. package/src/codegen/__goldens__/ts-flat--users/users.ts +126 -0
  341. package/src/codegen/__goldens__/ts-no-share-models--models/_client.ts +1319 -0
  342. package/src/codegen/__goldens__/ts-no-share-models--models/_errors.ts +90 -0
  343. package/src/codegen/__goldens__/ts-no-share-models--models/_types.ts +502 -0
  344. package/src/codegen/__goldens__/ts-no-share-models--models/billing-reports.ts +29 -0
  345. package/src/codegen/__goldens__/ts-no-share-models--models/billing.ts +111 -0
  346. package/src/codegen/__goldens__/ts-no-share-models--models/index.ts +48 -0
  347. package/src/codegen/__goldens__/ts-no-share-models--models/orders.ts +112 -0
  348. package/src/codegen/__goldens__/ts-no-share-models--users/_client.ts +1319 -0
  349. package/src/codegen/__goldens__/ts-no-share-models--users/_errors.ts +90 -0
  350. package/src/codegen/__goldens__/ts-no-share-models--users/_types.ts +502 -0
  351. package/src/codegen/__goldens__/ts-no-share-models--users/index.ts +38 -0
  352. package/src/codegen/__goldens__/ts-no-share-models--users/users.ts +169 -0
  353. package/src/codegen/__goldens__/ts-shared-models-module--models/_client.ts +1319 -0
  354. package/src/codegen/__goldens__/ts-shared-models-module--models/_errors.ts +90 -0
  355. package/src/codegen/__goldens__/ts-shared-models-module--models/_models.ts +7 -0
  356. package/src/codegen/__goldens__/ts-shared-models-module--models/_types.ts +502 -0
  357. package/src/codegen/__goldens__/ts-shared-models-module--models/billing-reports.ts +29 -0
  358. package/src/codegen/__goldens__/ts-shared-models-module--models/billing.ts +67 -0
  359. package/src/codegen/__goldens__/ts-shared-models-module--models/index.ts +48 -0
  360. package/src/codegen/__goldens__/ts-shared-models-module--models/orders.ts +80 -0
  361. package/src/codegen/bin/cli.test.ts +13 -2
  362. package/src/codegen/bin/cli.ts +181 -144
  363. package/src/codegen/bin/flag-specs.test.ts +16 -1
  364. package/src/codegen/bin/flag-specs.ts +43 -31
  365. package/src/codegen/bundle-size.test.ts +1 -1
  366. package/src/codegen/collect-models.ts +1 -1
  367. package/src/codegen/e2e.test.ts +1 -1
  368. package/src/codegen/emit/api-route.ts +184 -0
  369. package/src/codegen/emit/context.ts +32 -0
  370. package/src/codegen/emit/declarations.ts +49 -0
  371. package/src/codegen/emit/format-types.ts +232 -0
  372. package/src/codegen/emit/http-stream-route.ts +162 -0
  373. package/src/codegen/emit/route-shared.ts +104 -0
  374. package/src/codegen/emit/rpc-route.ts +49 -0
  375. package/src/codegen/emit/scope-file.ts +226 -0
  376. package/src/codegen/emit/stream-route.ts +81 -0
  377. package/src/codegen/emit-errors.integration.test.ts +2 -2
  378. package/src/codegen/emit-errors.test.ts +1 -1
  379. package/src/codegen/emit-errors.ts +1 -1
  380. package/src/codegen/emit-scope.test.ts +75 -2
  381. package/src/codegen/emit-scope.ts +15 -1048
  382. package/src/codegen/goldens.test.ts +89 -0
  383. package/src/codegen/group-routes.test.ts +1 -1
  384. package/src/codegen/group-routes.ts +1 -1
  385. package/src/codegen/pipeline.test.ts +1 -1
  386. package/src/codegen/pipeline.ts +1 -1
  387. package/src/codegen/resolve-envelope.test.ts +1 -1
  388. package/src/codegen/resolve-envelope.ts +1 -1
  389. package/src/codegen/targets/_shared/error-schemas.test.ts +1 -1
  390. package/src/codegen/targets/_shared/error-schemas.ts +1 -1
  391. package/src/codegen/targets/_shared/route-slots.test.ts +1 -1
  392. package/src/codegen/targets/_shared/route-slots.ts +1 -1
  393. package/src/codegen/targets/_shared/target-run.ts +1 -1
  394. package/src/codegen/targets/kotlin/emit-route-kotlin.test.ts +1 -1
  395. package/src/codegen/targets/kotlin/emit-route-kotlin.ts +1 -1
  396. package/src/codegen/targets/kotlin/emit-scope-kotlin.test.ts +1 -1
  397. package/src/codegen/targets/swift/access-level.test.ts +1 -1
  398. package/src/codegen/targets/swift/emit-route-swift.test.ts +1 -1
  399. package/src/codegen/targets/swift/emit-route-swift.ts +1 -1
  400. package/src/codegen/targets/swift/emit-scope-swift.test.ts +1 -1
  401. package/src/codegen/targets/ts/shared-models.test.ts +1 -1
  402. package/src/{create-http-stream.test.ts → core/create-http-stream.test.ts} +1 -1
  403. package/src/core/create-http-stream.ts +207 -0
  404. package/src/{create-http.test.ts → core/create-http.test.ts} +31 -4
  405. package/src/core/create-http.ts +126 -0
  406. package/src/{create-stream.test.ts → core/create-stream.test.ts} +28 -31
  407. package/src/core/create-stream.ts +142 -0
  408. package/src/{create.test.ts → core/create.test.ts} +25 -57
  409. package/src/core/create.ts +121 -0
  410. package/src/{stack-utils.test.ts → core/definition-site.test.ts} +14 -3
  411. package/src/{stack-utils.ts → core/definition-site.ts} +20 -23
  412. package/src/{errors.test.ts → core/errors.test.ts} +1 -1
  413. package/src/{errors.ts → core/errors.ts} +30 -28
  414. package/src/core/factory-options.test.ts +112 -0
  415. package/src/core/http-route.ts +73 -0
  416. package/src/core/internal.ts +203 -0
  417. package/src/{migration.test.ts → core/migration.test.ts} +23 -1
  418. package/src/{index.test.ts → core/procedures.test.ts} +13 -11
  419. package/src/core/procedures.ts +75 -0
  420. package/src/core/types.ts +196 -0
  421. package/src/exports.ts +60 -11
  422. package/src/schema/adapter.test.ts +58 -0
  423. package/src/schema/adapter.ts +45 -0
  424. package/src/schema/compile.test.ts +95 -0
  425. package/src/schema/compile.ts +64 -0
  426. package/src/schema/compute-schema.test.ts +222 -41
  427. package/src/schema/compute-schema.ts +145 -71
  428. package/src/schema/json-schema.ts +21 -0
  429. package/src/schema/typebox.test.ts +40 -0
  430. package/src/schema/typebox.ts +27 -0
  431. package/src/server/context.test.ts +22 -0
  432. package/src/server/context.ts +18 -0
  433. package/src/{doc-envelope.test.ts → server/doc-envelope.test.ts} +2 -2
  434. package/src/{doc-envelope.ts → server/doc-envelope.ts} +1 -1
  435. package/src/{implementations/http → server}/doc-registry.test.ts +32 -26
  436. package/src/{implementations/http → server}/doc-registry.ts +11 -7
  437. package/src/server/docs/docs.test.ts +287 -0
  438. package/src/{implementations/http/hono → server}/docs/http-doc.ts +3 -3
  439. package/src/{implementations/http/hono → server}/docs/http-stream-doc.ts +3 -3
  440. package/src/{implementations/http/hono → server}/docs/rpc-doc.ts +3 -3
  441. package/src/{implementations/http/hono → server}/docs/stream-doc.ts +3 -3
  442. package/src/server/errors/dispatch.test.ts +450 -0
  443. package/src/server/errors/dispatch.ts +189 -0
  444. package/src/{implementations/http/error-taxonomy.test.ts → server/errors/taxonomy.test.ts} +45 -39
  445. package/src/{implementations/http/error-taxonomy.ts → server/errors/taxonomy.ts} +8 -17
  446. package/src/server/index.ts +29 -0
  447. package/src/server/no-framework-imports.test.ts +43 -0
  448. package/src/server/paths.test.ts +141 -0
  449. package/src/{implementations/http/hono/path.ts → server/paths.ts} +2 -13
  450. package/src/server/request/params.test.ts +143 -0
  451. package/src/server/request/params.ts +68 -0
  452. package/src/server/request/query.test.ts +70 -0
  453. package/src/server/request/query.ts +24 -0
  454. package/src/server/sse.test.ts +113 -0
  455. package/src/server/sse.ts +117 -0
  456. package/src/{implementations → server}/types.ts +17 -16
  457. package/build/create-http-stream.d.ts +0 -58
  458. package/build/create-http-stream.js +0 -122
  459. package/build/create-http-stream.js.map +0 -1
  460. package/build/create-http-stream.test.js.map +0 -1
  461. package/build/create-http.d.ts +0 -49
  462. package/build/create-http.js +0 -108
  463. package/build/create-http.js.map +0 -1
  464. package/build/create-http.test.js.map +0 -1
  465. package/build/create-stream.d.ts +0 -35
  466. package/build/create-stream.js +0 -123
  467. package/build/create-stream.js.map +0 -1
  468. package/build/create-stream.test.js.map +0 -1
  469. package/build/create.d.ts +0 -28
  470. package/build/create.js +0 -82
  471. package/build/create.js.map +0 -1
  472. package/build/create.test.js.map +0 -1
  473. package/build/doc-envelope.js.map +0 -1
  474. package/build/doc-envelope.test.js.map +0 -1
  475. package/build/errors.js.map +0 -1
  476. package/build/errors.test.js.map +0 -1
  477. package/build/implementations/http/astro/astro-context.js.map +0 -1
  478. package/build/implementations/http/astro/create-handler.js.map +0 -1
  479. package/build/implementations/http/astro/index.js.map +0 -1
  480. package/build/implementations/http/astro/index.test.js.map +0 -1
  481. package/build/implementations/http/astro/rewrite-request.js.map +0 -1
  482. package/build/implementations/http/doc-registry.js.map +0 -1
  483. package/build/implementations/http/doc-registry.test.js.map +0 -1
  484. package/build/implementations/http/error-dispatch.d.ts +0 -76
  485. package/build/implementations/http/error-dispatch.js.map +0 -1
  486. package/build/implementations/http/error-dispatch.test.js +0 -254
  487. package/build/implementations/http/error-dispatch.test.js.map +0 -1
  488. package/build/implementations/http/error-taxonomy.js.map +0 -1
  489. package/build/implementations/http/error-taxonomy.test.js.map +0 -1
  490. package/build/implementations/http/hono/docs/http-doc.js.map +0 -1
  491. package/build/implementations/http/hono/docs/http-stream-doc.js.map +0 -1
  492. package/build/implementations/http/hono/docs/rpc-doc.js.map +0 -1
  493. package/build/implementations/http/hono/docs/stream-doc.js.map +0 -1
  494. package/build/implementations/http/hono/handlers/http-stream.js +0 -123
  495. package/build/implementations/http/hono/handlers/http-stream.js.map +0 -1
  496. package/build/implementations/http/hono/handlers/http-stream.test.js.map +0 -1
  497. package/build/implementations/http/hono/handlers/http.js +0 -110
  498. package/build/implementations/http/hono/handlers/http.js.map +0 -1
  499. package/build/implementations/http/hono/handlers/http.test.js.map +0 -1
  500. package/build/implementations/http/hono/handlers/rpc.js +0 -32
  501. package/build/implementations/http/hono/handlers/rpc.js.map +0 -1
  502. package/build/implementations/http/hono/handlers/rpc.test.js.map +0 -1
  503. package/build/implementations/http/hono/handlers/stream.d.ts +0 -23
  504. package/build/implementations/http/hono/handlers/stream.js +0 -147
  505. package/build/implementations/http/hono/handlers/stream.js.map +0 -1
  506. package/build/implementations/http/hono/handlers/stream.test.js.map +0 -1
  507. package/build/implementations/http/hono/index.js.map +0 -1
  508. package/build/implementations/http/hono/index.test.js.map +0 -1
  509. package/build/implementations/http/hono/path.js.map +0 -1
  510. package/build/implementations/http/hono/path.test.js +0 -83
  511. package/build/implementations/http/hono/path.test.js.map +0 -1
  512. package/build/implementations/http/hono/types.d.ts +0 -51
  513. package/build/implementations/http/hono/types.js.map +0 -1
  514. package/build/implementations/http/on-request-error.test.js.map +0 -1
  515. package/build/implementations/http/route-errors.test.js.map +0 -1
  516. package/build/index.d.ts +0 -175
  517. package/build/index.js +0 -47
  518. package/build/index.js.map +0 -1
  519. package/build/index.test.js.map +0 -1
  520. package/build/migration.test.js.map +0 -1
  521. package/build/schema/extract-json-schema.d.ts +0 -2
  522. package/build/schema/extract-json-schema.js +0 -12
  523. package/build/schema/extract-json-schema.js.map +0 -1
  524. package/build/schema/extract-json-schema.test.js +0 -23
  525. package/build/schema/extract-json-schema.test.js.map +0 -1
  526. package/build/schema/parser.d.ts +0 -36
  527. package/build/schema/parser.js +0 -210
  528. package/build/schema/parser.js.map +0 -1
  529. package/build/schema/parser.test.js +0 -120
  530. package/build/schema/parser.test.js.map +0 -1
  531. package/build/schema/resolve-schema-lib.d.ts +0 -12
  532. package/build/schema/resolve-schema-lib.js +0 -11
  533. package/build/schema/resolve-schema-lib.js.map +0 -1
  534. package/build/schema/resolve-schema-lib.test.js +0 -17
  535. package/build/schema/resolve-schema-lib.test.js.map +0 -1
  536. package/build/schema/types.d.ts +0 -8
  537. package/build/schema/types.js +0 -2
  538. package/build/stack-utils.d.ts +0 -25
  539. package/build/stack-utils.js.map +0 -1
  540. package/build/stack-utils.test.js.map +0 -1
  541. package/build/types.d.ts +0 -142
  542. package/build/types.js +0 -2
  543. package/build/types.js.map +0 -1
  544. package/docs/decisions/2026-06-02-monorepo-split-evaluation.md +0 -80
  545. package/docs/handoffs/2026-06-08-dx-round2-declines.md +0 -45
  546. package/docs/handoffs/ajsc-named-type-collision.md +0 -134
  547. package/docs/handoffs/ajsc-named-type-support.md +0 -181
  548. package/docs/handoffs/shared-models-auto-resolve-response.md +0 -181
  549. package/docs/npm-workspaces-migration-plan.md +0 -611
  550. package/docs/superpowers/plans/2026-04-24-doc-registry-simplification.md +0 -886
  551. package/docs/superpowers/plans/2026-04-24-kotlin-codegen-target.md +0 -1265
  552. package/docs/superpowers/plans/2026-04-25-ajsc-v7-kotlin-polish.md +0 -1993
  553. package/docs/superpowers/plans/2026-04-29-safe-result-api.md +0 -2293
  554. package/docs/superpowers/plans/2026-05-07-astro-adapter.md +0 -1391
  555. package/docs/superpowers/plans/2026-05-08-create-http.md +0 -3355
  556. package/docs/superpowers/plans/2026-05-08-hono-app-builder-convergence.md +0 -3365
  557. package/docs/superpowers/plans/2026-06-05-dx-feedback-round.md +0 -1292
  558. package/docs/superpowers/plans/2026-06-06-shared-models-convention-and-diagnostics.md +0 -659
  559. package/docs/superpowers/plans/2026-06-08-codegen-dx-surfacing.md +0 -428
  560. package/docs/superpowers/specs/2026-04-24-kotlin-swift-codegen-design.md +0 -401
  561. package/docs/superpowers/specs/2026-04-25-ajsc-v7-kotlin-polish-design.md +0 -314
  562. package/docs/superpowers/specs/2026-04-25-swift-codegen-design.md +0 -264
  563. package/docs/superpowers/specs/2026-04-29-safe-result-api-design.md +0 -324
  564. package/docs/superpowers/specs/2026-05-07-astro-adapter-design.md +0 -252
  565. package/docs/superpowers/specs/2026-05-08-create-http-design.md +0 -409
  566. package/docs/superpowers/specs/2026-05-08-hono-app-builder-convergence-design.md +0 -411
  567. package/docs/superpowers/specs/2026-06-05-dx-feedback-round-design.md +0 -285
  568. package/docs/superpowers/specs/2026-06-08-dx-feedback-round-2-design.md +0 -376
  569. package/src/create-http-stream.ts +0 -191
  570. package/src/create-http.ts +0 -210
  571. package/src/create-stream.ts +0 -228
  572. package/src/create.ts +0 -172
  573. package/src/implementations/http/README.md +0 -390
  574. package/src/implementations/http/error-dispatch.test.ts +0 -283
  575. package/src/implementations/http/error-dispatch.ts +0 -176
  576. package/src/implementations/http/hono/handlers/http-stream.ts +0 -152
  577. package/src/implementations/http/hono/handlers/http.ts +0 -145
  578. package/src/implementations/http/hono/handlers/rpc.ts +0 -54
  579. package/src/implementations/http/hono/path.test.ts +0 -96
  580. package/src/index.ts +0 -101
  581. package/src/schema/extract-json-schema.test.ts +0 -25
  582. package/src/schema/extract-json-schema.ts +0 -15
  583. package/src/schema/parser.test.ts +0 -182
  584. package/src/schema/parser.ts +0 -265
  585. package/src/schema/resolve-schema-lib.test.ts +0 -19
  586. package/src/schema/resolve-schema-lib.ts +0 -29
  587. package/src/schema/types.ts +0 -20
  588. package/src/types.ts +0 -133
  589. /package/build/{implementations/http → adapters}/astro/astro-context.d.ts +0 -0
  590. /package/build/{implementations/http → adapters}/astro/astro-context.js +0 -0
  591. /package/build/{implementations/http → adapters}/astro/create-handler.d.ts +0 -0
  592. /package/build/{implementations/http → adapters}/astro/create-handler.js +0 -0
  593. /package/build/{implementations/http → adapters}/astro/index.d.ts +0 -0
  594. /package/build/{implementations/http → adapters}/astro/index.js +0 -0
  595. /package/build/{implementations/http → adapters}/astro/index.test.d.ts +0 -0
  596. /package/build/{implementations/http → adapters}/astro/rewrite-request.d.ts +0 -0
  597. /package/build/{implementations/http → adapters}/astro/rewrite-request.js +0 -0
  598. /package/build/{create-http-stream.test.d.ts → adapters/hono/envelope-parity.test.d.ts} +0 -0
  599. /package/build/{implementations/http → adapters}/hono/handlers/http-stream.test.d.ts +0 -0
  600. /package/build/{implementations/http → adapters}/hono/handlers/http.test.d.ts +0 -0
  601. /package/build/{implementations/http → adapters}/hono/handlers/rpc.test.d.ts +0 -0
  602. /package/build/{implementations/http → adapters}/hono/handlers/stream.test.d.ts +0 -0
  603. /package/build/{implementations/http → adapters}/hono/index.test.d.ts +0 -0
  604. /package/build/{implementations/http → adapters/hono}/on-request-error.test.d.ts +0 -0
  605. /package/build/{implementations/http → adapters/hono}/route-errors.test.d.ts +0 -0
  606. /package/build/{create-http.test.d.ts → client/freeze.test.d.ts} +0 -0
  607. /package/build/{create-stream.test.d.ts → codegen/goldens.test.d.ts} +0 -0
  608. /package/build/{create.test.d.ts → core/create-http-stream.test.d.ts} +0 -0
  609. /package/build/{doc-envelope.test.d.ts → core/create-http.test.d.ts} +0 -0
  610. /package/build/{errors.test.d.ts → core/create-stream.test.d.ts} +0 -0
  611. /package/build/{implementations/http/doc-registry.test.d.ts → core/create.test.d.ts} +0 -0
  612. /package/build/{implementations/http/error-dispatch.test.d.ts → core/definition-site.test.d.ts} +0 -0
  613. /package/build/{implementations/http/error-taxonomy.test.d.ts → core/errors.test.d.ts} +0 -0
  614. /package/build/{errors.test.js → core/errors.test.js} +0 -0
  615. /package/build/{implementations/http/hono/path.test.d.ts → core/factory-options.test.d.ts} +0 -0
  616. /package/build/{migration.test.d.ts → core/migration.test.d.ts} +0 -0
  617. /package/build/{index.test.d.ts → core/procedures.test.d.ts} +0 -0
  618. /package/build/{implementations/http/hono → core}/types.js +0 -0
  619. /package/build/schema/{extract-json-schema.test.d.ts → adapter.test.d.ts} +0 -0
  620. /package/build/schema/{parser.test.d.ts → compile.test.d.ts} +0 -0
  621. /package/build/schema/{resolve-schema-lib.test.d.ts → typebox.test.d.ts} +0 -0
  622. /package/build/{stack-utils.test.d.ts → server/context.test.d.ts} +0 -0
  623. /package/build/{doc-envelope.js → server/doc-envelope.js} +0 -0
  624. /package/build/{doc-envelope.test.js → server/doc-envelope.test.js} +0 -0
  625. /package/build/{implementations → server}/types.js +0 -0
  626. /package/src/{implementations/http → adapters}/astro/README.md +0 -0
  627. /package/src/{implementations/http → adapters}/astro/astro-context.ts +0 -0
  628. /package/src/{implementations/http → adapters}/astro/create-handler.ts +0 -0
  629. /package/src/{implementations/http → adapters}/astro/index.ts +0 -0
  630. /package/src/{implementations/http → adapters}/astro/rewrite-request.ts +0 -0
@@ -1,1292 +0,0 @@
1
- # DX Feedback Round Implementation Plan
2
-
3
- > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
4
-
5
- **Goal:** Resolve five downstream DX findings — codegen `--help`, an offline `writeDocEnvelope` helper, shared codegen types keyed on `$id`, function-valued client headers for dynamic auth, and scaffolder file-naming knobs.
6
-
7
- **Architecture:** Five mostly-independent changes. #1/#2/#4 are self-contained and fully TDD-able. #3 (shared types) is gated behind a verification spike (Task 8) because the emission mechanism depends on how ajsc v7.2.0 handles a `$ref`→named-type; the spike picks `$ref`-reference vs post-emit substitution before the emission tasks run. #5 edits the AI-skill markdown + templates (no runtime code).
8
-
9
- **Tech Stack:** TypeScript (ESM, `.js` import specifiers), Vitest, AJV/TypeBox, ajsc (dynamic import), Hono.
10
-
11
- **Spec:** `docs/superpowers/specs/2026-06-05-dx-feedback-round-design.md`
12
-
13
- **Conventions to follow:**
14
- - Run a single test file: `npx vitest run <path>`. Full suite: `npm run test`. Lint: `npm run lint`. Build: `npm run build`.
15
- - ESM imports use `.js` specifiers even for `.ts` sources.
16
- - Commit after each green task. Branch is `dx-feedback` (already checked out).
17
-
18
- ---
19
-
20
- ## File Structure
21
-
22
- | File | Responsibility | Tasks |
23
- |------|----------------|-------|
24
- | `src/codegen/bin/flag-specs.ts` (new) | Structured flag catalog + `formatHelp()` | 1, 2 |
25
- | `src/codegen/bin/cli.ts` (modify) | Consume flag specs; `--help`/`-h`/bare → usage; exclude help from suggester | 1, 2, 3 |
26
- | `src/codegen/bin/cli.test.ts` (modify/locate) | CLI behaviour tests | 1, 2, 3 |
27
- | `src/doc-envelope.ts` (new) + package export | `writeDocEnvelope(source, path)` helper | 4, 5 |
28
- | `src/codegen/collect-models.ts` (new) | Walk routes, collect `$id` subschemas, dedup, collision-detect | 9, 10 |
29
- | `src/codegen/emit-models.ts` (new) | Emit `_models.ts` (generated + re-exported) | 11 |
30
- | `src/codegen/emit-scope.ts` (modify) | Reference hoisted models instead of inlining | 12 |
31
- | `src/codegen/targets/ts/run.ts` (modify) | Wire collect→emit-models→scope; emit `_models.ts` | 12 |
32
- | `src/codegen/index.ts` + `pipeline.ts` + `targets/_shared/target-run.ts` (modify) | Thread `shareModels` + `sharedTypesImport` options | 12 |
33
- | `src/codegen/bin/cli.ts` (modify) | `--share-models`/`--no-share-models` flag + `sharedTypesImport` config | 13 |
34
- | `src/client/types.ts` (modify) | `HeadersInit` type on `ProcedureCallDefaults.headers` | 6 |
35
- | `src/client/resolve-options.ts` (modify) | async `resolveHeaders`; `applyRequestOptions` async | 6 |
36
- | `src/client/call.ts` + `stream.ts` (modify) | `await applyRequestOptions(...)` | 6 |
37
- | `docs/client-and-codegen.md` (modify) | Auth seam section | 7 |
38
- | `agent_config/.../skills/ts-procedures/SKILL.md` + `templates/*.md` (modify) | `fileNameStyle` + `groupBy` scaffold args | 14 |
39
-
40
- ---
41
-
42
- ## #1 — `--help` for the codegen CLI
43
-
44
- ### Task 1: Structured flag catalog with `formatHelp()`
45
-
46
- **Files:**
47
- - Create: `src/codegen/bin/flag-specs.ts`
48
- - Test: `src/codegen/bin/flag-specs.test.ts`
49
-
50
- - [ ] **Step 1: Write the failing test**
51
-
52
- ```ts
53
- // src/codegen/bin/flag-specs.test.ts
54
- import { describe, it, expect } from 'vitest'
55
- import { FLAG_SPECS, KNOWN_FLAGS, formatHelp } from './flag-specs.js'
56
-
57
- describe('flag-specs', () => {
58
- it('derives KNOWN_FLAGS from the spec table', () => {
59
- expect(KNOWN_FLAGS).toContain('--url')
60
- expect(KNOWN_FLAGS).toContain('--service-name')
61
- expect(KNOWN_FLAGS).toContain('--target')
62
- // every spec name appears
63
- for (const spec of FLAG_SPECS) expect(KNOWN_FLAGS).toContain(spec.name)
64
- })
65
-
66
- it('formatHelp lists every flag with its description, grouped', () => {
67
- const help = formatHelp()
68
- expect(help).toMatch(/Usage: ts-procedures-codegen/)
69
- for (const spec of FLAG_SPECS) {
70
- expect(help).toContain(spec.name)
71
- expect(help).toContain(spec.description)
72
- }
73
- // group headers present
74
- expect(help).toMatch(/Source/)
75
- expect(help).toMatch(/Targets/)
76
- })
77
-
78
- it('does not list --help/-h as a real codegen flag in KNOWN_FLAGS suggestion set', () => {
79
- // help is handled separately; it must NOT be a did-you-mean candidate
80
- expect(KNOWN_FLAGS).not.toContain('--help')
81
- expect(KNOWN_FLAGS).not.toContain('-h')
82
- })
83
- })
84
- ```
85
-
86
- - [ ] **Step 2: Run test to verify it fails**
87
-
88
- Run: `npx vitest run src/codegen/bin/flag-specs.test.ts`
89
- Expected: FAIL — `flag-specs.js` not found / exports missing.
90
-
91
- - [ ] **Step 3: Write `flag-specs.ts`**
92
-
93
- Port every current flag (cli.ts:87-98, `KNOWN_FLAGS`) into a structured table. Include the value flags' `arg` placeholders and one-line descriptions.
94
-
95
- ```ts
96
- // src/codegen/bin/flag-specs.ts
97
- export interface FlagSpec {
98
- name: string
99
- arg?: string
100
- description: string
101
- group: 'Source' | 'Output' | 'Codegen' | 'Targets' | 'Misc'
102
- default?: string
103
- }
104
-
105
- export const FLAG_SPECS: readonly FlagSpec[] = [
106
- // Source
107
- { name: '--url', arg: '<url>', description: 'Fetch the doc envelope from a running server', group: 'Source' },
108
- { name: '--file', arg: '<path>', description: 'Read the doc envelope from a JSON file (see writeDocEnvelope)', group: 'Source' },
109
- { name: '--config', arg: '<path>', description: 'Load options from a JSON config file', group: 'Source' },
110
- // Output
111
- { name: '--out', arg: '<dir>', description: 'Output directory for generated files', group: 'Output' },
112
- { name: '--dry-run', description: 'Print what would be written without touching disk', group: 'Output' },
113
- { name: '--clean-out-dir', description: 'Prune orphaned generator-owned files before writing', group: 'Output', default: 'on' },
114
- { name: '--no-clean-out-dir', description: 'Disable orphan pruning', group: 'Output' },
115
- { name: '--watch', description: 'Regenerate on envelope change', group: 'Output' },
116
- { name: '--interval', arg: '<ms>', description: 'Poll interval for --watch (default 3000)', group: 'Output' },
117
- // Codegen
118
- { name: '--service-name', arg: '<name>', description: 'Service identifier driving generated names', group: 'Codegen', default: 'Api' },
119
- { name: '--namespace-types', description: 'Wrap types in nested namespaces', group: 'Codegen', default: 'on' },
120
- { name: '--no-namespace-types', description: 'Flat type names', group: 'Codegen' },
121
- { name: '--self-contained', description: 'Bundle the client runtime into the output dir', group: 'Codegen', default: 'on' },
122
- { name: '--no-self-contained', description: 'Import the client runtime from ts-procedures/client', group: 'Codegen' },
123
- { name: '--client-import-path', arg: '<path>', description: 'Override the client runtime import path', group: 'Codegen' },
124
- { name: '--share-models', description: 'Hoist $id-bearing schemas into a shared _models.ts', group: 'Codegen', default: 'on' },
125
- { name: '--no-share-models', description: 'Inline every type per route (legacy behaviour)', group: 'Codegen' },
126
- { name: '--jsdoc', description: 'Emit JSDoc on generated types', group: 'Codegen', default: 'on' },
127
- { name: '--no-jsdoc', description: 'Suppress JSDoc', group: 'Codegen' },
128
- { name: '--enum-style', arg: '<union|enum>', description: 'How to emit enums (namespace mode)', group: 'Codegen' },
129
- { name: '--depluralize', description: 'Depluralize extracted array-item type names', group: 'Codegen' },
130
- { name: '--array-item-naming', arg: '<name|false>', description: 'Naming for extracted array-item types', group: 'Codegen' },
131
- { name: '--uncountable-words', arg: '<csv>', description: 'Words exempt from depluralization', group: 'Codegen' },
132
- // Targets
133
- { name: '--target', arg: '<ts|kotlin|swift>', description: 'Output language', group: 'Targets', default: 'ts' },
134
- { name: '--kotlin-package', arg: '<pkg>', description: 'Package for Kotlin output (required for --target kotlin)', group: 'Targets' },
135
- { name: '--kotlin-serializer', arg: '<kotlinx|none>', description: 'Kotlin serialization annotations', group: 'Targets', default: 'kotlinx' },
136
- { name: '--swift-serializer', arg: '<codable|none>', description: 'Swift Codable conformance', group: 'Targets', default: 'codable' },
137
- { name: '--swift-access-level', arg: '<public|internal>', description: 'Swift access level', group: 'Targets', default: 'public' },
138
- { name: '--unsupported-unions', arg: '<throw|fallback>', description: 'Behaviour for untagged oneOf schemas', group: 'Targets', default: 'throw' },
139
- ]
140
-
141
- export const KNOWN_FLAGS: readonly string[] = FLAG_SPECS.map((s) => s.name)
142
-
143
- const GROUP_ORDER: FlagSpec['group'][] = ['Source', 'Output', 'Codegen', 'Targets', 'Misc']
144
-
145
- export function formatHelp(): string {
146
- const lines: string[] = []
147
- lines.push('Usage: ts-procedures-codegen --out <dir> (--url <url> | --file <path>) [options]')
148
- lines.push('')
149
- lines.push('Generate a typed client from a ts-procedures doc envelope.')
150
- lines.push('')
151
- const col = Math.max(...FLAG_SPECS.map((s) => (s.name + (s.arg ? ' ' + s.arg : '')).length)) + 2
152
- for (const group of GROUP_ORDER) {
153
- const specs = FLAG_SPECS.filter((s) => s.group === group)
154
- if (specs.length === 0) continue
155
- lines.push(`${group}:`)
156
- for (const s of specs) {
157
- const left = s.name + (s.arg ? ' ' + s.arg : '')
158
- const def = s.default ? ` (default: ${s.default})` : ''
159
- lines.push(` ${left.padEnd(col)}${s.description}${def}`)
160
- }
161
- lines.push('')
162
- }
163
- lines.push(' -h, --help Show this help and exit')
164
- return lines.join('\n')
165
- }
166
- ```
167
-
168
- - [ ] **Step 4: Run test to verify it passes**
169
-
170
- Run: `npx vitest run src/codegen/bin/flag-specs.test.ts`
171
- Expected: PASS.
172
-
173
- - [ ] **Step 5: Commit**
174
-
175
- ```bash
176
- git add src/codegen/bin/flag-specs.ts src/codegen/bin/flag-specs.test.ts
177
- git commit -m "feat(codegen): structured flag catalog with formatHelp()"
178
- ```
179
-
180
- ### Task 2: Replace `KNOWN_FLAGS` in cli.ts with the spec-derived one
181
-
182
- **Files:**
183
- - Modify: `src/codegen/bin/cli.ts` (remove the literal `KNOWN_FLAGS` at 87-98, import from `flag-specs.js`)
184
-
185
- - [ ] **Step 1: Update cli.ts to import the catalog**
186
-
187
- Delete the literal `const KNOWN_FLAGS = [...] as const` block (lines 87-98). Add at the top with the other imports:
188
-
189
- ```ts
190
- import { KNOWN_FLAGS, formatHelp } from './flag-specs.js'
191
- ```
192
-
193
- `closestKnownFlag` already iterates `KNOWN_FLAGS`; it now reads the imported array (a `readonly string[]`). No other change needed in `closestKnownFlag`.
194
-
195
- - [ ] **Step 2: Run the existing CLI tests to verify no regression**
196
-
197
- Run: `npx vitest run src/codegen/bin/cli.test.ts`
198
- Expected: PASS (unknown-flag/did-you-mean tests still green — same flag set).
199
-
200
- - [ ] **Step 3: Build to confirm no dangling references**
201
-
202
- Run: `npm run build`
203
- Expected: no TS errors referencing `KNOWN_FLAGS`.
204
-
205
- - [ ] **Step 4: Commit**
206
-
207
- ```bash
208
- git add src/codegen/bin/cli.ts
209
- git commit -m "refactor(codegen): derive KNOWN_FLAGS from the flag-specs catalog"
210
- ```
211
-
212
- ### Task 3: Handle `--help`/`-h`/bare invocation in the CLI entrypoint
213
-
214
- **Files:**
215
- - Modify: `src/codegen/bin/cli.ts` (the `main()` function — read lines 359-end to find argv handling and `process.exit`)
216
- - Test: `src/codegen/bin/cli.test.ts`
217
-
218
- - [ ] **Step 1: Write the failing test**
219
-
220
- ```ts
221
- // add to src/codegen/bin/cli.test.ts
222
- import { shouldShowHelp } from './cli.js'
223
- import { formatHelp } from './flag-specs.js'
224
-
225
- describe('--help handling', () => {
226
- it('shouldShowHelp true for --help, -h, and bare (no args)', () => {
227
- expect(shouldShowHelp(['--help'])).toBe(true)
228
- expect(shouldShowHelp(['-h'])).toBe(true)
229
- expect(shouldShowHelp([])).toBe(true)
230
- })
231
- it('shouldShowHelp false when real flags are present', () => {
232
- expect(shouldShowHelp(['--url', 'http://x', '--out', 'gen'])).toBe(false)
233
- })
234
- it('help text covers the full flag surface', () => {
235
- const help = formatHelp()
236
- expect(help).toContain('--watch')
237
- expect(help).toContain('--enum-style')
238
- expect(help).toContain('--target')
239
- })
240
- })
241
- ```
242
-
243
- - [ ] **Step 2: Run test to verify it fails**
244
-
245
- Run: `npx vitest run src/codegen/bin/cli.test.ts`
246
- Expected: FAIL — `shouldShowHelp` not exported.
247
-
248
- - [ ] **Step 3: Add `shouldShowHelp` and wire it into `main()`**
249
-
250
- Add the predicate near `parseArgs`:
251
-
252
- ```ts
253
- /**
254
- * True when the CLI should print usage and exit 0: an explicit --help/-h, or a
255
- * bare invocation with no args. --help is handled here (not in parseArgs) so it
256
- * never reaches the unknown-flag branch and never appears in did-you-mean.
257
- */
258
- export function shouldShowHelp(argv: string[]): boolean {
259
- if (argv.length === 0) return true
260
- return argv.includes('--help') || argv.includes('-h')
261
- }
262
- ```
263
-
264
- In `main()` (read the existing function; it currently does `extractConfigPath` → `loadConfigFile` → `parseArgs` → `generateClient`), add as the very first lines after reading `argv`:
265
-
266
- ```ts
267
- const argv = process.argv.slice(2)
268
- if (shouldShowHelp(argv)) {
269
- console.log(formatHelp())
270
- process.exit(0)
271
- }
272
- ```
273
-
274
- - [ ] **Step 4: Run test to verify it passes**
275
-
276
- Run: `npx vitest run src/codegen/bin/cli.test.ts`
277
- Expected: PASS.
278
-
279
- - [ ] **Step 5: Manual smoke check**
280
-
281
- Run: `npm run build && node build/codegen/bin/cli.js --help`
282
- Expected: usage text, exit 0. Then `node build/codegen/bin/cli.js --targt ts --out x --url y` still errors with the did-you-mean suggestion.
283
-
284
- - [ ] **Step 6: Commit**
285
-
286
- ```bash
287
- git add src/codegen/bin/cli.ts src/codegen/bin/cli.test.ts
288
- git commit -m "feat(codegen): --help/-h/bare invocation prints usage and exits 0"
289
- ```
290
-
291
- ---
292
-
293
- ## #2 — `writeDocEnvelope` offline helper
294
-
295
- ### Task 4: `writeDocEnvelope(source, path)` helper
296
-
297
- **Files:**
298
- - Create: `src/doc-envelope.ts`
299
- - Test: `src/doc-envelope.test.ts`
300
-
301
- - [ ] **Step 1: Write the failing test**
302
-
303
- ```ts
304
- // src/doc-envelope.test.ts
305
- import { describe, it, expect, afterEach } from 'vitest'
306
- import { readFile, rm, mkdtemp } from 'node:fs/promises'
307
- import { tmpdir } from 'node:os'
308
- import { join } from 'node:path'
309
- import { writeDocEnvelope } from './doc-envelope.js'
310
- import type { DocEnvelope } from './implementations/types.js'
311
-
312
- const envelope: DocEnvelope = { basePath: '', headers: [], errors: [], routes: [] }
313
-
314
- describe('writeDocEnvelope', () => {
315
- let dir: string
316
- afterEach(async () => { if (dir) await rm(dir, { recursive: true, force: true }) })
317
-
318
- it('writes a plain DocEnvelope as pretty JSON', async () => {
319
- dir = await mkdtemp(join(tmpdir(), 'tsp-'))
320
- const out = join(dir, 'nested', 'docs.json')
321
- await writeDocEnvelope(envelope, out)
322
- const parsed = JSON.parse(await readFile(out, 'utf8'))
323
- expect(parsed).toEqual(envelope)
324
- })
325
-
326
- it('accepts a builder-like object with toDocEnvelope()', async () => {
327
- dir = await mkdtemp(join(tmpdir(), 'tsp-'))
328
- const out = join(dir, 'docs.json')
329
- const builderLike = { toDocEnvelope: () => envelope }
330
- await writeDocEnvelope(builderLike, out)
331
- const parsed = JSON.parse(await readFile(out, 'utf8'))
332
- expect(parsed).toEqual(envelope)
333
- })
334
-
335
- it('accepts a DocRegistry-like object (toJSON)', async () => {
336
- dir = await mkdtemp(join(tmpdir(), 'tsp-'))
337
- const out = join(dir, 'docs.json')
338
- const registryLike = { toJSON: () => envelope }
339
- await writeDocEnvelope(registryLike, out)
340
- expect(JSON.parse(await readFile(out, 'utf8'))).toEqual(envelope)
341
- })
342
- })
343
- ```
344
-
345
- - [ ] **Step 2: Run test to verify it fails**
346
-
347
- Run: `npx vitest run src/doc-envelope.test.ts`
348
- Expected: FAIL — module not found.
349
-
350
- - [ ] **Step 3: Write the helper**
351
-
352
- ```ts
353
- // src/doc-envelope.ts
354
- import { mkdir, writeFile } from 'node:fs/promises'
355
- import { dirname } from 'node:path'
356
- import type { DocEnvelope } from './implementations/types.js'
357
-
358
- /** A built builder/registry that can produce a DocEnvelope. */
359
- export type DocEnvelopeSource =
360
- | DocEnvelope
361
- | { toDocEnvelope(): DocEnvelope }
362
- | { toJSON(): DocEnvelope }
363
-
364
- function resolveEnvelope(source: DocEnvelopeSource): DocEnvelope {
365
- if (typeof (source as { toDocEnvelope?: unknown }).toDocEnvelope === 'function') {
366
- return (source as { toDocEnvelope(): DocEnvelope }).toDocEnvelope()
367
- }
368
- if (typeof (source as { toJSON?: unknown }).toJSON === 'function') {
369
- return (source as { toJSON(): DocEnvelope }).toJSON()
370
- }
371
- return source as DocEnvelope
372
- }
373
-
374
- /**
375
- * Serializes a doc envelope to disk as pretty JSON so codegen can run offline
376
- * via `--file <path>` without a running server. Accepts a plain `DocEnvelope`,
377
- * a builder exposing `toDocEnvelope()`, or a `DocRegistry` exposing `toJSON()`.
378
- * Parent directories are created as needed.
379
- */
380
- export async function writeDocEnvelope(source: DocEnvelopeSource, path: string): Promise<void> {
381
- const envelope = resolveEnvelope(source)
382
- await mkdir(dirname(path), { recursive: true })
383
- await writeFile(path, JSON.stringify(envelope, null, 2), 'utf8')
384
- }
385
- ```
386
-
387
- > Note: if `DocEnvelope` is not exported from `./implementations/types.js`, import it from wherever `DocRegistry` re-exports it (`./implementations/http/doc-registry.js` re-exports `DocEnvelope`). Verify the correct specifier when implementing.
388
-
389
- - [ ] **Step 4: Run test to verify it passes**
390
-
391
- Run: `npx vitest run src/doc-envelope.test.ts`
392
- Expected: PASS.
393
-
394
- - [ ] **Step 5: Commit**
395
-
396
- ```bash
397
- git add src/doc-envelope.ts src/doc-envelope.test.ts
398
- git commit -m "feat: writeDocEnvelope helper for offline codegen"
399
- ```
400
-
401
- ### Task 5: Export `writeDocEnvelope` from the package entrypoint + document the loop
402
-
403
- **Files:**
404
- - Modify: the package root barrel (find it: `grep -n "writeDocEnvelope\|export" src/index.ts` and check `package.json` `exports`/`main`)
405
- - Modify: `docs/client-and-codegen.md` (add a short "Offline codegen" subsection)
406
-
407
- - [ ] **Step 1: Add the export**
408
-
409
- In the root barrel (`src/index.ts` or the file `package.json` `main`/`exports` points to), add:
410
-
411
- ```ts
412
- export { writeDocEnvelope, type DocEnvelopeSource } from './doc-envelope.js'
413
- ```
414
-
415
- Confirm it lands in a publicly-exported subpath. If the package exposes subpaths via `package.json` `exports`, ensure either the root or a sensible subpath re-exports it.
416
-
417
- - [ ] **Step 2: Document the offline loop**
418
-
419
- In `docs/client-and-codegen.md`, add a subsection:
420
-
421
- ```markdown
422
- ### Offline codegen (no running server)
423
-
424
- Emit the envelope to disk once, then generate against the file:
425
-
426
- ```ts
427
- // scripts/emit-docs.ts
428
- import { writeDocEnvelope } from 'ts-procedures'
429
- import { buildApp } from '../src/server/app.js' // your built HonoAppBuilder / DocRegistry
430
-
431
- const builder = buildApp() // whatever produces your built app/registry
432
- await writeDocEnvelope(builder, 'docs.json')
433
- ```
434
-
435
- ```bash
436
- tsx scripts/emit-docs.ts
437
- npx ts-procedures-codegen --file docs.json --out src/generated --service-name Api
438
- ```
439
-
440
- `writeDocEnvelope` accepts a built `HonoAppBuilder`, a `DocRegistry`, or a plain
441
- `DocEnvelope`. No running HTTP server required.
442
- ```
443
-
444
- - [ ] **Step 3: Build + test**
445
-
446
- Run: `npm run build && npm run test`
447
- Expected: PASS.
448
-
449
- - [ ] **Step 4: Commit**
450
-
451
- ```bash
452
- git add src/index.ts docs/client-and-codegen.md
453
- git commit -m "feat: export writeDocEnvelope; document offline codegen loop"
454
- ```
455
-
456
- ---
457
-
458
- ## #4 — Function-valued client headers (dynamic auth)
459
-
460
- ### Task 6: `HeadersInit` function support + async header resolution
461
-
462
- **Files:**
463
- - Modify: `src/client/types.ts:202-208` (`ProcedureCallDefaults.headers`)
464
- - Modify: `src/client/resolve-options.ts:85-95` (`resolveHeaders` → async) and `140-158` (`applyRequestOptions` → async)
465
- - Modify: `src/client/call.ts:58` and `src/client/stream.ts:189` (`await applyRequestOptions(...)`)
466
- - Test: `src/client/resolve-options.test.ts` (locate; create if absent)
467
-
468
- - [ ] **Step 1: Write the failing test**
469
-
470
- ```ts
471
- // src/client/resolve-options.test.ts (add)
472
- import { describe, it, expect } from 'vitest'
473
- import { applyRequestOptions } from './resolve-options.js'
474
- import type { AdapterRequest } from './types.js'
475
-
476
- const baseReq: AdapterRequest = { method: 'POST', url: '/x', headers: undefined, body: undefined }
477
-
478
- describe('function-valued headers', () => {
479
- it('resolves a sync function-valued default header', async () => {
480
- const { request } = await applyRequestOptions(baseReq, { headers: () => ({ Authorization: 'Bearer t1' }) }, undefined)
481
- expect(request.headers).toMatchObject({ Authorization: 'Bearer t1' })
482
- })
483
-
484
- it('resolves an async function-valued header', async () => {
485
- const { request } = await applyRequestOptions(
486
- baseReq,
487
- { headers: async () => ({ Authorization: 'Bearer async' }) },
488
- undefined,
489
- )
490
- expect(request.headers).toMatchObject({ Authorization: 'Bearer async' })
491
- })
492
-
493
- it('per-call headers win over default headers (both functions)', async () => {
494
- const { request } = await applyRequestOptions(
495
- baseReq,
496
- { headers: () => ({ Authorization: 'Bearer default', 'x-a': '1' }) },
497
- { headers: () => ({ Authorization: 'Bearer call' }) },
498
- )
499
- expect(request.headers).toMatchObject({ Authorization: 'Bearer call', 'x-a': '1' })
500
- })
501
-
502
- it('static record path still works', async () => {
503
- const { request } = await applyRequestOptions(baseReq, { headers: { 'x-s': 'v' } }, undefined)
504
- expect(request.headers).toMatchObject({ 'x-s': 'v' })
505
- })
506
- })
507
- ```
508
-
509
- - [ ] **Step 2: Run test to verify it fails**
510
-
511
- Run: `npx vitest run src/client/resolve-options.test.ts`
512
- Expected: FAIL — `applyRequestOptions` is sync / returns non-promise; function headers not resolved.
513
-
514
- - [ ] **Step 3: Update the `headers` type in `types.ts`**
515
-
516
- Replace `headers?: Record<string, string>` on `ProcedureCallDefaults` (line 205) with a named type, and add the alias above the interface:
517
-
518
- ```ts
519
- /**
520
- * Request headers as a static record OR a (possibly async) function evaluated
521
- * per request. Use the function form for values that change between calls —
522
- * e.g. a rotating bearer token: `headers: () => ({ Authorization: \`Bearer ${session.token}\` })`.
523
- * A static record captured at construction goes stale; a function is re-evaluated each call.
524
- */
525
- export type HeadersInit =
526
- | Record<string, string>
527
- | (() => Record<string, string> | Promise<Record<string, string>>)
528
- ```
529
-
530
- ```ts
531
- export interface ProcedureCallDefaults {
532
- signal?: AbortSignal
533
- timeout?: number
534
- headers?: HeadersInit
535
- basePath?: string
536
- meta?: RequestMeta
537
- }
538
- ```
539
-
540
- (`ProcedureCallOptions extends ProcedureCallDefaults`, so per-call inherits it automatically.) Update the `headers` JSDoc bullet (lines 196-197) to mention the function form.
541
-
542
- - [ ] **Step 4: Make `resolveHeaders` async and `applyRequestOptions` async**
543
-
544
- In `resolve-options.ts`:
545
-
546
- ```ts
547
- async function resolveHeadersValue(
548
- h: HeadersInit | undefined,
549
- ): Promise<Record<string, string> | undefined> {
550
- if (h == null) return undefined
551
- return typeof h === 'function' ? await h() : h
552
- }
553
-
554
- /**
555
- * Merges headers with precedence: default < per-call. Function-valued headers
556
- * are evaluated (awaited) per request. Returns undefined if none would be set.
557
- */
558
- export async function resolveHeaders(
559
- defaults: ProcedureCallDefaults | undefined,
560
- options: ProcedureCallOptions | undefined,
561
- ): Promise<Record<string, string> | undefined> {
562
- const defaultHeaders = await resolveHeadersValue(defaults?.headers)
563
- const callHeaders = await resolveHeadersValue(options?.headers)
564
- if (!defaultHeaders && !callHeaders) return undefined
565
- return { ...defaultHeaders, ...callHeaders }
566
- }
567
- ```
568
-
569
- Make `applyRequestOptions` async and await the headers:
570
-
571
- ```ts
572
- export async function applyRequestOptions(
573
- request: AdapterRequest,
574
- defaults: ProcedureCallDefaults | undefined,
575
- options: ProcedureCallOptions | undefined,
576
- ): Promise<ApplyRequestOptionsResult> {
577
- const signalSources = resolveSignalSources(defaults, options)
578
- const resolvedHeaders = await resolveHeaders(defaults, options)
579
- const meta = resolveMeta(defaults, options)
580
-
581
- const headers =
582
- resolvedHeaders || request.headers
583
- ? { ...resolvedHeaders, ...request.headers }
584
- : undefined
585
-
586
- return {
587
- request: { ...request, headers, signal: signalSources.combined, meta },
588
- signalSources,
589
- }
590
- }
591
- ```
592
-
593
- Add `HeadersInit` to the type imports at the top of `resolve-options.ts`.
594
-
595
- - [ ] **Step 5: Await the call sites**
596
-
597
- `src/client/call.ts:58` → `const applied = await applyRequestOptions(request, defaults, options)`.
598
- `src/client/stream.ts:189` → `const applied = await applyRequestOptions(request, defaults, options)`.
599
- Both are already inside `async` functions.
600
-
601
- - [ ] **Step 6: Run tests**
602
-
603
- Run: `npx vitest run src/client/resolve-options.test.ts src/client/call.test.ts`
604
- Expected: PASS. Then `npm run test` to catch any other caller of `applyRequestOptions`/`resolveHeaders` that now needs `await` (grep first: `grep -rn "applyRequestOptions\|resolveHeaders" src`).
605
-
606
- - [ ] **Step 7: Build**
607
-
608
- Run: `npm run build`
609
- Expected: clean. The self-contained `_client.ts`/`_types.ts` are emitted from these sources, so regeneration picks up the new type automatically.
610
-
611
- - [ ] **Step 8: Commit**
612
-
613
- ```bash
614
- git add src/client/types.ts src/client/resolve-options.ts src/client/call.ts src/client/stream.ts src/client/resolve-options.test.ts
615
- git commit -m "feat(client): function-valued headers for dynamic auth (async-resolved per request)"
616
- ```
617
-
618
- ### Task 7: Document the auth seam
619
-
620
- **Files:**
621
- - Modify: `docs/client-and-codegen.md` (the hooks/auth area near lines 36-39 and 356-389)
622
-
623
- - [ ] **Step 1: Add/expand the auth section**
624
-
625
- ```markdown
626
- ### Authentication (rotating tokens)
627
-
628
- A bearer token that only exists after login — and rotates — must NOT live in a
629
- static `headers` record: it is captured once at construction and goes stale after
630
- the next login. Use one of the two dynamic seams instead.
631
-
632
- **Function-valued `headers` (recommended for auth):**
633
-
634
- ```ts
635
- createClient({
636
- // ...
637
- defaults: {
638
- headers: () => ({ Authorization: `Bearer ${session.token}` }), // re-evaluated per call
639
- },
640
- })
641
- ```
642
-
643
- The function may be async (`async () => ({ ... })`) and is awaited before each
644
- request. Per-call `headers` still override defaults.
645
-
646
- **`onBeforeRequest` hook (when you need the full request):**
647
-
648
- ```ts
649
- hooks: {
650
- onBeforeRequest(ctx) {
651
- ctx.request.headers = { ...ctx.request.headers, Authorization: `Bearer ${getToken()}` }
652
- return ctx
653
- },
654
- }
655
- ```
656
-
657
- > ⚠️ A token placed in a static `headers: { Authorization: ... }` record compiles
658
- > but silently goes stale. Reach for the function form or the hook.
659
- ```
660
-
661
- - [ ] **Step 2: Commit**
662
-
663
- ```bash
664
- git add docs/client-and-codegen.md
665
- git commit -m "docs(client): document function-valued headers and onBeforeRequest as the auth seams"
666
- ```
667
-
668
- ---
669
-
670
- ## #3 — Shared types keyed on `$id`
671
-
672
- ### Task 8: Verification spike (gates the emission mechanism)
673
-
674
- **Files:**
675
- - Create (throwaway, removed after): `src/codegen/__spike__/ajsc-ref.test.ts`
676
-
677
- This task produces a **decision**, not shipped code. Record the outcome in the plan's Task 9-12 before implementing them.
678
-
679
- - [ ] **Step 1: Confirm nested `$id` survives `extractJsonSchema`**
680
-
681
- Write a quick test feeding a TypeBox schema with a nested `$id` through the same path `toDocEnvelope` uses (`src/schema/extract-json-schema.ts` → whatever the rpc/api doc builders call). Assert the nested `$id` is present in the resulting JSON schema.
682
-
683
- ```ts
684
- import { describe, it, expect } from 'vitest'
685
- import { Type } from 'typebox'
686
- import { extractJsonSchema } from '../../schema/extract-json-schema.js' // verify exact path/name
687
-
688
- describe('spike: nested $id survival', () => {
689
- it('keeps $id on a nested object schema', () => {
690
- const Message = Type.Object({ id: Type.String() }, { $id: 'urn:msg', title: 'Message' })
691
- const Thread = Type.Object({ messages: Type.Array(Message) }, { $id: 'urn:thread', title: 'Thread' })
692
- const js = extractJsonSchema(Thread) as any
693
- // assert urn:msg is reachable somewhere in js
694
- expect(JSON.stringify(js)).toContain('urn:msg')
695
- })
696
- })
697
- ```
698
-
699
- Run it. **If `$id` is stripped:** the first emission task (Task 9) must first fix extraction to preserve nested `$id`. Record which.
700
-
701
- - [ ] **Step 2: Probe ajsc `$ref` behaviour**
702
-
703
- Using the same dynamic `import('ajsc')` path as `src/codegen/emit-types.ts` (`jsonSchemaToExtractedTypes`), feed a schema shaped like:
704
-
705
- ```json
706
- { "type": "object", "properties": { "msg": { "$ref": "#/$defs/Message" } },
707
- "$defs": { "Message": { "type": "object", "title": "Message", "properties": { "id": { "type": "string" } } } } }
708
- ```
709
-
710
- Observe whether ajsc emits a **reference** to a `Message` type (preferred) or **inlines** the object. Record the result.
711
-
712
- - [ ] **Step 3: Record the decision**
713
-
714
- **SPIKE OUTCOME (2026-06-05):**
715
- - **Probe 1:** nested `$id`/`title` **fully survive** into the route JSON schema (`extractJsonSchema` is a no-op for TypeBox; TypeBox inlines referenced objects but preserves `$id`/`title` on every copy). → Task 9 does **not** need to fix extraction.
716
- - **Probe 2:** ajsc 7.2.0 **inlines** a `$ref` target and re-extracts it **named by property**, with no dedup and no verbatim-type / external-ref / `$defs`-reference option (full option surface checked; `tsType`/`x-tsType`/bare-`$ref` all ignored or rejected; `EmitResult.imports` is always empty for TS). So **Mechanism A is not viable.**
717
- - **Re-spike → chosen mechanism: Mechanism C (placeholder-token).** Empirically verified clean and property-name-independent:
718
- 1. Walk each route schema; replace every subschema whose `$id` is in the model registry with `{ const: '__MODELREF__<ModelName>__' }` (identity comes from `$id`, not property name; reused models get the same token → free dedup).
719
- 2. Call ajsc **unchanged** (both `inlineTypes` modes; the token is a string-literal type, never extracted as a sub-type, survives inside `Array<...>`).
720
- 3. Single deterministic global replace on the emitted string: `/["']__MODELREF__([A-Za-z_$][\w$]*)__["']/g` → `$1`, accumulating captured names into a per-file import set.
721
- 4. Emit `import type { Message, … } from './_models.js'` from the collected set. Emit `_models.ts` standalone via `emitTypescript(modelSchema, { rootTypeName, inlineTypes:false })`.
722
- - This avoids the brittle per-property rename machinery of Mechanism B entirely. **Task 12 implements Mechanism C.**
723
-
724
- - [ ] **Step 4: Remove the spike files; commit the decision note**
725
-
726
- ```bash
727
- rm -rf src/codegen/__spike__
728
- git commit --allow-empty -m "chore(codegen): spike — chose Mechanism C (placeholder-token) for \$id model emission (see plan Task 8)"
729
- ```
730
-
731
- ### Task 9: `collect-models.ts` — registry + collision detection
732
-
733
- **Files:**
734
- - Create: `src/codegen/collect-models.ts`
735
- - Test: `src/codegen/collect-models.test.ts`
736
-
737
- - [ ] **Step 1: Write the failing test**
738
-
739
- ```ts
740
- // src/codegen/collect-models.test.ts
741
- import { describe, it, expect } from 'vitest'
742
- import { collectModels } from './collect-models.js'
743
-
744
- const messageSchema = { type: 'object', $id: 'urn:msg', title: 'Message', properties: { id: { type: 'string' } } }
745
-
746
- it('collects each $id subschema once, named from title', () => {
747
- const routes = [
748
- { jsonSchema: { response: messageSchema } },
749
- { jsonSchema: { body: { type: 'object', properties: { msg: messageSchema } } } },
750
- ] as any
751
- const models = collectModels(routes)
752
- expect(models.map((m) => m.name)).toEqual(['Message'])
753
- expect(models[0].id).toBe('urn:msg')
754
- })
755
-
756
- it('throws on $id collision with divergent body', () => {
757
- const a = { type: 'object', $id: 'urn:x', title: 'X', properties: { a: { type: 'string' } } }
758
- const b = { type: 'object', $id: 'urn:x', title: 'X', properties: { b: { type: 'number' } } }
759
- const routes = [{ jsonSchema: { response: a } }, { jsonSchema: { body: b } }] as any
760
- expect(() => collectModels(routes)).toThrow(/urn:x/)
761
- })
762
-
763
- it('disambiguates distinct $ids that derive the same name', () => {
764
- const m1 = { type: 'object', $id: 'urn:a/message', title: 'Message', properties: { a: { type: 'string' } } }
765
- const m2 = { type: 'object', $id: 'urn:b/message', title: 'Message', properties: { b: { type: 'string' } } }
766
- const models = collectModels([{ jsonSchema: { response: m1 } }, { jsonSchema: { body: m2 } }] as any)
767
- expect(models.map((m) => m.name).sort()).toEqual(['Message', 'Message2'])
768
- })
769
-
770
- it('ignores schemas without $id', () => {
771
- const routes = [{ jsonSchema: { response: { type: 'object', title: 'Loose', properties: {} } } }] as any
772
- expect(collectModels(routes)).toEqual([])
773
- })
774
- ```
775
-
776
- - [ ] **Step 2: Run test to verify it fails**
777
-
778
- Run: `npx vitest run src/codegen/collect-models.test.ts`
779
- Expected: FAIL — module not found.
780
-
781
- - [ ] **Step 3: Implement `collect-models.ts`**
782
-
783
- ```ts
784
- // src/codegen/collect-models.ts
785
- import type { AnyHttpRouteDoc } from '../implementations/types.js'
786
-
787
- export interface CollectedModel {
788
- id: string
789
- name: string
790
- schema: Record<string, unknown>
791
- }
792
-
793
- function deriveName(schema: Record<string, unknown>, id: string): string {
794
- const title = typeof schema.title === 'string' ? schema.title : undefined
795
- const base = title ?? id.split(/[/#:]/).filter(Boolean).pop() ?? 'Model'
796
- // PascalCase-ish; reuse toPascalCase from naming.ts if it fits the input shape.
797
- return base.replace(/(^|[^A-Za-z0-9])([A-Za-z0-9])/g, (_, _s, c) => c.toUpperCase())
798
- }
799
-
800
- /** Stable structural key for collision detection (order-insensitive JSON). */
801
- function stableKey(schema: unknown): string {
802
- return JSON.stringify(schema, Object.keys(schema as object).sort?.() ?? undefined)
803
- }
804
-
805
- function walk(node: unknown, visit: (s: Record<string, unknown>) => void): void {
806
- if (node == null || typeof node !== 'object') return
807
- if (Array.isArray(node)) { for (const item of node) walk(item, visit); return }
808
- const obj = node as Record<string, unknown>
809
- if (typeof obj.$id === 'string') visit(obj)
810
- for (const value of Object.values(obj)) walk(value, visit)
811
- }
812
-
813
- /**
814
- * Walks every route's JSON schema and returns the deduped set of $id-bearing
815
- * subschemas, named from `title`. Hoisting is keyed on declared identity ($id)
816
- * — never on structure. Two subschemas sharing an $id but with divergent bodies
817
- * are a hard error. Distinct $ids that derive the same name are disambiguated
818
- * deterministically (Message, Message2, …) by first-seen order.
819
- */
820
- export function collectModels(routes: AnyHttpRouteDoc[]): CollectedModel[] {
821
- const byId = new Map<string, { schema: Record<string, unknown>; key: string }>()
822
- for (const route of routes) {
823
- walk((route as { jsonSchema?: unknown }).jsonSchema, (s) => {
824
- const id = s.$id as string
825
- const key = stableKey(s)
826
- const existing = byId.get(id)
827
- if (existing) {
828
- if (existing.key !== key) {
829
- throw new Error(
830
- `[ts-procedures-codegen] Two schemas share $id "${id}" but have different shapes. ` +
831
- `An $id must identify a single type. Give the divergent schema its own $id.`,
832
- )
833
- }
834
- return
835
- }
836
- byId.set(id, { schema: s, key })
837
- })
838
- }
839
-
840
- const models: CollectedModel[] = []
841
- const usedNames = new Map<string, number>()
842
- for (const [id, { schema }] of byId) {
843
- let name = deriveName(schema, id)
844
- const seen = usedNames.get(name)
845
- if (seen) { usedNames.set(name, seen + 1); name = `${name}${seen + 1}` }
846
- else usedNames.set(name, 1)
847
- models.push({ id, name, schema })
848
- }
849
- return models
850
- }
851
- ```
852
-
853
- > When implementing, prefer the existing `toPascalCase` from `src/codegen/naming.ts` for `deriveName` if its input contract matches; keep one naming utility.
854
-
855
- - [ ] **Step 4: Run test to verify it passes**
856
-
857
- Run: `npx vitest run src/codegen/collect-models.test.ts`
858
- Expected: PASS.
859
-
860
- - [ ] **Step 5: Commit**
861
-
862
- ```bash
863
- git add src/codegen/collect-models.ts src/codegen/collect-models.test.ts
864
- git commit -m "feat(codegen): collect-models — dedup \$id subschemas with collision detection"
865
- ```
866
-
867
- ### Task 10: Resolve the `sharedTypesImport` map onto collected models
868
-
869
- **Files:**
870
- - Modify: `src/codegen/collect-models.ts` (add import-map resolution)
871
- - Test: `src/codegen/collect-models.test.ts`
872
-
873
- - [ ] **Step 1: Write the failing test**
874
-
875
- ```ts
876
- import { resolveModelImports } from './collect-models.js'
877
-
878
- it('marks models mapped to a user package as imported', () => {
879
- const models = [{ id: 'urn:msg', name: 'Message', schema: {} as any }]
880
- const resolved = resolveModelImports(models, { 'urn:msg': { module: '@shared/schemas', name: 'Message' } })
881
- expect(resolved[0]).toMatchObject({ id: 'urn:msg', import: { module: '@shared/schemas', name: 'Message' } })
882
- })
883
-
884
- it('leaves unmapped models as generated (no import)', () => {
885
- const models = [{ id: 'urn:msg', name: 'Message', schema: {} as any }]
886
- const resolved = resolveModelImports(models, {})
887
- expect(resolved[0].import).toBeUndefined()
888
- })
889
- ```
890
-
891
- - [ ] **Step 2: Run test to verify it fails**
892
-
893
- Run: `npx vitest run src/codegen/collect-models.test.ts`
894
- Expected: FAIL — `resolveModelImports` not exported.
895
-
896
- - [ ] **Step 3: Implement**
897
-
898
- ```ts
899
- // add to collect-models.ts
900
- export interface SharedTypeImport { module: string; name: string }
901
- export type SharedTypesImportMap = Record<string, SharedTypeImport>
902
- export interface ResolvedModel extends CollectedModel { import?: SharedTypeImport }
903
-
904
- /** Tags each model with its user-package import when its $id is in the map. */
905
- export function resolveModelImports(
906
- models: CollectedModel[],
907
- map: SharedTypesImportMap | undefined,
908
- ): ResolvedModel[] {
909
- return models.map((m) => {
910
- const mapped = map?.[m.id]
911
- return mapped ? { ...m, import: mapped } : { ...m }
912
- })
913
- }
914
- ```
915
-
916
- - [ ] **Step 4: Run test to verify it passes**
917
-
918
- Run: `npx vitest run src/codegen/collect-models.test.ts`
919
- Expected: PASS.
920
-
921
- - [ ] **Step 5: Commit**
922
-
923
- ```bash
924
- git add src/codegen/collect-models.ts src/codegen/collect-models.test.ts
925
- git commit -m "feat(codegen): resolve sharedTypesImport map onto collected models"
926
- ```
927
-
928
- ### Task 11: `emit-models.ts` — generate `_models.ts`
929
-
930
- **Files:**
931
- - Create: `src/codegen/emit-models.ts`
932
- - Test: `src/codegen/emit-models.test.ts`
933
-
934
- Uses `jsonSchemaToTypeBody` (flat) / the extracted-types path from `emit-types.ts` to render each generated model as a root named type. Per Task 8 decision, generated models are rendered as standalone root types either way (`_models.ts` is target-of-reference in both mechanisms).
935
-
936
- - [ ] **Step 1: Write the failing test**
937
-
938
- ```ts
939
- // src/codegen/emit-models.test.ts
940
- import { describe, it, expect } from 'vitest'
941
- import { emitModelsFile } from './emit-models.js'
942
-
943
- it('re-exports mapped models and declares generated ones', async () => {
944
- const code = await emitModelsFile(
945
- [
946
- { id: 'urn:msg', name: 'Message', schema: {} as any, import: { module: '@shared/schemas', name: 'Message' } },
947
- { id: 'urn:thread', name: 'Thread', schema: { type: 'object', title: 'Thread', properties: { id: { type: 'string' } } } as any },
948
- ],
949
- { ajsc: {}, namespaceTypes: true, serviceName: 'Api' },
950
- )
951
- expect(code).toContain("export { Message } from '@shared/schemas'")
952
- expect(code).toMatch(/export (type|interface) Thread/)
953
- })
954
-
955
- it('aliases a re-export when the local name differs from the package name', async () => {
956
- const code = await emitModelsFile(
957
- [{ id: 'urn:msg', name: 'ChatMessage', schema: {} as any, import: { module: '@shared/schemas', name: 'Message' } }],
958
- { ajsc: {}, namespaceTypes: true, serviceName: 'Api' },
959
- )
960
- expect(code).toContain("export { Message as ChatMessage } from '@shared/schemas'")
961
- })
962
-
963
- it('returns null when there are no models', async () => {
964
- expect(await emitModelsFile([], { ajsc: {}, namespaceTypes: true, serviceName: 'Api' })).toBeNull()
965
- })
966
- ```
967
-
968
- - [ ] **Step 2: Run test to verify it fails**
969
-
970
- Run: `npx vitest run src/codegen/emit-models.test.ts`
971
- Expected: FAIL — module not found.
972
-
973
- - [ ] **Step 3: Implement `emit-models.ts`**
974
-
975
- Render order: re-exports first, then generated declarations. In namespace mode, wrap generated models in `export namespace ${serviceName}Models { ... }` and re-exports above it (re-exports can also live inside the namespace via `export import`, but a top-level `export { X } from '...'` is simpler and is what scopes import). Reuse `jsonSchemaToTypeBody`/`jsonSchemaToExtractedTypes` from `emit-types.ts`.
976
-
977
- ```ts
978
- // src/codegen/emit-models.ts
979
- import type { ResolvedModel } from './collect-models.js'
980
- import type { AjscOptions } from './emit-types.js'
981
- import { jsonSchemaToTypeBody } from './emit-types.js' // verify export
982
-
983
- export interface EmitModelsOptions {
984
- ajsc?: AjscOptions
985
- namespaceTypes: boolean
986
- serviceName: string
987
- }
988
-
989
- export async function emitModelsFile(
990
- models: ResolvedModel[],
991
- opts: EmitModelsOptions,
992
- ): Promise<string | null> {
993
- if (models.length === 0) return null
994
- const header = '// Generated by ts-procedures-codegen. DO NOT EDIT.'
995
- const reexports: string[] = []
996
- const decls: string[] = []
997
-
998
- for (const m of models) {
999
- if (m.import) {
1000
- const clause = m.import.name === m.name ? m.name : `${m.import.name} as ${m.name}`
1001
- reexports.push(`export { ${clause} } from '${m.import.module}'`)
1002
- } else {
1003
- const body = await jsonSchemaToTypeBody(m.schema, opts.ajsc)
1004
- if (body == null) continue
1005
- decls.push(`export type ${m.name} = ${body}`)
1006
- }
1007
- }
1008
-
1009
- const lines = [header, '']
1010
- if (reexports.length) lines.push(...reexports, '')
1011
- if (decls.length) {
1012
- if (opts.namespaceTypes) {
1013
- // keep flat top-level types; scopes reference them via the _models import.
1014
- lines.push(...decls)
1015
- } else {
1016
- lines.push(...decls)
1017
- }
1018
- }
1019
- return lines.join('\n')
1020
- }
1021
- ```
1022
-
1023
- > Decision note: keep `_models.ts` declarations **flat top-level** named types regardless of `namespaceTypes` — scope files import them by name (`import type { Message } from './_models.js'`). This avoids the value+type `export import` gymnastics the scope namespaces need; `_models.ts` is pure types. Confirm scope import wiring in Task 12 matches.
1024
-
1025
- - [ ] **Step 4: Run test to verify it passes**
1026
-
1027
- Run: `npx vitest run src/codegen/emit-models.test.ts`
1028
- Expected: PASS.
1029
-
1030
- - [ ] **Step 5: Commit**
1031
-
1032
- ```bash
1033
- git add src/codegen/emit-models.ts src/codegen/emit-models.test.ts
1034
- git commit -m "feat(codegen): emit-models — _models.ts hub (re-exports + generated types)"
1035
- ```
1036
-
1037
- ### Task 12: Wire collect→emit-models→scope-reference into the TS pipeline
1038
-
1039
- **Files:**
1040
- - Modify: `src/codegen/targets/_shared/target-run.ts` (add `shareModels?`, `sharedTypesImport?` to `TargetRunInput`)
1041
- - Modify: `src/codegen/pipeline.ts` (thread options through)
1042
- - Modify: `src/codegen/index.ts` (`GenerateClientOptions` + `generateClient` pass-through)
1043
- - Modify: `src/codegen/targets/ts/run.ts` (collect models, emit `_models.ts`, pass model registry to `emitScopeFile`)
1044
- - Modify: `src/codegen/emit-scope.ts` (reference hoisted models instead of inlining — per Task 8 mechanism)
1045
- - Test: `src/codegen/targets/ts/run.test.ts` (locate the existing run/pipeline test that uses `__fixtures__/users-envelope.json`)
1046
-
1047
- - [ ] **Step 1: Extend the fixture and write the failing integration test**
1048
-
1049
- Add a `$id`'d schema reused across two routes to a **copy** of the canonical fixture (or extend it if other targets tolerate the extra `$id` — they ignore it). Assert:
1050
-
1051
- ```ts
1052
- it('emits a single _models.ts entry for a reused $id and references it from scopes', async () => {
1053
- const files = await generateClient({
1054
- envelope: envelopeWithSharedMessage, outDir: tmp, serviceName: 'Api',
1055
- namespaceTypes: true, selfContained: false, shareModels: true,
1056
- })
1057
- const models = files.find((f) => f.path.endsWith('_models.ts'))!
1058
- expect(models.code.match(/export type Message =/g)).toHaveLength(1)
1059
- const scope = files.find((f) => f.path.endsWith('messages.ts'))!
1060
- expect(scope.code).toContain("from './_models.js'")
1061
- expect(scope.code).toContain('Message')
1062
- })
1063
-
1064
- it('--no-share-models (shareModels:false) restores byte-identical legacy output', async () => {
1065
- const off = await generateClient({ envelope: envelopeWithSharedMessage, outDir: tmpA, serviceName: 'Api', shareModels: false, namespaceTypes: true, selfContained: false })
1066
- // no _models.ts emitted
1067
- expect(off.find((f) => f.path.endsWith('_models.ts'))).toBeUndefined()
1068
- })
1069
-
1070
- it('schemas without $id are unchanged when shareModels is on', async () => {
1071
- const on = await generateClient({ envelope: envelopeNoIds, outDir: t1, shareModels: true, namespaceTypes: true, selfContained: false, serviceName: 'Api' })
1072
- const off = await generateClient({ envelope: envelopeNoIds, outDir: t2, shareModels: false, namespaceTypes: true, selfContained: false, serviceName: 'Api' })
1073
- const code = (fs: typeof on, n: string) => fs.find((f) => f.path.endsWith(n))!.code
1074
- // scope output identical (ignoring _models.ts which won't exist either way)
1075
- expect(code(on, 'users.ts')).toBe(code(off, 'users.ts'))
1076
- })
1077
- ```
1078
-
1079
- - [ ] **Step 2: Run test to verify it fails**
1080
-
1081
- Run: `npx vitest run src/codegen/targets/ts/run.test.ts`
1082
- Expected: FAIL — `shareModels` not accepted / `_models.ts` not emitted.
1083
-
1084
- - [ ] **Step 3: Thread the options**
1085
-
1086
- - `GenerateClientOptions` (index.ts:7-26): add `shareModels?: boolean` and `sharedTypesImport?: SharedTypesImportMap`. Pass both into `runPipeline` (index.ts:30-48).
1087
- - `pipeline.ts`: forward to the per-target input.
1088
- - `target-run.ts` `TargetRunInput`: add `shareModels?: boolean`, `sharedTypesImport?: SharedTypesImportMap`.
1089
- - Default `shareModels` to `true` at the public boundary (cli/pipeline), `false` fallback in low-level run module (mirror the `cleanOutDir` convention).
1090
-
1091
- - [ ] **Step 4: Implement in `targets/ts/run.ts`**
1092
-
1093
- After `const errorKeys = ...` and before the scope loop:
1094
-
1095
- ```ts
1096
- import { collectModels, resolveModelImports } from '../../collect-models.js'
1097
- import { emitModelsFile } from '../../emit-models.js'
1098
-
1099
- const shareModels = input.shareModels ?? false
1100
- const models = shareModels
1101
- ? resolveModelImports(collectModels(envelope.routes), input.sharedTypesImport)
1102
- : []
1103
- const modelsById = new Map(models.map((m) => [m.id, m]))
1104
- ```
1105
-
1106
- Pass `modelsById` into `emitScopeFile(group, { ..., models: modelsById })`. After the scope loop, emit `_models.ts`:
1107
-
1108
- ```ts
1109
- if (models.length > 0) {
1110
- const modelsCode = await emitModelsFile(models, { ajsc: ajscOpts, namespaceTypes, serviceName })
1111
- if (modelsCode != null) {
1112
- const ml = modelsCode.split('\n'); ml.splice(1, 0, hashComment)
1113
- files.push({ path: join(outDir, '_models.ts'), code: ml.join('\n') })
1114
- }
1115
- }
1116
- ```
1117
-
1118
- Guard the reserved-filename check (run.ts:44-52) to also reject a scope named `_models` in self-contained-or-share-models mode.
1119
-
1120
- - [ ] **Step 5: Implement the scope reference in `emit-scope.ts` (per Task 8 mechanism)**
1121
-
1122
- `emitScopeFile`/`formatTypes` gain a `models?: Map<string, ResolvedModel>` context field. Before converting a route schema:
1123
-
1124
- - **Mechanism A (`$ref`):** pre-process the route schema — replace any subschema whose `$id` is in `models` with `{ $ref: '#/$defs/<name>' }` and attach a `$defs` table containing each referenced model. Feed to ajsc; capture references to the model names. Emit `import type { Message } from './_models.js'` (flat) at the top of the scope file for every referenced model (collect referenced names per scope). In namespace mode, reference the bare `Message` (imported) inside the namespace — confirm the spike output uses the imported name.
1125
- - **Mechanism B (substitution):** convert the route schema as today, then for each model whose `$id` appears in this route, string-substitute the emitted structural literal with the model name (word-boundary patch, same approach as `renameExtractedTypes`), and add the `_models.js` import.
1126
-
1127
- Either way: add the `_models.js` type-import line for the set of model names referenced in the scope file. Match the existing import style (the file already imports from `./_types`/client path).
1128
-
1129
- - [ ] **Step 6: Run the test + full suite**
1130
-
1131
- Run: `npx vitest run src/codegen/targets/ts/run.test.ts && npm run test`
1132
- Expected: PASS. Verify the no-`$id` regression (byte-identical scope output) is green.
1133
-
1134
- - [ ] **Step 7: Build + smoke-generate against the demo server**
1135
-
1136
- Run: `npm run build`. Then regenerate the repo's own client per the documented loop and eyeball `messages.ts` + `_models.ts`: one `Message` type, scopes reference it.
1137
-
1138
- - [ ] **Step 8: Commit**
1139
-
1140
- ```bash
1141
- git add src/codegen
1142
- git commit -m "feat(codegen): hoist \$id schemas into _models.ts and reference them from scopes"
1143
- ```
1144
-
1145
- ### Task 13: `--share-models` flag + `sharedTypesImport` config
1146
-
1147
- **Files:**
1148
- - Modify: `src/codegen/bin/cli.ts` (`CodegenConfig`, `ParsedArgs`, parse loop, pass-through to `generateClient`)
1149
- - Modify: `src/codegen/bin/flag-specs.ts` is already done (Task 1 included the two flags)
1150
- - Test: `src/codegen/bin/cli.test.ts`
1151
-
1152
- - [ ] **Step 1: Write the failing test**
1153
-
1154
- ```ts
1155
- it('parses --share-models / --no-share-models', () => {
1156
- expect(parseArgs(['--out', 'g', '--url', 'u']).shareModels).toBe(true) // default on
1157
- expect(parseArgs(['--out', 'g', '--url', 'u', '--no-share-models']).shareModels).toBe(false)
1158
- })
1159
-
1160
- it('reads sharedTypesImport from config', () => {
1161
- const cfg = { sharedTypesImport: { 'urn:msg': { module: '@shared/schemas', name: 'Message' } } }
1162
- expect(parseArgs(['--out', 'g', '--url', 'u'], cfg as any).sharedTypesImport).toEqual(cfg.sharedTypesImport)
1163
- })
1164
- ```
1165
-
1166
- - [ ] **Step 2: Run test to verify it fails**
1167
-
1168
- Run: `npx vitest run src/codegen/bin/cli.test.ts`
1169
- Expected: FAIL — `shareModels`/`sharedTypesImport` absent.
1170
-
1171
- - [ ] **Step 3: Implement**
1172
-
1173
- - `CodegenConfig` (cli.ts:12-29): add `shareModels?: boolean` and `sharedTypesImport?: Record<string, { module: string; name: string }>`.
1174
- - `ParsedArgs` (31-48): add `shareModels: boolean` and `sharedTypesImport?: ...`.
1175
- - In `parseArgs`: `let shareModels = config?.shareModels ?? true` and `const sharedTypesImport = config?.sharedTypesImport`. Add parse branches:
1176
-
1177
- ```ts
1178
- } else if (arg === '--share-models') {
1179
- shareModels = true
1180
- } else if (arg === '--no-share-models') {
1181
- shareModels = false
1182
- ```
1183
-
1184
- - Return them in the object (spread `sharedTypesImport` only when defined).
1185
- - In `main()` where `generateClient` is called, pass `shareModels` and `sharedTypesImport`.
1186
-
1187
- (`--share-models`/`--no-share-models` are already in `FLAG_SPECS`/`KNOWN_FLAGS` from Task 1, so the unknown-flag guard accepts them — verify.)
1188
-
1189
- - [ ] **Step 4: Run test + build**
1190
-
1191
- Run: `npx vitest run src/codegen/bin/cli.test.ts && npm run build`
1192
- Expected: PASS.
1193
-
1194
- - [ ] **Step 5: Commit**
1195
-
1196
- ```bash
1197
- git add src/codegen/bin/cli.ts src/codegen/bin/cli.test.ts
1198
- git commit -m "feat(codegen): --share-models flag + sharedTypesImport config"
1199
- ```
1200
-
1201
- ---
1202
-
1203
- ## #5 — Scaffolder file-naming knobs
1204
-
1205
- ### Task 14: `fileNameStyle` + `groupBy` in the scaffold skill + templates
1206
-
1207
- **Files:**
1208
- - Modify: `agent_config/claude-code/skills/ts-procedures/SKILL.md` (scaffold-mode section, ~lines 260-304)
1209
- - Modify: `agent_config/claude-code/skills/ts-procedures/templates/procedure.md`, `stream-procedure.md`, `hono.md`, `client.md` (output-filename lines)
1210
-
1211
- This is markdown/instruction content (no runtime tests). Verify by reading the rendered instructions and the placeholder logic.
1212
-
1213
- - [ ] **Step 1: Add the two args to SKILL.md scaffold mode**
1214
-
1215
- In the scaffold-mode section, document:
1216
-
1217
- ```markdown
1218
- **Optional flags:**
1219
- - `fileNameStyle` — `PascalCase` (default) | `kebab.concern`.
1220
- - `PascalCase`: `GetUser.procedure.ts`
1221
- - `kebab.concern`: `get-user.procedure.ts`
1222
- - `groupBy` — `flat` (default, CWD) | `scope`.
1223
- - `scope`: writes under `<scope>/`, e.g. `users/get-user.procedure.ts`. Scope is the
1224
- procedure's `scope` (from its config), kebab-cased.
1225
-
1226
- **Default inference (when neither flag is passed):** inspect the target directory.
1227
- If existing procedure files are kebab-cased and grouped in per-scope folders, default
1228
- to `fileNameStyle: kebab.concern` + `groupBy: scope`. Otherwise default to
1229
- `PascalCase` + `flat`. State the inferred choice before writing files.
1230
- ```
1231
-
1232
- Add placeholder derivations:
1233
-
1234
- ```markdown
1235
- Derived placeholders:
1236
- - `{{Name}}` — PascalCase (e.g. `GetUser`)
1237
- - `{{kebab}}` — kebab-case (e.g. `get-user`)
1238
- - `{{scope}}` — kebab-cased scope (e.g. `users`)
1239
- - `{{fileName}}` — `{{Name}}` when fileNameStyle=PascalCase, else `{{kebab}}`
1240
- - `{{dir}}` — `` (empty) when groupBy=flat, else `{{scope}}/`
1241
- ```
1242
-
1243
- Update the "Files Generated" table to use the computed name, e.g.
1244
- `{{dir}}{{fileName}}.procedure.ts`, `{{dir}}{{fileName}}.procedure.test.ts`.
1245
-
1246
- - [ ] **Step 2: Update each template's output-filename line**
1247
-
1248
- In `templates/procedure.md` (line ~3) and siblings, replace the hardcoded
1249
- `{{Name}}.procedure.ts` output reference with `{{dir}}{{fileName}}.procedure.ts`
1250
- (and `.test.ts`). Same for `stream-procedure.md` (`.stream.ts`), `hono.md`
1251
- (`.hono.ts`), `client.md` (`.client.ts`). Leave the code bodies untouched.
1252
-
1253
- - [ ] **Step 3: Verify the skill still installs cleanly**
1254
-
1255
- The installer is a pure recursive copy (`agent_config/lib/install-claude.mjs`), so no transform to update. Run any agent_config check if present:
1256
-
1257
- Run: `node agent_config/bin/setup.mjs --dry-run` (or `npx ts-procedures-setup --dry-run` from a consumer) to confirm the files still copy. If a `--check` CI script exists, run it.
1258
-
1259
- - [ ] **Step 4: Commit**
1260
-
1261
- ```bash
1262
- git add agent_config/claude-code/skills/ts-procedures
1263
- git commit -m "feat(scaffold): fileNameStyle + groupBy with auto-default inference"
1264
- ```
1265
-
1266
- ---
1267
-
1268
- ## Final verification
1269
-
1270
- - [ ] **Run the full suite:** `npm run test` — all green.
1271
- - [ ] **Lint:** `npm run lint` — clean.
1272
- - [ ] **Build:** `npm run build` — clean.
1273
- - [ ] **Regenerate the repo's own client** via the documented offline loop; confirm `_models.ts` holds one `Message`, scopes reference it, and `--help` prints usage.
1274
- - [ ] **Update `CLAUDE.md`** if any new public surface needs documenting (writeDocEnvelope, `_models.ts`, `sharedTypesImport`, function-valued headers, `--share-models`).
1275
- - [ ] **Update the downstream feedback file** marking #1–#5 resolved (mirror the 8.3.0 reset note style).
1276
-
1277
- ---
1278
-
1279
- ## Spec coverage check
1280
-
1281
- | Spec section | Task(s) |
1282
- |---|---|
1283
- | #1 structured flags + `--help`/bare, exclude from suggester | 1, 2, 3 |
1284
- | #2 `writeDocEnvelope` + docs | 4, 5 |
1285
- | #3 collect by `$id`, dedup, collision error | 9 |
1286
- | #3 `sharedTypesImport` map | 10, 13 |
1287
- | #3 `_models.ts` hub (generated + re-export) | 11 |
1288
- | #3 scope references model; default-on; no-`$id` unchanged; TS-only | 12, 13 |
1289
- | #3 ajsc/nested-`$id` risk spike | 8 |
1290
- | #4 function-valued headers, async resolve, per-call merge | 6 |
1291
- | #4 auth-seam docs | 7 |
1292
- | #5 `fileNameStyle` + `groupBy` + auto-default | 14 |