ts-procedures 7.3.0 → 8.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 (332) hide show
  1. package/README.md +65 -3
  2. package/agent_config/claude-code/agents/ts-procedures-architect.md +6 -8
  3. package/agent_config/claude-code/skills/ts-procedures/SKILL.md +30 -33
  4. package/agent_config/claude-code/skills/ts-procedures/anti-patterns.md +104 -53
  5. package/agent_config/claude-code/skills/ts-procedures/api-reference.md +205 -232
  6. package/agent_config/claude-code/skills/ts-procedures/patterns.md +80 -153
  7. package/agent_config/claude-code/skills/ts-procedures-review/SKILL.md +1 -1
  8. package/agent_config/claude-code/skills/ts-procedures-review/checklist.md +4 -5
  9. package/agent_config/claude-code/skills/ts-procedures-scaffold/SKILL.md +4 -7
  10. package/agent_config/claude-code/skills/ts-procedures-scaffold/templates/hono.md +223 -0
  11. package/agent_config/copilot/copilot-instructions.md +34 -48
  12. package/agent_config/cursor/cursorrules +34 -48
  13. package/build/client/call.js +4 -1
  14. package/build/client/call.js.map +1 -1
  15. package/build/client/call.test.js +23 -0
  16. package/build/client/call.test.js.map +1 -1
  17. package/build/client/fetch-adapter.js +3 -1
  18. package/build/client/fetch-adapter.js.map +1 -1
  19. package/build/client/fetch-adapter.test.js +11 -1
  20. package/build/client/fetch-adapter.test.js.map +1 -1
  21. package/build/client/index.test.js +7 -7
  22. package/build/client/index.test.js.map +1 -1
  23. package/build/client/request-builder.d.ts +1 -1
  24. package/build/client/request-builder.js +2 -2
  25. package/build/client/request-builder.js.map +1 -1
  26. package/build/client/stream.js +13 -2
  27. package/build/client/stream.js.map +1 -1
  28. package/build/client/stream.test.js +32 -7
  29. package/build/client/stream.test.js.map +1 -1
  30. package/build/client/typed-error-dispatch.test.js +8 -92
  31. package/build/client/typed-error-dispatch.test.js.map +1 -1
  32. package/build/client/types.d.ts +21 -3
  33. package/build/codegen/bin/cli.js +0 -0
  34. package/build/codegen/e2e.test.js +418 -23
  35. package/build/codegen/e2e.test.js.map +1 -1
  36. package/build/codegen/emit-errors.integration.test.js +1 -1
  37. package/build/codegen/emit-errors.integration.test.js.map +1 -1
  38. package/build/codegen/emit-scope.js +351 -55
  39. package/build/codegen/emit-scope.js.map +1 -1
  40. package/build/codegen/emit-scope.test.js +540 -110
  41. package/build/codegen/emit-scope.test.js.map +1 -1
  42. package/build/codegen/emit-types.d.ts +6 -2
  43. package/build/codegen/emit-types.js +81 -20
  44. package/build/codegen/emit-types.js.map +1 -1
  45. package/build/codegen/emit-types.test.js +70 -1
  46. package/build/codegen/emit-types.test.js.map +1 -1
  47. package/build/codegen/pipeline.test.js +7 -7
  48. package/build/codegen/pipeline.test.js.map +1 -1
  49. package/build/codegen/resolve-envelope.js +1 -1
  50. package/build/codegen/resolve-envelope.js.map +1 -1
  51. package/build/codegen/resolve-envelope.test.js +5 -5
  52. package/build/codegen/resolve-envelope.test.js.map +1 -1
  53. package/build/codegen/targets/_shared/route-slots.d.ts +8 -3
  54. package/build/codegen/targets/_shared/route-slots.js +49 -8
  55. package/build/codegen/targets/_shared/route-slots.js.map +1 -1
  56. package/build/codegen/targets/_shared/route-slots.test.js +99 -26
  57. package/build/codegen/targets/_shared/route-slots.test.js.map +1 -1
  58. package/build/codegen/targets/kotlin/emit-route-kotlin.test.js +88 -17
  59. package/build/codegen/targets/kotlin/emit-route-kotlin.test.js.map +1 -1
  60. package/build/codegen/targets/kotlin/emit-scope-kotlin.test.js +9 -6
  61. package/build/codegen/targets/kotlin/emit-scope-kotlin.test.js.map +1 -1
  62. package/build/codegen/targets/kotlin/integration.test.js +6 -0
  63. package/build/codegen/targets/kotlin/integration.test.js.map +1 -1
  64. package/build/codegen/targets/swift/access-level.test.js +8 -11
  65. package/build/codegen/targets/swift/access-level.test.js.map +1 -1
  66. package/build/codegen/targets/swift/emit-route-swift.test.js +91 -20
  67. package/build/codegen/targets/swift/emit-route-swift.test.js.map +1 -1
  68. package/build/codegen/targets/swift/emit-scope-swift.test.js +12 -9
  69. package/build/codegen/targets/swift/emit-scope-swift.test.js.map +1 -1
  70. package/build/codegen/targets/swift/integration.test.js +6 -0
  71. package/build/codegen/targets/swift/integration.test.js.map +1 -1
  72. package/build/create-http-stream.d.ts +58 -0
  73. package/build/create-http-stream.js +122 -0
  74. package/build/create-http-stream.js.map +1 -0
  75. package/build/create-http-stream.test.js +88 -0
  76. package/build/create-http-stream.test.js.map +1 -0
  77. package/build/create-http.d.ts +49 -0
  78. package/build/create-http.js +108 -0
  79. package/build/create-http.js.map +1 -0
  80. package/build/create-http.test.js +137 -0
  81. package/build/create-http.test.js.map +1 -0
  82. package/build/create-stream.d.ts +35 -0
  83. package/build/create-stream.js +123 -0
  84. package/build/create-stream.js.map +1 -0
  85. package/build/create-stream.test.js +428 -0
  86. package/build/create-stream.test.js.map +1 -0
  87. package/build/create.d.ts +28 -0
  88. package/build/create.js +82 -0
  89. package/build/create.js.map +1 -0
  90. package/build/create.test.js +483 -0
  91. package/build/create.test.js.map +1 -0
  92. package/build/exports.d.ts +2 -0
  93. package/build/implementations/http/astro/index.test.js +20 -12
  94. package/build/implementations/http/astro/index.test.js.map +1 -1
  95. package/build/implementations/http/doc-registry.js +1 -1
  96. package/build/implementations/http/doc-registry.js.map +1 -1
  97. package/build/implementations/http/doc-registry.test.js +36 -5
  98. package/build/implementations/http/doc-registry.test.js.map +1 -1
  99. package/build/implementations/http/error-dispatch.d.ts +76 -0
  100. package/build/implementations/http/error-dispatch.js +77 -0
  101. package/build/implementations/http/error-dispatch.js.map +1 -0
  102. package/build/implementations/http/error-dispatch.test.js +254 -0
  103. package/build/implementations/http/error-dispatch.test.js.map +1 -0
  104. package/build/implementations/http/error-taxonomy.d.ts +5 -5
  105. package/build/implementations/http/hono/docs/http-doc.d.ts +6 -0
  106. package/build/implementations/http/hono/docs/http-doc.js +42 -0
  107. package/build/implementations/http/hono/docs/http-doc.js.map +1 -0
  108. package/build/implementations/http/hono/docs/http-stream-doc.d.ts +6 -0
  109. package/build/implementations/http/hono/docs/http-stream-doc.js +40 -0
  110. package/build/implementations/http/hono/docs/http-stream-doc.js.map +1 -0
  111. package/build/implementations/http/hono/docs/rpc-doc.d.ts +6 -0
  112. package/build/implementations/http/hono/docs/rpc-doc.js +24 -0
  113. package/build/implementations/http/hono/docs/rpc-doc.js.map +1 -0
  114. package/build/implementations/http/hono/docs/stream-doc.d.ts +6 -0
  115. package/build/implementations/http/hono/docs/stream-doc.js +42 -0
  116. package/build/implementations/http/hono/docs/stream-doc.js.map +1 -0
  117. package/build/implementations/http/hono/handlers/http-stream.d.ts +10 -0
  118. package/build/implementations/http/hono/handlers/http-stream.js +123 -0
  119. package/build/implementations/http/hono/handlers/http-stream.js.map +1 -0
  120. package/build/implementations/http/hono/handlers/http-stream.test.js +128 -0
  121. package/build/implementations/http/hono/handlers/http-stream.test.js.map +1 -0
  122. package/build/implementations/http/hono/handlers/http.d.ts +10 -0
  123. package/build/implementations/http/hono/handlers/http.js +115 -0
  124. package/build/implementations/http/hono/handlers/http.js.map +1 -0
  125. package/build/implementations/http/hono/handlers/http.test.js +118 -0
  126. package/build/implementations/http/hono/handlers/http.test.js.map +1 -0
  127. package/build/implementations/http/hono/handlers/rpc.d.ts +11 -0
  128. package/build/implementations/http/hono/handlers/rpc.js +32 -0
  129. package/build/implementations/http/hono/handlers/rpc.js.map +1 -0
  130. package/build/implementations/http/hono/handlers/rpc.test.js +73 -0
  131. package/build/implementations/http/hono/handlers/rpc.test.js.map +1 -0
  132. package/build/implementations/http/hono/handlers/stream.d.ts +23 -0
  133. package/build/implementations/http/hono/handlers/stream.js +147 -0
  134. package/build/implementations/http/hono/handlers/stream.js.map +1 -0
  135. package/build/implementations/http/hono/handlers/stream.test.d.ts +1 -0
  136. package/build/implementations/http/hono/handlers/stream.test.js +177 -0
  137. package/build/implementations/http/hono/handlers/stream.test.js.map +1 -0
  138. package/build/implementations/http/hono/index.d.ts +57 -0
  139. package/build/implementations/http/hono/index.js +149 -0
  140. package/build/implementations/http/hono/index.js.map +1 -0
  141. package/build/implementations/http/hono/index.test.d.ts +1 -0
  142. package/build/implementations/http/hono/index.test.js +274 -0
  143. package/build/implementations/http/hono/index.test.js.map +1 -0
  144. package/build/implementations/http/hono/path.d.ts +17 -0
  145. package/build/implementations/http/hono/path.js +39 -0
  146. package/build/implementations/http/hono/path.js.map +1 -0
  147. package/build/implementations/http/hono/path.test.d.ts +1 -0
  148. package/build/implementations/http/hono/path.test.js +83 -0
  149. package/build/implementations/http/hono/path.test.js.map +1 -0
  150. package/build/implementations/http/hono/types.d.ts +51 -0
  151. package/build/implementations/http/hono/types.js.map +1 -0
  152. package/build/implementations/http/on-request-error.test.js +6 -96
  153. package/build/implementations/http/on-request-error.test.js.map +1 -1
  154. package/build/implementations/http/route-errors.test.js +11 -59
  155. package/build/implementations/http/route-errors.test.js.map +1 -1
  156. package/build/implementations/types.d.ts +43 -9
  157. package/build/index.d.ts +124 -124
  158. package/build/index.js +10 -221
  159. package/build/index.js.map +1 -1
  160. package/build/index.test.js +20 -919
  161. package/build/index.test.js.map +1 -1
  162. package/build/migration.test.d.ts +1 -0
  163. package/build/migration.test.js +34 -0
  164. package/build/migration.test.js.map +1 -0
  165. package/build/schema/compute-schema.d.ts +11 -3
  166. package/build/schema/compute-schema.js +13 -7
  167. package/build/schema/compute-schema.js.map +1 -1
  168. package/build/schema/parser.d.ts +11 -3
  169. package/build/schema/parser.js +49 -9
  170. package/build/schema/parser.js.map +1 -1
  171. package/build/stack-utils.js +8 -0
  172. package/build/stack-utils.js.map +1 -1
  173. package/build/types.d.ts +142 -0
  174. package/build/types.js.map +1 -0
  175. package/docs/astro-adapter.md +5 -5
  176. package/docs/core.md +15 -17
  177. package/docs/http-integrations.md +83 -170
  178. package/docs/streaming.md +3 -60
  179. package/docs/superpowers/plans/2026-05-07-astro-adapter.md +2 -7
  180. package/docs/superpowers/plans/2026-05-08-create-http.md +3355 -0
  181. package/docs/superpowers/plans/2026-05-08-hono-app-builder-convergence.md +3365 -0
  182. package/docs/superpowers/specs/2026-05-07-astro-adapter-design.md +1 -3
  183. package/docs/superpowers/specs/2026-05-08-create-http-design.md +409 -0
  184. package/docs/superpowers/specs/2026-05-08-hono-app-builder-convergence-design.md +411 -0
  185. package/package.json +4 -22
  186. package/src/client/call.test.ts +26 -0
  187. package/src/client/call.ts +4 -1
  188. package/src/client/fetch-adapter.test.ts +14 -1
  189. package/src/client/fetch-adapter.ts +3 -1
  190. package/src/client/index.test.ts +7 -7
  191. package/src/client/request-builder.ts +2 -2
  192. package/src/client/stream.test.ts +39 -7
  193. package/src/client/stream.ts +16 -2
  194. package/src/client/typed-error-dispatch.test.ts +7 -97
  195. package/src/client/types.ts +21 -3
  196. package/src/codegen/__fixtures__/users-envelope.json +119 -38
  197. package/src/codegen/e2e.test.ts +452 -24
  198. package/src/codegen/emit-errors.integration.test.ts +1 -1
  199. package/src/codegen/emit-scope.test.ts +581 -110
  200. package/src/codegen/emit-scope.ts +390 -61
  201. package/src/codegen/emit-types.test.ts +73 -1
  202. package/src/codegen/emit-types.ts +82 -21
  203. package/src/codegen/pipeline.test.ts +7 -7
  204. package/src/codegen/resolve-envelope.test.ts +5 -5
  205. package/src/codegen/resolve-envelope.ts +1 -1
  206. package/src/codegen/targets/_shared/route-slots.test.ts +109 -26
  207. package/src/codegen/targets/_shared/route-slots.ts +48 -11
  208. package/src/codegen/targets/kotlin/__fixtures__/users-golden.kt +73 -0
  209. package/src/codegen/targets/kotlin/emit-route-kotlin.test.ts +100 -17
  210. package/src/codegen/targets/kotlin/emit-scope-kotlin.test.ts +9 -6
  211. package/src/codegen/targets/kotlin/integration.test.ts +19 -0
  212. package/src/codegen/targets/swift/__fixtures__/users-golden.swift +79 -0
  213. package/src/codegen/targets/swift/access-level.test.ts +8 -11
  214. package/src/codegen/targets/swift/emit-route-swift.test.ts +103 -20
  215. package/src/codegen/targets/swift/emit-scope-swift.test.ts +12 -9
  216. package/src/codegen/targets/swift/integration.test.ts +17 -0
  217. package/src/create-http-stream.test.ts +97 -0
  218. package/src/create-http-stream.ts +191 -0
  219. package/src/create-http.test.ts +163 -0
  220. package/src/create-http.ts +211 -0
  221. package/src/create-stream.test.ts +565 -0
  222. package/src/create-stream.ts +228 -0
  223. package/src/create.test.ts +658 -0
  224. package/src/create.ts +172 -0
  225. package/src/exports.ts +2 -0
  226. package/src/implementations/http/README.md +135 -95
  227. package/src/implementations/http/astro/README.md +4 -5
  228. package/src/implementations/http/astro/index.test.ts +25 -18
  229. package/src/implementations/http/doc-registry.test.ts +42 -5
  230. package/src/implementations/http/doc-registry.ts +1 -1
  231. package/src/implementations/http/error-dispatch.test.ts +283 -0
  232. package/src/implementations/http/error-dispatch.ts +176 -0
  233. package/src/implementations/http/error-taxonomy.ts +5 -5
  234. package/src/implementations/http/hono/docs/http-doc.ts +43 -0
  235. package/src/implementations/http/hono/docs/http-stream-doc.ts +44 -0
  236. package/src/implementations/http/hono/docs/rpc-doc.ts +34 -0
  237. package/src/implementations/http/hono/docs/stream-doc.ts +53 -0
  238. package/src/implementations/http/hono/handlers/http-stream.test.ts +150 -0
  239. package/src/implementations/http/hono/handlers/http-stream.ts +152 -0
  240. package/src/implementations/http/hono/handlers/http.test.ts +130 -0
  241. package/src/implementations/http/hono/handlers/http.ts +147 -0
  242. package/src/implementations/http/hono/handlers/rpc.test.ts +81 -0
  243. package/src/implementations/http/hono/handlers/rpc.ts +54 -0
  244. package/src/implementations/http/hono/handlers/stream.test.ts +198 -0
  245. package/src/implementations/http/hono/handlers/stream.ts +208 -0
  246. package/src/implementations/http/hono/index.test.ts +329 -0
  247. package/src/implementations/http/hono/index.ts +204 -0
  248. package/src/implementations/http/hono/path.test.ts +96 -0
  249. package/src/implementations/http/hono/path.ts +59 -0
  250. package/src/implementations/http/hono/types.ts +93 -0
  251. package/src/implementations/http/on-request-error.test.ts +10 -116
  252. package/src/implementations/http/route-errors.test.ts +11 -77
  253. package/src/implementations/types.ts +44 -9
  254. package/src/index.test.ts +22 -1249
  255. package/src/index.ts +49 -485
  256. package/src/migration.test.ts +48 -0
  257. package/src/schema/compute-schema.ts +26 -12
  258. package/src/schema/parser.ts +62 -12
  259. package/src/stack-utils.ts +8 -0
  260. package/src/types.ts +133 -0
  261. package/agent_config/claude-code/skills/ts-procedures-scaffold/templates/express-rpc.md +0 -137
  262. package/agent_config/claude-code/skills/ts-procedures-scaffold/templates/hono-api.md +0 -173
  263. package/agent_config/claude-code/skills/ts-procedures-scaffold/templates/hono-rpc.md +0 -142
  264. package/agent_config/claude-code/skills/ts-procedures-scaffold/templates/hono-stream.md +0 -147
  265. package/build/implementations/http/express-rpc/error-taxonomy.test.js +0 -83
  266. package/build/implementations/http/express-rpc/error-taxonomy.test.js.map +0 -1
  267. package/build/implementations/http/express-rpc/index.d.ts +0 -125
  268. package/build/implementations/http/express-rpc/index.js +0 -216
  269. package/build/implementations/http/express-rpc/index.js.map +0 -1
  270. package/build/implementations/http/express-rpc/index.test.js +0 -684
  271. package/build/implementations/http/express-rpc/index.test.js.map +0 -1
  272. package/build/implementations/http/express-rpc/types.d.ts +0 -11
  273. package/build/implementations/http/express-rpc/types.js.map +0 -1
  274. package/build/implementations/http/hono-api/error-taxonomy.test.js +0 -137
  275. package/build/implementations/http/hono-api/error-taxonomy.test.js.map +0 -1
  276. package/build/implementations/http/hono-api/index.d.ts +0 -151
  277. package/build/implementations/http/hono-api/index.js +0 -344
  278. package/build/implementations/http/hono-api/index.js.map +0 -1
  279. package/build/implementations/http/hono-api/index.test.js +0 -992
  280. package/build/implementations/http/hono-api/index.test.js.map +0 -1
  281. package/build/implementations/http/hono-api/types.d.ts +0 -13
  282. package/build/implementations/http/hono-api/types.js.map +0 -1
  283. package/build/implementations/http/hono-rpc/error-taxonomy.test.js +0 -64
  284. package/build/implementations/http/hono-rpc/error-taxonomy.test.js.map +0 -1
  285. package/build/implementations/http/hono-rpc/index.d.ts +0 -130
  286. package/build/implementations/http/hono-rpc/index.js +0 -209
  287. package/build/implementations/http/hono-rpc/index.js.map +0 -1
  288. package/build/implementations/http/hono-rpc/index.test.js +0 -828
  289. package/build/implementations/http/hono-rpc/index.test.js.map +0 -1
  290. package/build/implementations/http/hono-rpc/types.d.ts +0 -11
  291. package/build/implementations/http/hono-rpc/types.js +0 -2
  292. package/build/implementations/http/hono-rpc/types.js.map +0 -1
  293. package/build/implementations/http/hono-stream/error-taxonomy.test.js +0 -159
  294. package/build/implementations/http/hono-stream/error-taxonomy.test.js.map +0 -1
  295. package/build/implementations/http/hono-stream/index.d.ts +0 -171
  296. package/build/implementations/http/hono-stream/index.js +0 -415
  297. package/build/implementations/http/hono-stream/index.js.map +0 -1
  298. package/build/implementations/http/hono-stream/index.test.js +0 -1383
  299. package/build/implementations/http/hono-stream/index.test.js.map +0 -1
  300. package/build/implementations/http/hono-stream/types.d.ts +0 -15
  301. package/build/implementations/http/hono-stream/types.js +0 -2
  302. package/build/implementations/http/hono-stream/types.js.map +0 -1
  303. package/src/implementations/http/express-rpc/README.md +0 -280
  304. package/src/implementations/http/express-rpc/error-taxonomy.test.ts +0 -103
  305. package/src/implementations/http/express-rpc/index.test.ts +0 -957
  306. package/src/implementations/http/express-rpc/index.ts +0 -327
  307. package/src/implementations/http/express-rpc/types.ts +0 -16
  308. package/src/implementations/http/hono-api/README.md +0 -284
  309. package/src/implementations/http/hono-api/error-taxonomy.test.ts +0 -179
  310. package/src/implementations/http/hono-api/index.test.ts +0 -1341
  311. package/src/implementations/http/hono-api/index.ts +0 -519
  312. package/src/implementations/http/hono-api/types.ts +0 -16
  313. package/src/implementations/http/hono-rpc/README.md +0 -357
  314. package/src/implementations/http/hono-rpc/error-taxonomy.test.ts +0 -82
  315. package/src/implementations/http/hono-rpc/index.test.ts +0 -1107
  316. package/src/implementations/http/hono-rpc/index.ts +0 -320
  317. package/src/implementations/http/hono-rpc/types.ts +0 -16
  318. package/src/implementations/http/hono-stream/README.md +0 -559
  319. package/src/implementations/http/hono-stream/error-taxonomy.test.ts +0 -178
  320. package/src/implementations/http/hono-stream/index.test.ts +0 -1804
  321. package/src/implementations/http/hono-stream/index.ts +0 -622
  322. package/src/implementations/http/hono-stream/types.ts +0 -20
  323. /package/build/{implementations/http/express-rpc/error-taxonomy.test.d.ts → create-http-stream.test.d.ts} +0 -0
  324. /package/build/{implementations/http/express-rpc/index.test.d.ts → create-http.test.d.ts} +0 -0
  325. /package/build/{implementations/http/hono-api/error-taxonomy.test.d.ts → create-stream.test.d.ts} +0 -0
  326. /package/build/{implementations/http/hono-api/index.test.d.ts → create.test.d.ts} +0 -0
  327. /package/build/implementations/http/{hono-rpc/error-taxonomy.test.d.ts → error-dispatch.test.d.ts} +0 -0
  328. /package/build/implementations/http/{hono-rpc/index.test.d.ts → hono/handlers/http-stream.test.d.ts} +0 -0
  329. /package/build/implementations/http/{hono-stream/error-taxonomy.test.d.ts → hono/handlers/http.test.d.ts} +0 -0
  330. /package/build/implementations/http/{hono-stream/index.test.d.ts → hono/handlers/rpc.test.d.ts} +0 -0
  331. /package/build/implementations/http/{express-rpc → hono}/types.js +0 -0
  332. /package/build/{implementations/http/hono-api/types.js → types.js} +0 -0
@@ -3,8 +3,9 @@ import { generateClient } from './index.js'
3
3
  import { mkdirSync, rmSync, readFileSync, writeFileSync, existsSync } from 'node:fs'
4
4
  import { join } from 'node:path'
5
5
  import { tmpdir } from 'node:os'
6
- import type { DocEnvelope, RPCHttpRouteDoc, APIHttpRouteDoc, StreamHttpRouteDoc, ErrorDoc } from '../implementations/types.js'
6
+ import type { DocEnvelope, RPCHttpRouteDoc, APIHttpRouteDoc, StreamHttpRouteDoc, HttpStreamRouteDoc, ErrorDoc } from '../implementations/types.js'
7
7
  import { runTsc } from './test-helpers/run-tsc.js'
8
+ import { Type } from 'typebox'
8
9
 
9
10
  // ---------------------------------------------------------------------------
10
11
  // Fixtures
@@ -39,20 +40,24 @@ const apiRoute: APIHttpRouteDoc = {
39
40
  method: 'put',
40
41
  scope: 'users',
41
42
  jsonSchema: {
42
- pathParams: {
43
- type: 'object',
44
- properties: { id: { type: 'string' } },
45
- required: ['id'],
46
- },
47
- body: {
48
- type: 'object',
49
- properties: { name: { type: 'string' } },
50
- required: ['name'],
43
+ req: {
44
+ pathParams: {
45
+ type: 'object',
46
+ properties: { id: { type: 'string' } },
47
+ required: ['id'],
48
+ },
49
+ body: {
50
+ type: 'object',
51
+ properties: { name: { type: 'string' } },
52
+ required: ['name'],
53
+ },
51
54
  },
52
- response: {
53
- type: 'object',
54
- properties: { id: { type: 'string' }, name: { type: 'string' } },
55
- required: ['id', 'name'],
55
+ res: {
56
+ body: {
57
+ type: 'object',
58
+ properties: { id: { type: 'string' }, name: { type: 'string' } },
59
+ required: ['id', 'name'],
60
+ },
56
61
  },
57
62
  },
58
63
  }
@@ -94,6 +99,34 @@ const streamRoute: StreamHttpRouteDoc = {
94
99
  },
95
100
  }
96
101
 
102
+ const httpStreamRoute: HttpStreamRouteDoc = {
103
+ kind: 'http-stream',
104
+ name: 'WatchUsers',
105
+ scope: 'users',
106
+ method: 'get',
107
+ path: '/users/watch',
108
+ fullPath: '/api/users/watch',
109
+ streamMode: 'sse',
110
+ jsonSchema: {
111
+ req: {
112
+ query: {
113
+ type: 'object',
114
+ properties: { status: { type: 'string' } },
115
+ },
116
+ },
117
+ yield: {
118
+ type: 'object',
119
+ properties: { id: { type: 'string' }, event: { type: 'string' } },
120
+ required: ['id', 'event'],
121
+ },
122
+ returnType: {
123
+ type: 'object',
124
+ properties: { count: { type: 'number' } },
125
+ required: ['count'],
126
+ },
127
+ },
128
+ }
129
+
97
130
  const procedureErrorDoc: ErrorDoc = {
98
131
  name: 'ProcedureError',
99
132
  statusCode: 500,
@@ -129,7 +162,7 @@ const envelope: DocEnvelope = {
129
162
  basePath: '/api',
130
163
  headers: [],
131
164
  errors: [procedureErrorDoc, validationErrorDoc],
132
- routes: [rpcRoute, apiRoute, streamRoute],
165
+ routes: [rpcRoute, apiRoute, streamRoute, httpStreamRoute],
133
166
  }
134
167
 
135
168
  // ---------------------------------------------------------------------------
@@ -189,20 +222,20 @@ describe('E2E: generateClient full pipeline', () => {
189
222
 
190
223
  // ── users.ts — API route ───────────────────────────────────────────────────
191
224
 
192
- it('users.ts contains UpdateUserParams type', async () => {
225
+ it('users.ts contains UpdateUserReq type (structured request type)', async () => {
193
226
  tmpDir = makeTmpDir()
194
227
  await generateClient({ envelope, outDir: tmpDir })
195
228
 
196
229
  const content = readFileSync(join(tmpDir, 'users.ts'), 'utf-8')
197
- expect(content).toContain('UpdateUserParams')
230
+ expect(content).toContain('UpdateUserReq')
198
231
  })
199
232
 
200
- it('users.ts contains UpdateUserResponse type', async () => {
233
+ it('users.ts contains UpdateUserResponseBody type', async () => {
201
234
  tmpDir = makeTmpDir()
202
235
  await generateClient({ envelope, outDir: tmpDir })
203
236
 
204
237
  const content = readFileSync(join(tmpDir, 'users.ts'), 'utf-8')
205
- expect(content).toContain('UpdateUserResponse')
238
+ expect(content).toContain('UpdateUserResponseBody')
206
239
  })
207
240
 
208
241
  it('users.ts API callable uses fullPath in the path field', async () => {
@@ -289,6 +322,44 @@ describe('E2E: generateClient full pipeline', () => {
289
322
  expect(yieldTypeBody).not.toContain('retry')
290
323
  })
291
324
 
325
+ // ── users.ts — http-stream route ──────────────────────────────────────────
326
+
327
+ it('users.ts contains WatchUsers callable using client.stream with kind http-stream', async () => {
328
+ tmpDir = makeTmpDir()
329
+ await generateClient({ envelope, outDir: tmpDir })
330
+
331
+ const content = readFileSync(join(tmpDir, 'users.ts'), 'utf-8')
332
+ expect(content).toContain('client.stream')
333
+ expect(content).toContain("kind: 'http-stream'")
334
+ })
335
+
336
+ it('users.ts http-stream callable passes params: req (not bare req shorthand)', async () => {
337
+ tmpDir = makeTmpDir()
338
+ await generateClient({ envelope, outDir: tmpDir })
339
+
340
+ const content = readFileSync(join(tmpDir, 'users.ts'), 'utf-8')
341
+ // Must be 'params: req,' not just 'req,' — the shorthand drops channel info
342
+ expect(content).toContain('params: req,')
343
+ expect(content).not.toMatch(/^\s+req,\s*$/m)
344
+ })
345
+
346
+ it('users.ts http-stream callable uses fullPath', async () => {
347
+ tmpDir = makeTmpDir()
348
+ await generateClient({ envelope, outDir: tmpDir })
349
+
350
+ const content = readFileSync(join(tmpDir, 'users.ts'), 'utf-8')
351
+ expect(content).toContain("path: '/api/users/watch'")
352
+ })
353
+
354
+ it('users.ts http-stream callable returns TypedStream', async () => {
355
+ tmpDir = makeTmpDir()
356
+ await generateClient({ envelope, outDir: tmpDir })
357
+
358
+ const content = readFileSync(join(tmpDir, 'users.ts'), 'utf-8')
359
+ expect(content).toContain('TypedStream<')
360
+ expect(content).toContain("streamMode: 'sse'")
361
+ })
362
+
292
363
  // ── index.ts ───────────────────────────────────────────────────────────────
293
364
 
294
365
  it('index.ts contains the default Api bindings factory', async () => {
@@ -520,7 +591,7 @@ const typedAdapter: ClientAdapter = {
520
591
  async stream(req) {
521
592
  const trace: string | undefined = req.meta?.traceId
522
593
  void trace
523
- return { status: 200, headers: {}, body: (async function*() {})() }
594
+ return { status: 200, headers: new Headers(), body: (async function*() {})() }
524
595
  },
525
596
  }
526
597
 
@@ -735,7 +806,9 @@ void run
735
806
  const content = readFileSync(join(tmpDir, 'users.ts'), 'utf-8')
736
807
  expect(content).toContain('export namespace Users {')
737
808
  expect(content).toContain('export namespace GetUser {')
738
- expect(content).toContain('export namespace UpdateUser {')
809
+ // API routes now emit Req/Response sub-namespaces instead of a bare UpdateUser namespace
810
+ expect(content).toContain('export namespace Req {')
811
+ expect(content).toContain('export namespace Response {')
739
812
  })
740
813
 
741
814
  it('users.ts uses qualified type references in callables', async () => {
@@ -743,9 +816,10 @@ void run
743
816
  await generateClient({ envelope, outDir: tmpDir, namespaceTypes: true })
744
817
 
745
818
  const content = readFileSync(join(tmpDir, 'users.ts'), 'utf-8')
746
- // New emission: client.bindCallable<Users.GetUser.Params, Users.GetUser.Response>({...})
819
+ // RPC route still uses Params/Response naming
747
820
  expect(content).toContain('client.bindCallable<Users.GetUser.Params, Users.GetUser.Response>')
748
- expect(content).toContain('client.bindCallable<Users.UpdateUser.Params, Users.UpdateUser.Response>')
821
+ // API route uses Req sub-namespace for params and Response.Body for return
822
+ expect(content).toContain('client.bindCallable<Users.UpdateUser.Req, Users.UpdateUser.Response.Body>')
749
823
  })
750
824
 
751
825
  it('events.ts wraps stream types in namespace', async () => {
@@ -765,7 +839,8 @@ void run
765
839
  const content = readFileSync(join(tmpDir, 'users.ts'), 'utf-8')
766
840
  expect(content).not.toContain('export type GetUserParams')
767
841
  expect(content).not.toContain('export type GetUserResponse')
768
- expect(content).not.toContain('export type UpdateUserPathParams')
842
+ expect(content).not.toContain('export type UpdateUserReqPathParams')
843
+ expect(content).not.toContain('export type UpdateUserResponseBody')
769
844
  })
770
845
 
771
846
  it('enumStyle: enum produces named enums inside namespace', async () => {
@@ -855,4 +930,357 @@ void run
855
930
  })
856
931
  })
857
932
  })
933
+
934
+ // ── Duplicate-identifier bug: type shared between params and returnType ──────
935
+ //
936
+ // End-to-end gold-standard guards. ajsc glues sibling extracted sub-types
937
+ // with a single "\n"; emit-types only renames/dedups the first one, so a
938
+ // route whose body and response both yield >=2 sub-types emits the non-first
939
+ // sub-type twice -> `error TS2300: Duplicate identifier`. These compile the
940
+ // real generated output with tsc.
941
+ describe('shared nested types across body/response', () => {
942
+ const contactShape = {
943
+ type: 'object',
944
+ required: ['name', 'address'],
945
+ properties: {
946
+ name: { type: 'string' },
947
+ address: {
948
+ type: 'object',
949
+ required: ['street', 'city'],
950
+ properties: { street: { type: 'string' }, city: { type: 'string' } },
951
+ },
952
+ },
953
+ }
954
+
955
+ const sharedNestedEnvelope: DocEnvelope = {
956
+ basePath: '/api',
957
+ headers: [],
958
+ errors: [],
959
+ routes: [
960
+ {
961
+ kind: 'rpc',
962
+ name: 'SaveContact',
963
+ path: '/contacts/save/1',
964
+ method: 'post',
965
+ scope: 'contacts',
966
+ version: 1,
967
+ jsonSchema: {
968
+ body: { type: 'object', required: ['contact'], properties: { contact: contactShape } },
969
+ response: {
970
+ type: 'object',
971
+ required: ['contact', 'savedAt'],
972
+ properties: { savedAt: { type: 'string' }, contact: contactShape },
973
+ },
974
+ },
975
+ } satisfies RPCHttpRouteDoc,
976
+ ],
977
+ }
978
+
979
+ it('generated client compiles when a nested object is shared between body and response', async () => {
980
+ tmpDir = makeTmpDir()
981
+ await generateClient({
982
+ envelope: sharedNestedEnvelope,
983
+ outDir: tmpDir,
984
+ selfContained: true,
985
+ namespaceTypes: true,
986
+ })
987
+
988
+ runTsc({
989
+ tmpDir,
990
+ tsconfigInline: {
991
+ compilerOptions: {
992
+ strict: true,
993
+ target: 'ES2022',
994
+ module: 'ES2022',
995
+ moduleResolution: 'bundler',
996
+ verbatimModuleSyntax: true,
997
+ noEmit: true,
998
+ skipLibCheck: true,
999
+ },
1000
+ include: ['_types.ts', '_client.ts', 'index.ts', 'contacts.ts', '_errors.ts'],
1001
+ },
1002
+ })
1003
+ })
1004
+
1005
+ // Latent correctness guard: same property name (`detail`), DIFFERENT shape
1006
+ // in body vs response. If the rename leaves the response's sub-type pointing
1007
+ // at the body's `Detail`, a consumer that reads the response-only field fails
1008
+ // to compile — catching the silent wrong-reference bug that TS2300 hides when
1009
+ // the shapes happen to be structurally identical.
1010
+ const divergentEnvelope: DocEnvelope = {
1011
+ basePath: '/api',
1012
+ headers: [],
1013
+ errors: [],
1014
+ routes: [
1015
+ {
1016
+ kind: 'rpc',
1017
+ name: 'SyncData',
1018
+ path: '/sync/1',
1019
+ method: 'post',
1020
+ scope: 'sync',
1021
+ version: 1,
1022
+ jsonSchema: {
1023
+ body: {
1024
+ type: 'object',
1025
+ required: ['outer'],
1026
+ properties: {
1027
+ outer: {
1028
+ type: 'object',
1029
+ required: ['detail'],
1030
+ properties: {
1031
+ detail: {
1032
+ type: 'object',
1033
+ required: ['fromClient'],
1034
+ properties: { fromClient: { type: 'string' } },
1035
+ },
1036
+ },
1037
+ },
1038
+ },
1039
+ },
1040
+ response: {
1041
+ type: 'object',
1042
+ required: ['outer'],
1043
+ properties: {
1044
+ outer: {
1045
+ type: 'object',
1046
+ required: ['detail'],
1047
+ properties: {
1048
+ detail: {
1049
+ type: 'object',
1050
+ required: ['fromServer'],
1051
+ properties: { fromServer: { type: 'number' } },
1052
+ },
1053
+ },
1054
+ },
1055
+ },
1056
+ },
1057
+ },
1058
+ } satisfies RPCHttpRouteDoc,
1059
+ ],
1060
+ }
1061
+
1062
+ it('response sub-type keeps its own shape when a name collides with a differently-shaped body sub-type', async () => {
1063
+ tmpDir = makeTmpDir()
1064
+ await generateClient({
1065
+ envelope: divergentEnvelope,
1066
+ outDir: tmpDir,
1067
+ selfContained: true,
1068
+ namespaceTypes: true,
1069
+ })
1070
+
1071
+ // Consumer asserts the response's `detail` exposes `fromServer: number`
1072
+ // (its own shape) — not the body's `{ fromClient: string }`. Imports the
1073
+ // scope file directly to validate the generated route type precisely.
1074
+ const consumer = `
1075
+ import type { Sync } from './sync'
1076
+ const r: Sync.SyncData.Response = null as any
1077
+ const n: number = r.outer.detail.fromServer
1078
+ void n
1079
+ `
1080
+ writeFileSync(join(tmpDir, '__consumer.ts'), consumer)
1081
+
1082
+ runTsc({
1083
+ tmpDir,
1084
+ tsconfigInline: {
1085
+ compilerOptions: {
1086
+ strict: true,
1087
+ target: 'ES2022',
1088
+ module: 'ES2022',
1089
+ moduleResolution: 'bundler',
1090
+ verbatimModuleSyntax: true,
1091
+ noEmit: true,
1092
+ skipLibCheck: true,
1093
+ },
1094
+ include: ['_types.ts', '_client.ts', 'index.ts', 'sync.ts', '_errors.ts', '__consumer.ts'],
1095
+ },
1096
+ })
1097
+ })
1098
+ })
1099
+
1100
+ // ── Complex TypeBox-derived schemas (broad duplication / correctness sweep) ──
1101
+ //
1102
+ // Builds realistic schemas with TypeBox, round-trips them to plain JSON Schema
1103
+ // (exactly what lands in a DocEnvelope), and stresses every known collision
1104
+ // vector at once:
1105
+ // - a deeply-nested object (Product → Manufacturer → Hq/Warehouse → Geo)
1106
+ // shared between a route's body AND response (forces the response copy to
1107
+ // rename all of its sub-types and re-point every cross-reference);
1108
+ // - the same Product reused by a SECOND route in the same scope (separate
1109
+ // route namespaces must not clash);
1110
+ // - a sub-type (Geo) referenced by two siblings (Hq + Warehouse) — the
1111
+ // rename must patch BOTH referrers;
1112
+ // - shared Filter/Range across an API route's query + body channels;
1113
+ // - a body property literally named `params`, colliding with the route's
1114
+ // own `Params` shortName;
1115
+ // - arrays-of-objects, optionals, literal unions, and records.
1116
+ //
1117
+ // Run under the default (inline-union) mode AND `enumStyle: 'enum'`, where a
1118
+ // 3-literal union explodes into three separate enums (plus ajsc's own
1119
+ // internal renaming) — the largest fused declaration block we emit. The tsc
1120
+ // compile is the authoritative duplicate-identifier (TS2300) and
1121
+ // dangling/wrong-reference guard; the consumer additionally pins deep field
1122
+ // access through the renamed types.
1123
+ describe('complex TypeBox-derived schemas', () => {
1124
+ const clean = (s: unknown): Record<string, unknown> =>
1125
+ JSON.parse(JSON.stringify(s)) as Record<string, unknown>
1126
+
1127
+ const Geo = Type.Object({ lat: Type.Number(), lng: Type.Number() })
1128
+ const Address = Type.Object({ street: Type.String(), city: Type.String(), geo: Geo })
1129
+ const Manufacturer = Type.Object({
1130
+ name: Type.String(),
1131
+ hq: Address,
1132
+ warehouses: Type.Array(Address),
1133
+ })
1134
+ const Variant = Type.Object({
1135
+ sku: Type.String(),
1136
+ price: Type.Number(),
1137
+ inStock: Type.Boolean(),
1138
+ })
1139
+ const Product = Type.Object({
1140
+ id: Type.String(),
1141
+ category: Type.Union([Type.Literal('book'), Type.Literal('food'), Type.Literal('toy')]),
1142
+ status: Type.Optional(Type.Union([Type.Literal('active'), Type.Literal('archived')])),
1143
+ dimensions: Type.Object({ w: Type.Number(), h: Type.Number(), d: Type.Number() }),
1144
+ tags: Type.Array(Type.String()),
1145
+ variants: Type.Array(Variant),
1146
+ manufacturer: Manufacturer,
1147
+ })
1148
+
1149
+ const Range = Type.Object({ min: Type.Number(), max: Type.Number() })
1150
+ const Filter = Type.Object({
1151
+ category: Type.Optional(Type.Union([Type.Literal('book'), Type.Literal('food')])),
1152
+ price: Range,
1153
+ })
1154
+ const SavedSearch = Type.Object({ name: Type.String(), filter: Filter })
1155
+
1156
+ // Route 1: rpc — body + response share Product; body also has a `params`
1157
+ // property colliding with the route's Params shortName.
1158
+ const upsertProduct: RPCHttpRouteDoc = {
1159
+ kind: 'rpc',
1160
+ name: 'UpsertProduct',
1161
+ path: '/catalog/upsert/1',
1162
+ method: 'post',
1163
+ scope: 'catalog',
1164
+ version: 1,
1165
+ jsonSchema: {
1166
+ body: clean(Type.Object({ product: Product, params: Type.Object({ dryRun: Type.Boolean() }) })),
1167
+ response: clean(
1168
+ Type.Object({
1169
+ product: Product,
1170
+ audit: Type.Object({
1171
+ revision: Type.Integer(),
1172
+ editor: Type.Object({ id: Type.String(), name: Type.String() }),
1173
+ }),
1174
+ }),
1175
+ ),
1176
+ },
1177
+ }
1178
+
1179
+ // Route 2: api — query + body share Filter/Range; res reuses Product.
1180
+ const searchCatalog: APIHttpRouteDoc = {
1181
+ kind: 'api',
1182
+ name: 'SearchCatalog',
1183
+ path: '/catalog/search',
1184
+ fullPath: '/api/catalog/search',
1185
+ method: 'post',
1186
+ scope: 'catalog',
1187
+ jsonSchema: {
1188
+ req: {
1189
+ query: clean(Type.Object({ page: Type.Integer(), filter: Type.Optional(Filter) })),
1190
+ body: clean(Type.Object({ saved: Type.Array(SavedSearch) })),
1191
+ },
1192
+ res: {
1193
+ body: clean(
1194
+ Type.Object({
1195
+ items: Type.Array(Product),
1196
+ total: Type.Integer(),
1197
+ byCategory: Type.Record(Type.String(), Type.Integer()),
1198
+ }),
1199
+ ),
1200
+ },
1201
+ },
1202
+ }
1203
+
1204
+ const complexEnvelope: DocEnvelope = {
1205
+ basePath: '/api',
1206
+ headers: [],
1207
+ errors: [],
1208
+ routes: [upsertProduct, searchCatalog],
1209
+ }
1210
+
1211
+ const consumer = `
1212
+ import type { Catalog } from './catalog'
1213
+
1214
+ // Route 1 response: deep access through the shared-and-renamed Product, plus
1215
+ // the sub-type (Geo) shared by two referrers (hq + warehouses[]).
1216
+ const up: Catalog.UpsertProduct.Response = null as any
1217
+ const lat: number = up.product.manufacturer.hq.geo.lat
1218
+ const wlng: number = up.product.manufacturer.warehouses[0]!.geo.lng
1219
+ const sku: string = up.product.variants[0]!.sku
1220
+ const w: number = up.product.dimensions.w
1221
+ const rev: number = up.audit.revision
1222
+ const editorName: string = up.audit.editor.name
1223
+ void lat; void wlng; void sku; void w; void rev; void editorName
1224
+
1225
+ // Route 1 params (body): shared Product + the colliding 'params' property.
1226
+ const body: Catalog.UpsertProduct.Params = null as any
1227
+ const pid: string = body.product.id
1228
+ const dry: boolean = body.params.dryRun
1229
+ void pid; void dry
1230
+
1231
+ // Route 2 request: Filter/Range shared across query + body channels.
1232
+ const req: Catalog.SearchCatalog.Req = null as any
1233
+ const page: number = req.query.page
1234
+ const qMin: number | undefined = req.query.filter?.price.min
1235
+ const bMax: number = req.body.saved[0]!.filter.price.max
1236
+ void page; void qMin; void bMax
1237
+
1238
+ // Route 2 response: array of the shared Product + a record.
1239
+ const res: Catalog.SearchCatalog.Response.Body = null as any
1240
+ const total: number = res.total
1241
+ const firstSku: string = res.items[0]!.variants[0]!.sku
1242
+ const bookCount: number | undefined = res.byCategory['book']
1243
+ void total; void firstSku; void bookCount
1244
+ `
1245
+
1246
+ const tsconfig = {
1247
+ compilerOptions: {
1248
+ strict: true,
1249
+ target: 'ES2022',
1250
+ module: 'ES2022',
1251
+ moduleResolution: 'bundler',
1252
+ verbatimModuleSyntax: true,
1253
+ noEmit: true,
1254
+ skipLibCheck: true,
1255
+ },
1256
+ include: ['_types.ts', '_client.ts', 'index.ts', 'catalog.ts', '_errors.ts', '__consumer.ts'],
1257
+ }
1258
+
1259
+ it('compiles end-to-end with deep shared/renamed types (default inline-union mode)', async () => {
1260
+ tmpDir = makeTmpDir()
1261
+ await generateClient({
1262
+ envelope: complexEnvelope,
1263
+ outDir: tmpDir,
1264
+ selfContained: true,
1265
+ namespaceTypes: true,
1266
+ })
1267
+ writeFileSync(join(tmpDir, '__consumer.ts'), consumer)
1268
+ runTsc({ tmpDir, tsconfigInline: tsconfig })
1269
+ })
1270
+
1271
+ it('compiles end-to-end under enumStyle: enum (union-of-literals explodes into many enums)', async () => {
1272
+ tmpDir = makeTmpDir()
1273
+ await generateClient({
1274
+ envelope: complexEnvelope,
1275
+ outDir: tmpDir,
1276
+ selfContained: true,
1277
+ namespaceTypes: true,
1278
+ ajsc: { enumStyle: 'enum' },
1279
+ })
1280
+ // In enum mode `category` becomes enum unions; the deep object access still
1281
+ // holds, so reuse the consumer (it never pins the enum member types).
1282
+ writeFileSync(join(tmpDir, '__consumer.ts'), consumer)
1283
+ runTsc({ tmpDir, tsconfigInline: tsconfig })
1284
+ })
1285
+ })
858
1286
  })
@@ -52,7 +52,7 @@ describe('generated _errors.ts — runtime behavior', () => {
52
52
  path: '/ping',
53
53
  fullPath: '/api/ping',
54
54
  method: 'get',
55
- jsonSchema: { response: { type: 'object', properties: {} } },
55
+ jsonSchema: { response: { type: 'object', properties: {} } } as any, // TODO(Phase 8): migrate to new req/res envelope shape
56
56
  },
57
57
  ],
58
58
  }