ts-procedures 7.2.0 → 8.0.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 (325) 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 +139 -53
  5. package/agent_config/claude-code/skills/ts-procedures/api-reference.md +208 -231
  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 +36 -48
  12. package/agent_config/cursor/cursorrules +36 -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 +87 -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 +308 -47
  39. package/build/codegen/emit-scope.js.map +1 -1
  40. package/build/codegen/emit-scope.test.js +363 -110
  41. package/build/codegen/emit-scope.test.js.map +1 -1
  42. package/build/codegen/pipeline.test.js +7 -7
  43. package/build/codegen/pipeline.test.js.map +1 -1
  44. package/build/codegen/resolve-envelope.js +1 -1
  45. package/build/codegen/resolve-envelope.js.map +1 -1
  46. package/build/codegen/resolve-envelope.test.js +5 -5
  47. package/build/codegen/resolve-envelope.test.js.map +1 -1
  48. package/build/codegen/targets/_shared/route-slots.d.ts +8 -3
  49. package/build/codegen/targets/_shared/route-slots.js +49 -8
  50. package/build/codegen/targets/_shared/route-slots.js.map +1 -1
  51. package/build/codegen/targets/_shared/route-slots.test.js +99 -26
  52. package/build/codegen/targets/_shared/route-slots.test.js.map +1 -1
  53. package/build/codegen/targets/kotlin/emit-route-kotlin.test.js +88 -17
  54. package/build/codegen/targets/kotlin/emit-route-kotlin.test.js.map +1 -1
  55. package/build/codegen/targets/kotlin/emit-scope-kotlin.test.js +9 -6
  56. package/build/codegen/targets/kotlin/emit-scope-kotlin.test.js.map +1 -1
  57. package/build/codegen/targets/kotlin/integration.test.js +6 -0
  58. package/build/codegen/targets/kotlin/integration.test.js.map +1 -1
  59. package/build/codegen/targets/swift/access-level.test.js +8 -11
  60. package/build/codegen/targets/swift/access-level.test.js.map +1 -1
  61. package/build/codegen/targets/swift/emit-route-swift.test.js +91 -20
  62. package/build/codegen/targets/swift/emit-route-swift.test.js.map +1 -1
  63. package/build/codegen/targets/swift/emit-scope-swift.test.js +12 -9
  64. package/build/codegen/targets/swift/emit-scope-swift.test.js.map +1 -1
  65. package/build/codegen/targets/swift/integration.test.js +6 -0
  66. package/build/codegen/targets/swift/integration.test.js.map +1 -1
  67. package/build/create-http-stream.d.ts +58 -0
  68. package/build/create-http-stream.js +122 -0
  69. package/build/create-http-stream.js.map +1 -0
  70. package/build/create-http-stream.test.js +88 -0
  71. package/build/create-http-stream.test.js.map +1 -0
  72. package/build/create-http.d.ts +49 -0
  73. package/build/create-http.js +108 -0
  74. package/build/create-http.js.map +1 -0
  75. package/build/create-http.test.js +137 -0
  76. package/build/create-http.test.js.map +1 -0
  77. package/build/create-stream.d.ts +35 -0
  78. package/build/create-stream.js +123 -0
  79. package/build/create-stream.js.map +1 -0
  80. package/build/create-stream.test.js +428 -0
  81. package/build/create-stream.test.js.map +1 -0
  82. package/build/create.d.ts +28 -0
  83. package/build/create.js +82 -0
  84. package/build/create.js.map +1 -0
  85. package/build/create.test.js +483 -0
  86. package/build/create.test.js.map +1 -0
  87. package/build/exports.d.ts +2 -0
  88. package/build/implementations/http/astro/index.test.js +20 -12
  89. package/build/implementations/http/astro/index.test.js.map +1 -1
  90. package/build/implementations/http/doc-registry.js +1 -1
  91. package/build/implementations/http/doc-registry.js.map +1 -1
  92. package/build/implementations/http/doc-registry.test.js +36 -5
  93. package/build/implementations/http/doc-registry.test.js.map +1 -1
  94. package/build/implementations/http/error-dispatch.d.ts +76 -0
  95. package/build/implementations/http/error-dispatch.js +77 -0
  96. package/build/implementations/http/error-dispatch.js.map +1 -0
  97. package/build/implementations/http/error-dispatch.test.js +254 -0
  98. package/build/implementations/http/error-dispatch.test.js.map +1 -0
  99. package/build/implementations/http/error-taxonomy.d.ts +5 -5
  100. package/build/implementations/http/hono/docs/http-doc.d.ts +6 -0
  101. package/build/implementations/http/hono/docs/http-doc.js +42 -0
  102. package/build/implementations/http/hono/docs/http-doc.js.map +1 -0
  103. package/build/implementations/http/hono/docs/http-stream-doc.d.ts +6 -0
  104. package/build/implementations/http/hono/docs/http-stream-doc.js +40 -0
  105. package/build/implementations/http/hono/docs/http-stream-doc.js.map +1 -0
  106. package/build/implementations/http/hono/docs/rpc-doc.d.ts +6 -0
  107. package/build/implementations/http/hono/docs/rpc-doc.js +24 -0
  108. package/build/implementations/http/hono/docs/rpc-doc.js.map +1 -0
  109. package/build/implementations/http/hono/docs/stream-doc.d.ts +6 -0
  110. package/build/implementations/http/hono/docs/stream-doc.js +42 -0
  111. package/build/implementations/http/hono/docs/stream-doc.js.map +1 -0
  112. package/build/implementations/http/hono/handlers/http-stream.d.ts +10 -0
  113. package/build/implementations/http/hono/handlers/http-stream.js +123 -0
  114. package/build/implementations/http/hono/handlers/http-stream.js.map +1 -0
  115. package/build/implementations/http/hono/handlers/http-stream.test.js +128 -0
  116. package/build/implementations/http/hono/handlers/http-stream.test.js.map +1 -0
  117. package/build/implementations/http/hono/handlers/http.d.ts +10 -0
  118. package/build/implementations/http/hono/handlers/http.js +115 -0
  119. package/build/implementations/http/hono/handlers/http.js.map +1 -0
  120. package/build/implementations/http/hono/handlers/http.test.js +118 -0
  121. package/build/implementations/http/hono/handlers/http.test.js.map +1 -0
  122. package/build/implementations/http/hono/handlers/rpc.d.ts +11 -0
  123. package/build/implementations/http/hono/handlers/rpc.js +32 -0
  124. package/build/implementations/http/hono/handlers/rpc.js.map +1 -0
  125. package/build/implementations/http/hono/handlers/rpc.test.js +73 -0
  126. package/build/implementations/http/hono/handlers/rpc.test.js.map +1 -0
  127. package/build/implementations/http/hono/handlers/stream.d.ts +23 -0
  128. package/build/implementations/http/hono/handlers/stream.js +147 -0
  129. package/build/implementations/http/hono/handlers/stream.js.map +1 -0
  130. package/build/implementations/http/hono/handlers/stream.test.d.ts +1 -0
  131. package/build/implementations/http/hono/handlers/stream.test.js +177 -0
  132. package/build/implementations/http/hono/handlers/stream.test.js.map +1 -0
  133. package/build/implementations/http/hono/index.d.ts +57 -0
  134. package/build/implementations/http/hono/index.js +149 -0
  135. package/build/implementations/http/hono/index.js.map +1 -0
  136. package/build/implementations/http/hono/index.test.d.ts +1 -0
  137. package/build/implementations/http/hono/index.test.js +274 -0
  138. package/build/implementations/http/hono/index.test.js.map +1 -0
  139. package/build/implementations/http/hono/path.d.ts +17 -0
  140. package/build/implementations/http/hono/path.js +39 -0
  141. package/build/implementations/http/hono/path.js.map +1 -0
  142. package/build/implementations/http/hono/path.test.d.ts +1 -0
  143. package/build/implementations/http/hono/path.test.js +83 -0
  144. package/build/implementations/http/hono/path.test.js.map +1 -0
  145. package/build/implementations/http/hono/types.d.ts +51 -0
  146. package/build/implementations/http/hono/types.js.map +1 -0
  147. package/build/implementations/http/on-request-error.test.js +6 -96
  148. package/build/implementations/http/on-request-error.test.js.map +1 -1
  149. package/build/implementations/http/route-errors.test.js +11 -59
  150. package/build/implementations/http/route-errors.test.js.map +1 -1
  151. package/build/implementations/types.d.ts +43 -9
  152. package/build/index.d.ts +125 -115
  153. package/build/index.js +10 -222
  154. package/build/index.js.map +1 -1
  155. package/build/index.test.js +30 -822
  156. package/build/index.test.js.map +1 -1
  157. package/build/migration.test.d.ts +1 -0
  158. package/build/migration.test.js +34 -0
  159. package/build/migration.test.js.map +1 -0
  160. package/build/schema/compute-schema.d.ts +11 -3
  161. package/build/schema/compute-schema.js +13 -7
  162. package/build/schema/compute-schema.js.map +1 -1
  163. package/build/schema/parser.d.ts +11 -3
  164. package/build/schema/parser.js +49 -9
  165. package/build/schema/parser.js.map +1 -1
  166. package/build/stack-utils.js +8 -0
  167. package/build/stack-utils.js.map +1 -1
  168. package/build/types.d.ts +142 -0
  169. package/build/types.js.map +1 -0
  170. package/docs/astro-adapter.md +5 -5
  171. package/docs/core.md +34 -17
  172. package/docs/http-integrations.md +83 -170
  173. package/docs/streaming.md +3 -60
  174. package/docs/superpowers/plans/2026-05-07-astro-adapter.md +2 -7
  175. package/docs/superpowers/plans/2026-05-08-create-http.md +3355 -0
  176. package/docs/superpowers/plans/2026-05-08-hono-app-builder-convergence.md +3365 -0
  177. package/docs/superpowers/specs/2026-05-07-astro-adapter-design.md +1 -3
  178. package/docs/superpowers/specs/2026-05-08-create-http-design.md +409 -0
  179. package/docs/superpowers/specs/2026-05-08-hono-app-builder-convergence-design.md +411 -0
  180. package/package.json +4 -22
  181. package/src/client/call.test.ts +26 -0
  182. package/src/client/call.ts +4 -1
  183. package/src/client/fetch-adapter.test.ts +14 -1
  184. package/src/client/fetch-adapter.ts +3 -1
  185. package/src/client/index.test.ts +7 -7
  186. package/src/client/request-builder.ts +2 -2
  187. package/src/client/stream.test.ts +39 -7
  188. package/src/client/stream.ts +16 -2
  189. package/src/client/typed-error-dispatch.test.ts +7 -97
  190. package/src/client/types.ts +21 -3
  191. package/src/codegen/__fixtures__/users-envelope.json +119 -38
  192. package/src/codegen/e2e.test.ts +98 -24
  193. package/src/codegen/emit-errors.integration.test.ts +1 -1
  194. package/src/codegen/emit-scope.test.ts +395 -110
  195. package/src/codegen/emit-scope.ts +350 -55
  196. package/src/codegen/pipeline.test.ts +7 -7
  197. package/src/codegen/resolve-envelope.test.ts +5 -5
  198. package/src/codegen/resolve-envelope.ts +1 -1
  199. package/src/codegen/targets/_shared/route-slots.test.ts +109 -26
  200. package/src/codegen/targets/_shared/route-slots.ts +48 -11
  201. package/src/codegen/targets/kotlin/__fixtures__/users-golden.kt +73 -0
  202. package/src/codegen/targets/kotlin/emit-route-kotlin.test.ts +100 -17
  203. package/src/codegen/targets/kotlin/emit-scope-kotlin.test.ts +9 -6
  204. package/src/codegen/targets/kotlin/integration.test.ts +19 -0
  205. package/src/codegen/targets/swift/__fixtures__/users-golden.swift +79 -0
  206. package/src/codegen/targets/swift/access-level.test.ts +8 -11
  207. package/src/codegen/targets/swift/emit-route-swift.test.ts +103 -20
  208. package/src/codegen/targets/swift/emit-scope-swift.test.ts +12 -9
  209. package/src/codegen/targets/swift/integration.test.ts +17 -0
  210. package/src/create-http-stream.test.ts +97 -0
  211. package/src/create-http-stream.ts +191 -0
  212. package/src/create-http.test.ts +163 -0
  213. package/src/create-http.ts +211 -0
  214. package/src/create-stream.test.ts +565 -0
  215. package/src/create-stream.ts +228 -0
  216. package/src/create.test.ts +658 -0
  217. package/src/create.ts +172 -0
  218. package/src/exports.ts +2 -0
  219. package/src/implementations/http/README.md +135 -95
  220. package/src/implementations/http/astro/README.md +4 -5
  221. package/src/implementations/http/astro/index.test.ts +25 -18
  222. package/src/implementations/http/doc-registry.test.ts +42 -5
  223. package/src/implementations/http/doc-registry.ts +1 -1
  224. package/src/implementations/http/error-dispatch.test.ts +283 -0
  225. package/src/implementations/http/error-dispatch.ts +176 -0
  226. package/src/implementations/http/error-taxonomy.ts +5 -5
  227. package/src/implementations/http/hono/docs/http-doc.ts +43 -0
  228. package/src/implementations/http/hono/docs/http-stream-doc.ts +44 -0
  229. package/src/implementations/http/hono/docs/rpc-doc.ts +34 -0
  230. package/src/implementations/http/hono/docs/stream-doc.ts +53 -0
  231. package/src/implementations/http/hono/handlers/http-stream.test.ts +150 -0
  232. package/src/implementations/http/hono/handlers/http-stream.ts +152 -0
  233. package/src/implementations/http/hono/handlers/http.test.ts +130 -0
  234. package/src/implementations/http/hono/handlers/http.ts +147 -0
  235. package/src/implementations/http/hono/handlers/rpc.test.ts +81 -0
  236. package/src/implementations/http/hono/handlers/rpc.ts +54 -0
  237. package/src/implementations/http/hono/handlers/stream.test.ts +198 -0
  238. package/src/implementations/http/hono/handlers/stream.ts +208 -0
  239. package/src/implementations/http/hono/index.test.ts +329 -0
  240. package/src/implementations/http/hono/index.ts +204 -0
  241. package/src/implementations/http/hono/path.test.ts +96 -0
  242. package/src/implementations/http/hono/path.ts +59 -0
  243. package/src/implementations/http/hono/types.ts +93 -0
  244. package/src/implementations/http/on-request-error.test.ts +10 -116
  245. package/src/implementations/http/route-errors.test.ts +11 -77
  246. package/src/implementations/types.ts +44 -9
  247. package/src/index.test.ts +35 -1091
  248. package/src/index.ts +50 -474
  249. package/src/migration.test.ts +48 -0
  250. package/src/schema/compute-schema.ts +26 -12
  251. package/src/schema/parser.ts +62 -12
  252. package/src/stack-utils.ts +8 -0
  253. package/src/types.ts +133 -0
  254. package/agent_config/claude-code/skills/ts-procedures-scaffold/templates/express-rpc.md +0 -137
  255. package/agent_config/claude-code/skills/ts-procedures-scaffold/templates/hono-api.md +0 -173
  256. package/agent_config/claude-code/skills/ts-procedures-scaffold/templates/hono-rpc.md +0 -142
  257. package/agent_config/claude-code/skills/ts-procedures-scaffold/templates/hono-stream.md +0 -147
  258. package/build/implementations/http/express-rpc/error-taxonomy.test.js +0 -83
  259. package/build/implementations/http/express-rpc/error-taxonomy.test.js.map +0 -1
  260. package/build/implementations/http/express-rpc/index.d.ts +0 -125
  261. package/build/implementations/http/express-rpc/index.js +0 -216
  262. package/build/implementations/http/express-rpc/index.js.map +0 -1
  263. package/build/implementations/http/express-rpc/index.test.js +0 -684
  264. package/build/implementations/http/express-rpc/index.test.js.map +0 -1
  265. package/build/implementations/http/express-rpc/types.d.ts +0 -11
  266. package/build/implementations/http/express-rpc/types.js.map +0 -1
  267. package/build/implementations/http/hono-api/error-taxonomy.test.js +0 -137
  268. package/build/implementations/http/hono-api/error-taxonomy.test.js.map +0 -1
  269. package/build/implementations/http/hono-api/index.d.ts +0 -151
  270. package/build/implementations/http/hono-api/index.js +0 -344
  271. package/build/implementations/http/hono-api/index.js.map +0 -1
  272. package/build/implementations/http/hono-api/index.test.js +0 -992
  273. package/build/implementations/http/hono-api/index.test.js.map +0 -1
  274. package/build/implementations/http/hono-api/types.d.ts +0 -13
  275. package/build/implementations/http/hono-api/types.js.map +0 -1
  276. package/build/implementations/http/hono-rpc/error-taxonomy.test.js +0 -64
  277. package/build/implementations/http/hono-rpc/error-taxonomy.test.js.map +0 -1
  278. package/build/implementations/http/hono-rpc/index.d.ts +0 -130
  279. package/build/implementations/http/hono-rpc/index.js +0 -209
  280. package/build/implementations/http/hono-rpc/index.js.map +0 -1
  281. package/build/implementations/http/hono-rpc/index.test.js +0 -828
  282. package/build/implementations/http/hono-rpc/index.test.js.map +0 -1
  283. package/build/implementations/http/hono-rpc/types.d.ts +0 -11
  284. package/build/implementations/http/hono-rpc/types.js +0 -2
  285. package/build/implementations/http/hono-rpc/types.js.map +0 -1
  286. package/build/implementations/http/hono-stream/error-taxonomy.test.js +0 -159
  287. package/build/implementations/http/hono-stream/error-taxonomy.test.js.map +0 -1
  288. package/build/implementations/http/hono-stream/index.d.ts +0 -171
  289. package/build/implementations/http/hono-stream/index.js +0 -415
  290. package/build/implementations/http/hono-stream/index.js.map +0 -1
  291. package/build/implementations/http/hono-stream/index.test.js +0 -1383
  292. package/build/implementations/http/hono-stream/index.test.js.map +0 -1
  293. package/build/implementations/http/hono-stream/types.d.ts +0 -15
  294. package/build/implementations/http/hono-stream/types.js +0 -2
  295. package/build/implementations/http/hono-stream/types.js.map +0 -1
  296. package/src/implementations/http/express-rpc/README.md +0 -280
  297. package/src/implementations/http/express-rpc/error-taxonomy.test.ts +0 -103
  298. package/src/implementations/http/express-rpc/index.test.ts +0 -957
  299. package/src/implementations/http/express-rpc/index.ts +0 -327
  300. package/src/implementations/http/express-rpc/types.ts +0 -16
  301. package/src/implementations/http/hono-api/README.md +0 -284
  302. package/src/implementations/http/hono-api/error-taxonomy.test.ts +0 -179
  303. package/src/implementations/http/hono-api/index.test.ts +0 -1341
  304. package/src/implementations/http/hono-api/index.ts +0 -519
  305. package/src/implementations/http/hono-api/types.ts +0 -16
  306. package/src/implementations/http/hono-rpc/README.md +0 -357
  307. package/src/implementations/http/hono-rpc/error-taxonomy.test.ts +0 -82
  308. package/src/implementations/http/hono-rpc/index.test.ts +0 -1107
  309. package/src/implementations/http/hono-rpc/index.ts +0 -320
  310. package/src/implementations/http/hono-rpc/types.ts +0 -16
  311. package/src/implementations/http/hono-stream/README.md +0 -559
  312. package/src/implementations/http/hono-stream/error-taxonomy.test.ts +0 -178
  313. package/src/implementations/http/hono-stream/index.test.ts +0 -1804
  314. package/src/implementations/http/hono-stream/index.ts +0 -622
  315. package/src/implementations/http/hono-stream/types.ts +0 -20
  316. /package/build/{implementations/http/express-rpc/error-taxonomy.test.d.ts → create-http-stream.test.d.ts} +0 -0
  317. /package/build/{implementations/http/express-rpc/index.test.d.ts → create-http.test.d.ts} +0 -0
  318. /package/build/{implementations/http/hono-api/error-taxonomy.test.d.ts → create-stream.test.d.ts} +0 -0
  319. /package/build/{implementations/http/hono-api/index.test.d.ts → create.test.d.ts} +0 -0
  320. /package/build/implementations/http/{hono-rpc/error-taxonomy.test.d.ts → error-dispatch.test.d.ts} +0 -0
  321. /package/build/implementations/http/{hono-rpc/index.test.d.ts → hono/handlers/http-stream.test.d.ts} +0 -0
  322. /package/build/implementations/http/{hono-stream/error-taxonomy.test.d.ts → hono/handlers/http.test.d.ts} +0 -0
  323. /package/build/implementations/http/{hono-stream/index.test.d.ts → hono/handlers/rpc.test.d.ts} +0 -0
  324. /package/build/implementations/http/{express-rpc → hono}/types.js +0 -0
  325. /package/build/{implementations/http/hono-api/types.js → types.js} +0 -0
@@ -0,0 +1,3355 @@
1
+ # CreateHttp & CreateHttpStream (v8) — 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:** Split the core procedure surface into four purpose-shaped creators (`Create` / `CreateStream` / `CreateHttp` / `CreateHttpStream`), eliminating the long-standing `schema.params` vs `schema.input` ambiguity and adding first-class typed response (body + headers) to HTTP routes.
6
+
7
+ **Architecture:** `Create` and `CreateStream` lose `schema.input` entirely (RPC primitives only). New `CreateHttp` / `CreateHttpStream` carry HTTP fields (`path`/`method`/`successStatus`/`scope`/`errors`) and a structured `req` / `res` schema. A `kind` discriminant on every registration drives builder routing. `HonoAPIAppBuilder` becomes the unified HTTP builder serving both `'http'` and `'http-stream'` registrations. Doc envelope regroups `jsonSchema` under `req`/`res`. Codegen emits `Req.*` / `Response.*` namespaces and conditionally typed return shapes.
8
+
9
+ **Tech Stack:** TypeScript, Vitest, AJV, TypeBox, Hono. Existing `ts-procedures` codegen pipeline. No new runtime dependencies.
10
+
11
+ **Spec:** [docs/superpowers/specs/2026-05-08-create-http-design.md](../specs/2026-05-08-create-http-design.md)
12
+
13
+ **Branch:** Already on `refactor-http-api-v8` (worktree off master). This is a major version (8.0.0) so changes ship as a clean break — no v7 compat shim.
14
+
15
+ ---
16
+
17
+ ## File map
18
+
19
+ **Create:**
20
+ - `src/types.ts` — shared `TLocalContext`, `TStreamContext`, `TProcedureRegistration`, `TStreamProcedureRegistration`, `ProcedureKind`, `TBuilderConfig`
21
+ - `src/create.ts` — `Create` factory (RPC unary)
22
+ - `src/create-stream.ts` — `CreateStream` factory (RPC streaming)
23
+ - `src/create-http.ts` — `CreateHttp` factory + path-param consistency check (HTTP unary)
24
+ - `src/create-http-stream.ts` — `CreateHttpStream` factory (HTTP streaming)
25
+ - `src/create.test.ts` — extracted from `src/index.test.ts`
26
+ - `src/create-stream.test.ts` — extracted from `src/index.test.ts`
27
+ - `src/create-http.test.ts` — new
28
+ - `src/create-http-stream.test.ts` — new
29
+ - `src/migration.test.ts` — registration errors on v7 shapes (schema.input on Create, etc.)
30
+
31
+ **Modify:**
32
+ - `src/index.ts` — slimmed to `Procedures` factory glue (~80 lines, was 537)
33
+ - `src/index.test.ts` — keep only Procedures factory tests; per-creator tests moved out
34
+ - `src/schema/compute-schema.ts` — drop `input` shape, add `req`/`res` shape, update mutual-exclusivity guard
35
+ - `src/schema/parser.ts` — wire `req` channels through schemaParser (mirror today's `input` path)
36
+ - `src/implementations/types.ts` — restructure `APIHttpRouteDoc.jsonSchema` to `req`/`res`; add `HttpStreamRouteDoc`; remove `APIInput` / `APIConfig` (or simplify); extend `AnyHttpRouteDoc` union
37
+ - `src/implementations/http/hono-api/index.ts` — read `CreateHttp` registrations directly; add `'http-stream'` branch; apply response headers; drop `APIConfig` extension generic
38
+ - `src/implementations/http/hono-api/types.ts` — simplify `HonoAPIFactoryItem` (no APIConfig generic)
39
+ - `src/implementations/http/hono-api/index.test.ts` — both kinds, response headers
40
+ - `src/implementations/http/hono-rpc/index.ts` — filter by `kind === 'rpc'`
41
+ - `src/implementations/http/hono-stream/index.ts` — filter by `kind === 'rpc-stream'`
42
+ - `src/implementations/http/{hono-rpc,hono-stream}/index.test.ts` — kind-mismatch coverage via `skippedProcedures`
43
+ - `src/implementations/http/astro/*` — kind-aware updates mirroring hono-api
44
+ - `src/implementations/http/doc-registry.ts` — handle new `'http-stream'` kind, regrouped `req`/`res`
45
+ - `src/implementations/http/doc-registry.test.ts` — coverage updates
46
+ - `src/codegen/group-routes.ts` — recognize `'http-stream'` kind
47
+ - `src/codegen/targets/_shared/route-slots.ts` — add response headers slot
48
+ - `src/codegen/targets/ts/run.ts` + supporting `emit-scope.ts` — `Req` / `Response` namespacing, conditional return shape
49
+ - `src/codegen/targets/kotlin/run.ts` — `Response.Headers` data class when declared
50
+ - `src/codegen/targets/swift/run.ts` — `Response.Headers` struct when declared
51
+ - `src/codegen/__fixtures__/users-envelope.json` — regenerate to v8 envelope shape
52
+ - `src/codegen/emit-scope.test.ts` — snapshot updates
53
+ - `src/codegen/targets/{ts,kotlin,swift}/run.test.ts` — snapshot updates
54
+ - `src/client/types.ts` — `AdapterStreamResponse.headers`, `TypedStream.headers` (optional)
55
+ - `src/client/call.ts` — surface response headers when route declares `res.headers`
56
+ - `src/client/stream.ts` — wire initial response headers into `TypedStream`
57
+ - `src/client/fetch-adapter.ts` — populate `AdapterStreamResponse.headers`
58
+ - `src/client/{call,stream,fetch-adapter,index}.test.ts` — coverage updates
59
+ - `package.json` — bump to `8.0.0`
60
+ - `CHANGELOG.md` — major bump entry with migration table
61
+ - `README.md` — update headline examples
62
+ - `CLAUDE.md` — update architecture section
63
+ - `agent_config/claude-code/skills/ts-procedures/{patterns.md,anti-patterns.md,api-reference.md}` — CreateHttp examples
64
+ - `agent_config/claude-code/skills/ts-procedures-scaffold/templates/*` — new templates
65
+ - `agent_config/copilot/copilot-instructions.md`, `agent_config/cursor/cursorrules` — condensed mirror updates
66
+
67
+ **Delete:**
68
+ - (Nothing deleted outright — `APIInput` and the `schema.input` field are removed from their containing files but no whole-file deletions.)
69
+
70
+ ---
71
+
72
+ ## Phase 1 — Core file split (pure refactor, zero behavior change)
73
+
74
+ This phase moves code into new files without changing semantics. Run the existing test suite after each task to confirm green. Commit between every task.
75
+
76
+ ### Task 1: Extract shared types into `src/types.ts`
77
+
78
+ **Files:**
79
+ - Create: `src/types.ts`
80
+ - Modify: `src/index.ts`
81
+
82
+ - [ ] **Step 1: Create `src/types.ts` with the shared exports**
83
+
84
+ ```ts
85
+ import { ProcedureError } from './errors.js'
86
+ import { TJSONSchema } from './schema/types.js'
87
+
88
+ export type TNoContextProvided = unknown
89
+
90
+ export type TLocalContext = {
91
+ error: (message: string, meta?: object) => ProcedureError
92
+ signal?: AbortSignal
93
+ }
94
+
95
+ export type TStreamContext = TLocalContext & {
96
+ signal: AbortSignal
97
+ }
98
+
99
+ export type ProcedureKind = 'rpc' | 'rpc-stream' | 'http' | 'http-stream'
100
+
101
+ export type TProcedureRegistration<TContext = unknown, TExtendedConfig = unknown> = {
102
+ name: string
103
+ kind: 'rpc'
104
+ isStream?: false
105
+ config: {
106
+ description?: string
107
+ schema?: {
108
+ params?: TJSONSchema
109
+ returnType?: TJSONSchema
110
+ }
111
+ validation?: {
112
+ params?: (params: any) => { errors?: any[] }
113
+ }
114
+ } & TExtendedConfig
115
+ handler: (ctx: TContext, params?: any) => Promise<any>
116
+ }
117
+
118
+ export type TStreamProcedureRegistration<TContext = unknown, TExtendedConfig = unknown> = {
119
+ name: string
120
+ kind: 'rpc-stream'
121
+ isStream: true
122
+ config: {
123
+ description?: string
124
+ schema?: {
125
+ params?: TJSONSchema
126
+ yieldType?: TJSONSchema
127
+ returnType?: TJSONSchema
128
+ }
129
+ validation?: {
130
+ params?: (params: any) => { errors?: any[] }
131
+ yield?: (value: any) => { errors?: any[] }
132
+ }
133
+ validateYields?: boolean
134
+ } & TExtendedConfig
135
+ handler: (ctx: TContext, params?: any) => AsyncGenerator<any, any, unknown>
136
+ }
137
+
138
+ export type TBuilderConfig = {
139
+ /**
140
+ * Skip per-call AJV runtime validation. JSON Schema is still computed at
141
+ * registration time. Intended for trusted internal factories whose callers
142
+ * are already type-checked at build time.
143
+ */
144
+ noRuntimeValidation?: true
145
+ }
146
+ ```
147
+
148
+ (`'http'` and `'http-stream'` registration types are added in later phases when those creators land.)
149
+
150
+ - [ ] **Step 2: Update `src/index.ts` to re-export from types.ts**
151
+
152
+ Replace the type declarations at the top of `src/index.ts` with a re-export. The factory function and creators stay in place for now:
153
+
154
+ ```ts
155
+ // At the very top of src/index.ts, replace the local TLocalContext/TStreamContext/etc. with:
156
+ export {
157
+ type TNoContextProvided,
158
+ type TLocalContext,
159
+ type TStreamContext,
160
+ type ProcedureKind,
161
+ type TProcedureRegistration,
162
+ type TStreamProcedureRegistration,
163
+ type TBuilderConfig,
164
+ } from './types.js'
165
+
166
+ import { TLocalContext, TStreamContext, TBuilderConfig } from './types.js'
167
+ ```
168
+
169
+ Delete the original `export type T...` declarations from `src/index.ts` (lines around 10-69 in the current file). Keep the `Procedures` function and `Create` / `CreateStream` definitions intact.
170
+
171
+ - [ ] **Step 3: Run the full test suite**
172
+
173
+ ```bash
174
+ npm run test
175
+ ```
176
+
177
+ Expected: all existing tests pass. Type re-exports are transparent.
178
+
179
+ - [ ] **Step 4: Commit**
180
+
181
+ ```bash
182
+ git add src/types.ts src/index.ts
183
+ git commit -m "refactor(core): extract shared types to src/types.ts"
184
+ ```
185
+
186
+ ---
187
+
188
+ ### Task 2: Extract `Create` into `src/create.ts`
189
+
190
+ **Files:**
191
+ - Create: `src/create.ts`
192
+ - Modify: `src/index.ts`
193
+
194
+ - [ ] **Step 1: Create `src/create.ts`**
195
+
196
+ Move the `Create` function out of `src/index.ts` into a standalone factory. The function takes the `procedures` Map and `builder` config (so it can register and check `noRuntimeValidation`):
197
+
198
+ ```ts
199
+ import { ProcedureError, ProcedureValidationError } from './errors.js'
200
+ import { computeSchema } from './schema/compute-schema.js'
201
+ import { Prettify, TSchemaLib } from './schema/types.js'
202
+ import { captureDefinitionInfo } from './stack-utils.js'
203
+ import {
204
+ TBuilderConfig,
205
+ TLocalContext,
206
+ TProcedureRegistration,
207
+ TStreamProcedureRegistration,
208
+ } from './types.js'
209
+
210
+ export type CreateBuilderArg<TContext, TExtendedConfig> = {
211
+ config?: TBuilderConfig
212
+ onCreate?: (procedure: TProcedureRegistration<TContext, TExtendedConfig> | TStreamProcedureRegistration<TContext, TExtendedConfig>) => void
213
+ }
214
+
215
+ export function makeCreate<TContext, TExtendedConfig>(
216
+ procedures: Map<string, TProcedureRegistration<TContext, TExtendedConfig> | TStreamProcedureRegistration<TContext, TExtendedConfig>>,
217
+ builder?: CreateBuilderArg<TContext, TExtendedConfig>,
218
+ ) {
219
+ return function Create<TName extends string, TParams, TReturnType>(
220
+ name: TName,
221
+ config: {
222
+ description?: string
223
+ schema?: {
224
+ params?: TParams
225
+ returnType?: TReturnType
226
+ }
227
+ } & TExtendedConfig,
228
+ handler: (ctx: Prettify<TContext & TLocalContext>, params: TSchemaLib<TParams>) => Promise<TSchemaLib<TReturnType>>,
229
+ ) {
230
+ const definitionInfo = captureDefinitionInfo()
231
+
232
+ if (procedures.has(name)) {
233
+ throw new Error(`Procedure with name ${name} is already registered`)
234
+ }
235
+
236
+ const { jsonSchema, validations } = computeSchema(name, config.schema, definitionInfo)
237
+
238
+ const errorFactory = (message: string, meta?: object) =>
239
+ new ProcedureError(name, message, meta, definitionInfo)
240
+
241
+ const registeredProcedure: TProcedureRegistration<TContext, TExtendedConfig> = {
242
+ name,
243
+ kind: 'rpc',
244
+ config: {
245
+ ...config,
246
+ description: config.description,
247
+ schema: jsonSchema,
248
+ validation: {
249
+ params: validations.params,
250
+ },
251
+ },
252
+ handler: async (ctx: Prettify<TContext>, params: TSchemaLib<TParams>) => {
253
+ try {
254
+ const skipValidation =
255
+ (ctx as { isPrevalidated?: boolean }).isPrevalidated ||
256
+ builder?.config?.noRuntimeValidation
257
+
258
+ if (validations?.params && !skipValidation) {
259
+ const { errors } = validations.params(params)
260
+ if (errors) {
261
+ throw new ProcedureValidationError(
262
+ name,
263
+ `Validation error for ${name}`,
264
+ errors,
265
+ definitionInfo,
266
+ )
267
+ }
268
+ }
269
+
270
+ const localCtx: TLocalContext = { error: errorFactory }
271
+
272
+ return await handler({ ...ctx, ...localCtx } as Prettify<TContext & TLocalContext>, params as any)
273
+ } catch (error: any) {
274
+ if (error instanceof ProcedureError) throw error
275
+ const err = new ProcedureError(name, `Error in handler for ${name} - ${error?.message}`, undefined, definitionInfo)
276
+ err.cause = error
277
+ if (error.stack && definitionInfo.definedAt) {
278
+ const { file, line, column } = definitionInfo.definedAt
279
+ err.stack = error.stack + `\n--- Procedure "${name}" defined at ---\n at ${file}:${line}:${column}`
280
+ } else if (error.stack) {
281
+ err.stack = error.stack
282
+ }
283
+ throw err
284
+ }
285
+ },
286
+ }
287
+
288
+ procedures.set(name, registeredProcedure)
289
+ builder?.onCreate?.(registeredProcedure)
290
+
291
+ const info = { name, ...registeredProcedure.config }
292
+
293
+ return {
294
+ [name]: registeredProcedure.handler,
295
+ procedure: registeredProcedure.handler,
296
+ info,
297
+ } as {
298
+ [K in TName]: (ctx: Prettify<TContext>, params: TSchemaLib<TParams>) => Promise<TSchemaLib<TReturnType>>
299
+ } & {
300
+ procedure: (ctx: Prettify<TContext>, params: TSchemaLib<TParams>) => Promise<TSchemaLib<TReturnType>>
301
+ info: {
302
+ name: TName
303
+ description?: string
304
+ schema: { params?: TParams; returnType?: TReturnType }
305
+ validation?: { params?: (params: any) => { errors?: any[] } }
306
+ } & TExtendedConfig
307
+ }
308
+ }
309
+ }
310
+ ```
311
+
312
+ **Note:** This already drops `input` from the `Create` signature (Phase 2 will enforce the migration error). The conditional `TInput extends Record<string, unknown> ? ... : TSchemaLib<TParams>` is gone — `params` is always `TSchemaLib<TParams>`.
313
+
314
+ - [ ] **Step 2: Update `src/index.ts` to use `makeCreate`**
315
+
316
+ In the `Procedures` function body, replace the inline `Create` definition with:
317
+
318
+ ```ts
319
+ import { makeCreate } from './create.js'
320
+
321
+ // inside Procedures():
322
+ const Create = makeCreate<TContext, TExtendedConfig>(procedures, builder)
323
+ ```
324
+
325
+ Remove the original `function Create<...>` body.
326
+
327
+ - [ ] **Step 3: Run the test suite**
328
+
329
+ ```bash
330
+ npm run test
331
+ ```
332
+
333
+ Expected: all existing tests that don't use `schema.input` on `Create` pass. Tests using `schema.input` will fail with a TS error or runtime error — that's expected; Phase 2 handles the migration error formally. Note which tests fail.
334
+
335
+ - [ ] **Step 4: Mark failing tests with `it.todo` or temporarily skip**
336
+
337
+ For any tests in `src/index.test.ts` that exercise `schema.input` on `Create`, change `it(...)` to `it.todo(...)` with a comment `// migrated to schema.req in Phase 3+`. We'll restore them in Phase 3 against `CreateHttp`.
338
+
339
+ - [ ] **Step 5: Run tests again, expect green**
340
+
341
+ ```bash
342
+ npm run test
343
+ ```
344
+
345
+ - [ ] **Step 6: Commit**
346
+
347
+ ```bash
348
+ git add src/create.ts src/index.ts src/index.test.ts
349
+ git commit -m "refactor(core): extract Create into src/create.ts; drop schema.input from Create"
350
+ ```
351
+
352
+ ---
353
+
354
+ ### Task 3: Extract `CreateStream` into `src/create-stream.ts`
355
+
356
+ **Files:**
357
+ - Create: `src/create-stream.ts`
358
+ - Modify: `src/index.ts`
359
+
360
+ - [ ] **Step 1: Create `src/create-stream.ts`**
361
+
362
+ Mirror Task 2's pattern. Move the `CreateStream` function into its own file as a `makeCreateStream` factory. Drop the `TInput` generic and `input` from the schema shape — `CreateStream` now only accepts `params`/`yieldType`/`returnType`.
363
+
364
+ ```ts
365
+ import { ProcedureError, ProcedureValidationError, ProcedureYieldValidationError } from './errors.js'
366
+ import { computeSchema } from './schema/compute-schema.js'
367
+ import { Prettify, TSchemaLib } from './schema/types.js'
368
+ import { captureDefinitionInfo } from './stack-utils.js'
369
+ import {
370
+ TBuilderConfig,
371
+ TStreamContext,
372
+ TProcedureRegistration,
373
+ TStreamProcedureRegistration,
374
+ } from './types.js'
375
+
376
+ export function makeCreateStream<TContext, TExtendedConfig>(
377
+ procedures: Map<string, TProcedureRegistration<TContext, TExtendedConfig> | TStreamProcedureRegistration<TContext, TExtendedConfig>>,
378
+ builder?: { config?: TBuilderConfig; onCreate?: (procedure: any) => void },
379
+ ) {
380
+ return function CreateStream<TName extends string, TParams, TYieldType, TReturnType = void>(
381
+ name: TName,
382
+ config: {
383
+ description?: string
384
+ schema?: {
385
+ params?: TParams
386
+ yieldType?: TYieldType
387
+ returnType?: TReturnType
388
+ }
389
+ validateYields?: boolean
390
+ } & TExtendedConfig,
391
+ handler: (
392
+ ctx: Prettify<TContext & TStreamContext>,
393
+ params: TSchemaLib<TParams>,
394
+ ) => AsyncGenerator<TSchemaLib<TYieldType>, TSchemaLib<TReturnType> | void, unknown>,
395
+ ) {
396
+ // [body identical to today's CreateStream in src/index.ts:284-503,
397
+ // but with schema.input removed and `kind: 'rpc-stream'` set on the
398
+ // registration. Copy from existing code, delete the input handling
399
+ // blocks at lines ~366-380 and the TInput conditional return shapes.]
400
+ }
401
+ }
402
+ ```
403
+
404
+ **Reference:** Copy the existing `CreateStream` body from `src/index.ts` lines 284-503. Required edits:
405
+ 1. Set `kind: 'rpc-stream'` on the registration (alongside `isStream: true`).
406
+ 2. Remove the `TInput` generic and conditional return types.
407
+ 3. Remove the `validations.input` loop (around line 366-380 in current).
408
+ 4. Remove `input` from the `validation` object spread.
409
+
410
+ - [ ] **Step 2: Wire `makeCreateStream` into `Procedures`**
411
+
412
+ In `src/index.ts`:
413
+
414
+ ```ts
415
+ import { makeCreateStream } from './create-stream.js'
416
+
417
+ // inside Procedures():
418
+ const CreateStream = makeCreateStream<TContext, TExtendedConfig>(procedures, builder)
419
+ ```
420
+
421
+ Remove the inline `function CreateStream<...>` body from `src/index.ts`.
422
+
423
+ - [ ] **Step 3: Mark schema.input stream tests as `it.todo`**
424
+
425
+ Same treatment as Task 2 step 4.
426
+
427
+ - [ ] **Step 4: Run tests**
428
+
429
+ ```bash
430
+ npm run test
431
+ ```
432
+
433
+ Expected: green except for `it.todo` cases.
434
+
435
+ - [ ] **Step 5: Commit**
436
+
437
+ ```bash
438
+ git add src/create-stream.ts src/index.ts src/index.test.ts
439
+ git commit -m "refactor(core): extract CreateStream into src/create-stream.ts; drop schema.input"
440
+ ```
441
+
442
+ ---
443
+
444
+ ### Task 4: Slim `src/index.ts` to glue only
445
+
446
+ **Files:**
447
+ - Modify: `src/index.ts`
448
+
449
+ - [ ] **Step 1: Verify `src/index.ts` is now a thin glue file**
450
+
451
+ The remaining content should be: type re-exports, `Procedures` factory, factory's internal `procedures` Map, `getProcedures` / `getProcedure` / `removeProcedure` / `clear` helpers, and the import wiring for `makeCreate` / `makeCreateStream`. Should be ~100 lines.
452
+
453
+ ```bash
454
+ wc -l src/index.ts
455
+ ```
456
+
457
+ Expected: under 130 lines.
458
+
459
+ - [ ] **Step 2: Run lint and typecheck**
460
+
461
+ ```bash
462
+ npm run lint
463
+ npm run build
464
+ ```
465
+
466
+ Expected: green.
467
+
468
+ - [ ] **Step 3: Run tests**
469
+
470
+ ```bash
471
+ npm run test
472
+ ```
473
+
474
+ Expected: green (excluding `it.todo` migrations).
475
+
476
+ - [ ] **Step 4: Commit (if anything changed)**
477
+
478
+ ```bash
479
+ git add src/index.ts
480
+ git commit -m "refactor(core): finalize index.ts as Procedures factory glue" --allow-empty
481
+ ```
482
+
483
+ ---
484
+
485
+ ### Task 5: Add `kind` field to existing registrations and update `getProcedures` consumers
486
+
487
+ **Files:**
488
+ - Modify: `src/types.ts` (already done in Task 1 — verify)
489
+ - Modify: `src/index.test.ts` (test that `kind` is present on returned registrations)
490
+
491
+ - [ ] **Step 1: Add a test asserting `kind` appears on Create/CreateStream registrations**
492
+
493
+ In `src/index.test.ts`:
494
+
495
+ ```ts
496
+ import { Procedures } from './index.js'
497
+ import { Type } from 'typebox'
498
+
499
+ describe('Procedure registration kind discriminant', () => {
500
+ it('Create produces kind: rpc', () => {
501
+ const procs = Procedures()
502
+ procs.Create('Foo', { schema: { params: Type.Object({}) } }, async () => undefined)
503
+ const reg = procs.getProcedure('Foo')
504
+ expect(reg?.kind).toBe('rpc')
505
+ })
506
+
507
+ it('CreateStream produces kind: rpc-stream', () => {
508
+ const procs = Procedures()
509
+ procs.CreateStream('Bar', {
510
+ schema: { params: Type.Object({}), yieldType: Type.Number() },
511
+ }, async function* () {})
512
+ const reg = procs.getProcedure('Bar')
513
+ expect(reg?.kind).toBe('rpc-stream')
514
+ })
515
+ })
516
+ ```
517
+
518
+ - [ ] **Step 2: Run test, verify it passes**
519
+
520
+ ```bash
521
+ npx vitest run src/index.test.ts -t "kind discriminant"
522
+ ```
523
+
524
+ Expected: PASS (the `kind` was set in Tasks 2 and 3).
525
+
526
+ - [ ] **Step 3: Commit**
527
+
528
+ ```bash
529
+ git add src/index.test.ts
530
+ git commit -m "test(core): assert kind discriminant on registrations"
531
+ ```
532
+
533
+ ---
534
+
535
+ ## Phase 2 — Schema cleanup & migration errors
536
+
537
+ Lock down the v8 shape: `compute-schema.ts` accepts `req`/`res` instead of `input`, and `Create` / `CreateStream` reject HTTP fields with informative errors.
538
+
539
+ ### Task 6: Update `computeSchema` signature for `req`/`res`
540
+
541
+ **Files:**
542
+ - Modify: `src/schema/compute-schema.ts`
543
+ - Test: `src/schema/compute-schema.test.ts` (create if missing)
544
+
545
+ - [ ] **Step 1: Read the current `compute-schema.ts`**
546
+
547
+ ```bash
548
+ cat src/schema/compute-schema.ts
549
+ ```
550
+
551
+ Note the current `input` handling (lines 17-84).
552
+
553
+ - [ ] **Step 2: Update the schema input/output type signatures**
554
+
555
+ Replace the `input` field with `req` and add `res`:
556
+
557
+ ```ts
558
+ export function computeSchema<
559
+ TParamsSchemaType,
560
+ TReturnTypeSchemaType,
561
+ TYieldTypeSchemaType = unknown,
562
+ >(
563
+ name: string,
564
+ schema?: {
565
+ params?: TParamsSchemaType
566
+ returnType?: TReturnTypeSchemaType
567
+ yieldType?: TYieldTypeSchemaType
568
+ req?: Record<string, unknown> // was: input
569
+ res?: { body?: unknown; headers?: unknown } // new
570
+ },
571
+ definitionInfo?: DefinitionInfo,
572
+ ): {
573
+ jsonSchema: {
574
+ params?: TJSONSchema
575
+ returnType?: TJSONSchema
576
+ yieldType?: TJSONSchema
577
+ req?: Record<string, TJSONSchema>
578
+ res?: { body?: TJSONSchema; headers?: TJSONSchema }
579
+ }
580
+ validations: {
581
+ params?: (params?: any) => { errors?: TSchemaValidationError[] }
582
+ yield?: (value?: any) => { errors?: TSchemaValidationError[] }
583
+ req?: Record<string, (value?: any) => { errors?: TSchemaValidationError[] }>
584
+ }
585
+ }
586
+ ```
587
+
588
+ Update the mutual-exclusivity guard:
589
+
590
+ ```ts
591
+ if (schema?.params && schema?.req) {
592
+ throw new ProcedureRegistrationError(
593
+ name,
594
+ `schema.params and schema.req are mutually exclusive for procedure "${name}". Use schema.params for RPC procedures or schema.req for HTTP procedures.`,
595
+ definitionInfo,
596
+ )
597
+ }
598
+ ```
599
+
600
+ Adapt the `schemaParser` call to pass `req` where it previously passed `input`. The internal mechanics are identical — `req` is just a renamed `input`. (`res` validation is documentation-only; not parsed.)
601
+
602
+ - [ ] **Step 3: Update `src/schema/parser.ts` to accept `req` channels**
603
+
604
+ In `parser.ts`, find the block that processes `input` and rename to `req`. The validator factory pattern is unchanged — each channel gets its own AJV-compiled validator.
605
+
606
+ - [ ] **Step 4: Run schema tests**
607
+
608
+ ```bash
609
+ npx vitest run src/schema/
610
+ ```
611
+
612
+ Expected: tests pass (no callers yet pass `req`/`res` outside of internal renames).
613
+
614
+ - [ ] **Step 5: Commit**
615
+
616
+ ```bash
617
+ git add src/schema/compute-schema.ts src/schema/parser.ts
618
+ git commit -m "refactor(schema): rename input→req in compute-schema; add res slot"
619
+ ```
620
+
621
+ ---
622
+
623
+ ### Task 7: Add migration error when legacy `schema.input` is passed
624
+
625
+ **Files:**
626
+ - Modify: `src/schema/compute-schema.ts`
627
+ - Test: `src/migration.test.ts` (create)
628
+
629
+ - [ ] **Step 1: Write failing migration tests**
630
+
631
+ Create `src/migration.test.ts`:
632
+
633
+ ```ts
634
+ import { describe, it, expect } from 'vitest'
635
+ import { Procedures } from './index.js'
636
+ import { ProcedureRegistrationError } from './errors.js'
637
+ import { Type } from 'typebox'
638
+
639
+ describe('v7 → v8 migration errors', () => {
640
+ it('rejects schema.input on Create with v8 message', () => {
641
+ const procs = Procedures()
642
+ expect(() =>
643
+ procs.Create('Legacy', {
644
+ schema: {
645
+ // @ts-expect-error v8 removed schema.input
646
+ input: { body: Type.Object({}) },
647
+ },
648
+ }, async () => undefined)
649
+ ).toThrow(ProcedureRegistrationError)
650
+ expect(() =>
651
+ procs.Create('Legacy2', { schema: { input: { body: Type.Object({}) } } } as any, async () => undefined)
652
+ ).toThrow(/schema.input was removed in v8/)
653
+ })
654
+
655
+ it('rejects schema.input on CreateStream with v8 message', () => {
656
+ const procs = Procedures()
657
+ expect(() =>
658
+ procs.CreateStream('Legacy', {
659
+ schema: { input: { body: Type.Object({}) }, yieldType: Type.Number() } as any,
660
+ }, async function* () {})
661
+ ).toThrow(/schema.input was removed in v8/)
662
+ })
663
+ })
664
+ ```
665
+
666
+ - [ ] **Step 2: Run, verify they fail**
667
+
668
+ ```bash
669
+ npx vitest run src/migration.test.ts
670
+ ```
671
+
672
+ Expected: FAIL (no migration error implemented yet).
673
+
674
+ - [ ] **Step 3: Add migration check in `compute-schema.ts`**
675
+
676
+ At the top of `computeSchema`, before the existing mutual-exclusivity guard:
677
+
678
+ ```ts
679
+ if (schema && 'input' in schema && (schema as any).input !== undefined) {
680
+ throw new ProcedureRegistrationError(
681
+ name,
682
+ `schema.input was removed in v8. Use CreateHttp / CreateHttpStream for per-channel HTTP validation. Procedure: "${name}".`,
683
+ definitionInfo,
684
+ )
685
+ }
686
+ ```
687
+
688
+ - [ ] **Step 4: Run tests, verify they pass**
689
+
690
+ ```bash
691
+ npx vitest run src/migration.test.ts
692
+ ```
693
+
694
+ Expected: PASS.
695
+
696
+ - [ ] **Step 5: Commit**
697
+
698
+ ```bash
699
+ git add src/schema/compute-schema.ts src/migration.test.ts
700
+ git commit -m "feat(migration): reject schema.input with v8 migration error"
701
+ ```
702
+
703
+ ---
704
+
705
+ ### Task 8: Reject HTTP fields on `Create` / `CreateStream`
706
+
707
+ **Files:**
708
+ - Modify: `src/create.ts`, `src/create-stream.ts`
709
+ - Modify: `src/migration.test.ts`
710
+
711
+ - [ ] **Step 1: Add failing migration tests**
712
+
713
+ Append to `src/migration.test.ts`:
714
+
715
+ ```ts
716
+ describe('HTTP fields rejected on RPC creators', () => {
717
+ const HTTP_FIELDS = ['path', 'method', 'req', 'res', 'successStatus'] as const
718
+
719
+ for (const field of HTTP_FIELDS) {
720
+ it(`Create rejects ${field}`, () => {
721
+ const procs = Procedures()
722
+ expect(() =>
723
+ procs.Create('Foo', { [field]: 'x', schema: { params: Type.Object({}) } } as any, async () => undefined)
724
+ ).toThrow(/HTTP fields require CreateHttp/)
725
+ })
726
+
727
+ it(`CreateStream rejects ${field}`, () => {
728
+ const procs = Procedures()
729
+ expect(() =>
730
+ procs.CreateStream('Bar', {
731
+ [field]: 'x',
732
+ schema: { params: Type.Object({}), yieldType: Type.Number() },
733
+ } as any, async function* () {})
734
+ ).toThrow(/HTTP fields require CreateHttp/)
735
+ })
736
+ }
737
+ })
738
+ ```
739
+
740
+ - [ ] **Step 2: Run, verify they fail**
741
+
742
+ ```bash
743
+ npx vitest run src/migration.test.ts -t "HTTP fields"
744
+ ```
745
+
746
+ Expected: FAIL.
747
+
748
+ - [ ] **Step 3: Add HTTP-field guard helper to `src/create.ts`**
749
+
750
+ At the top of `makeCreate`'s returned `Create` function (after `definitionInfo = captureDefinitionInfo()`):
751
+
752
+ ```ts
753
+ import { ProcedureRegistrationError } from './errors.js'
754
+
755
+ const HTTP_FIELDS = ['path', 'method', 'req', 'res', 'successStatus'] as const
756
+ const presentHttpFields = HTTP_FIELDS.filter((f) => (config as any)[f] !== undefined)
757
+ if (presentHttpFields.length > 0) {
758
+ throw new ProcedureRegistrationError(
759
+ name,
760
+ `HTTP fields require CreateHttp / CreateHttpStream. Procedure "${name}" has [${presentHttpFields.join(', ')}] which are not valid on Create.`,
761
+ definitionInfo,
762
+ )
763
+ }
764
+ ```
765
+
766
+ - [ ] **Step 4: Mirror the guard in `src/create-stream.ts`**
767
+
768
+ Same block, adjusted message ("not valid on CreateStream").
769
+
770
+ - [ ] **Step 5: Run tests**
771
+
772
+ ```bash
773
+ npx vitest run src/migration.test.ts
774
+ ```
775
+
776
+ Expected: PASS.
777
+
778
+ - [ ] **Step 6: Commit**
779
+
780
+ ```bash
781
+ git add src/create.ts src/create-stream.ts src/migration.test.ts
782
+ git commit -m "feat(migration): reject HTTP fields on Create/CreateStream with v8 message"
783
+ ```
784
+
785
+ ---
786
+
787
+ ### Task 9: Move per-creator tests out of `src/index.test.ts`
788
+
789
+ **Files:**
790
+ - Create: `src/create.test.ts`, `src/create-stream.test.ts`
791
+ - Modify: `src/index.test.ts`
792
+
793
+ - [ ] **Step 1: Move `Create`-specific tests from `src/index.test.ts` to `src/create.test.ts`**
794
+
795
+ Identify tests in `src/index.test.ts` that exercise `Create` specifically (validation, error wrapping, `info` shape, etc.). Move them to a new file `src/create.test.ts`. Update imports.
796
+
797
+ - [ ] **Step 2: Move `CreateStream`-specific tests to `src/create-stream.test.ts`**
798
+
799
+ Same pattern.
800
+
801
+ - [ ] **Step 3: Keep `Procedures`-factory-level tests in `src/index.test.ts`**
802
+
803
+ Tests covering: `getProcedures`, `getProcedure`, `removeProcedure`, `clear`, duplicate-name rejection, `onCreate` hook, factory return shape.
804
+
805
+ - [ ] **Step 4: Run all three test files**
806
+
807
+ ```bash
808
+ npx vitest run src/create.test.ts src/create-stream.test.ts src/index.test.ts
809
+ ```
810
+
811
+ Expected: green.
812
+
813
+ - [ ] **Step 5: Commit**
814
+
815
+ ```bash
816
+ git add src/create.test.ts src/create-stream.test.ts src/index.test.ts
817
+ git commit -m "test(core): split index.test.ts into per-creator files"
818
+ ```
819
+
820
+ ---
821
+
822
+ ## Phase 3 — `CreateHttp` (req only, no res yet)
823
+
824
+ Add the new HTTP unary creator with structured request channels. Response shape (`res.body`/`res.headers`) lands in Phase 4. This phase keeps the surface minimal — handlers return whatever they want; we'll narrow in Phase 4.
825
+
826
+ ### Task 10: Define `CreateHttp` types in `src/types.ts`
827
+
828
+ **Files:**
829
+ - Modify: `src/types.ts`
830
+
831
+ - [ ] **Step 1: Add HTTP method type and `CreateHttp` config / handler types**
832
+
833
+ Append to `src/types.ts`:
834
+
835
+ ```ts
836
+ export type HttpMethod = 'get' | 'post' | 'put' | 'delete' | 'patch' | 'head'
837
+
838
+ export type TCreateHttpConfig<TReq, TRes, TErrorKey extends string = string> = {
839
+ path: string
840
+ method: HttpMethod
841
+ successStatus?: number
842
+ scope?: string
843
+ errors?: TErrorKey[]
844
+ description?: string
845
+ schema: {
846
+ req?: TReq
847
+ res?: TRes
848
+ }
849
+ }
850
+
851
+ export type THttpProcedureRegistration<TContext = unknown> = {
852
+ name: string
853
+ kind: 'http'
854
+ config: {
855
+ path: string
856
+ method: HttpMethod
857
+ successStatus?: number
858
+ scope?: string
859
+ errors?: string[]
860
+ description?: string
861
+ schema?: {
862
+ req?: Record<string, TJSONSchema>
863
+ res?: { body?: TJSONSchema; headers?: TJSONSchema }
864
+ }
865
+ validation?: {
866
+ req?: Record<string, (value: any) => { errors?: any[] }>
867
+ }
868
+ }
869
+ handler: (ctx: TContext, req?: any) => Promise<any>
870
+ }
871
+ ```
872
+
873
+ - [ ] **Step 2: Run lint to ensure imports resolve**
874
+
875
+ ```bash
876
+ npm run lint
877
+ ```
878
+
879
+ - [ ] **Step 3: Commit**
880
+
881
+ ```bash
882
+ git add src/types.ts
883
+ git commit -m "feat(types): add HttpMethod and CreateHttp registration types"
884
+ ```
885
+
886
+ ---
887
+
888
+ ### Task 11: Implement `makeCreateHttp` in `src/create-http.ts`
889
+
890
+ **Files:**
891
+ - Create: `src/create-http.ts`
892
+
893
+ - [ ] **Step 1: Write the file with the basic shape (req only, body return)**
894
+
895
+ ```ts
896
+ import { ProcedureError, ProcedureRegistrationError, ProcedureValidationError } from './errors.js'
897
+ import { computeSchema } from './schema/compute-schema.js'
898
+ import { Prettify, TSchemaLib } from './schema/types.js'
899
+ import { captureDefinitionInfo } from './stack-utils.js'
900
+ import {
901
+ HttpMethod,
902
+ TBuilderConfig,
903
+ TLocalContext,
904
+ THttpProcedureRegistration,
905
+ TProcedureRegistration,
906
+ TStreamProcedureRegistration,
907
+ } from './types.js'
908
+
909
+ const PATH_PARAM_RE = /:([a-zA-Z_][a-zA-Z0-9_]*)/g
910
+
911
+ function extractPathParamNames(path: string): string[] {
912
+ const matches = path.match(PATH_PARAM_RE)
913
+ return matches ? matches.map((m) => m.slice(1)) : []
914
+ }
915
+
916
+ function checkPathParamConsistency(
917
+ procedureName: string,
918
+ path: string,
919
+ pathParamsSchema: Record<string, unknown> | undefined,
920
+ definitionInfo: ReturnType<typeof captureDefinitionInfo>,
921
+ ) {
922
+ const pathParamNames = extractPathParamNames(path)
923
+ const hasPathParams = pathParamNames.length > 0
924
+ const hasSchema = pathParamsSchema !== undefined
925
+
926
+ if (hasPathParams && !hasSchema) {
927
+ throw new ProcedureRegistrationError(
928
+ procedureName,
929
+ `Path "${path}" has path parameters [${pathParamNames.join(', ')}] but schema.req.pathParams is not defined.`,
930
+ definitionInfo,
931
+ )
932
+ }
933
+ if (!hasPathParams && hasSchema) {
934
+ throw new ProcedureRegistrationError(
935
+ procedureName,
936
+ `schema.req.pathParams is defined but path "${path}" has no path parameters.`,
937
+ definitionInfo,
938
+ )
939
+ }
940
+ if (hasPathParams && hasSchema) {
941
+ const schemaProperties = (pathParamsSchema as { properties?: Record<string, unknown> }).properties
942
+ if (schemaProperties) {
943
+ const schemaKeys = Object.keys(schemaProperties)
944
+ const missing = pathParamNames.filter((p) => !schemaKeys.includes(p))
945
+ const extra = schemaKeys.filter((k) => !pathParamNames.includes(k))
946
+ if (missing.length > 0 || extra.length > 0) {
947
+ const parts: string[] = []
948
+ if (missing.length > 0) parts.push(`path has [${missing.join(', ')}] missing from schema`)
949
+ if (extra.length > 0) parts.push(`schema has [${extra.join(', ')}] not in path`)
950
+ throw new ProcedureRegistrationError(
951
+ procedureName,
952
+ `Path param mismatch for "${procedureName}": ${parts.join('; ')}.`,
953
+ definitionInfo,
954
+ )
955
+ }
956
+ }
957
+ }
958
+ }
959
+
960
+ export function makeCreateHttp<TContext>(
961
+ procedures: Map<string, TProcedureRegistration<TContext, any> | TStreamProcedureRegistration<TContext, any> | THttpProcedureRegistration<TContext>>,
962
+ builder?: { config?: TBuilderConfig; onCreate?: (procedure: any) => void },
963
+ ) {
964
+ return function CreateHttp<TName extends string, TReq extends Record<string, unknown> | undefined, TErrorKey extends string = string>(
965
+ name: TName,
966
+ config: {
967
+ path: string
968
+ method: HttpMethod
969
+ successStatus?: number
970
+ scope?: string
971
+ errors?: TErrorKey[]
972
+ description?: string
973
+ schema: {
974
+ req?: TReq
975
+ }
976
+ },
977
+ handler: (
978
+ ctx: Prettify<TContext & TLocalContext>,
979
+ req: TReq extends Record<string, unknown> ? Prettify<{ [K in keyof TReq]: TSchemaLib<TReq[K]> }> : undefined,
980
+ ) => Promise<unknown>,
981
+ ) {
982
+ const definitionInfo = captureDefinitionInfo()
983
+
984
+ if (procedures.has(name)) {
985
+ throw new Error(`Procedure with name ${name} is already registered`)
986
+ }
987
+
988
+ if ((config.schema as any)?.params) {
989
+ throw new ProcedureRegistrationError(
990
+ name,
991
+ `Use schema.req.body (or schema.req.query) instead of schema.params on CreateHttp. Procedure: "${name}".`,
992
+ definitionInfo,
993
+ )
994
+ }
995
+
996
+ const { jsonSchema, validations } = computeSchema(name, {
997
+ req: config.schema.req as Record<string, unknown> | undefined,
998
+ }, definitionInfo)
999
+
1000
+ const pathParamsSchema = (jsonSchema.req as Record<string, unknown> | undefined)?.pathParams
1001
+ checkPathParamConsistency(name, config.path, pathParamsSchema as Record<string, unknown> | undefined, definitionInfo)
1002
+
1003
+ const errorFactory = (message: string, meta?: object) =>
1004
+ new ProcedureError(name, message, meta, definitionInfo)
1005
+
1006
+ const registeredProcedure: THttpProcedureRegistration<TContext> = {
1007
+ name,
1008
+ kind: 'http',
1009
+ config: {
1010
+ path: config.path,
1011
+ method: config.method,
1012
+ successStatus: config.successStatus,
1013
+ scope: config.scope,
1014
+ errors: config.errors as string[] | undefined,
1015
+ description: config.description,
1016
+ schema: jsonSchema as any,
1017
+ validation: { req: validations.req },
1018
+ },
1019
+ handler: async (ctx: TContext, req: any) => {
1020
+ try {
1021
+ const skipValidation =
1022
+ (ctx as { isPrevalidated?: boolean }).isPrevalidated ||
1023
+ builder?.config?.noRuntimeValidation
1024
+
1025
+ if (validations?.req && !skipValidation) {
1026
+ for (const [channel, validator] of Object.entries(validations.req)) {
1027
+ const channelValue = (req as Record<string, unknown>)?.[channel]
1028
+ const { errors } = validator(channelValue)
1029
+ if (errors) {
1030
+ throw new ProcedureValidationError(
1031
+ name,
1032
+ `Validation error for ${name} in req.${channel}`,
1033
+ errors,
1034
+ definitionInfo,
1035
+ )
1036
+ }
1037
+ }
1038
+ }
1039
+
1040
+ const localCtx: TLocalContext = { error: errorFactory }
1041
+ return await handler({ ...ctx, ...localCtx } as any, req)
1042
+ } catch (error: any) {
1043
+ if (error instanceof ProcedureError) throw error
1044
+ const err = new ProcedureError(name, `Error in handler for ${name} - ${error?.message}`, undefined, definitionInfo)
1045
+ err.cause = error
1046
+ if (error.stack && definitionInfo.definedAt) {
1047
+ const { file, line, column } = definitionInfo.definedAt
1048
+ err.stack = error.stack + `\n--- Procedure "${name}" defined at ---\n at ${file}:${line}:${column}`
1049
+ } else if (error.stack) {
1050
+ err.stack = error.stack
1051
+ }
1052
+ throw err
1053
+ }
1054
+ },
1055
+ }
1056
+
1057
+ procedures.set(name, registeredProcedure as any)
1058
+ builder?.onCreate?.(registeredProcedure)
1059
+
1060
+ const info = { name, ...registeredProcedure.config }
1061
+
1062
+ return {
1063
+ [name]: registeredProcedure.handler,
1064
+ procedure: registeredProcedure.handler,
1065
+ info,
1066
+ } as {
1067
+ [K in TName]: typeof registeredProcedure.handler
1068
+ } & {
1069
+ procedure: typeof registeredProcedure.handler
1070
+ info: typeof info
1071
+ }
1072
+ }
1073
+ }
1074
+ ```
1075
+
1076
+ - [ ] **Step 2: Wire `makeCreateHttp` into `Procedures` factory**
1077
+
1078
+ In `src/index.ts`:
1079
+
1080
+ ```ts
1081
+ import { makeCreateHttp } from './create-http.js'
1082
+
1083
+ // inside Procedures():
1084
+ const CreateHttp = makeCreateHttp<TContext>(procedures as any, builder)
1085
+
1086
+ return {
1087
+ // existing fields ...
1088
+ Create,
1089
+ CreateStream,
1090
+ CreateHttp,
1091
+ }
1092
+ ```
1093
+
1094
+ Also update the `procedures` Map's type signature to accept `THttpProcedureRegistration`.
1095
+
1096
+ - [ ] **Step 3: Run lint and build**
1097
+
1098
+ ```bash
1099
+ npm run lint
1100
+ npm run build
1101
+ ```
1102
+
1103
+ Expected: green.
1104
+
1105
+ - [ ] **Step 4: Commit**
1106
+
1107
+ ```bash
1108
+ git add src/create-http.ts src/index.ts
1109
+ git commit -m "feat(core): add CreateHttp with req channels and path-param validation"
1110
+ ```
1111
+
1112
+ ---
1113
+
1114
+ ### Task 12: Test `CreateHttp` registration and req validation
1115
+
1116
+ **Files:**
1117
+ - Create: `src/create-http.test.ts`
1118
+
1119
+ - [ ] **Step 1: Write tests covering registration shape**
1120
+
1121
+ ```ts
1122
+ import { describe, it, expect } from 'vitest'
1123
+ import { Procedures } from './index.js'
1124
+ import { ProcedureRegistrationError, ProcedureValidationError } from './errors.js'
1125
+ import { Type } from 'typebox'
1126
+
1127
+ describe('CreateHttp registration', () => {
1128
+ it('registers with kind: http', () => {
1129
+ const procs = Procedures()
1130
+ procs.CreateHttp('GetUser', {
1131
+ path: '/users/:id',
1132
+ method: 'get',
1133
+ schema: { req: { pathParams: Type.Object({ id: Type.String() }) } },
1134
+ }, async (_, { pathParams }) => ({ id: pathParams.id }))
1135
+
1136
+ const reg = procs.getProcedure('GetUser')
1137
+ expect(reg?.kind).toBe('http')
1138
+ })
1139
+
1140
+ it('rejects schema.params with v8 message', () => {
1141
+ const procs = Procedures()
1142
+ expect(() =>
1143
+ procs.CreateHttp('Bad', {
1144
+ path: '/x', method: 'get',
1145
+ schema: { params: Type.Object({}) } as any,
1146
+ }, async () => undefined)
1147
+ ).toThrow(/Use schema.req.body \(or schema.req.query\) instead of schema.params/)
1148
+ })
1149
+
1150
+ it('rejects path with params but no pathParams schema', () => {
1151
+ const procs = Procedures()
1152
+ expect(() =>
1153
+ procs.CreateHttp('Bad', {
1154
+ path: '/users/:id', method: 'get',
1155
+ schema: { req: {} },
1156
+ }, async () => undefined)
1157
+ ).toThrow(/path parameters \[id\] but schema.req.pathParams is not defined/)
1158
+ })
1159
+
1160
+ it('rejects pathParams schema with no path params', () => {
1161
+ const procs = Procedures()
1162
+ expect(() =>
1163
+ procs.CreateHttp('Bad', {
1164
+ path: '/users', method: 'get',
1165
+ schema: { req: { pathParams: Type.Object({ id: Type.String() }) } },
1166
+ }, async () => undefined)
1167
+ ).toThrow(/has no path parameters/)
1168
+ })
1169
+
1170
+ it('rejects pathParams key mismatch', () => {
1171
+ const procs = Procedures()
1172
+ expect(() =>
1173
+ procs.CreateHttp('Bad', {
1174
+ path: '/users/:id', method: 'get',
1175
+ schema: { req: { pathParams: Type.Object({ wrongKey: Type.String() }) } },
1176
+ }, async () => undefined)
1177
+ ).toThrow(/Path param mismatch/)
1178
+ })
1179
+ })
1180
+
1181
+ describe('CreateHttp req channel validation', () => {
1182
+ it('validates each channel independently with channel-tagged error', async () => {
1183
+ const procs = Procedures()
1184
+ const { GetUser } = procs.CreateHttp('GetUser', {
1185
+ path: '/users/:id', method: 'get',
1186
+ schema: {
1187
+ req: {
1188
+ pathParams: Type.Object({ id: Type.String() }),
1189
+ query: Type.Object({ limit: Type.Number() }),
1190
+ },
1191
+ },
1192
+ }, async (_, { pathParams, query }) => ({ id: pathParams.id, limit: query.limit }))
1193
+
1194
+ await expect(GetUser({} as any, {
1195
+ pathParams: { id: 'abc' },
1196
+ query: { limit: 'not-a-number' as any },
1197
+ })).rejects.toThrow(ProcedureValidationError)
1198
+ await expect(GetUser({} as any, {
1199
+ pathParams: { id: 'abc' },
1200
+ query: { limit: 'not-a-number' as any },
1201
+ })).rejects.toThrow(/req\.query/)
1202
+ })
1203
+ })
1204
+ ```
1205
+
1206
+ - [ ] **Step 2: Run tests**
1207
+
1208
+ ```bash
1209
+ npx vitest run src/create-http.test.ts
1210
+ ```
1211
+
1212
+ Expected: PASS for all five registration tests and the validation test.
1213
+
1214
+ - [ ] **Step 3: Commit**
1215
+
1216
+ ```bash
1217
+ git add src/create-http.test.ts
1218
+ git commit -m "test(core): cover CreateHttp registration and req validation"
1219
+ ```
1220
+
1221
+ ---
1222
+
1223
+ ## Phase 4 — `CreateHttp` response shape (`res.body`, `res.headers`, conditional return)
1224
+
1225
+ ### Task 13: Add `res.body` to `CreateHttp` config and types
1226
+
1227
+ **Files:**
1228
+ - Modify: `src/types.ts`
1229
+ - Modify: `src/create-http.ts`
1230
+ - Test: `src/create-http.test.ts`
1231
+
1232
+ - [ ] **Step 1: Extend the config type to include `res.body`**
1233
+
1234
+ In `src/types.ts`, update `TCreateHttpConfig`:
1235
+
1236
+ ```ts
1237
+ export type TCreateHttpConfig<TReq, TRes, TErrorKey extends string = string> = {
1238
+ path: string
1239
+ method: HttpMethod
1240
+ successStatus?: number
1241
+ scope?: string
1242
+ errors?: TErrorKey[]
1243
+ description?: string
1244
+ schema: {
1245
+ req?: TReq
1246
+ res?: TRes // { body?, headers? } — narrowed at the creator call site
1247
+ }
1248
+ }
1249
+ ```
1250
+
1251
+ - [ ] **Step 2: Update `makeCreateHttp` signature to thread `TRes`**
1252
+
1253
+ In `src/create-http.ts`:
1254
+
1255
+ ```ts
1256
+ return function CreateHttp<
1257
+ TName extends string,
1258
+ TReq extends Record<string, unknown> | undefined,
1259
+ TRes extends { body?: unknown; headers?: unknown } | undefined = undefined,
1260
+ TErrorKey extends string = string,
1261
+ >(
1262
+ name: TName,
1263
+ config: TCreateHttpConfig<TReq, TRes, TErrorKey>,
1264
+ handler: (
1265
+ ctx: Prettify<TContext & TLocalContext>,
1266
+ req: TReq extends Record<string, unknown> ? Prettify<{ [K in keyof TReq]: TSchemaLib<TReq[K]> }> : undefined,
1267
+ ) => Promise<HttpReturn<TRes>>,
1268
+ ) { /* ... */ }
1269
+ ```
1270
+
1271
+ Add the `HttpReturn` helper type at the top of the file:
1272
+
1273
+ ```ts
1274
+ type Infer<T> = TSchemaLib<T>
1275
+
1276
+ export type HttpReturn<TRes> =
1277
+ TRes extends { body: infer B; headers: infer H } ? { body: Infer<B>; headers: Infer<H> }
1278
+ : TRes extends { headers: infer H } ? { headers: Infer<H> }
1279
+ : TRes extends { body: infer B } ? Infer<B>
1280
+ : void
1281
+ ```
1282
+
1283
+ - [ ] **Step 3: Pass `res` through to `computeSchema`**
1284
+
1285
+ In the body of `makeCreateHttp`'s creator function:
1286
+
1287
+ ```ts
1288
+ const { jsonSchema, validations } = computeSchema(name, {
1289
+ req: config.schema.req as Record<string, unknown> | undefined,
1290
+ res: config.schema.res as { body?: unknown; headers?: unknown } | undefined,
1291
+ }, definitionInfo)
1292
+ ```
1293
+
1294
+ - [ ] **Step 4: Add a test for body-only return**
1295
+
1296
+ In `src/create-http.test.ts`:
1297
+
1298
+ ```ts
1299
+ describe('CreateHttp res.body', () => {
1300
+ it('handler returns body bare when only res.body declared', async () => {
1301
+ const procs = Procedures()
1302
+ const { GetUser } = procs.CreateHttp('GetUser', {
1303
+ path: '/users/:id', method: 'get',
1304
+ schema: {
1305
+ req: { pathParams: Type.Object({ id: Type.String() }) },
1306
+ res: { body: Type.Object({ id: Type.String(), name: Type.String() }) },
1307
+ },
1308
+ }, async (_, { pathParams }) => ({ id: pathParams.id, name: 'Alice' }))
1309
+
1310
+ const result = await GetUser({} as any, { pathParams: { id: '1' } })
1311
+ expect(result).toEqual({ id: '1', name: 'Alice' })
1312
+ })
1313
+ })
1314
+ ```
1315
+
1316
+ - [ ] **Step 5: Run tests**
1317
+
1318
+ ```bash
1319
+ npx vitest run src/create-http.test.ts -t "res.body"
1320
+ ```
1321
+
1322
+ Expected: PASS.
1323
+
1324
+ - [ ] **Step 6: Commit**
1325
+
1326
+ ```bash
1327
+ git add src/types.ts src/create-http.ts src/create-http.test.ts
1328
+ git commit -m "feat(core): add CreateHttp res.body with bare-body return shape"
1329
+ ```
1330
+
1331
+ ---
1332
+
1333
+ ### Task 14: Add `res.headers` and conditional `{ body, headers }` return
1334
+
1335
+ **Files:**
1336
+ - Modify: `src/create-http.ts`
1337
+ - Test: `src/create-http.test.ts`
1338
+
1339
+ - [ ] **Step 1: Write failing tests for the four return shapes**
1340
+
1341
+ ```ts
1342
+ describe('CreateHttp conditional return shape', () => {
1343
+ it('res: { body, headers } → returns { body, headers }', async () => {
1344
+ const procs = Procedures()
1345
+ const { GetUser } = procs.CreateHttp('GetUser', {
1346
+ path: '/users/:id', method: 'get',
1347
+ schema: {
1348
+ req: { pathParams: Type.Object({ id: Type.String() }) },
1349
+ res: {
1350
+ body: Type.Object({ id: Type.String() }),
1351
+ headers: Type.Object({ 'x-rate-limit': Type.String() }),
1352
+ },
1353
+ },
1354
+ }, async (_, { pathParams }) => ({
1355
+ body: { id: pathParams.id },
1356
+ headers: { 'x-rate-limit': '99' },
1357
+ }))
1358
+
1359
+ const result = await GetUser({} as any, { pathParams: { id: '1' } })
1360
+ expect(result).toEqual({ body: { id: '1' }, headers: { 'x-rate-limit': '99' } })
1361
+ })
1362
+
1363
+ it('res: { headers } only → returns { headers }', async () => {
1364
+ const procs = Procedures()
1365
+ const { Healthcheck } = procs.CreateHttp('Healthcheck', {
1366
+ path: '/health', method: 'get',
1367
+ schema: {
1368
+ res: { headers: Type.Object({ 'x-version': Type.String() }) },
1369
+ },
1370
+ }, async () => ({ headers: { 'x-version': '8.0.0' } }))
1371
+
1372
+ const result = await Healthcheck({} as any, undefined)
1373
+ expect(result).toEqual({ headers: { 'x-version': '8.0.0' } })
1374
+ })
1375
+
1376
+ it('no res → returns void', async () => {
1377
+ const procs = Procedures()
1378
+ const { Ping } = procs.CreateHttp('Ping', {
1379
+ path: '/ping', method: 'post',
1380
+ schema: { req: { body: Type.Object({}) } },
1381
+ }, async () => undefined)
1382
+
1383
+ const result = await Ping({} as any, { body: {} })
1384
+ expect(result).toBeUndefined()
1385
+ })
1386
+ })
1387
+ ```
1388
+
1389
+ - [ ] **Step 2: Run, verify they pass**
1390
+
1391
+ The handler return shape is enforced at the **type level** by `HttpReturn<TRes>` (from Task 13). Runtime doesn't validate `res` body/headers (per spec out-of-scope). So if tests are typed correctly, runtime just passes through whatever the handler returns.
1392
+
1393
+ ```bash
1394
+ npx vitest run src/create-http.test.ts -t "conditional return"
1395
+ ```
1396
+
1397
+ Expected: PASS (no implementation change needed beyond Task 13's `HttpReturn` type — runtime is pass-through).
1398
+
1399
+ - [ ] **Step 3: Add a type-level test (using `expectTypeOf`)**
1400
+
1401
+ ```ts
1402
+ import { expectTypeOf } from 'vitest'
1403
+
1404
+ describe('CreateHttp HttpReturn type inference', () => {
1405
+ it('infers correct return shapes', () => {
1406
+ const procs = Procedures()
1407
+ const { GetUser } = procs.CreateHttp('GetUser', {
1408
+ path: '/users/:id', method: 'get',
1409
+ schema: {
1410
+ req: { pathParams: Type.Object({ id: Type.String() }) },
1411
+ res: {
1412
+ body: Type.Object({ id: Type.String() }),
1413
+ headers: Type.Object({ 'x-rate-limit': Type.String() }),
1414
+ },
1415
+ },
1416
+ }, async () => ({ body: { id: '1' }, headers: { 'x-rate-limit': '99' } }))
1417
+
1418
+ type ReturnT = Awaited<ReturnType<typeof GetUser>>
1419
+ expectTypeOf<ReturnT>().toEqualTypeOf<{ body: { id: string }; headers: { 'x-rate-limit': string } }>()
1420
+ })
1421
+ })
1422
+ ```
1423
+
1424
+ - [ ] **Step 4: Run tests**
1425
+
1426
+ ```bash
1427
+ npx vitest run src/create-http.test.ts
1428
+ ```
1429
+
1430
+ Expected: PASS.
1431
+
1432
+ - [ ] **Step 5: Commit**
1433
+
1434
+ ```bash
1435
+ git add src/create-http.ts src/create-http.test.ts
1436
+ git commit -m "feat(core): conditional return shape on CreateHttp (body, headers, both, void)"
1437
+ ```
1438
+
1439
+ ---
1440
+
1441
+ ## Phase 5 — `CreateHttpStream`
1442
+
1443
+ ### Task 15: Define `CreateHttpStream` types
1444
+
1445
+ **Files:**
1446
+ - Modify: `src/types.ts`
1447
+
1448
+ - [ ] **Step 1: Add the streaming registration type and config**
1449
+
1450
+ ```ts
1451
+ export type THttpStreamProcedureRegistration<TContext = unknown> = {
1452
+ name: string
1453
+ kind: 'http-stream'
1454
+ config: {
1455
+ path: string
1456
+ method: HttpMethod
1457
+ scope?: string
1458
+ errors?: string[]
1459
+ description?: string
1460
+ schema?: {
1461
+ req?: Record<string, TJSONSchema>
1462
+ res?: { headers?: TJSONSchema }
1463
+ yield?: TJSONSchema
1464
+ returnType?: TJSONSchema
1465
+ }
1466
+ validation?: {
1467
+ req?: Record<string, (value: any) => { errors?: any[] }>
1468
+ yield?: (value: any) => { errors?: any[] }
1469
+ }
1470
+ validateYields?: boolean
1471
+ }
1472
+ handler: (ctx: TContext, req?: any) => AsyncGenerator<any, any, unknown> | Promise<{ headers: Record<string, string>; stream: AsyncGenerator<any, any, unknown> }>
1473
+ }
1474
+ ```
1475
+
1476
+ - [ ] **Step 2: Commit**
1477
+
1478
+ ```bash
1479
+ git add src/types.ts
1480
+ git commit -m "feat(types): add CreateHttpStream registration type"
1481
+ ```
1482
+
1483
+ ---
1484
+
1485
+ ### Task 16: Implement `makeCreateHttpStream` (no res.headers branch — plain async generator)
1486
+
1487
+ **Files:**
1488
+ - Create: `src/create-http-stream.ts`
1489
+
1490
+ - [ ] **Step 1: Write the file**
1491
+
1492
+ Use the existing `CreateStream` body from `src/create-stream.ts` as the template. Required adaptations:
1493
+ 1. Accept `path`/`method`/`scope`/`errors` config fields.
1494
+ 2. Use `req` channels (mirror `create-http.ts` validation loop).
1495
+ 3. Set `kind: 'http-stream'` on registration.
1496
+ 4. Reject `schema.params` with v8 message (mirror `create-http.ts`).
1497
+ 5. Run path-param consistency check (call the helper exported from `create-http.ts` — extract it or duplicate).
1498
+
1499
+ ```ts
1500
+ import { ProcedureError, ProcedureRegistrationError, ProcedureValidationError, ProcedureYieldValidationError } from './errors.js'
1501
+ import { computeSchema } from './schema/compute-schema.js'
1502
+ import { Prettify, TSchemaLib } from './schema/types.js'
1503
+ import { captureDefinitionInfo } from './stack-utils.js'
1504
+ import {
1505
+ HttpMethod, TBuilderConfig, TStreamContext,
1506
+ THttpStreamProcedureRegistration, TProcedureRegistration, TStreamProcedureRegistration, THttpProcedureRegistration,
1507
+ } from './types.js'
1508
+ import { checkPathParamConsistency } from './create-http.js' // export it from create-http.ts
1509
+
1510
+ export function makeCreateHttpStream<TContext>(
1511
+ procedures: Map<string, TProcedureRegistration<TContext, any> | TStreamProcedureRegistration<TContext, any> | THttpProcedureRegistration<TContext> | THttpStreamProcedureRegistration<TContext>>,
1512
+ builder?: { config?: TBuilderConfig; onCreate?: (procedure: any) => void },
1513
+ ) {
1514
+ return function CreateHttpStream<TName extends string, TReq extends Record<string, unknown> | undefined, TYieldType, TReturnType = void, TErrorKey extends string = string>(
1515
+ name: TName,
1516
+ config: {
1517
+ path: string
1518
+ method: HttpMethod
1519
+ scope?: string
1520
+ errors?: TErrorKey[]
1521
+ description?: string
1522
+ schema: {
1523
+ req?: TReq
1524
+ yield?: TYieldType
1525
+ returnType?: TReturnType
1526
+ // res: { headers? } added in Task 18
1527
+ }
1528
+ validateYields?: boolean
1529
+ },
1530
+ handler: (
1531
+ ctx: Prettify<TContext & TStreamContext>,
1532
+ req: TReq extends Record<string, unknown> ? Prettify<{ [K in keyof TReq]: TSchemaLib<TReq[K]> }> : undefined,
1533
+ ) => AsyncGenerator<TSchemaLib<TYieldType>, TSchemaLib<TReturnType> | void, unknown>,
1534
+ ) {
1535
+ const definitionInfo = captureDefinitionInfo()
1536
+
1537
+ if (procedures.has(name)) throw new Error(`Procedure with name ${name} is already registered`)
1538
+ if ((config.schema as any)?.params) {
1539
+ throw new ProcedureRegistrationError(name, `Use schema.req.body (or schema.req.query) instead of schema.params on CreateHttpStream. Procedure: "${name}".`, definitionInfo)
1540
+ }
1541
+
1542
+ const { jsonSchema, validations } = computeSchema(name, {
1543
+ req: config.schema.req as Record<string, unknown> | undefined,
1544
+ yieldType: config.schema.yield,
1545
+ returnType: config.schema.returnType,
1546
+ }, definitionInfo)
1547
+
1548
+ const pathParamsSchema = (jsonSchema.req as Record<string, unknown> | undefined)?.pathParams
1549
+ checkPathParamConsistency(name, config.path, pathParamsSchema as Record<string, unknown> | undefined, definitionInfo)
1550
+
1551
+ const errorFactory = (message: string, meta?: object) => new ProcedureError(name, message, meta, definitionInfo)
1552
+ const validateYields = config.validateYields ?? false
1553
+
1554
+ const wrappedHandler = async function* (ctx: TContext, req: any) {
1555
+ const abortController = new AbortController()
1556
+ const skipValidation = (ctx as { isPrevalidated?: boolean }).isPrevalidated || builder?.config?.noRuntimeValidation
1557
+
1558
+ if (validations?.req && !skipValidation) {
1559
+ for (const [channel, validator] of Object.entries(validations.req)) {
1560
+ const channelValue = (req as Record<string, unknown>)?.[channel]
1561
+ const { errors } = validator(channelValue)
1562
+ if (errors) {
1563
+ throw new ProcedureValidationError(name, `Validation error for ${name} in req.${channel}`, errors, definitionInfo)
1564
+ }
1565
+ }
1566
+ }
1567
+
1568
+ const incomingSignal = (ctx as { signal?: AbortSignal }).signal
1569
+ const signal = incomingSignal ? AbortSignal.any([incomingSignal, abortController.signal]) : abortController.signal
1570
+ const streamCtx: TStreamContext = { error: errorFactory, signal }
1571
+
1572
+ const userGenerator = handler({ ...ctx, ...streamCtx } as any, req)
1573
+ const userIterator = userGenerator[Symbol.asyncIterator]()
1574
+
1575
+ try {
1576
+ let userIterResult = await userIterator.next()
1577
+ while (!userIterResult.done) {
1578
+ const value = userIterResult.value
1579
+ if (validateYields && validations.yield) {
1580
+ const { errors } = validations.yield(value)
1581
+ if (errors) throw new ProcedureYieldValidationError(name, `Yield validation error for ${name}`, errors, definitionInfo)
1582
+ }
1583
+ yield value
1584
+ userIterResult = await userIterator.next()
1585
+ }
1586
+ return userIterResult.value
1587
+ } catch (error: any) {
1588
+ if (definitionInfo.definedAt && error && typeof error.stack === 'string') {
1589
+ const { file, line, column } = definitionInfo.definedAt
1590
+ error.stack = `${error.stack}\n--- Procedure "${name}" defined at ---\n at ${file}:${line}:${column}`
1591
+ }
1592
+ throw error
1593
+ } finally {
1594
+ try { await userIterator.return?.(undefined) } catch { /* swallow cleanup */ }
1595
+ abortController.abort('stream-completed')
1596
+ }
1597
+ }
1598
+
1599
+ const registeredProcedure: THttpStreamProcedureRegistration<TContext> = {
1600
+ name,
1601
+ kind: 'http-stream',
1602
+ config: {
1603
+ path: config.path,
1604
+ method: config.method,
1605
+ scope: config.scope,
1606
+ errors: config.errors as string[] | undefined,
1607
+ description: config.description,
1608
+ schema: jsonSchema as any,
1609
+ validation: { req: validations.req, yield: validations.yield },
1610
+ validateYields,
1611
+ },
1612
+ handler: wrappedHandler as any,
1613
+ }
1614
+
1615
+ procedures.set(name, registeredProcedure as any)
1616
+ builder?.onCreate?.(registeredProcedure)
1617
+
1618
+ const info = { name, kind: 'http-stream' as const, ...registeredProcedure.config }
1619
+
1620
+ return {
1621
+ [name]: registeredProcedure.handler,
1622
+ procedure: registeredProcedure.handler,
1623
+ info,
1624
+ } as {
1625
+ [K in TName]: typeof registeredProcedure.handler
1626
+ } & {
1627
+ procedure: typeof registeredProcedure.handler
1628
+ info: typeof info
1629
+ }
1630
+ }
1631
+ }
1632
+ ```
1633
+
1634
+ - [ ] **Step 2: Export `checkPathParamConsistency` from `create-http.ts`**
1635
+
1636
+ Add `export` keyword to the helper.
1637
+
1638
+ - [ ] **Step 3: Wire `CreateHttpStream` into `Procedures` factory**
1639
+
1640
+ In `src/index.ts`:
1641
+
1642
+ ```ts
1643
+ import { makeCreateHttpStream } from './create-http-stream.js'
1644
+
1645
+ // inside Procedures():
1646
+ const CreateHttpStream = makeCreateHttpStream<TContext>(procedures as any, builder)
1647
+
1648
+ return { /* ... */ Create, CreateStream, CreateHttp, CreateHttpStream }
1649
+ ```
1650
+
1651
+ - [ ] **Step 4: Build**
1652
+
1653
+ ```bash
1654
+ npm run build
1655
+ ```
1656
+
1657
+ Expected: green.
1658
+
1659
+ - [ ] **Step 5: Commit**
1660
+
1661
+ ```bash
1662
+ git add src/create-http-stream.ts src/create-http.ts src/index.ts
1663
+ git commit -m "feat(core): add CreateHttpStream (plain async generator, no res.headers yet)"
1664
+ ```
1665
+
1666
+ ---
1667
+
1668
+ ### Task 17: Test `CreateHttpStream` plain-generator path
1669
+
1670
+ **Files:**
1671
+ - Create: `src/create-http-stream.test.ts`
1672
+
1673
+ - [ ] **Step 1: Write tests covering the basic shape**
1674
+
1675
+ ```ts
1676
+ import { describe, it, expect, vi } from 'vitest'
1677
+ import { Procedures } from './index.js'
1678
+ import { Type } from 'typebox'
1679
+ import { ProcedureValidationError } from './errors.js'
1680
+
1681
+ describe('CreateHttpStream basic generator', () => {
1682
+ it('registers with kind: http-stream', () => {
1683
+ const procs = Procedures()
1684
+ procs.CreateHttpStream('Tail', {
1685
+ path: '/streams/logs', method: 'get',
1686
+ schema: {
1687
+ req: { query: Type.Object({ source: Type.String() }) },
1688
+ yield: Type.Object({ line: Type.String() }),
1689
+ },
1690
+ }, async function* () { yield { line: 'a' } })
1691
+
1692
+ expect(procs.getProcedure('Tail')?.kind).toBe('http-stream')
1693
+ })
1694
+
1695
+ it('yields and returns', async () => {
1696
+ const procs = Procedures()
1697
+ const { Tail } = procs.CreateHttpStream('Tail', {
1698
+ path: '/streams/logs', method: 'get',
1699
+ schema: {
1700
+ req: { query: Type.Object({ source: Type.String() }) },
1701
+ yield: Type.Object({ line: Type.String() }),
1702
+ returnType: Type.Object({ totalLines: Type.Number() }),
1703
+ },
1704
+ }, async function* (_, { query }) {
1705
+ yield { line: `from-${query.source}-1` }
1706
+ yield { line: `from-${query.source}-2` }
1707
+ return { totalLines: 2 }
1708
+ })
1709
+
1710
+ const lines: string[] = []
1711
+ let returnVal: unknown
1712
+ const gen = Tail({} as any, { query: { source: 'app' } })
1713
+ let result = await gen.next()
1714
+ while (!result.done) {
1715
+ lines.push(result.value.line)
1716
+ result = await gen.next()
1717
+ }
1718
+ returnVal = result.value
1719
+ expect(lines).toEqual(['from-app-1', 'from-app-2'])
1720
+ expect(returnVal).toEqual({ totalLines: 2 })
1721
+ })
1722
+
1723
+ it('rejects req.query validation error before opening stream', async () => {
1724
+ const procs = Procedures()
1725
+ const { Tail } = procs.CreateHttpStream('Tail', {
1726
+ path: '/streams/logs', method: 'get',
1727
+ schema: {
1728
+ req: { query: Type.Object({ source: Type.String() }) },
1729
+ yield: Type.Object({ line: Type.String() }),
1730
+ },
1731
+ }, async function* () { yield { line: 'should not run' } })
1732
+
1733
+ const gen = Tail({} as any, { query: {} as any })
1734
+ await expect(gen.next()).rejects.toThrow(ProcedureValidationError)
1735
+ await expect(gen.next()).rejects.toThrow(/req\.query/)
1736
+ })
1737
+ })
1738
+ ```
1739
+
1740
+ - [ ] **Step 2: Run tests**
1741
+
1742
+ ```bash
1743
+ npx vitest run src/create-http-stream.test.ts
1744
+ ```
1745
+
1746
+ Expected: PASS.
1747
+
1748
+ - [ ] **Step 3: Commit**
1749
+
1750
+ ```bash
1751
+ git add src/create-http-stream.test.ts
1752
+ git commit -m "test(core): cover CreateHttpStream basic generator path"
1753
+ ```
1754
+
1755
+ ---
1756
+
1757
+ ### Task 18: Add `res.headers` async-preamble shape to `CreateHttpStream`
1758
+
1759
+ **Files:**
1760
+ - Modify: `src/create-http-stream.ts`
1761
+ - Modify: `src/types.ts`
1762
+ - Test: `src/create-http-stream.test.ts`
1763
+
1764
+ - [ ] **Step 1: Extend the config and handler types**
1765
+
1766
+ In `src/create-http-stream.ts`, widen the config and handler signature:
1767
+
1768
+ ```ts
1769
+ return function CreateHttpStream<
1770
+ TName extends string,
1771
+ TReq extends Record<string, unknown> | undefined,
1772
+ TYieldType,
1773
+ TReturnType = void,
1774
+ TResHeaders = undefined,
1775
+ TErrorKey extends string = string,
1776
+ >(
1777
+ name: TName,
1778
+ config: {
1779
+ path: string
1780
+ method: HttpMethod
1781
+ scope?: string
1782
+ errors?: TErrorKey[]
1783
+ description?: string
1784
+ schema: {
1785
+ req?: TReq
1786
+ yield?: TYieldType
1787
+ returnType?: TReturnType
1788
+ res?: TResHeaders extends undefined ? undefined : { headers: TResHeaders }
1789
+ }
1790
+ validateYields?: boolean
1791
+ },
1792
+ handler: TResHeaders extends undefined
1793
+ ? (
1794
+ ctx: Prettify<TContext & TStreamContext>,
1795
+ req: TReq extends Record<string, unknown> ? Prettify<{ [K in keyof TReq]: TSchemaLib<TReq[K]> }> : undefined,
1796
+ ) => AsyncGenerator<TSchemaLib<TYieldType>, TSchemaLib<TReturnType> | void, unknown>
1797
+ : (
1798
+ ctx: Prettify<TContext & TStreamContext>,
1799
+ req: TReq extends Record<string, unknown> ? Prettify<{ [K in keyof TReq]: TSchemaLib<TReq[K]> }> : undefined,
1800
+ ) => Promise<{ headers: TSchemaLib<TResHeaders>; stream: AsyncGenerator<TSchemaLib<TYieldType>, TSchemaLib<TReturnType> | void, unknown> }>,
1801
+ ) { /* ... */ }
1802
+ ```
1803
+
1804
+ - [ ] **Step 2: Update the wrapped handler to detect the async-preamble shape at runtime**
1805
+
1806
+ The runtime needs to introspect what the user handler returns. If it's a generator (has `Symbol.asyncIterator`), use today's path. If it's a Promise, await it for `{ headers, stream }` and use the `stream` field.
1807
+
1808
+ Replace the `wrappedHandler` body in `makeCreateHttpStream`:
1809
+
1810
+ ```ts
1811
+ const wrappedHandler = async function* (ctx: TContext, req: any) {
1812
+ const abortController = new AbortController()
1813
+ const skipValidation = (ctx as { isPrevalidated?: boolean }).isPrevalidated || builder?.config?.noRuntimeValidation
1814
+
1815
+ if (validations?.req && !skipValidation) {
1816
+ for (const [channel, validator] of Object.entries(validations.req)) {
1817
+ const channelValue = (req as Record<string, unknown>)?.[channel]
1818
+ const { errors } = validator(channelValue)
1819
+ if (errors) throw new ProcedureValidationError(name, `Validation error for ${name} in req.${channel}`, errors, definitionInfo)
1820
+ }
1821
+ }
1822
+
1823
+ const incomingSignal = (ctx as { signal?: AbortSignal }).signal
1824
+ const signal = incomingSignal ? AbortSignal.any([incomingSignal, abortController.signal]) : abortController.signal
1825
+ const streamCtx: TStreamContext = { error: errorFactory, signal }
1826
+
1827
+ const handlerResult: any = (handler as any)({ ...ctx, ...streamCtx }, req)
1828
+
1829
+ // Detect async-preamble shape: handler returned a Promise
1830
+ let userGenerator: AsyncGenerator<any, any, unknown>
1831
+ let initialHeaders: Record<string, string> | undefined
1832
+
1833
+ if (typeof handlerResult?.[Symbol.asyncIterator] === 'function') {
1834
+ // Plain async generator
1835
+ userGenerator = handlerResult
1836
+ } else if (typeof handlerResult?.then === 'function') {
1837
+ // Promise → await for { headers, stream }
1838
+ const resolved = await handlerResult
1839
+ if (!resolved || typeof resolved !== 'object' || !('stream' in resolved)) {
1840
+ throw new ProcedureError(name, `CreateHttpStream handler returned a Promise that did not resolve to { headers, stream }. Got: ${JSON.stringify(resolved)}`, undefined, definitionInfo)
1841
+ }
1842
+ initialHeaders = resolved.headers
1843
+ userGenerator = resolved.stream
1844
+ } else {
1845
+ throw new ProcedureError(name, `CreateHttpStream handler must return an AsyncGenerator or Promise<{ headers, stream }>.`, undefined, definitionInfo)
1846
+ }
1847
+
1848
+ // Surface initial headers via a synthetic first yield with a known sentinel — but
1849
+ // that's not transport-clean. Instead we attach headers to the wrappedHandler's
1850
+ // generator object so the Hono builder can read them. AsyncGenerators don't
1851
+ // support arbitrary properties cleanly through the typed surface, so we expose
1852
+ // them via the registration's `getInitialHeaders` accessor rather than the
1853
+ // generator itself.
1854
+ ;(wrappedHandler as any)._lastInitialHeaders = initialHeaders
1855
+
1856
+ const userIterator = userGenerator[Symbol.asyncIterator]()
1857
+ try {
1858
+ let userIterResult = await userIterator.next()
1859
+ while (!userIterResult.done) {
1860
+ const value = userIterResult.value
1861
+ if (validateYields && validations.yield) {
1862
+ const { errors } = validations.yield(value)
1863
+ if (errors) throw new ProcedureYieldValidationError(name, `Yield validation error for ${name}`, errors, definitionInfo)
1864
+ }
1865
+ yield value
1866
+ userIterResult = await userIterator.next()
1867
+ }
1868
+ return userIterResult.value
1869
+ } catch (error: any) {
1870
+ if (definitionInfo.definedAt && error && typeof error.stack === 'string') {
1871
+ const { file, line, column } = definitionInfo.definedAt
1872
+ error.stack = `${error.stack}\n--- Procedure "${name}" defined at ---\n at ${file}:${line}:${column}`
1873
+ }
1874
+ throw error
1875
+ } finally {
1876
+ try { await userIterator.return?.(undefined) } catch { /* swallow */ }
1877
+ abortController.abort('stream-completed')
1878
+ }
1879
+ }
1880
+ ```
1881
+
1882
+ **Note on the initial-headers handoff:** stashing on `wrappedHandler._lastInitialHeaders` is fragile under concurrent calls. Better: make the wrappedHandler return a custom iterable that exposes `.initialHeaders` when iterated. Or — simpler — yield a sentinel first value `{ __initialHeaders: {...} }` that the Hono builder strips. Choose the sentinel approach for thread safety:
1883
+
1884
+ Replace the marked block with:
1885
+
1886
+ ```ts
1887
+ // Yield headers sentinel as the first value if async-preamble was used.
1888
+ // The Hono builder strips this sentinel and applies headers before opening SSE.
1889
+ if (initialHeaders) {
1890
+ yield { __initialHeaders: initialHeaders } as any
1891
+ }
1892
+ ```
1893
+
1894
+ And document the sentinel shape in `src/types.ts`:
1895
+
1896
+ ```ts
1897
+ export const HTTP_STREAM_HEADERS_SENTINEL = '__initialHeaders' as const
1898
+ ```
1899
+
1900
+ - [ ] **Step 3: Write failing tests for the async-preamble shape**
1901
+
1902
+ ```ts
1903
+ describe('CreateHttpStream async-preamble shape (res.headers)', () => {
1904
+ it('handler returning Promise<{ headers, stream }> yields headers sentinel first', async () => {
1905
+ const procs = Procedures()
1906
+ const { Tail } = procs.CreateHttpStream('Tail', {
1907
+ path: '/streams/logs', method: 'get',
1908
+ schema: {
1909
+ req: { query: Type.Object({ source: Type.String() }) },
1910
+ yield: Type.Object({ line: Type.String() }),
1911
+ res: { headers: Type.Object({ 'x-stream-id': Type.String() }) },
1912
+ },
1913
+ }, async (_, { query }) => ({
1914
+ headers: { 'x-stream-id': `id-${query.source}` },
1915
+ stream: (async function* () { yield { line: 'a' } })(),
1916
+ }))
1917
+
1918
+ const gen = Tail({} as any, { query: { source: 'app' } })
1919
+ const first = await gen.next()
1920
+ expect(first.value).toEqual({ __initialHeaders: { 'x-stream-id': 'id-app' } })
1921
+ const second = await gen.next()
1922
+ expect(second.value).toEqual({ line: 'a' })
1923
+ })
1924
+
1925
+ it('throws when handler returns malformed shape', async () => {
1926
+ const procs = Procedures()
1927
+ const { Bad } = procs.CreateHttpStream('Bad', {
1928
+ path: '/streams', method: 'get',
1929
+ schema: {
1930
+ yield: Type.Number(),
1931
+ res: { headers: Type.Object({}) },
1932
+ },
1933
+ }, (async () => 'not-a-shape') as any)
1934
+
1935
+ const gen = Bad({} as any, undefined)
1936
+ await expect(gen.next()).rejects.toThrow(/did not resolve to/)
1937
+ })
1938
+ })
1939
+ ```
1940
+
1941
+ - [ ] **Step 4: Run tests**
1942
+
1943
+ ```bash
1944
+ npx vitest run src/create-http-stream.test.ts
1945
+ ```
1946
+
1947
+ Expected: PASS.
1948
+
1949
+ - [ ] **Step 5: Commit**
1950
+
1951
+ ```bash
1952
+ git add src/create-http-stream.ts src/types.ts src/create-http-stream.test.ts
1953
+ git commit -m "feat(core): add CreateHttpStream async-preamble shape with headers sentinel"
1954
+ ```
1955
+
1956
+ ---
1957
+
1958
+ ## Phase 6 — Hono builder unification
1959
+
1960
+ ### Task 19: Strip `APIConfig` generic from `HonoAPIFactoryItem`
1961
+
1962
+ **Files:**
1963
+ - Modify: `src/implementations/http/hono-api/types.ts`
1964
+
1965
+ - [ ] **Step 1: Replace `HonoAPIFactoryItem` definition**
1966
+
1967
+ ```ts
1968
+ import { ExtractContext, APIHttpRouteDoc } from '../../types.js'
1969
+ import { Procedures } from '../../../index.js'
1970
+ import { THttpProcedureRegistration, THttpStreamProcedureRegistration } from '../../../types.js'
1971
+ import { Context } from 'hono'
1972
+
1973
+ export type HonoAPIFactoryItem<TFactory = ReturnType<typeof Procedures<any>>> = {
1974
+ factory: TFactory
1975
+ factoryContext: ExtractContext<TFactory> | ((c: Context) => ExtractContext<TFactory> | Promise<ExtractContext<TFactory>>)
1976
+ extendProcedureDoc?: (params: {
1977
+ base: APIHttpRouteDoc
1978
+ procedure: THttpProcedureRegistration<any> | THttpStreamProcedureRegistration<any>
1979
+ }) => Record<string, any>
1980
+ }
1981
+ ```
1982
+
1983
+ - [ ] **Step 2: Build to surface compile errors**
1984
+
1985
+ ```bash
1986
+ npm run build
1987
+ ```
1988
+
1989
+ Expected: errors in `src/implementations/http/hono-api/index.ts` because the factory is no longer generic over `APIConfig`. Those will be fixed in Task 20.
1990
+
1991
+ - [ ] **Step 3: Commit (without build green — Task 20 finishes the chain)**
1992
+
1993
+ ```bash
1994
+ git add src/implementations/http/hono-api/types.ts
1995
+ git commit -m "refactor(hono-api): drop APIConfig generic from HonoAPIFactoryItem"
1996
+ ```
1997
+
1998
+ ---
1999
+
2000
+ ### Task 20: Update `HonoAPIAppBuilder` to read `CreateHttp` registrations and apply response headers
2001
+
2002
+ **Files:**
2003
+ - Modify: `src/implementations/http/hono-api/index.ts`
2004
+
2005
+ - [ ] **Step 1: Update `register` signature**
2006
+
2007
+ ```ts
2008
+ register<TFactory extends ProceduresFactory>(
2009
+ factory: TFactory,
2010
+ factoryContext: ExtractContext<TFactory> | ((c: Context) => ExtractContext<TFactory> | Promise<ExtractContext<TFactory>>),
2011
+ extendProcedureDoc?: (params: {
2012
+ base: APIHttpRouteDoc
2013
+ procedure: THttpProcedureRegistration<any> | THttpStreamProcedureRegistration<any>
2014
+ }) => Record<string, any>,
2015
+ ): this
2016
+ ```
2017
+
2018
+ - [ ] **Step 2: In `build()`, filter procedures by `kind === 'http'` and capture skipped procedures**
2019
+
2020
+ ```ts
2021
+ build(): Hono {
2022
+ const queryParser = this.config?.queryParser ?? parseQueryNative
2023
+ const skipped: { name: string; reason: string }[] = []
2024
+
2025
+ this.factories.forEach(({ factory, factoryContext, extendProcedureDoc }) => {
2026
+ factory.getProcedures().forEach((procedure: any) => {
2027
+ if (procedure.kind === 'http') {
2028
+ // existing http registration flow
2029
+ this.registerHttpRoute(procedure, factoryContext, extendProcedureDoc, queryParser)
2030
+ } else if (procedure.kind === 'http-stream') {
2031
+ // Phase 6 Task 22 wires this up
2032
+ this.registerHttpStreamRoute(procedure, factoryContext, extendProcedureDoc, queryParser)
2033
+ } else {
2034
+ skipped.push({
2035
+ name: procedure.name,
2036
+ reason: `Procedure has kind "${procedure.kind}"; HonoAPIAppBuilder serves only "http" and "http-stream".`,
2037
+ })
2038
+ }
2039
+ })
2040
+ })
2041
+
2042
+ this._skippedProcedures = skipped
2043
+ return this._app
2044
+ }
2045
+ ```
2046
+
2047
+ Add the `_skippedProcedures` private field and a `get skippedProcedures()` accessor.
2048
+
2049
+ - [ ] **Step 3: Refactor existing route handler logic into `registerHttpRoute`**
2050
+
2051
+ Move the existing per-procedure body (path resolution, doc build, route handler, mount) into `private registerHttpRoute(procedure, factoryContext, extendProcedureDoc, queryParser)`. Adapt it to read `procedure.config` directly (no more `APIConfig` generic — fields are first-class).
2052
+
2053
+ - [ ] **Step 4: Update `extractInputParams` to read `req` instead of `input`**
2054
+
2055
+ The function body is unchanged in logic — just rename the variable from `inputSchema` to `reqSchema` and update the parameter type. Channel extraction (`pathParams`/`query`/`body`/`headers`) is identical to today.
2056
+
2057
+ - [ ] **Step 5: Apply response headers when handler returns `{ body, headers }`**
2058
+
2059
+ In the route handler:
2060
+
2061
+ ```ts
2062
+ const result = await procedure.handler({ ...context, signal: c.req.raw.signal }, params)
2063
+
2064
+ if (this.config?.onSuccess) this.config.onSuccess(procedure, c)
2065
+ if (successStatus === 204) {
2066
+ // Apply headers if present
2067
+ if (result && typeof result === 'object' && 'headers' in result && result.headers) {
2068
+ for (const [k, v] of Object.entries(result.headers as Record<string, string>)) {
2069
+ c.header(k, v)
2070
+ }
2071
+ }
2072
+ return c.body(null, 204)
2073
+ }
2074
+
2075
+ // Detect { body, headers } shape vs bare body
2076
+ let body: unknown = result
2077
+ let headers: Record<string, string> | undefined
2078
+ if (result && typeof result === 'object' && 'body' in result && 'headers' in result) {
2079
+ body = (result as any).body
2080
+ headers = (result as any).headers
2081
+ } else if (result && typeof result === 'object' && 'headers' in result && !('body' in result)) {
2082
+ // headers-only case
2083
+ for (const [k, v] of Object.entries((result as any).headers as Record<string, string>)) {
2084
+ c.header(k, v)
2085
+ }
2086
+ return c.body(null, successStatus as any)
2087
+ }
2088
+
2089
+ if (headers) {
2090
+ for (const [k, v] of Object.entries(headers)) c.header(k, v)
2091
+ }
2092
+ return c.json(body, successStatus as any)
2093
+ ```
2094
+
2095
+ - [ ] **Step 6: Build to confirm compile errors are resolved**
2096
+
2097
+ ```bash
2098
+ npm run build
2099
+ ```
2100
+
2101
+ Expected: green.
2102
+
2103
+ - [ ] **Step 7: Run hono-api tests (will likely fail — fixed in next task)**
2104
+
2105
+ ```bash
2106
+ npx vitest run src/implementations/http/hono-api/
2107
+ ```
2108
+
2109
+ Note any failing tests; they'll need updates (Task 23).
2110
+
2111
+ - [ ] **Step 8: Commit**
2112
+
2113
+ ```bash
2114
+ git add src/implementations/http/hono-api/index.ts
2115
+ git commit -m "refactor(hono-api): read CreateHttp registrations directly; apply response headers"
2116
+ ```
2117
+
2118
+ ---
2119
+
2120
+ ### Task 21: Add `'http-stream'` branch to `HonoAPIAppBuilder`
2121
+
2122
+ **Files:**
2123
+ - Modify: `src/implementations/http/hono-api/index.ts`
2124
+
2125
+ - [ ] **Step 1: Implement `registerHttpStreamRoute`**
2126
+
2127
+ ```ts
2128
+ import { streamSSE } from 'hono/streaming'
2129
+ import { HTTP_STREAM_HEADERS_SENTINEL } from '../../../types.js'
2130
+
2131
+ private registerHttpStreamRoute(
2132
+ procedure: THttpStreamProcedureRegistration<any>,
2133
+ factoryContext: any,
2134
+ extendProcedureDoc: any,
2135
+ queryParser: QueryParser,
2136
+ ) {
2137
+ const fullPath = this.resolveFullPath(procedure.config.path)
2138
+ const route = this.buildHttpStreamRouteDoc(procedure, fullPath, extendProcedureDoc)
2139
+ this._docs.push(route)
2140
+
2141
+ const method = procedure.config.method.toUpperCase()
2142
+ const reqSchema = procedure.config.schema?.req
2143
+
2144
+ this._app.on(method, fullPath, async (c: Context) => {
2145
+ try {
2146
+ const context = typeof factoryContext === 'function' ? await factoryContext(c) : factoryContext
2147
+ const params = reqSchema
2148
+ ? await this.extractInputParams(c, procedure.config.method, reqSchema, queryParser)
2149
+ : undefined
2150
+
2151
+ // Pre-stream phase: open the generator, await first value (which may be sentinel)
2152
+ const gen = procedure.handler({ ...context, signal: c.req.raw.signal, isPrevalidated: false }, params)
2153
+ const first = await gen.next()
2154
+
2155
+ // If first value is the headers sentinel, apply headers and consume next
2156
+ let initialYield: any
2157
+ if (first.value && typeof first.value === 'object' && HTTP_STREAM_HEADERS_SENTINEL in first.value) {
2158
+ for (const [k, v] of Object.entries((first.value as any)[HTTP_STREAM_HEADERS_SENTINEL] as Record<string, string>)) {
2159
+ c.header(k, v)
2160
+ }
2161
+ initialYield = await gen.next()
2162
+ } else {
2163
+ initialYield = first
2164
+ }
2165
+
2166
+ // Open SSE response and stream remaining yields
2167
+ return streamSSE(c, async (stream) => {
2168
+ let it = initialYield
2169
+ while (!it.done) {
2170
+ await stream.writeSSE({ data: JSON.stringify(it.value) })
2171
+ it = await gen.next()
2172
+ }
2173
+ if (it.value !== undefined) {
2174
+ await stream.writeSSE({ event: 'return', data: JSON.stringify(it.value) })
2175
+ }
2176
+ })
2177
+ } catch (error) {
2178
+ // Pre-stream error path — same dispatch as registerHttpRoute
2179
+ return this.handleError(error, procedure, c)
2180
+ }
2181
+ })
2182
+ }
2183
+ ```
2184
+
2185
+ Extract the error-dispatch block from `registerHttpRoute` into a shared `private handleError(error, procedure, c)` method so both branches use the same dispatch flow (taxonomy → onError → hard default + onRequestError observer).
2186
+
2187
+ - [ ] **Step 2: Add `buildHttpStreamRouteDoc` helper**
2188
+
2189
+ ```ts
2190
+ private buildHttpStreamRouteDoc(
2191
+ procedure: THttpStreamProcedureRegistration<any>,
2192
+ fullPath: string,
2193
+ extendProcedureDoc?: HonoAPIFactoryItem['extendProcedureDoc'],
2194
+ ): HttpStreamRouteDoc {
2195
+ const config = procedure.config
2196
+ const reqSchema = config.schema?.req
2197
+ const resSchema = config.schema?.res
2198
+ const jsonSchema: HttpStreamRouteDoc['jsonSchema'] = {}
2199
+ if (reqSchema) jsonSchema.req = reqSchema
2200
+ if (resSchema) jsonSchema.res = resSchema
2201
+ if (config.schema?.yield) jsonSchema.yield = config.schema.yield
2202
+ if (config.schema?.returnType) jsonSchema.returnType = config.schema.returnType
2203
+
2204
+ const base: HttpStreamRouteDoc = {
2205
+ kind: 'http-stream',
2206
+ name: procedure.name,
2207
+ scope: config.scope,
2208
+ path: config.path,
2209
+ method: config.method,
2210
+ fullPath,
2211
+ streamMode: 'sse',
2212
+ jsonSchema,
2213
+ }
2214
+ if (config.errors && config.errors.length > 0) base.errors = [...config.errors]
2215
+
2216
+ let extendedDoc: object = {}
2217
+ if (extendProcedureDoc) extendedDoc = extendProcedureDoc({ base: base as any, procedure })
2218
+ return { ...extendedDoc, ...base }
2219
+ }
2220
+ ```
2221
+
2222
+ (`HttpStreamRouteDoc` type is added in Phase 7 / Task 24. For now use `any` if needed and revisit.)
2223
+
2224
+ - [ ] **Step 3: Build**
2225
+
2226
+ ```bash
2227
+ npm run build
2228
+ ```
2229
+
2230
+ Expected: green (or near-green pending the type added in Phase 7).
2231
+
2232
+ - [ ] **Step 4: Commit**
2233
+
2234
+ ```bash
2235
+ git add src/implementations/http/hono-api/index.ts
2236
+ git commit -m "feat(hono-api): add http-stream branch with sentinel-based headers handoff"
2237
+ ```
2238
+
2239
+ ---
2240
+
2241
+ ### Task 22: Test `HonoAPIAppBuilder` for both `'http'` and `'http-stream'` kinds + response headers
2242
+
2243
+ **Files:**
2244
+ - Modify: `src/implementations/http/hono-api/index.test.ts`
2245
+
2246
+ - [ ] **Step 1: Update existing tests to use `CreateHttp` instead of `Create` + `APIConfig`**
2247
+
2248
+ Find tests that registered procedures via `Procedures<Ctx, APIConfig>().Create('Foo', { path, method, schema: { input } }, ...)`. Migrate them to `Procedures<Ctx>().CreateHttp('Foo', { path, method, schema: { req } }, ...)`.
2249
+
2250
+ - [ ] **Step 2: Add new tests for response headers**
2251
+
2252
+ ```ts
2253
+ it('applies res.headers to the response', async () => {
2254
+ const procs = Procedures()
2255
+ procs.CreateHttp('GetUser', {
2256
+ path: '/users/:id', method: 'get',
2257
+ schema: {
2258
+ req: { pathParams: Type.Object({ id: Type.String() }) },
2259
+ res: {
2260
+ body: Type.Object({ id: Type.String() }),
2261
+ headers: Type.Object({ 'x-rate-limit': Type.String() }),
2262
+ },
2263
+ },
2264
+ }, async (_, { pathParams }) => ({
2265
+ body: { id: pathParams.id },
2266
+ headers: { 'x-rate-limit': '99' },
2267
+ }))
2268
+
2269
+ const app = new HonoAPIAppBuilder().register(procs, {}).build()
2270
+ const res = await app.request('/users/abc')
2271
+ expect(res.status).toBe(200)
2272
+ expect(res.headers.get('x-rate-limit')).toBe('99')
2273
+ expect(await res.json()).toEqual({ id: 'abc' })
2274
+ })
2275
+
2276
+ it('handles headers-only response (204)', async () => {
2277
+ const procs = Procedures()
2278
+ procs.CreateHttp('Touch', {
2279
+ path: '/touch/:id', method: 'delete',
2280
+ schema: {
2281
+ req: { pathParams: Type.Object({ id: Type.String() }) },
2282
+ res: { headers: Type.Object({ 'x-touched-at': Type.String() }) },
2283
+ },
2284
+ }, async () => ({ headers: { 'x-touched-at': '2026-05-08' } }))
2285
+
2286
+ const app = new HonoAPIAppBuilder().register(procs, {}).build()
2287
+ const res = await app.request('/touch/abc', { method: 'DELETE' })
2288
+ expect(res.status).toBe(204)
2289
+ expect(res.headers.get('x-touched-at')).toBe('2026-05-08')
2290
+ })
2291
+ ```
2292
+
2293
+ - [ ] **Step 3: Add tests for the http-stream branch**
2294
+
2295
+ ```ts
2296
+ it('serves CreateHttpStream as SSE', async () => {
2297
+ const procs = Procedures()
2298
+ procs.CreateHttpStream('TailLogs', {
2299
+ path: '/streams/logs', method: 'get',
2300
+ schema: {
2301
+ req: { query: Type.Object({ source: Type.String() }) },
2302
+ yield: Type.Object({ line: Type.String() }),
2303
+ returnType: Type.Object({ totalLines: Type.Number() }),
2304
+ },
2305
+ }, async function* (_, { query }) {
2306
+ yield { line: `from-${query.source}-1` }
2307
+ yield { line: `from-${query.source}-2` }
2308
+ return { totalLines: 2 }
2309
+ })
2310
+
2311
+ const app = new HonoAPIAppBuilder().register(procs, {}).build()
2312
+ const res = await app.request('/streams/logs?source=app')
2313
+ const text = await res.text()
2314
+ expect(text).toContain('data: {"line":"from-app-1"}')
2315
+ expect(text).toContain('event: return\ndata: {"totalLines":2}')
2316
+ })
2317
+
2318
+ it('applies http-stream initial response headers from async preamble', async () => {
2319
+ const procs = Procedures()
2320
+ procs.CreateHttpStream('TailLogs', {
2321
+ path: '/streams/logs', method: 'get',
2322
+ schema: {
2323
+ req: { query: Type.Object({ source: Type.String() }) },
2324
+ yield: Type.Object({ line: Type.String() }),
2325
+ res: { headers: Type.Object({ 'x-stream-id': Type.String() }) },
2326
+ },
2327
+ }, async (_, { query }) => ({
2328
+ headers: { 'x-stream-id': `sid-${query.source}` },
2329
+ stream: (async function* () { yield { line: 'first' } })(),
2330
+ }))
2331
+
2332
+ const app = new HonoAPIAppBuilder().register(procs, {}).build()
2333
+ const res = await app.request('/streams/logs?source=app')
2334
+ expect(res.headers.get('x-stream-id')).toBe('sid-app')
2335
+ expect(await res.text()).toContain('data: {"line":"first"}')
2336
+ })
2337
+ ```
2338
+
2339
+ - [ ] **Step 4: Run tests**
2340
+
2341
+ ```bash
2342
+ npx vitest run src/implementations/http/hono-api/
2343
+ ```
2344
+
2345
+ Expected: PASS.
2346
+
2347
+ - [ ] **Step 5: Commit**
2348
+
2349
+ ```bash
2350
+ git add src/implementations/http/hono-api/index.test.ts
2351
+ git commit -m "test(hono-api): cover CreateHttp response headers and http-stream branch"
2352
+ ```
2353
+
2354
+ ---
2355
+
2356
+ ### Task 23: Add `kind`-based filtering to `HonoRPCAppBuilder`, `HonoStreamAppBuilder`
2357
+
2358
+ **Files:**
2359
+ - Modify: `src/implementations/http/hono-rpc/index.ts`
2360
+ - Modify: `src/implementations/http/hono-stream/index.ts`
2361
+
2362
+ - [ ] **Step 1: For each of the two builders, change `getProcedures().forEach` filtering**
2363
+
2364
+ In `hono-rpc/index.ts` `build()`:
2365
+
2366
+ ```ts
2367
+ factory.getProcedures().forEach((procedure: any) => {
2368
+ if (procedure.kind !== 'rpc') {
2369
+ skipped.push({
2370
+ name: procedure.name,
2371
+ reason: `Procedure has kind "${procedure.kind}"; HonoRPCAppBuilder serves only "rpc".`,
2372
+ })
2373
+ return
2374
+ }
2375
+ // existing rpc registration flow
2376
+ })
2377
+ ```
2378
+
2379
+ - [ ] **Step 2: Same pattern for `hono-stream` (kind === 'rpc-stream')**
2380
+
2381
+ - [ ] **Step 3: Add `_skippedProcedures` and accessor on each builder**
2382
+
2383
+ Mirror the `HonoAPIAppBuilder._skippedProcedures` pattern from Task 20.
2384
+
2385
+ - [ ] **Step 4: Update each builder's tests to assert skipped procedures**
2386
+
2387
+ For each test file, add:
2388
+
2389
+ ```ts
2390
+ it('records http-kind procedures in skippedProcedures', () => {
2391
+ const procs = Procedures()
2392
+ procs.CreateHttp('Skipped', {
2393
+ path: '/x', method: 'get',
2394
+ schema: { req: { query: Type.Object({}) } },
2395
+ }, async () => undefined)
2396
+
2397
+ const builder = new HonoRPCAppBuilder().register(procs, {})
2398
+ builder.build()
2399
+ expect(builder.skippedProcedures).toContainEqual({
2400
+ name: 'Skipped',
2401
+ reason: expect.stringContaining('HonoRPCAppBuilder serves only "rpc"'),
2402
+ })
2403
+ })
2404
+ ```
2405
+
2406
+ - [ ] **Step 5: Run all builder tests**
2407
+
2408
+ ```bash
2409
+ npx vitest run src/implementations/http/
2410
+ ```
2411
+
2412
+ Expected: PASS.
2413
+
2414
+ - [ ] **Step 6: Commit**
2415
+
2416
+ ```bash
2417
+ git add src/implementations/http/hono-rpc/ src/implementations/http/hono-stream/
2418
+ git commit -m "refactor(builders): kind-based filtering with skippedProcedures across rpc/stream builders"
2419
+ ```
2420
+
2421
+ ---
2422
+
2423
+ ### Task 24: Update Astro adapter for kind-aware updates
2424
+
2425
+ **Files:**
2426
+ - Modify: `src/implementations/http/astro/*`
2427
+ - Modify: `src/implementations/http/astro/*.test.ts`
2428
+
2429
+ - [ ] **Step 1: Inspect the Astro adapter to identify how it consumes procedures**
2430
+
2431
+ ```bash
2432
+ ls src/implementations/http/astro/
2433
+ cat src/implementations/http/astro/*.ts | head -200
2434
+ ```
2435
+
2436
+ - [ ] **Step 2: Apply the same pattern as `HonoAPIAppBuilder`**
2437
+
2438
+ Filter by `kind === 'http'` (and `'http-stream'` if Astro supports streaming). Stash skipped procedures. Read `path` / `method` / `req` / `res` from `CreateHttp` registrations directly. Apply response headers from handler return.
2439
+
2440
+ - [ ] **Step 3: Update Astro tests**
2441
+
2442
+ Migrate any tests using `Create` + `schema.input` to `CreateHttp` + `schema.req`.
2443
+
2444
+ - [ ] **Step 4: Run Astro tests**
2445
+
2446
+ ```bash
2447
+ npx vitest run src/implementations/http/astro/
2448
+ ```
2449
+
2450
+ Expected: PASS.
2451
+
2452
+ - [ ] **Step 5: Commit**
2453
+
2454
+ ```bash
2455
+ git add src/implementations/http/astro/
2456
+ git commit -m "refactor(astro): kind-aware procedure filtering and response headers"
2457
+ ```
2458
+
2459
+ ---
2460
+
2461
+ ## Phase 7 — Doc envelope updates
2462
+
2463
+ ### Task 25: Restructure `APIHttpRouteDoc` and add `HttpStreamRouteDoc`
2464
+
2465
+ **Files:**
2466
+ - Modify: `src/implementations/types.ts`
2467
+
2468
+ - [ ] **Step 1: Replace `APIHttpRouteDoc` with regrouped shape**
2469
+
2470
+ ```ts
2471
+ export interface APIHttpRouteDoc {
2472
+ kind: 'api'
2473
+ name: string
2474
+ scope?: string
2475
+ path: string
2476
+ method: HttpMethod
2477
+ fullPath: string
2478
+ successStatus?: number
2479
+ errors?: string[]
2480
+ jsonSchema: {
2481
+ req?: { pathParams?: Record<string, unknown>; query?: Record<string, unknown>; body?: Record<string, unknown>; headers?: Record<string, unknown> }
2482
+ res?: { body?: Record<string, unknown>; headers?: Record<string, unknown> }
2483
+ }
2484
+ }
2485
+ ```
2486
+
2487
+ - [ ] **Step 2: Add `HttpStreamRouteDoc`**
2488
+
2489
+ ```ts
2490
+ export interface HttpStreamRouteDoc {
2491
+ kind: 'http-stream'
2492
+ name: string
2493
+ scope?: string
2494
+ path: string
2495
+ method: HttpMethod
2496
+ fullPath: string
2497
+ streamMode: StreamMode
2498
+ errors?: string[]
2499
+ jsonSchema: {
2500
+ req?: { pathParams?: Record<string, unknown>; query?: Record<string, unknown>; body?: Record<string, unknown>; headers?: Record<string, unknown> }
2501
+ res?: { headers?: Record<string, unknown> }
2502
+ yield?: Record<string, unknown>
2503
+ returnType?: Record<string, unknown>
2504
+ }
2505
+ }
2506
+ ```
2507
+
2508
+ - [ ] **Step 3: Extend `AnyHttpRouteDoc` union**
2509
+
2510
+ ```ts
2511
+ export type AnyHttpRouteDoc = RPCHttpRouteDoc | APIHttpRouteDoc | StreamHttpRouteDoc | HttpStreamRouteDoc
2512
+ ```
2513
+
2514
+ - [ ] **Step 4: Remove `APIInput` and `APIConfig` (or simplify)**
2515
+
2516
+ `APIConfig` is no longer needed as a `TExtendedConfig` shape since HTTP fields live on `CreateHttp` directly. But it may still be referenced from external types — search for usages first:
2517
+
2518
+ ```bash
2519
+ grep -rn "APIConfig\|APIInput" src/ --include="*.ts"
2520
+ ```
2521
+
2522
+ If any internal code still references `APIConfig`, either inline its fields or leave the type as a deprecation-marked alias. Prefer removal — the migration error in Phase 2 catches v7 callers.
2523
+
2524
+ - [ ] **Step 5: Build**
2525
+
2526
+ ```bash
2527
+ npm run build
2528
+ ```
2529
+
2530
+ Expected: surfaces compile errors in `hono-api/index.ts` (the `buildApiHttpRouteDoc` and `buildHttpStreamRouteDoc` need to populate the new shape) and in codegen files (Phase 8 work). Fix the hono-api build errors now:
2531
+
2532
+ In `buildApiHttpRouteDoc` (Task 20):
2533
+
2534
+ ```ts
2535
+ const reqSchema = procedure.config.schema?.req
2536
+ const resSchema = procedure.config.schema?.res
2537
+ const jsonSchema: APIHttpRouteDoc['jsonSchema'] = {}
2538
+ if (reqSchema) jsonSchema.req = reqSchema as any
2539
+ if (resSchema) jsonSchema.res = resSchema as any
2540
+ ```
2541
+
2542
+ - [ ] **Step 6: Run hono-api tests**
2543
+
2544
+ ```bash
2545
+ npx vitest run src/implementations/http/hono-api/
2546
+ ```
2547
+
2548
+ Expected: PASS.
2549
+
2550
+ - [ ] **Step 7: Commit**
2551
+
2552
+ ```bash
2553
+ git add src/implementations/types.ts src/implementations/http/hono-api/index.ts
2554
+ git commit -m "feat(envelope): regroup APIHttpRouteDoc under req/res; add HttpStreamRouteDoc"
2555
+ ```
2556
+
2557
+ ---
2558
+
2559
+ ### Task 26: Update `DocRegistry` for `'http-stream'` kind
2560
+
2561
+ **Files:**
2562
+ - Modify: `src/implementations/http/doc-registry.ts`
2563
+ - Modify: `src/implementations/http/doc-registry.test.ts`
2564
+
2565
+ - [ ] **Step 1: Search for kind-discriminating logic**
2566
+
2567
+ ```bash
2568
+ grep -n "kind\|RPCHttpRouteDoc\|APIHttpRouteDoc\|StreamHttpRouteDoc" src/implementations/http/doc-registry.ts
2569
+ ```
2570
+
2571
+ - [ ] **Step 2: Add `'http-stream'` to any switch/filter logic**
2572
+
2573
+ If `doc-registry.ts` has explicit kind-handling, add the new case. If it just iterates `AnyHttpRouteDoc[]`, the type widening in Task 25 is sufficient.
2574
+
2575
+ - [ ] **Step 3: Add a test that aggregates docs from multiple builders including http-stream**
2576
+
2577
+ ```ts
2578
+ it('aggregates http-stream routes from HonoAPIAppBuilder', () => {
2579
+ const procs = Procedures()
2580
+ procs.CreateHttpStream('Tail', {
2581
+ path: '/streams/logs', method: 'get',
2582
+ schema: { req: { query: Type.Object({ source: Type.String() }) }, yield: Type.Object({ line: Type.String() }) },
2583
+ }, async function* () {})
2584
+
2585
+ const builder = new HonoAPIAppBuilder().register(procs, {})
2586
+ builder.build()
2587
+
2588
+ const registry = DocRegistry.from(builder)
2589
+ const envelope = registry.toJSON()
2590
+ const route = envelope.routes.find((r) => r.name === 'Tail')
2591
+ expect(route?.kind).toBe('http-stream')
2592
+ })
2593
+ ```
2594
+
2595
+ - [ ] **Step 4: Run doc-registry tests**
2596
+
2597
+ ```bash
2598
+ npx vitest run src/implementations/http/doc-registry.test.ts
2599
+ ```
2600
+
2601
+ Expected: PASS.
2602
+
2603
+ - [ ] **Step 5: Commit**
2604
+
2605
+ ```bash
2606
+ git add src/implementations/http/doc-registry.ts src/implementations/http/doc-registry.test.ts
2607
+ git commit -m "feat(doc-registry): aggregate http-stream routes alongside http"
2608
+ ```
2609
+
2610
+ ---
2611
+
2612
+ ## Phase 8 — Codegen TS target
2613
+
2614
+ ### Task 27: Update `group-routes.ts` and `route-slots.ts` for new kind and req/res grouping
2615
+
2616
+ **Files:**
2617
+ - Modify: `src/codegen/group-routes.ts`
2618
+ - Modify: `src/codegen/targets/_shared/route-slots.ts`
2619
+
2620
+ - [ ] **Step 1: Read current `route-slots.ts`**
2621
+
2622
+ ```bash
2623
+ cat src/codegen/targets/_shared/route-slots.ts
2624
+ ```
2625
+
2626
+ - [ ] **Step 2: Add `responseHeaders` slot to `RouteSlot` definition**
2627
+
2628
+ ```ts
2629
+ export type RouteSlot = {
2630
+ pathParams?: Record<string, unknown>
2631
+ query?: Record<string, unknown>
2632
+ body?: Record<string, unknown>
2633
+ headers?: Record<string, unknown> // request headers
2634
+ response?: Record<string, unknown> // response body (for back-compat / unary)
2635
+ responseHeaders?: Record<string, unknown> // new
2636
+ yield?: Record<string, unknown>
2637
+ returnType?: Record<string, unknown>
2638
+ }
2639
+ ```
2640
+
2641
+ - [ ] **Step 3: Update `extractRouteSlots` to read from new envelope shape**
2642
+
2643
+ ```ts
2644
+ export function extractRouteSlots(route: AnyHttpRouteDoc): RouteSlot {
2645
+ const slots: RouteSlot = {}
2646
+ if (route.kind === 'api' || route.kind === 'http-stream') {
2647
+ const req = route.jsonSchema.req
2648
+ if (req?.pathParams) slots.pathParams = req.pathParams
2649
+ if (req?.query) slots.query = req.query
2650
+ if (req?.body) slots.body = req.body
2651
+ if (req?.headers) slots.headers = req.headers
2652
+ const res = route.jsonSchema.res
2653
+ if (res?.body) slots.response = res.body
2654
+ if (res?.headers) slots.responseHeaders = res.headers
2655
+ }
2656
+ if (route.kind === 'http-stream') {
2657
+ if (route.jsonSchema.yield) slots.yield = route.jsonSchema.yield
2658
+ if (route.jsonSchema.returnType) slots.returnType = route.jsonSchema.returnType
2659
+ }
2660
+ // ... existing rpc and stream cases
2661
+ return slots
2662
+ }
2663
+ ```
2664
+
2665
+ - [ ] **Step 4: Update `group-routes.ts` to recognize `'http-stream'` kind**
2666
+
2667
+ If `group-routes.ts` has any kind-specific switching, add the case. Streams group by scope same as `'http'`.
2668
+
2669
+ - [ ] **Step 5: Build**
2670
+
2671
+ ```bash
2672
+ npm run build
2673
+ ```
2674
+
2675
+ Expected: green (or surfaces emit-scope.ts errors fixed in Task 28).
2676
+
2677
+ - [ ] **Step 6: Commit**
2678
+
2679
+ ```bash
2680
+ git add src/codegen/group-routes.ts src/codegen/targets/_shared/route-slots.ts
2681
+ git commit -m "feat(codegen): add responseHeaders slot and http-stream kind handling"
2682
+ ```
2683
+
2684
+ ---
2685
+
2686
+ ### Task 28: Update `emit-scope.ts` for `Req` / `Response` namespace naming and conditional return
2687
+
2688
+ **Files:**
2689
+ - Modify: `src/codegen/emit-scope.ts`
2690
+ - Modify: `src/codegen/targets/ts/run.ts` (if emit logic is split there)
2691
+
2692
+ - [ ] **Step 1: Read existing emission for `'api'` routes**
2693
+
2694
+ ```bash
2695
+ grep -n "emit\(API\|Api\|Stream\|Rpc\)Route\|Params\|Response" src/codegen/emit-scope.ts | head -40
2696
+ ```
2697
+
2698
+ - [ ] **Step 2: Restructure type namespace emission**
2699
+
2700
+ Where today's code emits flat `<Route>Params`, `<Route>Body`, `<Route>Response`, change to nested under `Req` and `Response`:
2701
+
2702
+ Namespace mode (the default):
2703
+ ```
2704
+ <Route>.Req.PathParams
2705
+ <Route>.Req.Query
2706
+ <Route>.Req.Body
2707
+ <Route>.Req.Headers
2708
+ <Route>.Response.Body
2709
+ <Route>.Response.Headers
2710
+ <Route>.Errors
2711
+ ```
2712
+
2713
+ Flat mode:
2714
+ ```
2715
+ <Route>ReqPathParams
2716
+ <Route>ReqQuery
2717
+ <Route>ReqBody
2718
+ <Route>ReqHeaders
2719
+ <Route>ResponseBody
2720
+ <Route>ResponseHeaders
2721
+ ```
2722
+
2723
+ - [ ] **Step 3: Update generated callable signature with conditional return**
2724
+
2725
+ When `slots.responseHeaders` is present, emit:
2726
+
2727
+ ```ts
2728
+ declare function getUser(req: GetUser.Req, opts?): Promise<{ body: GetUser.Response.Body; headers: GetUser.Response.Headers }>
2729
+ ```
2730
+
2731
+ When only `slots.response` (body) is present:
2732
+ ```ts
2733
+ declare function getUser(req: GetUser.Req, opts?): Promise<GetUser.Response.Body>
2734
+ ```
2735
+
2736
+ When only `slots.responseHeaders`:
2737
+ ```ts
2738
+ declare function getUser(req: GetUser.Req, opts?): Promise<{ headers: GetUser.Response.Headers }>
2739
+ ```
2740
+
2741
+ When neither:
2742
+ ```ts
2743
+ declare function getUser(req: GetUser.Req, opts?): Promise<void>
2744
+ ```
2745
+
2746
+ - [ ] **Step 4: Add `'http-stream'` callable emission**
2747
+
2748
+ Mirror the existing `'stream'` emission but with `Req` namespace and optional `Response.Headers`. Returns `TypedStream<Yield, Return>` where `TypedStream` has optional `headers` field.
2749
+
2750
+ - [ ] **Step 5: Update `.safe()` sibling to widen with the conditional return**
2751
+
2752
+ If `slots.responseHeaders`, the typed result becomes `Result<{ body, headers }, ETyped>` not `Result<Body, ETyped>`.
2753
+
2754
+ - [ ] **Step 6: Regenerate the fixture envelope**
2755
+
2756
+ ```bash
2757
+ # Find the test that constructs the fixture and run it manually to dump current output
2758
+ grep -rn "users-envelope.json" src/codegen/
2759
+ ```
2760
+
2761
+ Update `src/codegen/__fixtures__/users-envelope.json` with the new `req`/`res` grouping and `'http-stream'` kind. Easiest path: write a small fixture-generation script that registers procs via `CreateHttp`/`CreateHttpStream`, builds the doc registry, and writes the JSON. Or hand-edit the JSON.
2762
+
2763
+ - [ ] **Step 7: Run codegen tests**
2764
+
2765
+ ```bash
2766
+ npx vitest run src/codegen/
2767
+ ```
2768
+
2769
+ Expected: snapshot mismatches at first. Inspect each snapshot diff carefully — confirm the new shape is correct, then update snapshots:
2770
+
2771
+ ```bash
2772
+ npx vitest run src/codegen/ -u
2773
+ ```
2774
+
2775
+ - [ ] **Step 8: Commit**
2776
+
2777
+ ```bash
2778
+ git add src/codegen/emit-scope.ts src/codegen/__fixtures__/users-envelope.json src/codegen/**/*.snap*
2779
+ git commit -m "feat(codegen-ts): emit Req/Response namespaces and conditional return shapes"
2780
+ ```
2781
+
2782
+ ---
2783
+
2784
+ ### Task 29: Update self-contained `_client.ts` and `_types.ts` emission
2785
+
2786
+ **Files:**
2787
+ - Modify: `src/codegen/targets/ts/run.ts` (or wherever `_client.ts` is bundled)
2788
+ - Modify: `src/client/*.ts` source files (which get bundled)
2789
+
2790
+ - [ ] **Step 1: Find where `_client.ts` is assembled**
2791
+
2792
+ ```bash
2793
+ grep -rn "_client.ts\|selfContained" src/codegen/
2794
+ ```
2795
+
2796
+ - [ ] **Step 2: Ensure new client runtime files (added in Phase 10) are bundled**
2797
+
2798
+ Phase 10 will modify `src/client/call.ts`, `src/client/stream.ts`, `src/client/types.ts`, `src/client/fetch-adapter.ts` to surface response headers. The self-contained bundler reads these files — verify the bundling logic picks up changes automatically (typically a `readFileSync` over the client source files).
2799
+
2800
+ - [ ] **Step 3: Run codegen tests for self-contained mode**
2801
+
2802
+ ```bash
2803
+ npx vitest run src/codegen/ -t "self-contained"
2804
+ ```
2805
+
2806
+ Expected: tests pass after Phase 10 lands. For now, may pass or have type-only mismatches deferred to Phase 10.
2807
+
2808
+ - [ ] **Step 4: Commit any changes**
2809
+
2810
+ ```bash
2811
+ git add -p
2812
+ git commit -m "feat(codegen-ts): wire self-contained _client.ts to surface response headers"
2813
+ ```
2814
+
2815
+ ---
2816
+
2817
+ ## Phase 9 — Codegen Kotlin & Swift targets
2818
+
2819
+ ### Task 30: Kotlin target — emit `Response.Headers` data class
2820
+
2821
+ **Files:**
2822
+ - Modify: `src/codegen/targets/kotlin/run.ts`
2823
+ - Modify: `src/codegen/targets/kotlin/run.test.ts`
2824
+
2825
+ - [ ] **Step 1: Read the current Kotlin emit logic**
2826
+
2827
+ ```bash
2828
+ cat src/codegen/targets/kotlin/run.ts | head -80
2829
+ ```
2830
+
2831
+ Identify where `Response` data class is emitted for `'api'` routes.
2832
+
2833
+ - [ ] **Step 2: Update Kotlin emission for new envelope shape**
2834
+
2835
+ Read `req` channel schemas instead of flat `pathParams` / `query` / etc. Adapt the nested object emission accordingly:
2836
+
2837
+ ```kotlin
2838
+ object Users {
2839
+ object GetUser {
2840
+ const val method = "GET"
2841
+ fun path(p: PathParams): String = "/users/${p.id}"
2842
+ @Serializable data class PathParams(val id: String)
2843
+ @Serializable data class Query(val include: String? = null)
2844
+ object Response {
2845
+ @Serializable data class Body(val id: String, val name: String)
2846
+ @Serializable data class Headers(@SerialName("x-rate-limit") val xRateLimit: String) // only when res.headers declared
2847
+ }
2848
+ }
2849
+ }
2850
+ ```
2851
+
2852
+ - [ ] **Step 3: Add `'http-stream'` kind emission for Kotlin**
2853
+
2854
+ Streams in Kotlin emit the same `path`/`method`/types/`Req` constructs. Yield/return types are emitted as `Yield` and `Return` data classes. The runtime/HTTP layer is consumer-owned (per spec posture). No SSE adapter emission.
2855
+
2856
+ - [ ] **Step 4: Update Kotlin snapshot tests**
2857
+
2858
+ ```bash
2859
+ npx vitest run src/codegen/targets/kotlin/run.test.ts -u
2860
+ ```
2861
+
2862
+ Inspect each snapshot diff carefully before accepting.
2863
+
2864
+ - [ ] **Step 5: Commit**
2865
+
2866
+ ```bash
2867
+ git add src/codegen/targets/kotlin/
2868
+ git commit -m "feat(codegen-kotlin): emit Req/Response namespaces and Response.Headers data class"
2869
+ ```
2870
+
2871
+ ---
2872
+
2873
+ ### Task 31: Swift target — emit `Response.Headers` struct
2874
+
2875
+ **Files:**
2876
+ - Modify: `src/codegen/targets/swift/run.ts`
2877
+ - Modify: `src/codegen/targets/swift/run.test.ts`
2878
+
2879
+ - [ ] **Step 1: Mirror Task 30's pattern for Swift**
2880
+
2881
+ Emit nested caseless-enum namespaces with `Req.PathParams`, `Req.Query`, `Req.Body`, `Req.Headers`, `Response.Body`, `Response.Headers` structs. Conditional `Response.Headers` only when declared.
2882
+
2883
+ ```swift
2884
+ enum Users {
2885
+ enum GetUser {
2886
+ static let method = "GET"
2887
+ static func path(_ p: PathParams) -> String { return "/users/\(p.id)" }
2888
+ struct PathParams: Codable { let id: String }
2889
+ struct Query: Codable { let include: String? }
2890
+ enum Response {
2891
+ struct Body: Codable { let id: String; let name: String }
2892
+ struct Headers: Codable { let xRateLimit: String; enum CodingKeys: String, CodingKey { case xRateLimit = "x-rate-limit" } }
2893
+ }
2894
+ }
2895
+ }
2896
+ ```
2897
+
2898
+ - [ ] **Step 2: Add `'http-stream'` kind emission for Swift**
2899
+
2900
+ Same posture as Kotlin — types only, no HTTP/SSE runtime.
2901
+
2902
+ - [ ] **Step 3: Update Swift snapshot tests**
2903
+
2904
+ ```bash
2905
+ npx vitest run src/codegen/targets/swift/run.test.ts -u
2906
+ ```
2907
+
2908
+ - [ ] **Step 4: Commit**
2909
+
2910
+ ```bash
2911
+ git add src/codegen/targets/swift/
2912
+ git commit -m "feat(codegen-swift): emit Req/Response namespaces and Response.Headers struct"
2913
+ ```
2914
+
2915
+ ---
2916
+
2917
+ ## Phase 10 — Client runtime updates
2918
+
2919
+ ### Task 32: Add `headers` to `AdapterStreamResponse` and `TypedStream`
2920
+
2921
+ **Files:**
2922
+ - Modify: `src/client/types.ts`
2923
+
2924
+ - [ ] **Step 1: Extend `AdapterStreamResponse` with `headers: Headers`**
2925
+
2926
+ Find the `AdapterStreamResponse` interface in `src/client/types.ts` and add:
2927
+
2928
+ ```ts
2929
+ export interface AdapterStreamResponse {
2930
+ // existing fields...
2931
+ headers: Headers // always populated by the adapter
2932
+ }
2933
+ ```
2934
+
2935
+ - [ ] **Step 2: Add optional `.headers` to `TypedStream`**
2936
+
2937
+ ```ts
2938
+ export interface TypedStream<TYield, TReturn> {
2939
+ [Symbol.asyncIterator](): AsyncIterator<TYield>
2940
+ result: Promise<TReturn>
2941
+ headers?: Headers // populated for routes declaring res.headers
2942
+ }
2943
+ ```
2944
+
2945
+ - [ ] **Step 3: Build**
2946
+
2947
+ ```bash
2948
+ npm run build
2949
+ ```
2950
+
2951
+ Expected: errors in `fetch-adapter.ts` (now must populate `.headers`) and `stream.ts` (now must wire `.headers` onto `TypedStream`). Fixed in next tasks.
2952
+
2953
+ - [ ] **Step 4: Commit**
2954
+
2955
+ ```bash
2956
+ git add src/client/types.ts
2957
+ git commit -m "feat(client): add headers to AdapterStreamResponse and TypedStream"
2958
+ ```
2959
+
2960
+ ---
2961
+
2962
+ ### Task 33: Update `fetch-adapter.ts` to populate `AdapterStreamResponse.headers`
2963
+
2964
+ **Files:**
2965
+ - Modify: `src/client/fetch-adapter.ts`
2966
+ - Modify: `src/client/fetch-adapter.test.ts`
2967
+
2968
+ - [ ] **Step 1: In `fetch-adapter.ts` `stream()` function, set `headers: response.headers` on the returned `AdapterStreamResponse`**
2969
+
2970
+ ```ts
2971
+ return {
2972
+ // existing fields...
2973
+ headers: response.headers,
2974
+ }
2975
+ ```
2976
+
2977
+ - [ ] **Step 2: Add a test asserting headers are propagated**
2978
+
2979
+ ```ts
2980
+ it('propagates response headers to AdapterStreamResponse.headers', async () => {
2981
+ const fetchSpy = vi.fn().mockResolvedValue(
2982
+ new Response('data: x\n\n', {
2983
+ status: 200,
2984
+ headers: { 'x-stream-id': 'abc', 'content-type': 'text/event-stream' },
2985
+ })
2986
+ )
2987
+ const adapter = createFetchAdapter({ fetch: fetchSpy })
2988
+ const result = await adapter.stream({ url: 'https://x', method: 'GET', headers: {} })
2989
+ expect(result.headers.get('x-stream-id')).toBe('abc')
2990
+ })
2991
+ ```
2992
+
2993
+ - [ ] **Step 3: Run tests**
2994
+
2995
+ ```bash
2996
+ npx vitest run src/client/fetch-adapter.test.ts
2997
+ ```
2998
+
2999
+ Expected: PASS.
3000
+
3001
+ - [ ] **Step 4: Commit**
3002
+
3003
+ ```bash
3004
+ git add src/client/fetch-adapter.ts src/client/fetch-adapter.test.ts
3005
+ git commit -m "feat(client): populate AdapterStreamResponse.headers in fetch adapter"
3006
+ ```
3007
+
3008
+ ---
3009
+
3010
+ ### Task 34: Update `executeCall` to surface response headers in conditional return
3011
+
3012
+ **Files:**
3013
+ - Modify: `src/client/call.ts`
3014
+ - Modify: `src/client/call.test.ts`
3015
+
3016
+ - [ ] **Step 1: Inspect current `executeCall` body**
3017
+
3018
+ ```bash
3019
+ cat src/client/call.ts
3020
+ ```
3021
+
3022
+ Identify where the response body is parsed and returned.
3023
+
3024
+ - [ ] **Step 2: Read response headers from the descriptor's metadata**
3025
+
3026
+ The descriptor (in `src/client/types.ts`) carries the route's declared shape. Add a `responseHeadersDeclared?: boolean` field on the descriptor (set by codegen — see Task 28). When `true`, `executeCall` returns `{ body, headers }` instead of bare body.
3027
+
3028
+ ```ts
3029
+ // Inside executeCall after parsing the response:
3030
+ if (descriptor.responseHeadersDeclared) {
3031
+ return { body: parsedBody, headers: response.headers }
3032
+ }
3033
+ return parsedBody
3034
+ ```
3035
+
3036
+ (`response.headers` here is the platform `Response.headers` — already a `Headers` object.)
3037
+
3038
+ - [ ] **Step 3: Update codegen to emit `responseHeadersDeclared: true` when `slots.responseHeaders` is present**
3039
+
3040
+ Back in `emit-scope.ts` (Task 28), include the flag on the descriptor literal. Inspect a generated file to confirm:
3041
+
3042
+ ```ts
3043
+ const descriptor: RouteDescriptor = {
3044
+ kind: 'api',
3045
+ // ...
3046
+ responseHeadersDeclared: true, // only when res.headers in envelope
3047
+ }
3048
+ ```
3049
+
3050
+ - [ ] **Step 4: Add tests for both shapes**
3051
+
3052
+ ```ts
3053
+ it('returns bare body when response headers not declared', async () => {
3054
+ // ...
3055
+ expect(result).toEqual({ id: 'abc' })
3056
+ })
3057
+
3058
+ it('returns { body, headers } when response headers declared', async () => {
3059
+ // ...
3060
+ expect(result).toEqual({ body: { id: 'abc' }, headers: expect.any(Headers) })
3061
+ expect(result.headers.get('x-rate-limit')).toBe('99')
3062
+ })
3063
+ ```
3064
+
3065
+ - [ ] **Step 5: Run tests**
3066
+
3067
+ ```bash
3068
+ npx vitest run src/client/call.test.ts
3069
+ ```
3070
+
3071
+ Expected: PASS.
3072
+
3073
+ - [ ] **Step 6: Commit**
3074
+
3075
+ ```bash
3076
+ git add src/client/call.ts src/client/call.test.ts src/client/types.ts src/codegen/emit-scope.ts
3077
+ git commit -m "feat(client): surface response headers via descriptor.responseHeadersDeclared flag"
3078
+ ```
3079
+
3080
+ ---
3081
+
3082
+ ### Task 35: Update `executeStream` to wire initial headers onto `TypedStream`
3083
+
3084
+ **Files:**
3085
+ - Modify: `src/client/stream.ts`
3086
+ - Modify: `src/client/stream.test.ts`
3087
+
3088
+ - [ ] **Step 1: When constructing the `TypedStream`, set `.headers` from `adapterResponse.headers`**
3089
+
3090
+ ```ts
3091
+ // Inside executeStream, when constructing the TypedStream:
3092
+ const typedStream: TypedStream<TYield, TReturn> = {
3093
+ [Symbol.asyncIterator]() { /* ... */ },
3094
+ result: resultPromise,
3095
+ headers: descriptor.responseHeadersDeclared ? adapterResponse.headers : undefined,
3096
+ }
3097
+ ```
3098
+
3099
+ - [ ] **Step 2: Add a test**
3100
+
3101
+ ```ts
3102
+ it('TypedStream.headers is populated when route declares res.headers', async () => {
3103
+ // mock adapter to return headers
3104
+ // construct stream
3105
+ // expect stream.headers.get('x-stream-id') === '...'
3106
+ })
3107
+ ```
3108
+
3109
+ - [ ] **Step 3: Run tests**
3110
+
3111
+ ```bash
3112
+ npx vitest run src/client/stream.test.ts
3113
+ ```
3114
+
3115
+ Expected: PASS.
3116
+
3117
+ - [ ] **Step 4: Commit**
3118
+
3119
+ ```bash
3120
+ git add src/client/stream.ts src/client/stream.test.ts
3121
+ git commit -m "feat(client): wire initial response headers onto TypedStream.headers"
3122
+ ```
3123
+
3124
+ ---
3125
+
3126
+ ## Phase 11 — Migration polish
3127
+
3128
+ ### Task 36: Update CLAUDE.md architecture section
3129
+
3130
+ **Files:**
3131
+ - Modify: `CLAUDE.md`
3132
+
3133
+ - [ ] **Step 1: Update the "Schema System" and "Important Patterns" sections to reflect v8**
3134
+
3135
+ Replace the `schema.input` / `APIInput` references with the four-creator surface and `req`/`res` shape.
3136
+
3137
+ - [ ] **Step 2: Add a "Procedure kinds" section near "Architecture"**
3138
+
3139
+ Brief paragraph explaining the four creators and their `kind` discriminants.
3140
+
3141
+ - [ ] **Step 3: Commit**
3142
+
3143
+ ```bash
3144
+ git add CLAUDE.md
3145
+ git commit -m "docs(claude): update architecture for v8 four-creator surface"
3146
+ ```
3147
+
3148
+ ---
3149
+
3150
+ ### Task 37: Update `agent_config/` skills, patterns, and templates
3151
+
3152
+ **Files:**
3153
+ - Modify: `agent_config/claude-code/skills/ts-procedures/{patterns.md,anti-patterns.md,api-reference.md}`
3154
+ - Modify: `agent_config/claude-code/skills/ts-procedures-scaffold/templates/*`
3155
+ - Modify: `agent_config/copilot/copilot-instructions.md`
3156
+ - Modify: `agent_config/cursor/cursorrules`
3157
+
3158
+ - [ ] **Step 1: Replace HTTP examples in `patterns.md` to use `CreateHttp`**
3159
+
3160
+ Find all examples using `Procedures<Ctx, APIConfig>().Create(...)` with `schema.input`. Replace with `Procedures<Ctx>().CreateHttp(...)` with `schema.req`.
3161
+
3162
+ - [ ] **Step 2: Add an `anti-patterns.md` entry**
3163
+
3164
+ ```markdown
3165
+ ## Don't use Create for HTTP routes
3166
+
3167
+ Wrong:
3168
+ \`\`\`ts
3169
+ Procedures<Ctx, APIConfig>().Create('GetUser', { path: '/users/:id', ... })
3170
+ \`\`\`
3171
+
3172
+ Right:
3173
+ \`\`\`ts
3174
+ Procedures<Ctx>().CreateHttp('GetUser', { path: '/users/:id', schema: { req: ... } })
3175
+ \`\`\`
3176
+
3177
+ `Create` is the RPC primitive. HTTP routes belong on `CreateHttp` / `CreateHttpStream`.
3178
+ ```
3179
+
3180
+ - [ ] **Step 3: Update scaffold templates**
3181
+
3182
+ Replace `procedure-template.ts`, `hono-rpc-template.ts`, etc. as needed. Add new `create-http.ts` and `create-http-stream.ts` templates.
3183
+
3184
+ - [ ] **Step 4: Mirror updates to Copilot and Cursor files**
3185
+
3186
+ Both files are condensed equivalents — copy the relevant sections.
3187
+
3188
+ - [ ] **Step 5: Run agent_config setup dry-run to verify**
3189
+
3190
+ ```bash
3191
+ node agent_config/bin/setup.mjs --dry-run
3192
+ ```
3193
+
3194
+ Expected: shows what would be installed; no errors.
3195
+
3196
+ - [ ] **Step 6: Commit**
3197
+
3198
+ ```bash
3199
+ git add agent_config/
3200
+ git commit -m "docs(agent_config): update skills/templates for v8 CreateHttp surface"
3201
+ ```
3202
+
3203
+ ---
3204
+
3205
+ ### Task 38: Update CHANGELOG and README
3206
+
3207
+ **Files:**
3208
+ - Modify: `CHANGELOG.md`
3209
+ - Modify: `README.md`
3210
+
3211
+ - [ ] **Step 1: Add v8.0.0 entry to CHANGELOG**
3212
+
3213
+ ```markdown
3214
+ ## 8.0.0 — 2026-05-08
3215
+
3216
+ ### Breaking changes
3217
+
3218
+ - **`schema.input` removed** from `Create` / `CreateStream`. Use `CreateHttp` / `CreateHttpStream` for per-channel HTTP validation.
3219
+ - **HTTP routes use `CreateHttp`**, not `Create` + `APIConfig`. HTTP fields (`path`, `method`, `successStatus`, `scope`, `errors`) are first-class on `CreateHttp` config.
3220
+ - **`schema.input.X` → `schema.req.X`** (channels: `pathParams`, `query`, `body`, `headers`).
3221
+ - **`APIInput` and `APIConfig` removed.**
3222
+ - **`APIHttpRouteDoc.jsonSchema` regrouped under `req` / `res`** — extendProcedureDoc callbacks reading `base.jsonSchema.body` need to read `base.jsonSchema.req.body`.
3223
+ - **`Procedures<Ctx, APIConfig>()` → `Procedures<Ctx>()`** for HTTP-only factories.
3224
+ - **Generated client flat-mode aliases renamed:** `<Route>Params` → `<Route>ReqPathParams` (and similar). Namespace-mode users get `Users.GetUser.Req.PathParams` automatically.
3225
+
3226
+ ### New
3227
+
3228
+ - **`CreateHttp` / `CreateHttpStream`** — first-class HTTP creators with structured `req` channels and typed `res: { body?, headers? }`.
3229
+ - **Conditional return shape** — `CreateHttp` handler returns body bare unless `res.headers` declared, in which case `{ body, headers }`.
3230
+ - **`TypedStream.headers`** — initial response headers surfaced on streams that declare `res.headers`.
3231
+ - **`kind` discriminant** on every registration (`'rpc' | 'rpc-stream' | 'http' | 'http-stream'`) drives builder routing and `skippedProcedures` warnings.
3232
+
3233
+ ### Migration
3234
+
3235
+ See full table at [docs/superpowers/specs/2026-05-08-create-http-design.md](docs/superpowers/specs/2026-05-08-create-http-design.md#migration). Quick checklist:
3236
+
3237
+ 1. Replace `Procedures<Ctx, APIConfig>()` with `Procedures<Ctx>()` for HTTP factories.
3238
+ 2. Replace `Create('Foo', { path, method, schema: { input } }, h)` with `CreateHttp('Foo', { path, method, schema: { req } }, h)`.
3239
+ 3. Replace `schema.input.X` with `schema.req.X`.
3240
+ 4. Re-run `npx ts-procedures-codegen` to regenerate clients.
3241
+ 5. Update flat-mode-alias call sites if applicable.
3242
+ ```
3243
+
3244
+ - [ ] **Step 2: Update README.md headline examples**
3245
+
3246
+ Replace any `Create` + `schema.input` example with `CreateHttp` + `schema.req`. Add a new section showcasing response headers.
3247
+
3248
+ - [ ] **Step 3: Commit**
3249
+
3250
+ ```bash
3251
+ git add CHANGELOG.md README.md
3252
+ git commit -m "docs: add v8.0.0 changelog entry and README migration"
3253
+ ```
3254
+
3255
+ ---
3256
+
3257
+ ### Task 39: Bump package version to 8.0.0
3258
+
3259
+ **Files:**
3260
+ - Modify: `package.json`
3261
+
3262
+ - [ ] **Step 1: Update version**
3263
+
3264
+ ```bash
3265
+ npm version 8.0.0 --no-git-tag-version
3266
+ ```
3267
+
3268
+ - [ ] **Step 2: Verify**
3269
+
3270
+ ```bash
3271
+ grep '"version"' package.json
3272
+ ```
3273
+
3274
+ Expected: `"version": "8.0.0"`.
3275
+
3276
+ - [ ] **Step 3: Commit**
3277
+
3278
+ ```bash
3279
+ git add package.json
3280
+ git commit -m "chore: bump version to 8.0.0"
3281
+ ```
3282
+
3283
+ ---
3284
+
3285
+ ### Task 40: Final integration check — build, lint, full test suite
3286
+
3287
+ **Files:** none
3288
+
3289
+ - [ ] **Step 1: Run full build**
3290
+
3291
+ ```bash
3292
+ npm run build
3293
+ ```
3294
+
3295
+ Expected: green, no warnings.
3296
+
3297
+ - [ ] **Step 2: Run lint**
3298
+
3299
+ ```bash
3300
+ npm run lint
3301
+ ```
3302
+
3303
+ Expected: green.
3304
+
3305
+ - [ ] **Step 3: Run full test suite**
3306
+
3307
+ ```bash
3308
+ npm run test
3309
+ ```
3310
+
3311
+ Expected: all tests green. Any `it.todo` from Phase 1 (legacy `schema.input` tests) should now either be deleted (if obsolete) or migrated to `CreateHttp` / `CreateHttpStream` equivalents.
3312
+
3313
+ - [ ] **Step 4: Address any failures by fixing forward (no skip / no `.todo`)**
3314
+
3315
+ - [ ] **Step 5: Commit any final fixes**
3316
+
3317
+ ```bash
3318
+ git add -p
3319
+ git commit -m "test: final cleanup of v7 schema.input migrations"
3320
+ ```
3321
+
3322
+ - [ ] **Step 6: Verify clean working tree**
3323
+
3324
+ ```bash
3325
+ git status
3326
+ ```
3327
+
3328
+ Expected: `nothing to commit, working tree clean`.
3329
+
3330
+ ---
3331
+
3332
+ ## Self-review notes
3333
+
3334
+ Spec coverage check (against `docs/superpowers/specs/2026-05-08-create-http-design.md`):
3335
+
3336
+ - ✅ Four-creator surface (Tasks 1-18)
3337
+ - ✅ `schema.input` removed from Create/CreateStream (Tasks 7-8)
3338
+ - ✅ HTTP fields baked into CreateHttp (Tasks 10-11)
3339
+ - ✅ Conditional return shape (Task 14)
3340
+ - ✅ Async-preamble for CreateHttpStream initial headers (Task 18)
3341
+ - ✅ `kind` discriminant + builder filtering (Tasks 5, 23)
3342
+ - ✅ Path-param consistency moved to core (Task 11)
3343
+ - ✅ HonoAPIAppBuilder unification serving both http and http-stream (Tasks 19-22)
3344
+ - ✅ APIHttpRouteDoc regrouped to req/res (Task 25)
3345
+ - ✅ HttpStreamRouteDoc added (Task 25)
3346
+ - ✅ Codegen TS namespacing (Req/Response) and conditional return (Task 28)
3347
+ - ✅ Codegen Kotlin/Swift Response.Headers (Tasks 30-31)
3348
+ - ✅ Client runtime: TypedStream.headers, descriptor.responseHeadersDeclared (Tasks 32-35)
3349
+ - ✅ Migration messages on legacy shapes (Tasks 7-8)
3350
+ - ✅ CHANGELOG, README, agent_config (Tasks 37-38)
3351
+ - ✅ Version bump (Task 39)
3352
+ - ✅ Astro adapter (Task 24)
3353
+ - ✅ skippedProcedures across all builders (Tasks 20, 23)
3354
+
3355
+ No placeholders. Type names consistent across tasks (`THttpProcedureRegistration`, `THttpStreamProcedureRegistration`, `HttpReturn<TRes>`, `HTTP_STREAM_HEADERS_SENTINEL`, `responseHeadersDeclared`).