ts-procedures 7.3.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 +104 -53
  5. package/agent_config/claude-code/skills/ts-procedures/api-reference.md +205 -232
  6. package/agent_config/claude-code/skills/ts-procedures/patterns.md +80 -153
  7. package/agent_config/claude-code/skills/ts-procedures-review/SKILL.md +1 -1
  8. package/agent_config/claude-code/skills/ts-procedures-review/checklist.md +4 -5
  9. package/agent_config/claude-code/skills/ts-procedures-scaffold/SKILL.md +4 -7
  10. package/agent_config/claude-code/skills/ts-procedures-scaffold/templates/hono.md +223 -0
  11. package/agent_config/copilot/copilot-instructions.md +34 -48
  12. package/agent_config/cursor/cursorrules +34 -48
  13. package/build/client/call.js +4 -1
  14. package/build/client/call.js.map +1 -1
  15. package/build/client/call.test.js +23 -0
  16. package/build/client/call.test.js.map +1 -1
  17. package/build/client/fetch-adapter.js +3 -1
  18. package/build/client/fetch-adapter.js.map +1 -1
  19. package/build/client/fetch-adapter.test.js +11 -1
  20. package/build/client/fetch-adapter.test.js.map +1 -1
  21. package/build/client/index.test.js +7 -7
  22. package/build/client/index.test.js.map +1 -1
  23. package/build/client/request-builder.d.ts +1 -1
  24. package/build/client/request-builder.js +2 -2
  25. package/build/client/request-builder.js.map +1 -1
  26. package/build/client/stream.js +13 -2
  27. package/build/client/stream.js.map +1 -1
  28. package/build/client/stream.test.js +32 -7
  29. package/build/client/stream.test.js.map +1 -1
  30. package/build/client/typed-error-dispatch.test.js +8 -92
  31. package/build/client/typed-error-dispatch.test.js.map +1 -1
  32. package/build/client/types.d.ts +21 -3
  33. package/build/codegen/bin/cli.js +0 -0
  34. package/build/codegen/e2e.test.js +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 +124 -124
  153. package/build/index.js +10 -221
  154. package/build/index.js.map +1 -1
  155. package/build/index.test.js +20 -919
  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 +15 -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 +22 -1249
  248. package/src/index.ts +49 -485
  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,3365 @@
1
+ # HonoAppBuilder Convergence 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:** Replace the three Hono builders (`HonoRPCAppBuilder`, `HonoAPIAppBuilder`, `HonoStreamAppBuilder`) with a single `HonoAppBuilder` that accepts any `Procedures<>()` factory and dispatches each procedure to the correct handler by `kind`.
6
+
7
+ **Architecture:** New `src/implementations/http/hono/` directory containing the public builder + per-kind handler/doc modules. Shared error-dispatch primitives extracted to `src/implementations/http/error-dispatch.ts` (one level up so it can be reused by future framework adapters). Old `hono-rpc/`, `hono-api/`, `hono-stream/` directories deleted entirely.
8
+
9
+ **Tech Stack:** TypeScript, Hono 4.x, Vitest 4.x, AJV (validation, unchanged), TypeBox (schema authoring, unchanged).
10
+
11
+ **Spec:** `docs/superpowers/specs/2026-05-08-hono-app-builder-convergence-design.md`
12
+
13
+ **Pre-existing infrastructure this plan depends on (do not modify):**
14
+ - `src/implementations/http/error-taxonomy.ts` — `defineErrorTaxonomy`, `resolveErrorResponse`, `defaultErrorTaxonomy`, `UnknownErrorConfig`, `taxonomyToErrorDocs`, `PROCEDURE_REGISTRATION_ERROR_DOC`
15
+ - `src/implementations/http/doc-registry.ts` — `DocRegistry` class
16
+ - `src/implementations/types.ts` — `RPCConfig`, `APIConfig`, `RPCHttpRouteDoc`, `APIHttpRouteDoc`, `StreamHttpRouteDoc`, `HttpStreamRouteDoc`, `AnyHttpRouteDoc`, `DocSource`, `DocEnvelope`, `ProceduresFactory`, `ExtractContext`, `ExtractConfig`, `HttpMethod`, `StreamMode`, `APIInput`
17
+ - `src/types.ts` — `TProcedureRegistration`, `TStreamProcedureRegistration`, `THttpProcedureRegistration`, `THttpStreamProcedureRegistration`, `ProcedureKind`
18
+ - `src/index.ts` — `Procedures` factory
19
+
20
+ ---
21
+
22
+ ## File Structure
23
+
24
+ **Created:**
25
+ - `src/implementations/http/error-dispatch.ts` — shared `dispatchPreStreamError` + `dispatchMidStreamError`
26
+ - `src/implementations/http/error-dispatch.test.ts`
27
+ - `src/implementations/http/hono/index.ts` — `HonoAppBuilder` class (public surface)
28
+ - `src/implementations/http/hono/index.test.ts` — public-surface tests
29
+ - `src/implementations/http/hono/types.ts` — `HonoAppBuilderConfig`, `HonoFactoryItem`, `OnRequestErrorContext`, `AnyProcedureRegistration`
30
+ - `src/implementations/http/hono/path.ts` — `makeRoutePath`, `resolveFullPath`
31
+ - `src/implementations/http/hono/path.test.ts`
32
+ - `src/implementations/http/hono/handlers/rpc.ts` — `installRpcRoute`
33
+ - `src/implementations/http/hono/handlers/rpc.test.ts`
34
+ - `src/implementations/http/hono/handlers/http.ts` — `installHttpRoute`
35
+ - `src/implementations/http/hono/handlers/http.test.ts`
36
+ - `src/implementations/http/hono/handlers/stream.ts` — `installRpcStreamRoute`
37
+ - `src/implementations/http/hono/handlers/stream.test.ts`
38
+ - `src/implementations/http/hono/handlers/http-stream.ts` — `installHttpStreamRoute`
39
+ - `src/implementations/http/hono/handlers/http-stream.test.ts`
40
+ - `src/implementations/http/hono/docs/rpc-doc.ts`
41
+ - `src/implementations/http/hono/docs/http-doc.ts`
42
+ - `src/implementations/http/hono/docs/stream-doc.ts`
43
+ - `src/implementations/http/hono/docs/http-stream-doc.ts`
44
+
45
+ **Deleted entirely (after the new code passes its tests):**
46
+ - `src/implementations/http/hono-rpc/` (4 files — index.ts, types.ts, index.test.ts, error-taxonomy.test.ts)
47
+ - `src/implementations/http/hono-api/` (4 files)
48
+ - `src/implementations/http/hono-stream/` (4 files)
49
+
50
+ **Modified:**
51
+ - `package.json` — remove `./hono-rpc`, `./hono-api`, `./hono-stream` exports; add `./hono` export
52
+ - `src/implementations/http/doc-registry.test.ts` — update imports + assertions to point at `HonoAppBuilder`
53
+ - `src/implementations/http/on-request-error.test.ts` — collapse three describe blocks into one, retarget at `HonoAppBuilder`
54
+ - `src/implementations/http/route-errors.test.ts` — retarget imports
55
+ - `src/implementations/http/error-taxonomy.test.ts` — retarget imports (top-level file, not the per-builder ones)
56
+ - `src/implementations/http/README.md` — rewrite around single builder
57
+ - `docs/http-integrations.md` — rewrite around single builder
58
+ - `CLAUDE.md` — update references to old builder names
59
+ - `README.md` — update v8 changelog/migration entry
60
+ - `agent_config/claude-code/skills/ts-procedures/SKILL.md`, `patterns.md`, `anti-patterns.md`, `api-reference.md` — update examples
61
+ - `agent_config/claude-code/skills/ts-procedures-scaffold/templates/` — delete `hono-rpc.ts`, `hono-stream.ts`; rename/rewrite `hono-api.ts` → `hono.ts`
62
+ - `agent_config/copilot/copilot-instructions.md`, `agent_config/cursor/cursorrules` — same content updates
63
+ - `src/types.ts` — update `ProcedureKind` JSDoc references from old builder names to `HonoAppBuilder`
64
+ - `src/create-stream.ts` — update comments referring to `HonoStreamAppBuilder`
65
+ - `src/implementations/http/doc-registry.ts` — update comment referring to `HonoRPCAppBuilder`
66
+
67
+ ---
68
+
69
+ ## Phase 1 — Foundation (independent, no impact on existing code)
70
+
71
+ ### Task 1: Create `path.ts` — `makeRoutePath` + `resolveFullPath`
72
+
73
+ **Files:**
74
+ - Create: `src/implementations/http/hono/path.ts`
75
+ - Test: `src/implementations/http/hono/path.test.ts`
76
+
77
+ The `makeRoutePath` function dispatches by kind: rpc/rpc-stream get scope+name+version generation, http/http-stream prepend the prefix to a developer-supplied path. `resolveFullPath` is the helper for the http case (also used directly by the http handlers).
78
+
79
+ - [ ] **Step 1: Write the failing test**
80
+
81
+ Create `src/implementations/http/hono/path.test.ts`:
82
+
83
+ ```ts
84
+ import { describe, expect, test } from 'vitest'
85
+ import { makeRoutePath, resolveFullPath } from './path.js'
86
+ import type {
87
+ TProcedureRegistration,
88
+ TStreamProcedureRegistration,
89
+ THttpProcedureRegistration,
90
+ THttpStreamProcedureRegistration,
91
+ } from '../../../types.js'
92
+
93
+ describe('makeRoutePath', () => {
94
+ test('rpc: scope/name/version with kebab-case', () => {
95
+ const proc = {
96
+ name: 'GetUser',
97
+ kind: 'rpc',
98
+ config: { scope: 'users', version: 1 },
99
+ handler: async () => undefined,
100
+ } as unknown as TProcedureRegistration
101
+ expect(makeRoutePath({ procedure: proc })).toBe('/users/get-user/1')
102
+ })
103
+
104
+ test('rpc: array scope joined with /', () => {
105
+ const proc = {
106
+ name: 'List',
107
+ kind: 'rpc',
108
+ config: { scope: ['UserModule', 'admin'], version: 2 },
109
+ handler: async () => undefined,
110
+ } as unknown as TProcedureRegistration
111
+ expect(makeRoutePath({ procedure: proc })).toBe('/user-module/admin/list/2')
112
+ })
113
+
114
+ test('rpc: pathPrefix is normalized (leading slash inserted)', () => {
115
+ const proc = {
116
+ name: 'Echo',
117
+ kind: 'rpc',
118
+ config: { scope: 'echo', version: 1 },
119
+ handler: async () => undefined,
120
+ } as unknown as TProcedureRegistration
121
+ expect(makeRoutePath({ procedure: proc, prefix: 'api/v1' })).toBe('/api/v1/echo/echo/1')
122
+ })
123
+
124
+ test('rpc-stream: same shape as rpc', () => {
125
+ const proc = {
126
+ name: 'Tail',
127
+ kind: 'rpc-stream',
128
+ isStream: true,
129
+ config: { scope: 'logs', version: 1 },
130
+ handler: async function* () {},
131
+ } as unknown as TStreamProcedureRegistration
132
+ expect(makeRoutePath({ procedure: proc, prefix: '/api' })).toBe('/api/logs/tail/1')
133
+ })
134
+
135
+ test('http: prepends prefix to config.path', () => {
136
+ const proc = {
137
+ name: 'GetUser',
138
+ kind: 'http',
139
+ config: { path: '/users/:id', method: 'get' },
140
+ handler: async () => undefined,
141
+ } as unknown as THttpProcedureRegistration
142
+ expect(makeRoutePath({ procedure: proc, prefix: '/api/v1' })).toBe('/api/v1/users/:id')
143
+ })
144
+
145
+ test('http: leading slash on path is preserved (not duplicated)', () => {
146
+ const proc = {
147
+ name: 'Get',
148
+ kind: 'http',
149
+ config: { path: 'users', method: 'get' },
150
+ handler: async () => undefined,
151
+ } as unknown as THttpProcedureRegistration
152
+ expect(makeRoutePath({ procedure: proc, prefix: '/api' })).toBe('/api/users')
153
+ })
154
+
155
+ test('http-stream: same shape as http', () => {
156
+ const proc = {
157
+ name: 'TailLogs',
158
+ kind: 'http-stream',
159
+ config: { path: '/logs/tail', method: 'get' },
160
+ handler: async () => ({ stream: (async function* () {})(), initialHeaders: undefined }),
161
+ } as unknown as THttpStreamProcedureRegistration
162
+ expect(makeRoutePath({ procedure: proc })).toBe('/logs/tail')
163
+ })
164
+ })
165
+
166
+ describe('resolveFullPath', () => {
167
+ test('combines prefix and path', () => {
168
+ expect(resolveFullPath('/users', '/api/v1')).toBe('/api/v1/users')
169
+ })
170
+ test('normalizes prefix without leading slash', () => {
171
+ expect(resolveFullPath('/users', 'api')).toBe('/api/users')
172
+ })
173
+ test('normalizes path without leading slash', () => {
174
+ expect(resolveFullPath('users', '/api')).toBe('/api/users')
175
+ })
176
+ test('no prefix', () => {
177
+ expect(resolveFullPath('/users')).toBe('/users')
178
+ })
179
+ })
180
+ ```
181
+
182
+ - [ ] **Step 2: Run test to verify it fails**
183
+
184
+ Run: `npx vitest run src/implementations/http/hono/path.test.ts`
185
+ Expected: FAIL — module `./path.js` does not exist.
186
+
187
+ - [ ] **Step 3: Implement `path.ts`**
188
+
189
+ Create `src/implementations/http/hono/path.ts`:
190
+
191
+ ```ts
192
+ import { kebabCase } from 'es-toolkit/string'
193
+ import { castArray } from 'es-toolkit/compat'
194
+ import type {
195
+ TProcedureRegistration,
196
+ TStreamProcedureRegistration,
197
+ THttpProcedureRegistration,
198
+ THttpStreamProcedureRegistration,
199
+ } from '../../../types.js'
200
+
201
+ export type AnyProcedureRegistration =
202
+ | TProcedureRegistration
203
+ | TStreamProcedureRegistration
204
+ | THttpProcedureRegistration<any>
205
+ | THttpStreamProcedureRegistration<any>
206
+
207
+ function normalizePrefix(prefix?: string): string {
208
+ if (!prefix) return ''
209
+ return prefix.startsWith('/') ? prefix : `/${prefix}`
210
+ }
211
+
212
+ /**
213
+ * Resolves the full route path for any procedure kind.
214
+ *
215
+ * - `rpc` / `rpc-stream`: `{prefix}/{kebab(scope)}/{kebab(name)}/{version}`
216
+ * - `http` / `http-stream`: `{prefix}{config.path}` (path-as-given, prefix normalized)
217
+ */
218
+ export function makeRoutePath({
219
+ procedure,
220
+ prefix,
221
+ }: {
222
+ procedure: AnyProcedureRegistration
223
+ prefix?: string
224
+ }): string {
225
+ const normalizedPrefix = normalizePrefix(prefix)
226
+
227
+ switch (procedure.kind) {
228
+ case 'rpc':
229
+ case 'rpc-stream': {
230
+ const cfg = procedure.config as { scope: string | string[]; version: number }
231
+ const scopeSegments = castArray(cfg.scope).map(kebabCase).join('/')
232
+ return `${normalizedPrefix}/${scopeSegments}/${kebabCase(procedure.name)}/${String(cfg.version).trim()}`
233
+ }
234
+ case 'http':
235
+ case 'http-stream': {
236
+ const cfg = procedure.config as { path: string }
237
+ return resolveFullPath(cfg.path, prefix)
238
+ }
239
+ }
240
+ }
241
+
242
+ /**
243
+ * Combines a developer-supplied path with an optional prefix. Both are
244
+ * normalized to begin with a single `/`.
245
+ */
246
+ export function resolveFullPath(procedurePath: string, prefix?: string): string {
247
+ const normalizedPrefix = normalizePrefix(prefix)
248
+ const normalizedPath = procedurePath.startsWith('/') ? procedurePath : `/${procedurePath}`
249
+ return `${normalizedPrefix}${normalizedPath}`
250
+ }
251
+ ```
252
+
253
+ - [ ] **Step 4: Run test to verify it passes**
254
+
255
+ Run: `npx vitest run src/implementations/http/hono/path.test.ts`
256
+ Expected: PASS — all 11 tests green.
257
+
258
+ - [ ] **Step 5: Commit**
259
+
260
+ ```bash
261
+ git add src/implementations/http/hono/path.ts src/implementations/http/hono/path.test.ts
262
+ git commit -m "$(cat <<'EOF'
263
+ feat(hono): add unified path resolver for all four procedure kinds
264
+
265
+ makeRoutePath dispatches by procedure.kind: rpc/rpc-stream use
266
+ scope+name+version generation; http/http-stream prepend the prefix to
267
+ config.path. Foundation for HonoAppBuilder convergence.
268
+
269
+ Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
270
+ EOF
271
+ )"
272
+ ```
273
+
274
+ ---
275
+
276
+ ### Task 2: `error-dispatch.ts` — `dispatchPreStreamError`
277
+
278
+ **Files:**
279
+ - Create: `src/implementations/http/error-dispatch.ts`
280
+ - Test: `src/implementations/http/error-dispatch.test.ts`
281
+
282
+ Pulls the inline three-step dispatch (observer → taxonomy → onError → hard default) out of the existing builders into one shared module. The pre-stream form returns a `Response`; mid-stream form (added in Task 3) returns body data.
283
+
284
+ - [ ] **Step 1: Write the failing test**
285
+
286
+ Create `src/implementations/http/error-dispatch.test.ts`:
287
+
288
+ ```ts
289
+ import { describe, expect, test, vi } from 'vitest'
290
+ import { Hono } from 'hono'
291
+ import { dispatchPreStreamError } from './error-dispatch.js'
292
+ import { defineErrorTaxonomy } from './error-taxonomy.js'
293
+ import { ProcedureValidationError } from '../../errors.js'
294
+
295
+ function makeContext() {
296
+ // Hono's Context isn't directly constructible, so spin a request through
297
+ // a Hono app and capture the context inside a route handler.
298
+ return new Promise<import('hono').Context>((resolve) => {
299
+ const app = new Hono()
300
+ app.get('/', (c) => {
301
+ resolve(c)
302
+ return c.text('ok')
303
+ })
304
+ app.request('/')
305
+ })
306
+ }
307
+
308
+ const dummyProcedure = {
309
+ name: 'Test',
310
+ kind: 'rpc',
311
+ config: { scope: 'test', version: 1 },
312
+ handler: async () => undefined,
313
+ } as any
314
+
315
+ describe('dispatchPreStreamError', () => {
316
+ test('default taxonomy: ProcedureValidationError → 400', async () => {
317
+ const c = await makeContext()
318
+ const err = new ProcedureValidationError('Test', 'Validation error for Test', [
319
+ { instancePath: '/x', message: 'expected number' } as any,
320
+ ])
321
+
322
+ const res = await dispatchPreStreamError({
323
+ err,
324
+ procedure: dummyProcedure,
325
+ raw: c,
326
+ cfg: {},
327
+ })
328
+
329
+ expect(res.status).toBe(400)
330
+ const body = await res.json()
331
+ expect(body.name).toBe('ProcedureValidationError')
332
+ expect(body.procedureName).toBe('Test')
333
+ })
334
+
335
+ test('user taxonomy entry takes precedence', async () => {
336
+ const c = await makeContext()
337
+ class MyError extends Error {
338
+ constructor(public reason: string) { super(reason) }
339
+ }
340
+ const errors = defineErrorTaxonomy({
341
+ MyError: { class: MyError, statusCode: 422, toResponse: (e) => ({ name: 'MyError', reason: e.reason }) },
342
+ })
343
+
344
+ const res = await dispatchPreStreamError({
345
+ err: new MyError('boom'),
346
+ procedure: dummyProcedure,
347
+ raw: c,
348
+ cfg: { errors },
349
+ })
350
+
351
+ expect(res.status).toBe(422)
352
+ expect(await res.json()).toEqual({ name: 'MyError', reason: 'boom' })
353
+ })
354
+
355
+ test('falls back to onError imperative callback', async () => {
356
+ const c = await makeContext()
357
+ const onError = vi.fn().mockResolvedValue(c.json({ ok: false }, 503))
358
+
359
+ const res = await dispatchPreStreamError({
360
+ err: new Error('unmatched'),
361
+ procedure: dummyProcedure,
362
+ raw: c,
363
+ cfg: { onError: (proc, raw, e) => onError(proc, raw, e) },
364
+ })
365
+
366
+ expect(onError).toHaveBeenCalled()
367
+ expect(res.status).toBe(503)
368
+ })
369
+
370
+ test('hard default 500 when nothing matches', async () => {
371
+ const c = await makeContext()
372
+ const res = await dispatchPreStreamError({
373
+ err: new Error('boom'),
374
+ procedure: dummyProcedure,
375
+ raw: c,
376
+ cfg: {},
377
+ })
378
+ expect(res.status).toBe(500)
379
+ expect(await res.json()).toEqual({ error: 'boom' })
380
+ })
381
+
382
+ test('onRequestError observer fires before dispatch and is awaited', async () => {
383
+ const c = await makeContext()
384
+ const calls: string[] = []
385
+ const onRequestError = vi.fn(async () => {
386
+ calls.push('observer')
387
+ })
388
+ const errors = defineErrorTaxonomy({
389
+ AnyError: {
390
+ match: (e): e is Error => e instanceof Error,
391
+ statusCode: 500,
392
+ toResponse: () => {
393
+ calls.push('toResponse')
394
+ return { name: 'AnyError' }
395
+ },
396
+ },
397
+ })
398
+
399
+ await dispatchPreStreamError({
400
+ err: new Error('x'),
401
+ procedure: dummyProcedure,
402
+ raw: c,
403
+ cfg: { errors, onRequestError },
404
+ })
405
+
406
+ expect(calls).toEqual(['observer', 'toResponse'])
407
+ expect(onRequestError).toHaveBeenCalledWith({ err: expect.any(Error), procedure: dummyProcedure, raw: c })
408
+ })
409
+
410
+ test('observer throw is swallowed and logged', async () => {
411
+ const c = await makeContext()
412
+ const consoleErr = vi.spyOn(console, 'error').mockImplementation(() => {})
413
+ const onRequestError = vi.fn(() => { throw new Error('observer broke') })
414
+
415
+ const res = await dispatchPreStreamError({
416
+ err: new Error('x'),
417
+ procedure: dummyProcedure,
418
+ raw: c,
419
+ cfg: { onRequestError },
420
+ })
421
+
422
+ expect(res.status).toBe(500)
423
+ expect(consoleErr).toHaveBeenCalled()
424
+ consoleErr.mockRestore()
425
+ })
426
+ })
427
+ ```
428
+
429
+ - [ ] **Step 2: Run test to verify it fails**
430
+
431
+ Run: `npx vitest run src/implementations/http/error-dispatch.test.ts`
432
+ Expected: FAIL — module does not exist.
433
+
434
+ - [ ] **Step 3: Implement `error-dispatch.ts` (pre-stream half)**
435
+
436
+ Create `src/implementations/http/error-dispatch.ts`:
437
+
438
+ ```ts
439
+ import type { Context } from 'hono'
440
+ import {
441
+ resolveErrorResponse,
442
+ type ErrorTaxonomy,
443
+ type UnknownErrorConfig,
444
+ } from './error-taxonomy.js'
445
+ import type {
446
+ TProcedureRegistration,
447
+ TStreamProcedureRegistration,
448
+ THttpProcedureRegistration,
449
+ THttpStreamProcedureRegistration,
450
+ } from '../../types.js'
451
+
452
+ export type AnyProcedureRegistration =
453
+ | TProcedureRegistration
454
+ | TStreamProcedureRegistration
455
+ | THttpProcedureRegistration<any>
456
+ | THttpStreamProcedureRegistration<any>
457
+
458
+ export type OnRequestErrorContext = {
459
+ err: unknown
460
+ procedure: AnyProcedureRegistration
461
+ raw: Context
462
+ }
463
+
464
+ export type OnRequestErrorObserver = (
465
+ ctx: OnRequestErrorContext,
466
+ ) => void | Promise<void>
467
+
468
+ export type PreStreamOnError = (
469
+ procedure: AnyProcedureRegistration,
470
+ raw: Context,
471
+ err: Error,
472
+ ) => Response | Promise<Response>
473
+
474
+ export type PreStreamErrorConfig = {
475
+ errors?: ErrorTaxonomy
476
+ unknownError?: UnknownErrorConfig
477
+ onError?: PreStreamOnError
478
+ onRequestError?: OnRequestErrorObserver
479
+ }
480
+
481
+ /**
482
+ * Used by rpc, http, and the pre-stream guard in stream / http-stream
483
+ * handlers. Order: onRequestError observer (awaited, throws swallowed) →
484
+ * taxonomy resolver → imperative onError → hard default 500.
485
+ */
486
+ export async function dispatchPreStreamError(params: {
487
+ err: unknown
488
+ procedure: AnyProcedureRegistration
489
+ raw: Context
490
+ cfg: PreStreamErrorConfig
491
+ }): Promise<Response> {
492
+ const { err, procedure, raw, cfg } = params
493
+
494
+ if (cfg.onRequestError) {
495
+ try {
496
+ await cfg.onRequestError({ err, procedure, raw })
497
+ } catch (observerErr) {
498
+ console.error(
499
+ '[ts-procedures hono] onRequestError threw — swallowed:',
500
+ observerErr,
501
+ )
502
+ }
503
+ }
504
+
505
+ if (cfg.errors || cfg.unknownError) {
506
+ const resolved = resolveErrorResponse({
507
+ err,
508
+ userTaxonomy: cfg.errors,
509
+ unknownError: cfg.unknownError,
510
+ procedure,
511
+ raw,
512
+ })
513
+ if (resolved) {
514
+ await resolved.runOnCatch()
515
+ return raw.json(resolved.body, resolved.statusCode as never)
516
+ }
517
+ }
518
+
519
+ if (cfg.onError) {
520
+ return cfg.onError(procedure, raw, err as Error)
521
+ }
522
+
523
+ return raw.json({ error: (err as Error).message }, 500)
524
+ }
525
+ ```
526
+
527
+ - [ ] **Step 4: Run tests to verify they pass**
528
+
529
+ Run: `npx vitest run src/implementations/http/error-dispatch.test.ts`
530
+ Expected: PASS — 6 tests green.
531
+
532
+ - [ ] **Step 5: Commit**
533
+
534
+ ```bash
535
+ git add src/implementations/http/error-dispatch.ts src/implementations/http/error-dispatch.test.ts
536
+ git commit -m "$(cat <<'EOF'
537
+ feat(http): extract dispatchPreStreamError shared helper
538
+
539
+ Consolidates the observer→taxonomy→onError→default chain duplicated
540
+ across hono-rpc, hono-api, and hono-stream into one helper. Lives one
541
+ level up from hono/ so future framework adapters can reuse it.
542
+
543
+ Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
544
+ EOF
545
+ )"
546
+ ```
547
+
548
+ ---
549
+
550
+ ### Task 3: `error-dispatch.ts` — `dispatchMidStreamError`
551
+
552
+ **Files:**
553
+ - Modify: `src/implementations/http/error-dispatch.ts`
554
+ - Modify: `src/implementations/http/error-dispatch.test.ts`
555
+
556
+ Mid-stream dispatcher returns body bytes (not a `Response`) because the HTTP status is already committed when streaming starts. The result includes optional SSE event/id/retry overrides and a deferred `runOnCatch`.
557
+
558
+ - [ ] **Step 1: Add failing tests for the mid-stream dispatcher**
559
+
560
+ Append to `src/implementations/http/error-dispatch.test.ts`:
561
+
562
+ ```ts
563
+ import { dispatchMidStreamError } from './error-dispatch.js'
564
+ // `sse` currently lives in hono-stream/index.ts. Task 11 moves it into
565
+ // hono/handlers/stream.ts and rewrites this import to './hono/handlers/stream.js'.
566
+ import { sse } from './hono-stream/index.js'
567
+
568
+ const streamProcedure = {
569
+ name: 'Tail',
570
+ kind: 'rpc-stream',
571
+ isStream: true,
572
+ config: { scope: 'logs', version: 1 },
573
+ handler: async function* () {},
574
+ } as any
575
+
576
+ describe('dispatchMidStreamError', () => {
577
+ test('uses taxonomy body when configured', async () => {
578
+ const c = await makeContext()
579
+ class MidErr extends Error {}
580
+ const errors = defineErrorTaxonomy({
581
+ MidErr: { class: MidErr, statusCode: 500, toResponse: () => ({ name: 'MidErr', detail: 'x' }) },
582
+ })
583
+
584
+ const result = await dispatchMidStreamError({
585
+ err: new MidErr('mid'),
586
+ procedure: streamProcedure,
587
+ raw: c,
588
+ cfg: { errors },
589
+ })
590
+
591
+ expect(result.data).toEqual({ name: 'MidErr', detail: 'x' })
592
+ expect(result.sseEvent).toBe('error')
593
+ expect(result.runOnCatch).toBeTypeOf('function')
594
+ })
595
+
596
+ test('falls back to onMidStreamError', async () => {
597
+ const c = await makeContext()
598
+ const result = await dispatchMidStreamError({
599
+ err: new Error('boom'),
600
+ procedure: streamProcedure,
601
+ raw: c,
602
+ cfg: {
603
+ onMidStreamError: () => ({ data: { kind: 'fail', msg: 'boom' } }),
604
+ },
605
+ })
606
+
607
+ expect(result.data).toEqual({ kind: 'fail', msg: 'boom' })
608
+ expect(result.sseEvent).toBe('Tail') // procedure name override
609
+ })
610
+
611
+ test('hard default { error: msg } when nothing matches', async () => {
612
+ const c = await makeContext()
613
+ const result = await dispatchMidStreamError({
614
+ err: new Error('plain'),
615
+ procedure: streamProcedure,
616
+ raw: c,
617
+ cfg: {},
618
+ })
619
+
620
+ expect(result.data).toEqual({ error: 'plain' })
621
+ expect(result.sseEvent).toBe('error')
622
+ })
623
+
624
+ test('onRequestError observer fires before dispatch (mid-stream form)', async () => {
625
+ const c = await makeContext()
626
+ const onRequestError = vi.fn(async () => {})
627
+
628
+ await dispatchMidStreamError({
629
+ err: new Error('x'),
630
+ procedure: streamProcedure,
631
+ raw: c,
632
+ cfg: { onRequestError },
633
+ })
634
+
635
+ expect(onRequestError).toHaveBeenCalled()
636
+ })
637
+ })
638
+ ```
639
+
640
+ - [ ] **Step 2: Run test to verify it fails**
641
+
642
+ Run: `npx vitest run src/implementations/http/error-dispatch.test.ts`
643
+ Expected: FAIL — `dispatchMidStreamError` not exported.
644
+
645
+ - [ ] **Step 3: Add `dispatchMidStreamError` to `error-dispatch.ts`**
646
+
647
+ Append to `src/implementations/http/error-dispatch.ts`:
648
+
649
+ ```ts
650
+ export type MidStreamErrorResult<TErrorData = unknown> = {
651
+ data: TErrorData
652
+ closeStream?: boolean
653
+ }
654
+
655
+ export type OnMidStreamError<TErrorData = unknown> = (
656
+ procedure: TStreamProcedureRegistration | THttpStreamProcedureRegistration<any>,
657
+ raw: Context,
658
+ err: Error,
659
+ ) => MidStreamErrorResult<TErrorData> | undefined
660
+
661
+ export type MidStreamErrorConfig<TErrorData = unknown> = {
662
+ errors?: ErrorTaxonomy
663
+ unknownError?: UnknownErrorConfig
664
+ onMidStreamError?: OnMidStreamError<TErrorData>
665
+ onRequestError?: OnRequestErrorObserver
666
+ }
667
+
668
+ export type DispatchedMidStreamError = {
669
+ data: unknown
670
+ sseEvent?: string
671
+ sseId?: string
672
+ sseRetry?: number
673
+ runOnCatch?: () => Promise<void>
674
+ }
675
+
676
+ /**
677
+ * Used inside SSE / text-stream generator catch blocks. Returns the bytes
678
+ * to write to the open stream — the HTTP status is already committed, so we
679
+ * can't return a Response. Order matches dispatchPreStreamError: observer
680
+ * (awaited, throws swallowed) → taxonomy → onMidStreamError → hard default.
681
+ */
682
+ export async function dispatchMidStreamError<TErrorData = unknown>(params: {
683
+ err: unknown
684
+ procedure: TStreamProcedureRegistration | THttpStreamProcedureRegistration<any>
685
+ raw: Context
686
+ cfg: MidStreamErrorConfig<TErrorData>
687
+ }): Promise<DispatchedMidStreamError> {
688
+ const { err, procedure, raw, cfg } = params
689
+
690
+ if (cfg.onRequestError) {
691
+ try {
692
+ await cfg.onRequestError({ err, procedure, raw })
693
+ } catch (observerErr) {
694
+ console.error(
695
+ '[ts-procedures hono] onRequestError (mid-stream) threw — swallowed:',
696
+ observerErr,
697
+ )
698
+ }
699
+ }
700
+
701
+ if (cfg.errors || cfg.unknownError) {
702
+ const resolved = resolveErrorResponse({
703
+ err,
704
+ userTaxonomy: cfg.errors,
705
+ unknownError: cfg.unknownError,
706
+ procedure,
707
+ raw,
708
+ })
709
+ if (resolved) {
710
+ return {
711
+ data: resolved.body,
712
+ sseEvent: 'error',
713
+ runOnCatch: resolved.runOnCatch,
714
+ }
715
+ }
716
+ }
717
+
718
+ if (cfg.onMidStreamError) {
719
+ const result = cfg.onMidStreamError(procedure, raw, err as Error)
720
+ if (result?.data !== undefined) {
721
+ return {
722
+ data: result.data,
723
+ sseEvent: procedure.name,
724
+ }
725
+ }
726
+ }
727
+
728
+ return {
729
+ data: { error: (err as Error).message },
730
+ sseEvent: 'error',
731
+ }
732
+ }
733
+ ```
734
+
735
+ You also need to add the import at the top of `error-dispatch.ts`:
736
+
737
+ ```ts
738
+ import type { TStreamProcedureRegistration, THttpStreamProcedureRegistration } from '../../types.js'
739
+ ```
740
+
741
+ For the test imports of `sse`, use the existing path until Task 9 moves it: `import { sse } from './hono-stream/index.js'`.
742
+
743
+ - [ ] **Step 4: Run tests to verify they pass**
744
+
745
+ Run: `npx vitest run src/implementations/http/error-dispatch.test.ts`
746
+ Expected: PASS — all 10 tests green.
747
+
748
+ - [ ] **Step 5: Commit**
749
+
750
+ ```bash
751
+ git add src/implementations/http/error-dispatch.ts src/implementations/http/error-dispatch.test.ts
752
+ git commit -m "$(cat <<'EOF'
753
+ feat(http): add dispatchMidStreamError for stream catch blocks
754
+
755
+ Returns body bytes plus optional SSE event/id/retry overrides, since
756
+ HTTP status is already committed once streaming starts. Same order as
757
+ the pre-stream form: observer → taxonomy → onMidStreamError → default.
758
+
759
+ Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
760
+ EOF
761
+ )"
762
+ ```
763
+
764
+ ---
765
+
766
+ ## Phase 2 — Doc Builders (one per kind, pure functions)
767
+
768
+ ### Task 4: `docs/rpc-doc.ts` — `buildRpcRouteDoc`
769
+
770
+ **Files:**
771
+ - Create: `src/implementations/http/hono/docs/rpc-doc.ts`
772
+
773
+ Pure function — extracts an `RPCHttpRouteDoc` from a `TProcedureRegistration<any, RPCConfig>`. No tests for the pure builders in isolation; they're covered by the handler integration tests in Phase 3 and by the `index.test.ts` envelope tests in Phase 4.
774
+
775
+ - [ ] **Step 1: Implement `rpc-doc.ts`**
776
+
777
+ Create `src/implementations/http/hono/docs/rpc-doc.ts`:
778
+
779
+ ```ts
780
+ import type { TProcedureRegistration } from '../../../../types.js'
781
+ import type { RPCConfig, RPCHttpRouteDoc } from '../../../types.js'
782
+ import { makeRoutePath } from '../path.js'
783
+
784
+ export function buildRpcRouteDoc(
785
+ procedure: TProcedureRegistration<any, RPCConfig>,
786
+ prefix: string | undefined,
787
+ extend?: (params: { base: RPCHttpRouteDoc; procedure: TProcedureRegistration<any, RPCConfig> }) => Record<string, unknown>,
788
+ ): RPCHttpRouteDoc {
789
+ const config = procedure.config as RPCConfig & {
790
+ schema?: { params?: Record<string, unknown>; returnType?: Record<string, unknown> }
791
+ errors?: string[]
792
+ }
793
+
794
+ const jsonSchema: { body?: Record<string, unknown>; response?: Record<string, unknown> } = {}
795
+ if (config.schema?.params) jsonSchema.body = config.schema.params
796
+ if (config.schema?.returnType) jsonSchema.response = config.schema.returnType
797
+
798
+ const base: RPCHttpRouteDoc = {
799
+ kind: 'rpc',
800
+ name: procedure.name,
801
+ version: config.version,
802
+ scope: config.scope,
803
+ path: makeRoutePath({ procedure, prefix }),
804
+ method: 'post',
805
+ jsonSchema,
806
+ }
807
+ if (config.errors && config.errors.length > 0) {
808
+ base.errors = [...config.errors]
809
+ }
810
+
811
+ const extended = extend ? extend({ base, procedure }) : {}
812
+ return { ...extended, ...base }
813
+ }
814
+ ```
815
+
816
+ - [ ] **Step 2: Verify it builds**
817
+
818
+ Run: `npx tsc --noEmit`
819
+ Expected: PASS — no type errors. (No test file yet; integration tests come in Phase 3.)
820
+
821
+ - [ ] **Step 3: Commit**
822
+
823
+ ```bash
824
+ git add src/implementations/http/hono/docs/rpc-doc.ts
825
+ git commit -m "feat(hono): add rpc-doc builder
826
+
827
+ Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"
828
+ ```
829
+
830
+ ---
831
+
832
+ ### Task 5: `docs/stream-doc.ts` — `buildStreamRouteDoc`
833
+
834
+ **Files:**
835
+ - Create: `src/implementations/http/hono/docs/stream-doc.ts`
836
+
837
+ - [ ] **Step 1: Implement `stream-doc.ts`**
838
+
839
+ Create `src/implementations/http/hono/docs/stream-doc.ts`:
840
+
841
+ ```ts
842
+ import type { TStreamProcedureRegistration } from '../../../../types.js'
843
+ import type { RPCConfig, StreamHttpRouteDoc, StreamMode } from '../../../types.js'
844
+ import { makeRoutePath } from '../path.js'
845
+
846
+ export function buildStreamRouteDoc(
847
+ procedure: TStreamProcedureRegistration<any, RPCConfig>,
848
+ streamMode: StreamMode,
849
+ prefix: string | undefined,
850
+ extend?: (params: { base: StreamHttpRouteDoc; procedure: TStreamProcedureRegistration<any, RPCConfig> }) => Record<string, unknown>,
851
+ ): StreamHttpRouteDoc {
852
+ const config = procedure.config as RPCConfig & {
853
+ schema?: { params?: Record<string, unknown>; yieldType?: Record<string, unknown>; returnType?: Record<string, unknown> }
854
+ errors?: string[]
855
+ }
856
+
857
+ const jsonSchema: StreamHttpRouteDoc['jsonSchema'] = {}
858
+ if (config.schema?.params) jsonSchema.params = config.schema.params
859
+
860
+ if (streamMode === 'sse') {
861
+ jsonSchema.yieldType = {
862
+ type: 'object',
863
+ description: 'SSE message envelope. The data field contains the procedure yield value.',
864
+ required: ['data', 'event', 'id'],
865
+ properties: {
866
+ data: config.schema?.yieldType ?? {},
867
+ event: { type: 'string' },
868
+ id: { type: 'string' },
869
+ retry: { type: 'number' },
870
+ },
871
+ }
872
+ } else if (config.schema?.yieldType) {
873
+ jsonSchema.yieldType = config.schema.yieldType
874
+ }
875
+
876
+ if (config.schema?.returnType) jsonSchema.returnType = config.schema.returnType
877
+
878
+ // POST first so codegen (which reads `methods[0]`) defaults to POST. POST is
879
+ // the canonical method for streams because it can carry a body for params.
880
+ const base: StreamHttpRouteDoc = {
881
+ kind: 'stream',
882
+ name: procedure.name,
883
+ version: config.version,
884
+ scope: config.scope,
885
+ path: makeRoutePath({ procedure, prefix }),
886
+ methods: ['post', 'get'],
887
+ streamMode,
888
+ jsonSchema,
889
+ }
890
+ if (config.errors && config.errors.length > 0) base.errors = [...config.errors]
891
+
892
+ const extended = extend ? extend({ base, procedure }) : {}
893
+ return { ...extended, ...base }
894
+ }
895
+ ```
896
+
897
+ - [ ] **Step 2: Verify build**
898
+
899
+ Run: `npx tsc --noEmit`
900
+ Expected: PASS.
901
+
902
+ - [ ] **Step 3: Commit**
903
+
904
+ ```bash
905
+ git add src/implementations/http/hono/docs/stream-doc.ts
906
+ git commit -m "feat(hono): add stream-doc builder
907
+
908
+ Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"
909
+ ```
910
+
911
+ ---
912
+
913
+ ### Task 6: `docs/http-doc.ts` — `buildHttpRouteDoc`
914
+
915
+ **Files:**
916
+ - Create: `src/implementations/http/hono/docs/http-doc.ts`
917
+
918
+ - [ ] **Step 1: Implement `http-doc.ts`**
919
+
920
+ Create `src/implementations/http/hono/docs/http-doc.ts`:
921
+
922
+ ```ts
923
+ import type { THttpProcedureRegistration } from '../../../../types.js'
924
+ import type { APIHttpRouteDoc } from '../../../types.js'
925
+ import { resolveFullPath } from '../path.js'
926
+
927
+ export function buildHttpRouteDoc(
928
+ procedure: THttpProcedureRegistration<any>,
929
+ prefix: string | undefined,
930
+ extend?: (params: { base: APIHttpRouteDoc; procedure: THttpProcedureRegistration<any> }) => Record<string, unknown>,
931
+ ): APIHttpRouteDoc {
932
+ const config = procedure.config
933
+ const fullPath = resolveFullPath(config.path, prefix)
934
+ const reqSchema = config.schema?.req
935
+ const resSchema = config.schema?.res
936
+ const jsonSchema: APIHttpRouteDoc['jsonSchema'] = {}
937
+
938
+ if (reqSchema && (reqSchema.pathParams || reqSchema.query || reqSchema.body || reqSchema.headers)) {
939
+ jsonSchema.req = {}
940
+ if (reqSchema.pathParams) jsonSchema.req.pathParams = reqSchema.pathParams as any
941
+ if (reqSchema.query) jsonSchema.req.query = reqSchema.query as any
942
+ if (reqSchema.body) jsonSchema.req.body = reqSchema.body as any
943
+ if (reqSchema.headers) jsonSchema.req.headers = reqSchema.headers as any
944
+ }
945
+ if (resSchema && (resSchema.body || resSchema.headers)) {
946
+ jsonSchema.res = {}
947
+ if (resSchema.body) jsonSchema.res.body = resSchema.body as any
948
+ if (resSchema.headers) jsonSchema.res.headers = resSchema.headers as any
949
+ }
950
+
951
+ const base: APIHttpRouteDoc = {
952
+ kind: 'api',
953
+ name: procedure.name,
954
+ scope: config.scope,
955
+ path: config.path,
956
+ method: config.method,
957
+ fullPath,
958
+ jsonSchema,
959
+ }
960
+ if (config.successStatus) base.successStatus = config.successStatus
961
+ if (config.errors && config.errors.length > 0) base.errors = [...config.errors]
962
+
963
+ const extended = extend ? extend({ base, procedure }) : {}
964
+ return { ...extended, ...base }
965
+ }
966
+ ```
967
+
968
+ - [ ] **Step 2: Verify build**
969
+
970
+ Run: `npx tsc --noEmit`
971
+ Expected: PASS.
972
+
973
+ - [ ] **Step 3: Commit**
974
+
975
+ ```bash
976
+ git add src/implementations/http/hono/docs/http-doc.ts
977
+ git commit -m "feat(hono): add http-doc builder
978
+
979
+ Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"
980
+ ```
981
+
982
+ ---
983
+
984
+ ### Task 7: `docs/http-stream-doc.ts` — `buildHttpStreamRouteDoc`
985
+
986
+ **Files:**
987
+ - Create: `src/implementations/http/hono/docs/http-stream-doc.ts`
988
+
989
+ - [ ] **Step 1: Implement `http-stream-doc.ts`**
990
+
991
+ Create `src/implementations/http/hono/docs/http-stream-doc.ts`:
992
+
993
+ ```ts
994
+ import type { THttpStreamProcedureRegistration } from '../../../../types.js'
995
+ import type { HttpStreamRouteDoc } from '../../../types.js'
996
+ import { resolveFullPath } from '../path.js'
997
+
998
+ export function buildHttpStreamRouteDoc(
999
+ procedure: THttpStreamProcedureRegistration<any>,
1000
+ prefix: string | undefined,
1001
+ extend?: (params: {
1002
+ base: HttpStreamRouteDoc
1003
+ procedure: THttpStreamProcedureRegistration<any>
1004
+ }) => Record<string, unknown>,
1005
+ ): HttpStreamRouteDoc {
1006
+ const config = procedure.config
1007
+ const fullPath = resolveFullPath(config.path, prefix)
1008
+ const reqSchema = config.schema?.req
1009
+ const resSchema = config.schema?.res
1010
+ const jsonSchema: HttpStreamRouteDoc['jsonSchema'] = {}
1011
+
1012
+ if (reqSchema && (reqSchema.pathParams || reqSchema.query || reqSchema.body || reqSchema.headers)) {
1013
+ jsonSchema.req = {}
1014
+ if (reqSchema.pathParams) jsonSchema.req.pathParams = reqSchema.pathParams as any
1015
+ if (reqSchema.query) jsonSchema.req.query = reqSchema.query as any
1016
+ if (reqSchema.body) jsonSchema.req.body = reqSchema.body as any
1017
+ if (reqSchema.headers) jsonSchema.req.headers = reqSchema.headers as any
1018
+ }
1019
+ if (resSchema?.headers) jsonSchema.res = { headers: resSchema.headers as any }
1020
+ if (config.schema?.yield) jsonSchema.yield = config.schema.yield as any
1021
+ if (config.schema?.returnType) jsonSchema.returnType = config.schema.returnType as any
1022
+
1023
+ const base: HttpStreamRouteDoc = {
1024
+ kind: 'http-stream',
1025
+ name: procedure.name,
1026
+ scope: config.scope,
1027
+ path: config.path,
1028
+ method: config.method,
1029
+ fullPath,
1030
+ streamMode: 'sse',
1031
+ jsonSchema,
1032
+ }
1033
+ if (config.errors && config.errors.length > 0) base.errors = [...config.errors]
1034
+
1035
+ const extended = extend ? extend({ base, procedure }) : {}
1036
+ return { ...extended, ...base }
1037
+ }
1038
+ ```
1039
+
1040
+ - [ ] **Step 2: Verify build**
1041
+
1042
+ Run: `npx tsc --noEmit`
1043
+ Expected: PASS.
1044
+
1045
+ - [ ] **Step 3: Commit**
1046
+
1047
+ ```bash
1048
+ git add src/implementations/http/hono/docs/http-stream-doc.ts
1049
+ git commit -m "feat(hono): add http-stream-doc builder
1050
+
1051
+ Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"
1052
+ ```
1053
+
1054
+ ---
1055
+
1056
+ ## Phase 3 — Per-Kind Handler Modules
1057
+
1058
+ Each handler installer takes `(app, procedure, factoryItem, config, docs)` and:
1059
+ 1. Builds the route doc, pushes to `docs`.
1060
+ 2. Registers a Hono handler that resolves context, extracts params, invokes the procedure, applies result, calls `dispatchPreStreamError` on throw.
1061
+
1062
+ ### Task 8: Define shared `types.ts` for the hono module
1063
+
1064
+ **Files:**
1065
+ - Create: `src/implementations/http/hono/types.ts`
1066
+
1067
+ This file is referenced by all handler modules and the public `index.ts`. No tests — pure type declarations.
1068
+
1069
+ - [ ] **Step 1: Implement `types.ts`**
1070
+
1071
+ Create `src/implementations/http/hono/types.ts`:
1072
+
1073
+ ```ts
1074
+ import type { Context, Hono } from 'hono'
1075
+ import type { ErrorTaxonomy, UnknownErrorConfig } from '../error-taxonomy.js'
1076
+ import type {
1077
+ AnyProcedureRegistration,
1078
+ OnRequestErrorObserver,
1079
+ OnMidStreamError,
1080
+ PreStreamOnError,
1081
+ } from '../error-dispatch.js'
1082
+ import type {
1083
+ ProceduresFactory,
1084
+ ExtractContext,
1085
+ StreamMode,
1086
+ AnyHttpRouteDoc,
1087
+ RPCHttpRouteDoc,
1088
+ APIHttpRouteDoc,
1089
+ StreamHttpRouteDoc,
1090
+ HttpStreamRouteDoc,
1091
+ } from '../../types.js'
1092
+ import type {
1093
+ TProcedureRegistration,
1094
+ TStreamProcedureRegistration,
1095
+ THttpProcedureRegistration,
1096
+ } from '../../../types.js'
1097
+
1098
+ export type { AnyProcedureRegistration } from '../error-dispatch.js'
1099
+
1100
+ /** Default query parser using URLSearchParams. */
1101
+ export type QueryParser = (queryString: string) => Record<string, unknown>
1102
+
1103
+ export type OnRequestErrorContext = {
1104
+ err: unknown
1105
+ procedure: AnyProcedureRegistration
1106
+ raw: Context
1107
+ }
1108
+
1109
+ export type ExtendProcedureDoc = (params: {
1110
+ base: RPCHttpRouteDoc | APIHttpRouteDoc | StreamHttpRouteDoc | HttpStreamRouteDoc
1111
+ procedure: AnyProcedureRegistration
1112
+ }) => Record<string, unknown>
1113
+
1114
+ export type HonoFactoryItem<TFactory extends ProceduresFactory = ProceduresFactory> = {
1115
+ factory: TFactory
1116
+ factoryContext:
1117
+ | ExtractContext<TFactory>
1118
+ | ((c: Context) => ExtractContext<TFactory> | Promise<ExtractContext<TFactory>>)
1119
+ streamMode?: StreamMode
1120
+ extendProcedureDoc?: ExtendProcedureDoc
1121
+ }
1122
+
1123
+ export type HonoAppBuilderConfig<TStreamErrorData = unknown> = {
1124
+ /** Existing Hono app. If omitted, a new instance is created. */
1125
+ app?: Hono
1126
+ /** Path prefix applied to every route across all kinds. */
1127
+ pathPrefix?: string
1128
+
1129
+ // Error handling — peers
1130
+ errors?: ErrorTaxonomy
1131
+ unknownError?: UnknownErrorConfig
1132
+ onError?: PreStreamOnError
1133
+ /** Cross-cutting observer — fires for every caught error before dispatch. */
1134
+ onRequestError?: OnRequestErrorObserver
1135
+
1136
+ // Lifecycle (every kind)
1137
+ onRequestStart?: (c: Context) => void
1138
+ onRequestEnd?: (c: Context) => void
1139
+
1140
+ // Kind-specific blocks
1141
+ rpc?: {
1142
+ onSuccess?: (procedure: TProcedureRegistration, c: Context) => void
1143
+ }
1144
+ api?: {
1145
+ queryParser?: QueryParser
1146
+ onSuccess?: (procedure: THttpProcedureRegistration<any>, c: Context) => void
1147
+ }
1148
+ stream?: {
1149
+ /** Default for both rpc-stream and http-stream. */
1150
+ defaultStreamMode?: StreamMode
1151
+ onStreamStart?: (
1152
+ procedure: TStreamProcedureRegistration | THttpProcedureRegistration<any>,
1153
+ c: Context,
1154
+ streamMode: StreamMode,
1155
+ ) => void
1156
+ onStreamEnd?: (
1157
+ procedure: TStreamProcedureRegistration | THttpProcedureRegistration<any>,
1158
+ c: Context,
1159
+ streamMode: StreamMode,
1160
+ ) => void
1161
+ onMidStreamError?: OnMidStreamError<TStreamErrorData>
1162
+ }
1163
+ }
1164
+
1165
+ export type DocAccumulator = AnyHttpRouteDoc[]
1166
+ ```
1167
+
1168
+ - [ ] **Step 2: Verify build**
1169
+
1170
+ Run: `npx tsc --noEmit`
1171
+ Expected: PASS.
1172
+
1173
+ - [ ] **Step 3: Commit**
1174
+
1175
+ ```bash
1176
+ git add src/implementations/http/hono/types.ts
1177
+ git commit -m "feat(hono): add HonoAppBuilderConfig + factory item types
1178
+
1179
+ Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"
1180
+ ```
1181
+
1182
+ ---
1183
+
1184
+ ### Task 9: `handlers/rpc.ts` — `installRpcRoute`
1185
+
1186
+ **Files:**
1187
+ - Create: `src/implementations/http/hono/handlers/rpc.ts`
1188
+ - Test: `src/implementations/http/hono/handlers/rpc.test.ts`
1189
+
1190
+ - [ ] **Step 1: Write the failing test**
1191
+
1192
+ Create `src/implementations/http/hono/handlers/rpc.test.ts`:
1193
+
1194
+ ```ts
1195
+ import { describe, expect, test, vi } from 'vitest'
1196
+ import { Hono } from 'hono'
1197
+ import { Type } from 'typebox'
1198
+ import { Procedures } from '../../../../index.js'
1199
+ import type { RPCConfig } from '../../../types.js'
1200
+ import { installRpcRoute } from './rpc.js'
1201
+
1202
+ function buildApp(register: (factory: ReturnType<typeof Procedures<{}, RPCConfig>>) => void, cfg: any = {}) {
1203
+ const RPC = Procedures<{ userId: string }, RPCConfig>()
1204
+ register(RPC)
1205
+ const app = new Hono()
1206
+ const docs: any[] = []
1207
+ for (const proc of RPC.getProcedures().values()) {
1208
+ if (proc.kind !== 'rpc') continue
1209
+ installRpcRoute({
1210
+ app,
1211
+ procedure: proc as any,
1212
+ factoryItem: { factory: RPC, factoryContext: () => ({ userId: '123' }) },
1213
+ cfg,
1214
+ docs,
1215
+ })
1216
+ }
1217
+ return { app, docs }
1218
+ }
1219
+
1220
+ describe('installRpcRoute', () => {
1221
+ test('mounts POST route at scope/name/version path', async () => {
1222
+ const { app, docs } = buildApp((RPC) => {
1223
+ RPC.Create('Echo', {
1224
+ scope: 'echo',
1225
+ version: 1,
1226
+ schema: { params: Type.Object({ msg: Type.String() }) },
1227
+ }, async (_ctx, params) => params)
1228
+ })
1229
+
1230
+ const res = await app.request('/echo/echo/1', {
1231
+ method: 'POST',
1232
+ headers: { 'Content-Type': 'application/json' },
1233
+ body: JSON.stringify({ msg: 'hi' }),
1234
+ })
1235
+ expect(res.status).toBe(200)
1236
+ expect(await res.json()).toEqual({ msg: 'hi' })
1237
+ expect(docs).toHaveLength(1)
1238
+ expect(docs[0].kind).toBe('rpc')
1239
+ expect(docs[0].path).toBe('/echo/echo/1')
1240
+ })
1241
+
1242
+ test('rpc.onSuccess fires after handler', async () => {
1243
+ const onSuccess = vi.fn()
1244
+ const { app } = buildApp((RPC) => {
1245
+ RPC.Create('Ping', { scope: 'p', version: 1 }, async () => ({ ok: true }))
1246
+ }, { rpc: { onSuccess } })
1247
+
1248
+ await app.request('/p/ping/1', { method: 'POST', body: '{}', headers: { 'Content-Type': 'application/json' } })
1249
+ expect(onSuccess).toHaveBeenCalledOnce()
1250
+ })
1251
+
1252
+ test('thrown error → dispatchPreStreamError default 500', async () => {
1253
+ const { app } = buildApp((RPC) => {
1254
+ RPC.Create('Boom', { scope: 'b', version: 1 }, async () => { throw new Error('boom') })
1255
+ })
1256
+
1257
+ const res = await app.request('/b/boom/1', { method: 'POST', body: '{}', headers: { 'Content-Type': 'application/json' } })
1258
+ expect(res.status).toBe(500)
1259
+ expect(await res.json()).toEqual({ error: 'boom' })
1260
+ })
1261
+
1262
+ test('AbortSignal is injected into ctx', async () => {
1263
+ let receivedSignal: AbortSignal | undefined
1264
+ const { app } = buildApp((RPC) => {
1265
+ RPC.Create('Sig', { scope: 's', version: 1 }, async (ctx) => {
1266
+ receivedSignal = ctx.signal
1267
+ return {}
1268
+ })
1269
+ })
1270
+
1271
+ await app.request('/s/sig/1', { method: 'POST', body: '{}', headers: { 'Content-Type': 'application/json' } })
1272
+ expect(receivedSignal).toBeInstanceOf(AbortSignal)
1273
+ })
1274
+ })
1275
+ ```
1276
+
1277
+ - [ ] **Step 2: Run test to verify it fails**
1278
+
1279
+ Run: `npx vitest run src/implementations/http/hono/handlers/rpc.test.ts`
1280
+ Expected: FAIL — `./rpc.js` not found.
1281
+
1282
+ - [ ] **Step 3: Implement `handlers/rpc.ts`**
1283
+
1284
+ Create `src/implementations/http/hono/handlers/rpc.ts`:
1285
+
1286
+ ```ts
1287
+ import type { Context, Hono } from 'hono'
1288
+ import type { TProcedureRegistration } from '../../../../types.js'
1289
+ import type { RPCConfig } from '../../../types.js'
1290
+ import { dispatchPreStreamError } from '../../error-dispatch.js'
1291
+ import { buildRpcRouteDoc } from '../docs/rpc-doc.js'
1292
+ import type { DocAccumulator, HonoAppBuilderConfig, HonoFactoryItem } from '../types.js'
1293
+
1294
+ export function installRpcRoute(params: {
1295
+ app: Hono
1296
+ procedure: TProcedureRegistration<any, RPCConfig>
1297
+ factoryItem: HonoFactoryItem
1298
+ cfg: HonoAppBuilderConfig
1299
+ docs: DocAccumulator
1300
+ }): void {
1301
+ const { app, procedure, factoryItem, cfg, docs } = params
1302
+
1303
+ const route = buildRpcRouteDoc(
1304
+ procedure,
1305
+ cfg.pathPrefix,
1306
+ factoryItem.extendProcedureDoc as any,
1307
+ )
1308
+ docs.push(route)
1309
+
1310
+ app.post(route.path, async (c: Context) => {
1311
+ try {
1312
+ const context =
1313
+ typeof factoryItem.factoryContext === 'function'
1314
+ ? await (factoryItem.factoryContext as (c: Context) => any)(c)
1315
+ : factoryItem.factoryContext
1316
+
1317
+ const body = await c.req.json().catch(() => ({}))
1318
+ const result = await procedure.handler(
1319
+ { ...context, signal: c.req.raw.signal },
1320
+ body,
1321
+ )
1322
+
1323
+ cfg.rpc?.onSuccess?.(procedure, c)
1324
+
1325
+ return c.json(result)
1326
+ } catch (error) {
1327
+ return dispatchPreStreamError({
1328
+ err: error,
1329
+ procedure,
1330
+ raw: c,
1331
+ cfg: {
1332
+ errors: cfg.errors,
1333
+ unknownError: cfg.unknownError,
1334
+ onError: cfg.onError,
1335
+ onRequestError: cfg.onRequestError,
1336
+ },
1337
+ })
1338
+ }
1339
+ })
1340
+ }
1341
+ ```
1342
+
1343
+ - [ ] **Step 4: Run test to verify it passes**
1344
+
1345
+ Run: `npx vitest run src/implementations/http/hono/handlers/rpc.test.ts`
1346
+ Expected: PASS — 4 tests green.
1347
+
1348
+ - [ ] **Step 5: Commit**
1349
+
1350
+ ```bash
1351
+ git add src/implementations/http/hono/handlers/rpc.ts src/implementations/http/hono/handlers/rpc.test.ts
1352
+ git commit -m "feat(hono): add installRpcRoute handler
1353
+
1354
+ Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"
1355
+ ```
1356
+
1357
+ ---
1358
+
1359
+ ### Task 10: `handlers/http.ts` — `installHttpRoute`
1360
+
1361
+ **Files:**
1362
+ - Create: `src/implementations/http/hono/handlers/http.ts`
1363
+ - Test: `src/implementations/http/hono/handlers/http.test.ts`
1364
+
1365
+ Implements the API/REST-style handler. Extracts structured params per channel (`pathParams`, `query`, `body`, `headers`), invokes the procedure, supports `{ body, headers }` and bare-result return shapes, and applies `successStatus` defaults.
1366
+
1367
+ - [ ] **Step 1: Write the failing test**
1368
+
1369
+ Create `src/implementations/http/hono/handlers/http.test.ts`:
1370
+
1371
+ ```ts
1372
+ import { describe, expect, test, vi } from 'vitest'
1373
+ import { Hono } from 'hono'
1374
+ import { Type } from 'typebox'
1375
+ import { Procedures } from '../../../../index.js'
1376
+ import { installHttpRoute } from './http.js'
1377
+
1378
+ function buildApp(setup: (P: ReturnType<typeof Procedures<any>>) => void, cfg: any = {}) {
1379
+ const P = Procedures<{ uid: string }>()
1380
+ setup(P)
1381
+ const app = new Hono()
1382
+ const docs: any[] = []
1383
+ for (const proc of P.getProcedures().values()) {
1384
+ if (proc.kind !== 'http') continue
1385
+ installHttpRoute({
1386
+ app,
1387
+ procedure: proc as any,
1388
+ factoryItem: { factory: P, factoryContext: () => ({ uid: 'u1' }) },
1389
+ cfg,
1390
+ docs,
1391
+ })
1392
+ }
1393
+ return { app, docs }
1394
+ }
1395
+
1396
+ describe('installHttpRoute', () => {
1397
+ test('GET with pathParams and query', async () => {
1398
+ const { app, docs } = buildApp((P) => {
1399
+ P.CreateHttp('GetUser', {
1400
+ path: '/users/:id',
1401
+ method: 'get',
1402
+ scope: 'users',
1403
+ schema: {
1404
+ req: {
1405
+ pathParams: Type.Object({ id: Type.String() }),
1406
+ query: Type.Object({ include: Type.Optional(Type.String()) }),
1407
+ },
1408
+ res: { body: Type.Object({ id: Type.String(), include: Type.Optional(Type.String()) }) },
1409
+ },
1410
+ }, async (_ctx, { pathParams, query }) => ({ body: { id: pathParams.id, include: query.include } }))
1411
+ })
1412
+
1413
+ const res = await app.request('/users/abc?include=foo')
1414
+ expect(res.status).toBe(200)
1415
+ expect(await res.json()).toEqual({ id: 'abc', include: 'foo' })
1416
+ expect(docs[0].kind).toBe('api')
1417
+ expect(docs[0].fullPath).toBe('/users/:id')
1418
+ })
1419
+
1420
+ test('POST with body, default 201', async () => {
1421
+ const { app } = buildApp((P) => {
1422
+ P.CreateHttp('Create', {
1423
+ path: '/items',
1424
+ method: 'post',
1425
+ schema: {
1426
+ req: { body: Type.Object({ name: Type.String() }) },
1427
+ res: { body: Type.Object({ name: Type.String() }) },
1428
+ },
1429
+ }, async (_ctx, { body }) => ({ body }))
1430
+ })
1431
+
1432
+ const res = await app.request('/items', {
1433
+ method: 'POST',
1434
+ headers: { 'Content-Type': 'application/json' },
1435
+ body: JSON.stringify({ name: 'gizmo' }),
1436
+ })
1437
+ expect(res.status).toBe(201)
1438
+ expect(await res.json()).toEqual({ name: 'gizmo' })
1439
+ })
1440
+
1441
+ test('DELETE returns 204 with no body', async () => {
1442
+ const { app } = buildApp((P) => {
1443
+ P.CreateHttp('Delete', {
1444
+ path: '/items/:id',
1445
+ method: 'delete',
1446
+ schema: { req: { pathParams: Type.Object({ id: Type.String() }) } },
1447
+ }, async () => ({}))
1448
+ })
1449
+
1450
+ const res = await app.request('/items/x', { method: 'DELETE' })
1451
+ expect(res.status).toBe(204)
1452
+ expect(await res.text()).toBe('')
1453
+ })
1454
+
1455
+ test('headers in result are forwarded', async () => {
1456
+ const { app } = buildApp((P) => {
1457
+ P.CreateHttp('WithHeaders', {
1458
+ path: '/h', method: 'get',
1459
+ schema: { res: { body: Type.Object({ ok: Type.Boolean() }), headers: Type.Object({}) } },
1460
+ }, async () => ({ body: { ok: true }, headers: { 'x-trace': 'abc' } }))
1461
+ })
1462
+ const res = await app.request('/h')
1463
+ expect(res.headers.get('x-trace')).toBe('abc')
1464
+ expect(await res.json()).toEqual({ ok: true })
1465
+ })
1466
+
1467
+ test('api.onSuccess fires after handler', async () => {
1468
+ const onSuccess = vi.fn()
1469
+ const { app } = buildApp((P) => {
1470
+ P.CreateHttp('Ping', { path: '/ping', method: 'get' }, async () => ({}))
1471
+ }, { api: { onSuccess } })
1472
+
1473
+ await app.request('/ping')
1474
+ expect(onSuccess).toHaveBeenCalledOnce()
1475
+ })
1476
+
1477
+ test('thrown error → dispatchPreStreamError default 500', async () => {
1478
+ const { app } = buildApp((P) => {
1479
+ P.CreateHttp('Boom', { path: '/boom', method: 'get' }, async () => { throw new Error('x') })
1480
+ })
1481
+ const res = await app.request('/boom')
1482
+ expect(res.status).toBe(500)
1483
+ expect(await res.json()).toEqual({ error: 'x' })
1484
+ })
1485
+
1486
+ test('custom queryParser is used', async () => {
1487
+ const queryParser = vi.fn((q: string) => ({ parsed: true, raw: q }))
1488
+ const { app } = buildApp((P) => {
1489
+ P.CreateHttp('Q', { path: '/q', method: 'get', schema: { req: { query: Type.Any() } } },
1490
+ async (_ctx, { query }) => ({ body: query }))
1491
+ }, { api: { queryParser } })
1492
+
1493
+ const res = await app.request('/q?a=1&b=2')
1494
+ expect(queryParser).toHaveBeenCalledWith('a=1&b=2')
1495
+ expect(await res.json()).toEqual({ parsed: true, raw: 'a=1&b=2' })
1496
+ })
1497
+ })
1498
+ ```
1499
+
1500
+ - [ ] **Step 2: Run test to verify it fails**
1501
+
1502
+ Run: `npx vitest run src/implementations/http/hono/handlers/http.test.ts`
1503
+ Expected: FAIL — `./http.js` not found.
1504
+
1505
+ - [ ] **Step 3: Implement `handlers/http.ts`**
1506
+
1507
+ Create `src/implementations/http/hono/handlers/http.ts`:
1508
+
1509
+ ```ts
1510
+ import type { Context, Hono } from 'hono'
1511
+ import type { THttpProcedureRegistration } from '../../../../types.js'
1512
+ import type { HttpMethod } from '../../../types.js'
1513
+ import { dispatchPreStreamError } from '../../error-dispatch.js'
1514
+ import { buildHttpRouteDoc } from '../docs/http-doc.js'
1515
+ import type { DocAccumulator, HonoAppBuilderConfig, HonoFactoryItem, QueryParser } from '../types.js'
1516
+
1517
+ const BODY_METHODS: HttpMethod[] = ['post', 'put', 'patch']
1518
+
1519
+ function defaultSuccessStatus(method: HttpMethod): number {
1520
+ switch (method) {
1521
+ case 'post': return 201
1522
+ case 'delete': return 204
1523
+ default: return 200
1524
+ }
1525
+ }
1526
+
1527
+ function parseQueryNative(queryString: string): Record<string, unknown> {
1528
+ const sp = new URLSearchParams(queryString)
1529
+ const result: Record<string, unknown> = {}
1530
+ for (const key of new Set(sp.keys())) {
1531
+ const values = sp.getAll(key)
1532
+ result[key] = values.length > 1 ? values : values[0]
1533
+ }
1534
+ return result
1535
+ }
1536
+
1537
+ function extractQuery(url: string, parser: QueryParser): Record<string, unknown> {
1538
+ const q = url.indexOf('?')
1539
+ if (q === -1) return {}
1540
+ const raw = url.slice(q + 1)
1541
+ return raw ? parser(raw) : {}
1542
+ }
1543
+
1544
+ async function extractParams(
1545
+ c: Context,
1546
+ method: HttpMethod,
1547
+ reqSchema: Record<string, unknown>,
1548
+ parser: QueryParser,
1549
+ ): Promise<Record<string, unknown>> {
1550
+ const params: Record<string, unknown> = {}
1551
+ for (const channel of Object.keys(reqSchema)) {
1552
+ switch (channel) {
1553
+ case 'pathParams':
1554
+ params.pathParams = c.req.param()
1555
+ break
1556
+ case 'query':
1557
+ params.query = extractQuery(c.req.url, parser)
1558
+ break
1559
+ case 'body':
1560
+ if (BODY_METHODS.includes(method)) {
1561
+ params.body = await c.req.json().catch(() => ({}))
1562
+ }
1563
+ break
1564
+ case 'headers': {
1565
+ const obj: Record<string, string> = {}
1566
+ c.req.raw.headers.forEach((v, k) => { obj[k] = v })
1567
+ params.headers = obj
1568
+ break
1569
+ }
1570
+ default:
1571
+ params[channel] = undefined
1572
+ }
1573
+ }
1574
+ return params
1575
+ }
1576
+
1577
+ export function installHttpRoute(params: {
1578
+ app: Hono
1579
+ procedure: THttpProcedureRegistration<any>
1580
+ factoryItem: HonoFactoryItem
1581
+ cfg: HonoAppBuilderConfig
1582
+ docs: DocAccumulator
1583
+ }): void {
1584
+ const { app, procedure, factoryItem, cfg, docs } = params
1585
+ const queryParser = cfg.api?.queryParser ?? parseQueryNative
1586
+
1587
+ const route = buildHttpRouteDoc(
1588
+ procedure,
1589
+ cfg.pathPrefix,
1590
+ factoryItem.extendProcedureDoc as any,
1591
+ )
1592
+ docs.push(route)
1593
+
1594
+ const successStatus = procedure.config.successStatus ?? defaultSuccessStatus(procedure.config.method)
1595
+ const reqSchema = procedure.config.schema?.req
1596
+
1597
+ app.on(procedure.config.method.toUpperCase(), route.fullPath, async (c: Context) => {
1598
+ try {
1599
+ const context =
1600
+ typeof factoryItem.factoryContext === 'function'
1601
+ ? await (factoryItem.factoryContext as (c: Context) => any)(c)
1602
+ : factoryItem.factoryContext
1603
+
1604
+ const reqParams = reqSchema
1605
+ ? await extractParams(c, procedure.config.method, reqSchema, queryParser)
1606
+ : undefined
1607
+
1608
+ const result = await procedure.handler(
1609
+ { ...context, signal: c.req.raw.signal },
1610
+ reqParams,
1611
+ )
1612
+
1613
+ cfg.api?.onSuccess?.(procedure, c)
1614
+
1615
+ // 204 No Content — no body, just optional headers
1616
+ if (successStatus === 204) {
1617
+ if (result && typeof result === 'object' && 'headers' in result && (result as any).headers) {
1618
+ for (const [k, v] of Object.entries((result as any).headers as Record<string, string>)) {
1619
+ c.header(k, v)
1620
+ }
1621
+ }
1622
+ return c.body(null, 204)
1623
+ }
1624
+
1625
+ let body: unknown = result
1626
+ let headers: Record<string, string> | undefined
1627
+
1628
+ if (result && typeof result === 'object' && 'body' in result && 'headers' in result) {
1629
+ body = (result as any).body
1630
+ headers = (result as any).headers as Record<string, string>
1631
+ } else if (result && typeof result === 'object' && 'headers' in result && !('body' in result)) {
1632
+ for (const [k, v] of Object.entries((result as any).headers as Record<string, string>)) {
1633
+ c.header(k, v)
1634
+ }
1635
+ return c.body(null, successStatus as any)
1636
+ } else if (result && typeof result === 'object' && 'body' in result && !('headers' in result)) {
1637
+ body = (result as any).body
1638
+ }
1639
+
1640
+ if (headers) for (const [k, v] of Object.entries(headers)) c.header(k, v)
1641
+ return c.json(body, successStatus as any)
1642
+ } catch (error) {
1643
+ return dispatchPreStreamError({
1644
+ err: error,
1645
+ procedure,
1646
+ raw: c,
1647
+ cfg: {
1648
+ errors: cfg.errors,
1649
+ unknownError: cfg.unknownError,
1650
+ onError: cfg.onError,
1651
+ onRequestError: cfg.onRequestError,
1652
+ },
1653
+ })
1654
+ }
1655
+ })
1656
+ }
1657
+ ```
1658
+
1659
+ - [ ] **Step 4: Run test to verify it passes**
1660
+
1661
+ Run: `npx vitest run src/implementations/http/hono/handlers/http.test.ts`
1662
+ Expected: PASS — 7 tests green.
1663
+
1664
+ - [ ] **Step 5: Commit**
1665
+
1666
+ ```bash
1667
+ git add src/implementations/http/hono/handlers/http.ts src/implementations/http/hono/handlers/http.test.ts
1668
+ git commit -m "feat(hono): add installHttpRoute handler
1669
+
1670
+ Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"
1671
+ ```
1672
+
1673
+ ---
1674
+
1675
+ ### Task 11: `handlers/stream.ts` — `installRpcStreamRoute` + `sse` helper
1676
+
1677
+ **Files:**
1678
+ - Create: `src/implementations/http/hono/handlers/stream.ts`
1679
+ - Test: `src/implementations/http/hono/handlers/stream.test.ts`
1680
+
1681
+ Moves `sse(data, options)` from `hono-stream/index.ts` into this module. Pre-stream validates params, throws `ProcedureValidationError` (caught by `dispatchPreStreamError`). Mid-stream uses `dispatchMidStreamError`. Pumps SSE or text mode.
1682
+
1683
+ - [ ] **Step 1: Write the failing test**
1684
+
1685
+ Create `src/implementations/http/hono/handlers/stream.test.ts`:
1686
+
1687
+ ```ts
1688
+ import { describe, expect, test, vi } from 'vitest'
1689
+ import { Hono } from 'hono'
1690
+ import { Type } from 'typebox'
1691
+ import { Procedures } from '../../../../index.js'
1692
+ import type { RPCConfig } from '../../../types.js'
1693
+ import { installRpcStreamRoute } from './stream.js'
1694
+
1695
+ function buildApp(setup: (P: ReturnType<typeof Procedures<any, RPCConfig>>) => void, cfg: any = {}) {
1696
+ const P = Procedures<{ uid: string }, RPCConfig>()
1697
+ setup(P)
1698
+ const app = new Hono()
1699
+ const docs: any[] = []
1700
+ for (const proc of P.getProcedures().values()) {
1701
+ if (proc.kind !== 'rpc-stream') continue
1702
+ installRpcStreamRoute({
1703
+ app,
1704
+ procedure: proc as any,
1705
+ factoryItem: { factory: P, factoryContext: () => ({ uid: 'u1' }) },
1706
+ cfg,
1707
+ docs,
1708
+ streamMode: cfg.stream?.defaultStreamMode ?? 'sse',
1709
+ })
1710
+ }
1711
+ return { app, docs }
1712
+ }
1713
+
1714
+ describe('installRpcStreamRoute', () => {
1715
+ test('SSE: yields are written as data events; final return as event:return', async () => {
1716
+ const { app, docs } = buildApp((P) => {
1717
+ P.CreateStream('Counts', { scope: 'c', version: 1 }, async function* () {
1718
+ yield 1
1719
+ yield 2
1720
+ return 'done'
1721
+ })
1722
+ })
1723
+ expect(docs[0].kind).toBe('stream')
1724
+ expect(docs[0].path).toBe('/c/counts/1')
1725
+
1726
+ const res = await app.request('/c/counts/1', { method: 'POST', body: '{}', headers: { 'Content-Type': 'application/json' } })
1727
+ expect(res.status).toBe(200)
1728
+ const text = await res.text()
1729
+ expect(text).toContain('data: 1')
1730
+ expect(text).toContain('data: 2')
1731
+ expect(text).toContain('event: return')
1732
+ expect(text).toContain('data: "done"')
1733
+ })
1734
+
1735
+ test('mid-stream throw → dispatchMidStreamError default { error }', async () => {
1736
+ const { app } = buildApp((P) => {
1737
+ P.CreateStream('Boom', { scope: 'b', version: 1 }, async function* () {
1738
+ yield 'first'
1739
+ throw new Error('mid-boom')
1740
+ })
1741
+ })
1742
+ const res = await app.request('/b/boom/1', { method: 'POST', body: '{}', headers: { 'Content-Type': 'application/json' } })
1743
+ const text = await res.text()
1744
+ expect(text).toContain('event: error')
1745
+ expect(text).toContain('"error":"mid-boom"')
1746
+ })
1747
+
1748
+ test('pre-stream validation error → 400 via dispatchPreStreamError', async () => {
1749
+ const { app } = buildApp((P) => {
1750
+ P.CreateStream('NeedsParams', {
1751
+ scope: 'n', version: 1,
1752
+ schema: { params: Type.Object({ id: Type.String() }) },
1753
+ }, async function* () { yield 'ok' })
1754
+ })
1755
+ const res = await app.request('/n/needs-params/1', { method: 'POST', body: '{}', headers: { 'Content-Type': 'application/json' } })
1756
+ expect(res.status).toBe(400)
1757
+ const body = await res.json()
1758
+ expect(body.name).toBe('ProcedureValidationError')
1759
+ })
1760
+
1761
+ test('onStreamStart and onStreamEnd fire', async () => {
1762
+ const onStreamStart = vi.fn()
1763
+ const onStreamEnd = vi.fn()
1764
+ const { app } = buildApp((P) => {
1765
+ P.CreateStream('Ev', { scope: 'e', version: 1 }, async function* () { yield 'x' })
1766
+ }, { stream: { onStreamStart, onStreamEnd } })
1767
+
1768
+ await app.request('/e/ev/1', { method: 'POST', body: '{}', headers: { 'Content-Type': 'application/json' } })
1769
+ expect(onStreamStart).toHaveBeenCalled()
1770
+ expect(onStreamEnd).toHaveBeenCalled()
1771
+ })
1772
+ })
1773
+ ```
1774
+
1775
+ - [ ] **Step 2: Run test to verify it fails**
1776
+
1777
+ Run: `npx vitest run src/implementations/http/hono/handlers/stream.test.ts`
1778
+ Expected: FAIL — `./stream.js` not found.
1779
+
1780
+ - [ ] **Step 3: Implement `handlers/stream.ts`**
1781
+
1782
+ Create `src/implementations/http/hono/handlers/stream.ts`:
1783
+
1784
+ ```ts
1785
+ import type { Context, Hono } from 'hono'
1786
+ import { streamSSE, streamText } from 'hono/streaming'
1787
+ import type { TStreamProcedureRegistration } from '../../../../types.js'
1788
+ import type { RPCConfig, StreamMode } from '../../../types.js'
1789
+ import { ProcedureValidationError } from '../../../../errors.js'
1790
+ import { dispatchMidStreamError, dispatchPreStreamError } from '../../error-dispatch.js'
1791
+ import { buildStreamRouteDoc } from '../docs/stream-doc.js'
1792
+ import type { DocAccumulator, HonoAppBuilderConfig, HonoFactoryItem } from '../types.js'
1793
+
1794
+ export type SSEOptions = {
1795
+ event?: string
1796
+ id?: string
1797
+ retry?: number
1798
+ }
1799
+
1800
+ const sseMetadata = new WeakMap<object, SSEOptions>()
1801
+
1802
+ /**
1803
+ * Marks an object yield as an SSE event with custom metadata. Stream handlers
1804
+ * read this metadata to set the SSE `event:`, `id:`, and `retry:` fields.
1805
+ */
1806
+ export function sse<T extends object>(data: T, options?: SSEOptions): T {
1807
+ sseMetadata.set(data, options ?? {})
1808
+ return data
1809
+ }
1810
+
1811
+ function getSSEMeta(value: unknown): SSEOptions | undefined {
1812
+ if (typeof value === 'object' && value !== null) {
1813
+ return sseMetadata.get(value as object)
1814
+ }
1815
+ return undefined
1816
+ }
1817
+
1818
+ export function installRpcStreamRoute(params: {
1819
+ app: Hono
1820
+ procedure: TStreamProcedureRegistration<any, RPCConfig>
1821
+ factoryItem: HonoFactoryItem
1822
+ cfg: HonoAppBuilderConfig
1823
+ docs: DocAccumulator
1824
+ streamMode: StreamMode
1825
+ }): void {
1826
+ const { app, procedure, factoryItem, cfg, docs, streamMode } = params
1827
+
1828
+ const route = buildStreamRouteDoc(
1829
+ procedure,
1830
+ streamMode,
1831
+ cfg.pathPrefix,
1832
+ factoryItem.extendProcedureDoc as any,
1833
+ )
1834
+ docs.push(route)
1835
+
1836
+ const handler = async (c: Context) => {
1837
+ try {
1838
+ const context =
1839
+ typeof factoryItem.factoryContext === 'function'
1840
+ ? await (factoryItem.factoryContext as (c: Context) => any)(c)
1841
+ : factoryItem.factoryContext
1842
+
1843
+ const reqParams =
1844
+ c.req.method === 'GET'
1845
+ ? Object.fromEntries(new URL(c.req.url).searchParams)
1846
+ : await c.req.json().catch(() => ({}))
1847
+
1848
+ // Pre-stream validation — throw so the catch routes through dispatchPreStreamError.
1849
+ if ((procedure.config as any).validation?.params) {
1850
+ const { errors } = (procedure.config as any).validation.params(reqParams)
1851
+ if (errors) {
1852
+ throw new ProcedureValidationError(
1853
+ procedure.name,
1854
+ `Validation error for ${procedure.name}`,
1855
+ errors,
1856
+ )
1857
+ }
1858
+ }
1859
+
1860
+ cfg.stream?.onStreamStart?.(procedure, c, streamMode)
1861
+
1862
+ return streamMode === 'sse'
1863
+ ? handleSSE(procedure, context, reqParams, c, cfg)
1864
+ : handleText(procedure, context, reqParams, c, cfg)
1865
+ } catch (error) {
1866
+ return dispatchPreStreamError({
1867
+ err: error,
1868
+ procedure,
1869
+ raw: c,
1870
+ cfg: {
1871
+ errors: cfg.errors,
1872
+ unknownError: cfg.unknownError,
1873
+ onError: cfg.onError,
1874
+ onRequestError: cfg.onRequestError,
1875
+ },
1876
+ })
1877
+ }
1878
+ }
1879
+
1880
+ app.get(route.path, handler)
1881
+ app.post(route.path, handler)
1882
+ }
1883
+
1884
+ function handleSSE(
1885
+ procedure: TStreamProcedureRegistration<any, RPCConfig>,
1886
+ context: any,
1887
+ reqParams: any,
1888
+ c: Context,
1889
+ cfg: HonoAppBuilderConfig,
1890
+ ) {
1891
+ return streamSSE(c, async (stream) => {
1892
+ const generator = procedure.handler(
1893
+ { ...context, signal: c.req.raw.signal, isPrevalidated: true } as any,
1894
+ reqParams,
1895
+ )
1896
+ stream.onAbort(async () => { await generator.return(undefined) })
1897
+
1898
+ let eventId = 0
1899
+ try {
1900
+ const iterator = generator[Symbol.asyncIterator]()
1901
+ let it = await iterator.next()
1902
+
1903
+ while (!it.done) {
1904
+ const value = it.value
1905
+ const meta = getSSEMeta(value)
1906
+ const data =
1907
+ typeof value === 'string' ? value
1908
+ : value != null ? JSON.stringify(value)
1909
+ : ''
1910
+
1911
+ await stream.writeSSE({
1912
+ data,
1913
+ event: meta?.event ?? procedure.name,
1914
+ id: meta?.id ?? String(eventId++),
1915
+ ...(meta?.retry !== undefined && { retry: meta.retry }),
1916
+ })
1917
+ it = await iterator.next()
1918
+ }
1919
+
1920
+ if (it.value !== undefined) {
1921
+ const data = typeof it.value === 'string' ? it.value : JSON.stringify(it.value)
1922
+ await stream.writeSSE({ data, event: 'return', id: String(eventId++) })
1923
+ }
1924
+ } catch (error) {
1925
+ const dispatched = await dispatchMidStreamError({
1926
+ err: error,
1927
+ procedure,
1928
+ raw: c,
1929
+ cfg: {
1930
+ errors: cfg.errors,
1931
+ unknownError: cfg.unknownError,
1932
+ onMidStreamError: cfg.stream?.onMidStreamError,
1933
+ onRequestError: cfg.onRequestError,
1934
+ },
1935
+ })
1936
+
1937
+ const meta = getSSEMeta(dispatched.data)
1938
+ await stream.writeSSE({
1939
+ data: typeof dispatched.data === 'string' ? dispatched.data : JSON.stringify(dispatched.data),
1940
+ event: meta?.event ?? dispatched.sseEvent ?? 'error',
1941
+ id: meta?.id ?? dispatched.sseId ?? String(eventId++),
1942
+ ...((meta?.retry ?? dispatched.sseRetry) !== undefined && {
1943
+ retry: (meta?.retry ?? dispatched.sseRetry) as number,
1944
+ }),
1945
+ })
1946
+
1947
+ if (dispatched.runOnCatch) await dispatched.runOnCatch()
1948
+ } finally {
1949
+ cfg.stream?.onStreamEnd?.(procedure, c, 'sse')
1950
+ cfg.onRequestEnd?.(c)
1951
+ }
1952
+ })
1953
+ }
1954
+
1955
+ function handleText(
1956
+ procedure: TStreamProcedureRegistration<any, RPCConfig>,
1957
+ context: any,
1958
+ reqParams: any,
1959
+ c: Context,
1960
+ cfg: HonoAppBuilderConfig,
1961
+ ) {
1962
+ return streamText(c, async (stream) => {
1963
+ const generator = procedure.handler(
1964
+ { ...context, signal: c.req.raw.signal, isPrevalidated: true } as any,
1965
+ reqParams,
1966
+ )
1967
+ stream.onAbort(async () => { await generator.return(undefined) })
1968
+
1969
+ try {
1970
+ for await (const value of generator) {
1971
+ await stream.writeln(JSON.stringify(value))
1972
+ }
1973
+ } catch (error) {
1974
+ const dispatched = await dispatchMidStreamError({
1975
+ err: error,
1976
+ procedure,
1977
+ raw: c,
1978
+ cfg: {
1979
+ errors: cfg.errors,
1980
+ unknownError: cfg.unknownError,
1981
+ onMidStreamError: cfg.stream?.onMidStreamError,
1982
+ onRequestError: cfg.onRequestError,
1983
+ },
1984
+ })
1985
+ await stream.writeln(JSON.stringify(dispatched.data))
1986
+ if (dispatched.runOnCatch) await dispatched.runOnCatch()
1987
+ } finally {
1988
+ cfg.stream?.onStreamEnd?.(procedure, c, 'text')
1989
+ cfg.onRequestEnd?.(c)
1990
+ }
1991
+ })
1992
+ }
1993
+
1994
+ export type { MidStreamErrorResult } from '../../error-dispatch.js'
1995
+ ```
1996
+
1997
+ Now update the test file in `error-dispatch.test.ts` (Task 3) — change the placeholder import comment to actually import `sse` from this new module. Edit the file: replace the placeholder import line with `import { sse } from './hono/handlers/stream.js'`.
1998
+
1999
+ - [ ] **Step 4: Run test to verify it passes**
2000
+
2001
+ Run: `npx vitest run src/implementations/http/hono/handlers/stream.test.ts src/implementations/http/error-dispatch.test.ts`
2002
+ Expected: PASS — 4 + 10 = 14 tests green.
2003
+
2004
+ - [ ] **Step 5: Commit**
2005
+
2006
+ ```bash
2007
+ git add src/implementations/http/hono/handlers/stream.ts src/implementations/http/hono/handlers/stream.test.ts src/implementations/http/error-dispatch.test.ts
2008
+ git commit -m "feat(hono): add installRpcStreamRoute handler + sse helper
2009
+
2010
+ Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"
2011
+ ```
2012
+
2013
+ ---
2014
+
2015
+ ### Task 12: `handlers/http-stream.ts` — `installHttpStreamRoute`
2016
+
2017
+ **Files:**
2018
+ - Create: `src/implementations/http/hono/handlers/http-stream.ts`
2019
+ - Test: `src/implementations/http/hono/handlers/http-stream.test.ts`
2020
+
2021
+ Mounts http-stream procedures (developer-supplied path, `{ stream, initialHeaders }` return shape, SSE only). Adds mid-stream error handling that the existing `hono-api.ts` lacks — calls `dispatchMidStreamError` inside the SSE pump catch.
2022
+
2023
+ - [ ] **Step 1: Write the failing test**
2024
+
2025
+ Create `src/implementations/http/hono/handlers/http-stream.test.ts`:
2026
+
2027
+ ```ts
2028
+ import { describe, expect, test, vi } from 'vitest'
2029
+ import { Hono } from 'hono'
2030
+ import { Type } from 'typebox'
2031
+ import { Procedures } from '../../../../index.js'
2032
+ import { installHttpStreamRoute } from './http-stream.js'
2033
+
2034
+ function buildApp(setup: (P: ReturnType<typeof Procedures<any>>) => void, cfg: any = {}) {
2035
+ const P = Procedures<{ uid: string }>()
2036
+ setup(P)
2037
+ const app = new Hono()
2038
+ const docs: any[] = []
2039
+ for (const proc of P.getProcedures().values()) {
2040
+ if (proc.kind !== 'http-stream') continue
2041
+ installHttpStreamRoute({
2042
+ app,
2043
+ procedure: proc as any,
2044
+ factoryItem: { factory: P, factoryContext: () => ({ uid: 'u1' }) },
2045
+ cfg,
2046
+ docs,
2047
+ })
2048
+ }
2049
+ return { app, docs }
2050
+ }
2051
+
2052
+ describe('installHttpStreamRoute', () => {
2053
+ test('SSE pumping with initialHeaders and event:return', async () => {
2054
+ const { app, docs } = buildApp((P) => {
2055
+ P.CreateHttpStream('Tail', {
2056
+ path: '/logs/tail', method: 'get',
2057
+ schema: { yield: Type.Object({ line: Type.String() }) },
2058
+ }, async () => ({
2059
+ stream: (async function* () {
2060
+ yield { line: 'a' }
2061
+ yield { line: 'b' }
2062
+ return 'fin'
2063
+ })(),
2064
+ initialHeaders: { 'x-trace': 'abc' },
2065
+ }))
2066
+ })
2067
+ expect(docs[0].kind).toBe('http-stream')
2068
+
2069
+ const res = await app.request('/logs/tail')
2070
+ expect(res.headers.get('x-trace')).toBe('abc')
2071
+ const text = await res.text()
2072
+ expect(text).toContain('"line":"a"')
2073
+ expect(text).toContain('"line":"b"')
2074
+ expect(text).toContain('event: return')
2075
+ expect(text).toContain('"fin"')
2076
+ })
2077
+
2078
+ test('mid-stream throw → dispatchMidStreamError default { error }', async () => {
2079
+ const { app } = buildApp((P) => {
2080
+ P.CreateHttpStream('Boom', { path: '/boom', method: 'get' }, async () => ({
2081
+ stream: (async function* () {
2082
+ yield { ok: 1 }
2083
+ throw new Error('mid')
2084
+ })(),
2085
+ }))
2086
+ })
2087
+ const res = await app.request('/boom')
2088
+ const text = await res.text()
2089
+ expect(text).toContain('event: error')
2090
+ expect(text).toContain('"error":"mid"')
2091
+ })
2092
+
2093
+ test('pre-stream throw → dispatchPreStreamError 500', async () => {
2094
+ const { app } = buildApp((P) => {
2095
+ P.CreateHttpStream('Pre', { path: '/pre', method: 'get' }, async () => {
2096
+ throw new Error('pre')
2097
+ })
2098
+ })
2099
+ const res = await app.request('/pre')
2100
+ expect(res.status).toBe(500)
2101
+ expect(await res.json()).toEqual({ error: 'pre' })
2102
+ })
2103
+ })
2104
+ ```
2105
+
2106
+ - [ ] **Step 2: Run test to verify it fails**
2107
+
2108
+ Run: `npx vitest run src/implementations/http/hono/handlers/http-stream.test.ts`
2109
+ Expected: FAIL — `./http-stream.js` not found.
2110
+
2111
+ - [ ] **Step 3: Implement `handlers/http-stream.ts`**
2112
+
2113
+ Create `src/implementations/http/hono/handlers/http-stream.ts`:
2114
+
2115
+ ```ts
2116
+ import type { Context, Hono } from 'hono'
2117
+ import { streamSSE } from 'hono/streaming'
2118
+ import type { THttpStreamProcedureRegistration } from '../../../../types.js'
2119
+ import type { HttpMethod } from '../../../types.js'
2120
+ import { dispatchMidStreamError, dispatchPreStreamError } from '../../error-dispatch.js'
2121
+ import { buildHttpStreamRouteDoc } from '../docs/http-stream-doc.js'
2122
+ import type { DocAccumulator, HonoAppBuilderConfig, HonoFactoryItem, QueryParser } from '../types.js'
2123
+
2124
+ const BODY_METHODS: HttpMethod[] = ['post', 'put', 'patch']
2125
+
2126
+ function parseQueryNative(queryString: string): Record<string, unknown> {
2127
+ const sp = new URLSearchParams(queryString)
2128
+ const result: Record<string, unknown> = {}
2129
+ for (const key of new Set(sp.keys())) {
2130
+ const values = sp.getAll(key)
2131
+ result[key] = values.length > 1 ? values : values[0]
2132
+ }
2133
+ return result
2134
+ }
2135
+
2136
+ function extractQuery(url: string, parser: QueryParser): Record<string, unknown> {
2137
+ const q = url.indexOf('?')
2138
+ if (q === -1) return {}
2139
+ const raw = url.slice(q + 1)
2140
+ return raw ? parser(raw) : {}
2141
+ }
2142
+
2143
+ async function extractParams(
2144
+ c: Context,
2145
+ method: HttpMethod,
2146
+ reqSchema: Record<string, unknown>,
2147
+ parser: QueryParser,
2148
+ ): Promise<Record<string, unknown>> {
2149
+ const params: Record<string, unknown> = {}
2150
+ for (const channel of Object.keys(reqSchema)) {
2151
+ switch (channel) {
2152
+ case 'pathParams':
2153
+ params.pathParams = c.req.param()
2154
+ break
2155
+ case 'query':
2156
+ params.query = extractQuery(c.req.url, parser)
2157
+ break
2158
+ case 'body':
2159
+ if (BODY_METHODS.includes(method)) {
2160
+ params.body = await c.req.json().catch(() => ({}))
2161
+ }
2162
+ break
2163
+ case 'headers': {
2164
+ const obj: Record<string, string> = {}
2165
+ c.req.raw.headers.forEach((v, k) => { obj[k] = v })
2166
+ params.headers = obj
2167
+ break
2168
+ }
2169
+ default:
2170
+ params[channel] = undefined
2171
+ }
2172
+ }
2173
+ return params
2174
+ }
2175
+
2176
+ export function installHttpStreamRoute(params: {
2177
+ app: Hono
2178
+ procedure: THttpStreamProcedureRegistration<any>
2179
+ factoryItem: HonoFactoryItem
2180
+ cfg: HonoAppBuilderConfig
2181
+ docs: DocAccumulator
2182
+ }): void {
2183
+ const { app, procedure, factoryItem, cfg, docs } = params
2184
+ const queryParser = cfg.api?.queryParser ?? parseQueryNative
2185
+
2186
+ const route = buildHttpStreamRouteDoc(
2187
+ procedure,
2188
+ cfg.pathPrefix,
2189
+ factoryItem.extendProcedureDoc as any,
2190
+ )
2191
+ docs.push(route)
2192
+
2193
+ const reqSchema = procedure.config.schema?.req
2194
+
2195
+ app.on(procedure.config.method.toUpperCase(), route.fullPath, async (c: Context) => {
2196
+ try {
2197
+ const context =
2198
+ typeof factoryItem.factoryContext === 'function'
2199
+ ? await (factoryItem.factoryContext as (c: Context) => any)(c)
2200
+ : factoryItem.factoryContext
2201
+
2202
+ const reqParams = reqSchema
2203
+ ? await extractParams(c, procedure.config.method, reqSchema as Record<string, unknown>, queryParser)
2204
+ : undefined
2205
+
2206
+ const { stream: gen, initialHeaders } = await procedure.handler(
2207
+ { ...context, signal: c.req.raw.signal },
2208
+ reqParams,
2209
+ )
2210
+
2211
+ if (initialHeaders) {
2212
+ for (const [k, v] of Object.entries(initialHeaders)) c.header(k, v)
2213
+ }
2214
+
2215
+ cfg.stream?.onStreamStart?.(procedure, c, 'sse')
2216
+
2217
+ return streamSSE(c, async (stream) => {
2218
+ let eventId = 0
2219
+ try {
2220
+ let it = await gen.next()
2221
+ while (!it.done) {
2222
+ const data = typeof it.value === 'string' ? it.value : JSON.stringify(it.value)
2223
+ await stream.writeSSE({ data, event: procedure.name, id: String(eventId++) })
2224
+ it = await gen.next()
2225
+ }
2226
+ if (it.value !== undefined) {
2227
+ const data = typeof it.value === 'string' ? it.value : JSON.stringify(it.value)
2228
+ await stream.writeSSE({ data, event: 'return', id: String(eventId++) })
2229
+ }
2230
+ } catch (error) {
2231
+ const dispatched = await dispatchMidStreamError({
2232
+ err: error,
2233
+ procedure,
2234
+ raw: c,
2235
+ cfg: {
2236
+ errors: cfg.errors,
2237
+ unknownError: cfg.unknownError,
2238
+ onMidStreamError: cfg.stream?.onMidStreamError,
2239
+ onRequestError: cfg.onRequestError,
2240
+ },
2241
+ })
2242
+ await stream.writeSSE({
2243
+ data: typeof dispatched.data === 'string' ? dispatched.data : JSON.stringify(dispatched.data),
2244
+ event: dispatched.sseEvent ?? 'error',
2245
+ id: String(eventId++),
2246
+ ...(dispatched.sseRetry !== undefined && { retry: dispatched.sseRetry }),
2247
+ })
2248
+ if (dispatched.runOnCatch) await dispatched.runOnCatch()
2249
+ } finally {
2250
+ cfg.stream?.onStreamEnd?.(procedure, c, 'sse')
2251
+ cfg.onRequestEnd?.(c)
2252
+ }
2253
+ })
2254
+ } catch (error) {
2255
+ return dispatchPreStreamError({
2256
+ err: error,
2257
+ procedure,
2258
+ raw: c,
2259
+ cfg: {
2260
+ errors: cfg.errors,
2261
+ unknownError: cfg.unknownError,
2262
+ onError: cfg.onError,
2263
+ onRequestError: cfg.onRequestError,
2264
+ },
2265
+ })
2266
+ }
2267
+ })
2268
+ }
2269
+ ```
2270
+
2271
+ - [ ] **Step 4: Run test to verify it passes**
2272
+
2273
+ Run: `npx vitest run src/implementations/http/hono/handlers/http-stream.test.ts`
2274
+ Expected: PASS — 3 tests green.
2275
+
2276
+ - [ ] **Step 5: Commit**
2277
+
2278
+ ```bash
2279
+ git add src/implementations/http/hono/handlers/http-stream.ts src/implementations/http/hono/handlers/http-stream.test.ts
2280
+ git commit -m "feat(hono): add installHttpStreamRoute handler
2281
+
2282
+ Adds mid-stream error handling that the existing hono-api http-stream
2283
+ branch was missing — generator throws now dispatch through
2284
+ dispatchMidStreamError instead of propagating into an unhandled SSE
2285
+ abort.
2286
+
2287
+ Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"
2288
+ ```
2289
+
2290
+ ---
2291
+
2292
+ ## Phase 4 — HonoAppBuilder Public Class
2293
+
2294
+ ### Task 13: `index.ts` — class skeleton + `register` + lazy `docs`
2295
+
2296
+ **Files:**
2297
+ - Create: `src/implementations/http/hono/index.ts`
2298
+ - Test: `src/implementations/http/hono/index.test.ts`
2299
+
2300
+ - [ ] **Step 1: Write the failing test**
2301
+
2302
+ Create `src/implementations/http/hono/index.test.ts`:
2303
+
2304
+ ```ts
2305
+ import { describe, expect, test } from 'vitest'
2306
+ import { Hono } from 'hono'
2307
+ import { Type } from 'typebox'
2308
+ import { Procedures } from '../../../index.js'
2309
+ import type { RPCConfig } from '../../types.js'
2310
+ import { HonoAppBuilder } from './index.js'
2311
+
2312
+ describe('HonoAppBuilder — public surface', () => {
2313
+ test('constructor with no args', () => {
2314
+ const b = new HonoAppBuilder()
2315
+ expect(b.app).toBeInstanceOf(Hono)
2316
+ expect(b.docs).toEqual([])
2317
+ expect(b.skippedProcedures).toEqual([])
2318
+ })
2319
+
2320
+ test('constructor accepts existing Hono app', () => {
2321
+ const custom = new Hono()
2322
+ const b = new HonoAppBuilder({ app: custom })
2323
+ expect(b.app).toBe(custom)
2324
+ })
2325
+
2326
+ test('register is chainable', () => {
2327
+ const b = new HonoAppBuilder()
2328
+ const P = Procedures<{}, RPCConfig>()
2329
+ const result = b.register(P, () => ({}))
2330
+ expect(result).toBe(b)
2331
+ })
2332
+
2333
+ test('docs is lazily computed before build()', () => {
2334
+ const b = new HonoAppBuilder()
2335
+ const P = Procedures<{}, RPCConfig>()
2336
+ P.Create('A', { scope: 's', version: 1 }, async () => ({}))
2337
+ b.register(P, () => ({}))
2338
+
2339
+ const docs1 = b.docs
2340
+ expect(docs1).toHaveLength(1)
2341
+ expect(docs1[0].kind).toBe('rpc')
2342
+ // Calling docs again returns the same cached array reference
2343
+ expect(b.docs).toBe(docs1)
2344
+ })
2345
+
2346
+ test('docs reads after build() return the same content', () => {
2347
+ const b = new HonoAppBuilder()
2348
+ const P = Procedures<{}, RPCConfig>()
2349
+ P.Create('A', { scope: 's', version: 1 }, async () => ({}))
2350
+ b.register(P, () => ({}))
2351
+ b.build()
2352
+ expect(b.docs).toHaveLength(1)
2353
+ })
2354
+ })
2355
+ ```
2356
+
2357
+ - [ ] **Step 2: Run test to verify it fails**
2358
+
2359
+ Run: `npx vitest run src/implementations/http/hono/index.test.ts`
2360
+ Expected: FAIL — `./index.js` not found.
2361
+
2362
+ - [ ] **Step 3: Implement `index.ts` skeleton**
2363
+
2364
+ Create `src/implementations/http/hono/index.ts`:
2365
+
2366
+ ```ts
2367
+ import { Context, Hono } from 'hono'
2368
+ import type {
2369
+ AnyHttpRouteDoc,
2370
+ ProceduresFactory,
2371
+ ExtractContext,
2372
+ StreamMode,
2373
+ } from '../../types.js'
2374
+ import { buildRpcRouteDoc } from './docs/rpc-doc.js'
2375
+ import { buildStreamRouteDoc } from './docs/stream-doc.js'
2376
+ import { buildHttpRouteDoc } from './docs/http-doc.js'
2377
+ import { buildHttpStreamRouteDoc } from './docs/http-stream-doc.js'
2378
+ import { installRpcRoute } from './handlers/rpc.js'
2379
+ import { installRpcStreamRoute } from './handlers/stream.js'
2380
+ import { installHttpRoute } from './handlers/http.js'
2381
+ import { installHttpStreamRoute } from './handlers/http-stream.js'
2382
+ import { makeRoutePath as _makeRoutePath } from './path.js'
2383
+ import type {
2384
+ HonoAppBuilderConfig,
2385
+ HonoFactoryItem,
2386
+ ExtendProcedureDoc,
2387
+ AnyProcedureRegistration,
2388
+ OnRequestErrorContext,
2389
+ } from './types.js'
2390
+
2391
+ export type { HonoAppBuilderConfig, OnRequestErrorContext, ExtendProcedureDoc } from './types.js'
2392
+ export type { AnyProcedureRegistration } from './types.js'
2393
+ export { sse } from './handlers/stream.js'
2394
+ export type { SSEOptions, MidStreamErrorResult } from './handlers/stream.js'
2395
+ export { defineErrorTaxonomy } from '../error-taxonomy.js'
2396
+ export type { ErrorTaxonomy, ErrorTaxonomyEntry, UnknownErrorConfig } from '../error-taxonomy.js'
2397
+ export type { QueryParser } from './types.js'
2398
+
2399
+ export class HonoAppBuilder<TStreamErrorData = unknown> {
2400
+ private readonly _app: Hono
2401
+ private readonly factories: HonoFactoryItem<any>[] = []
2402
+ private _docs: AnyHttpRouteDoc[] | null = null
2403
+ private _skipped: { name: string; reason: string }[] = []
2404
+ private _built = false
2405
+
2406
+ constructor(readonly config?: HonoAppBuilderConfig<TStreamErrorData>) {
2407
+ this._app = config?.app ?? new Hono()
2408
+
2409
+ if (config?.onRequestStart) {
2410
+ this._app.use('*', async (c, next) => {
2411
+ config.onRequestStart!(c)
2412
+ await next()
2413
+ })
2414
+ }
2415
+ if (config?.onRequestEnd) {
2416
+ this._app.use('*', async (c, next) => {
2417
+ await next()
2418
+ config.onRequestEnd!(c)
2419
+ })
2420
+ }
2421
+ }
2422
+
2423
+ static makeRoutePath = _makeRoutePath
2424
+
2425
+ get app(): Hono {
2426
+ return this._app
2427
+ }
2428
+
2429
+ /**
2430
+ * Lazily computed on first read or `build()`. Computing without `build()`
2431
+ * lets tests and tooling introspect routes without spinning up handlers.
2432
+ */
2433
+ get docs(): AnyHttpRouteDoc[] {
2434
+ if (this._docs === null) this.computeDocs()
2435
+ return this._docs!
2436
+ }
2437
+
2438
+ get skippedProcedures(): { name: string; reason: string }[] {
2439
+ return this._skipped
2440
+ }
2441
+
2442
+ register<TFactory extends ProceduresFactory>(
2443
+ factory: TFactory,
2444
+ factoryContext:
2445
+ | ExtractContext<TFactory>
2446
+ | ((c: Context) => ExtractContext<TFactory> | Promise<ExtractContext<TFactory>>),
2447
+ options?: {
2448
+ streamMode?: StreamMode
2449
+ extendProcedureDoc?: ExtendProcedureDoc
2450
+ },
2451
+ ): this {
2452
+ this.factories.push({
2453
+ factory,
2454
+ factoryContext,
2455
+ streamMode: options?.streamMode,
2456
+ extendProcedureDoc: options?.extendProcedureDoc,
2457
+ } as HonoFactoryItem<any>)
2458
+ // Invalidate the docs cache — next read recomputes.
2459
+ this._docs = null
2460
+ return this
2461
+ }
2462
+
2463
+ private computeDocs(): void {
2464
+ const docs: AnyHttpRouteDoc[] = []
2465
+ const skipped: { name: string; reason: string }[] = []
2466
+
2467
+ for (const item of this.factories) {
2468
+ for (const procedure of item.factory.getProcedures().values() as Iterable<AnyProcedureRegistration>) {
2469
+ const prefix = this.config?.pathPrefix
2470
+ switch (procedure.kind) {
2471
+ case 'rpc':
2472
+ docs.push(buildRpcRouteDoc(procedure as any, prefix, item.extendProcedureDoc as any))
2473
+ break
2474
+ case 'rpc-stream':
2475
+ docs.push(buildStreamRouteDoc(
2476
+ procedure as any,
2477
+ item.streamMode ?? this.config?.stream?.defaultStreamMode ?? 'sse',
2478
+ prefix,
2479
+ item.extendProcedureDoc as any,
2480
+ ))
2481
+ break
2482
+ case 'http':
2483
+ docs.push(buildHttpRouteDoc(procedure as any, prefix, item.extendProcedureDoc as any))
2484
+ break
2485
+ case 'http-stream':
2486
+ docs.push(buildHttpStreamRouteDoc(procedure as any, prefix, item.extendProcedureDoc as any))
2487
+ break
2488
+ default: {
2489
+ const reason = `Unknown procedure kind "${(procedure as any).kind}"`
2490
+ skipped.push({ name: (procedure as any).name, reason })
2491
+ }
2492
+ }
2493
+ }
2494
+ }
2495
+
2496
+ this._docs = docs
2497
+ this._skipped = skipped
2498
+ }
2499
+
2500
+ /**
2501
+ * Mounts every registered procedure on the Hono app and returns it.
2502
+ * Idempotent: calling twice is a no-op on the second call.
2503
+ */
2504
+ build(): Hono {
2505
+ if (this._built) return this._app
2506
+ this._built = true
2507
+
2508
+ const docs: AnyHttpRouteDoc[] = []
2509
+ const skipped: { name: string; reason: string }[] = []
2510
+ const cfg = this.config ?? {}
2511
+
2512
+ for (const item of this.factories) {
2513
+ for (const procedure of item.factory.getProcedures().values() as Iterable<AnyProcedureRegistration>) {
2514
+ switch (procedure.kind) {
2515
+ case 'rpc':
2516
+ installRpcRoute({ app: this._app, procedure: procedure as any, factoryItem: item, cfg, docs })
2517
+ break
2518
+ case 'rpc-stream': {
2519
+ const streamMode = item.streamMode ?? cfg.stream?.defaultStreamMode ?? 'sse'
2520
+ installRpcStreamRoute({ app: this._app, procedure: procedure as any, factoryItem: item, cfg, docs, streamMode })
2521
+ break
2522
+ }
2523
+ case 'http':
2524
+ installHttpRoute({ app: this._app, procedure: procedure as any, factoryItem: item, cfg, docs })
2525
+ break
2526
+ case 'http-stream':
2527
+ installHttpStreamRoute({ app: this._app, procedure: procedure as any, factoryItem: item, cfg, docs })
2528
+ break
2529
+ default: {
2530
+ const reason = `Unknown procedure kind "${(procedure as any).kind}"`
2531
+ skipped.push({ name: (procedure as any).name, reason })
2532
+ console.warn(`[ts-procedures hono] Skipping procedure "${(procedure as any).name}": ${reason}`)
2533
+ }
2534
+ }
2535
+ }
2536
+ }
2537
+
2538
+ this._docs = docs
2539
+ this._skipped = skipped
2540
+ return this._app
2541
+ }
2542
+ }
2543
+ ```
2544
+
2545
+ - [ ] **Step 4: Run test to verify it passes**
2546
+
2547
+ Run: `npx vitest run src/implementations/http/hono/index.test.ts`
2548
+ Expected: PASS — 5 tests green.
2549
+
2550
+ - [ ] **Step 5: Commit**
2551
+
2552
+ ```bash
2553
+ git add src/implementations/http/hono/index.ts src/implementations/http/hono/index.test.ts
2554
+ git commit -m "feat(hono): add HonoAppBuilder class with lazy docs + dispatch
2555
+
2556
+ Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"
2557
+ ```
2558
+
2559
+ ---
2560
+
2561
+ ### Task 14: Mixed-kind dispatch integration test + `toDocEnvelope`
2562
+
2563
+ **Files:**
2564
+ - Modify: `src/implementations/http/hono/index.ts` — add `toDocEnvelope`
2565
+ - Modify: `src/implementations/http/hono/index.test.ts`
2566
+
2567
+ Verifies that one factory containing all four procedure kinds dispatches correctly under one builder. Adds `toDocEnvelope()` mirroring `DocRegistry.toJSON()`.
2568
+
2569
+ - [ ] **Step 1: Append failing tests**
2570
+
2571
+ Append to `src/implementations/http/hono/index.test.ts`:
2572
+
2573
+ ```ts
2574
+ import { Type } from 'typebox'
2575
+ import { Procedures } from '../../../index.js'
2576
+ import type { RPCConfig } from '../../types.js'
2577
+ import { defineErrorTaxonomy } from '../error-taxonomy.js'
2578
+
2579
+ describe('HonoAppBuilder — mixed-kind dispatch', () => {
2580
+ test('one factory with all four kinds mounts each at the correct path', async () => {
2581
+ const P = Procedures<{ uid: string }, RPCConfig>()
2582
+ P.Create('Echo', { scope: 'rpc', version: 1 }, async (_ctx, p) => p)
2583
+ P.CreateStream('Tail', { scope: 'rpc', version: 1 }, async function* () { yield 1 })
2584
+ P.CreateHttp('GetUser', { path: '/users/:id', method: 'get' }, async () => ({ body: { id: '1' } }))
2585
+ P.CreateHttpStream('Watch', { path: '/watch', method: 'get' }, async () => ({
2586
+ stream: (async function* () { yield 'tick' })(),
2587
+ }))
2588
+
2589
+ const app = new HonoAppBuilder()
2590
+ .register(P, () => ({ uid: 'u1' }))
2591
+ .build()
2592
+
2593
+ const rpc = await app.request('/rpc/echo/1', { method: 'POST', body: '{}', headers: { 'Content-Type': 'application/json' } })
2594
+ expect(rpc.status).toBe(200)
2595
+
2596
+ const stream = await app.request('/rpc/tail/1', { method: 'POST', body: '{}', headers: { 'Content-Type': 'application/json' } })
2597
+ expect(stream.status).toBe(200)
2598
+
2599
+ const http = await app.request('/users/1')
2600
+ expect(http.status).toBe(200)
2601
+
2602
+ const httpStream = await app.request('/watch')
2603
+ expect(httpStream.status).toBe(200)
2604
+ })
2605
+
2606
+ test('toDocEnvelope produces same shape as DocRegistry.toJSON', async () => {
2607
+ const { DocRegistry } = await import('../doc-registry.js')
2608
+
2609
+ const P = Procedures<{}, RPCConfig>()
2610
+ P.Create('Echo', { scope: 'e', version: 1 }, async () => ({}))
2611
+
2612
+ const errors = defineErrorTaxonomy({})
2613
+ const builder = new HonoAppBuilder({ pathPrefix: '/api', errors })
2614
+ builder.register(P, () => ({}))
2615
+ builder.build()
2616
+
2617
+ const builderEnvelope = builder.toDocEnvelope({ basePath: '/api', errors })
2618
+ const registryEnvelope = new DocRegistry({ basePath: '/api', errors }).from(builder).toJSON()
2619
+
2620
+ expect(builderEnvelope.basePath).toBe(registryEnvelope.basePath)
2621
+ expect(builderEnvelope.routes).toEqual(registryEnvelope.routes)
2622
+ expect(builderEnvelope.errors.map(e => e.name).sort()).toEqual(registryEnvelope.errors.map(e => e.name).sort())
2623
+ })
2624
+
2625
+ test('toDocEnvelope filter and transform options work', () => {
2626
+ const P = Procedures<{}, RPCConfig>()
2627
+ P.Create('A', { scope: 's', version: 1 }, async () => ({}))
2628
+ P.Create('B', { scope: 's', version: 1 }, async () => ({}))
2629
+
2630
+ const builder = new HonoAppBuilder().register(P, () => ({}))
2631
+ const onlyA = builder.toDocEnvelope({ filter: r => r.name === 'A' })
2632
+ expect(onlyA.routes).toHaveLength(1)
2633
+ expect(onlyA.routes[0].name).toBe('A')
2634
+
2635
+ const tagged = builder.toDocEnvelope({
2636
+ transform: env => ({ ...env, tagged: true }) as any,
2637
+ }) as any
2638
+ expect(tagged.tagged).toBe(true)
2639
+ })
2640
+ })
2641
+ ```
2642
+
2643
+ - [ ] **Step 2: Add `toDocEnvelope` to `index.ts`**
2644
+
2645
+ First, add a static import to the top of `src/implementations/http/hono/index.ts` (alongside the other imports):
2646
+
2647
+ ```ts
2648
+ import { DocRegistry } from '../doc-registry.js'
2649
+ import type { DocEnvelope, ErrorDoc, HeaderDoc } from '../../types.js'
2650
+ import type { ErrorTaxonomy } from '../error-taxonomy.js'
2651
+ ```
2652
+
2653
+ Then append the method to the `HonoAppBuilder` class body:
2654
+
2655
+ ```ts
2656
+ /**
2657
+ * Produces a {@link DocEnvelope} for single-app usage. For multi-app
2658
+ * aggregation, use {@link DocRegistry} and `from(builder)` instead.
2659
+ *
2660
+ * Mirrors `DocRegistry.toJSON()` options so the two paths produce
2661
+ * interchangeable envelopes for the codegen pipeline.
2662
+ */
2663
+ toDocEnvelope<T = DocEnvelope>(options?: {
2664
+ basePath?: string
2665
+ errors?: ErrorTaxonomy | ErrorDoc[]
2666
+ includeDefaults?: boolean
2667
+ headers?: HeaderDoc[]
2668
+ filter?: (route: AnyHttpRouteDoc) => boolean
2669
+ transform?: (envelope: DocEnvelope) => T
2670
+ }): T {
2671
+ return new DocRegistry({
2672
+ basePath: options?.basePath ?? this.config?.pathPrefix,
2673
+ errors: options?.errors ?? this.config?.errors,
2674
+ includeDefaults: options?.includeDefaults,
2675
+ headers: options?.headers,
2676
+ }).from(this).toJSON<T>(options)
2677
+ }
2678
+ ```
2679
+
2680
+ - [ ] **Step 3: Run tests to verify they pass**
2681
+
2682
+ Run: `npx vitest run src/implementations/http/hono/index.test.ts`
2683
+ Expected: PASS — 8 tests green (5 existing + 3 new).
2684
+
2685
+ - [ ] **Step 4: Commit**
2686
+
2687
+ ```bash
2688
+ git add src/implementations/http/hono/index.ts src/implementations/http/hono/index.test.ts
2689
+ git commit -m "feat(hono): add toDocEnvelope + mixed-kind dispatch coverage
2690
+
2691
+ Delegates to DocRegistry internally so single-app and multi-app paths
2692
+ produce byte-identical envelopes for codegen.
2693
+
2694
+ Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"
2695
+ ```
2696
+
2697
+ ---
2698
+
2699
+ ## Phase 5 — Wire Up Exports
2700
+
2701
+ ### Task 15: Add `./hono` to `package.json#exports`
2702
+
2703
+ **Files:**
2704
+ - Modify: `package.json`
2705
+
2706
+ - [ ] **Step 1: Edit `package.json`**
2707
+
2708
+ In `package.json`, add the new export after `./hono-stream` (which is still present — Phase 6 deletes it). Add this entry inside the `exports` object:
2709
+
2710
+ ```json
2711
+ "./hono": {
2712
+ "types": "./build/implementations/http/hono/index.d.ts",
2713
+ "import": "./build/implementations/http/hono/index.js"
2714
+ },
2715
+ ```
2716
+
2717
+ - [ ] **Step 2: Verify build**
2718
+
2719
+ Run: `npm run build`
2720
+ Expected: PASS — TypeScript compiles `src/implementations/http/hono/` into `build/implementations/http/hono/`.
2721
+
2722
+ - [ ] **Step 3: Commit**
2723
+
2724
+ ```bash
2725
+ git add package.json
2726
+ git commit -m "chore(pkg): add ts-procedures/hono subpath export
2727
+
2728
+ Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"
2729
+ ```
2730
+
2731
+ ---
2732
+
2733
+ ## Phase 6 — Migrate Shared Tests
2734
+
2735
+ These four files at `src/implementations/http/` import from the soon-to-be-deleted directories. Retarget them at `HonoAppBuilder` BEFORE deleting the old directories so the test suite stays green throughout.
2736
+
2737
+ ### Task 16: Retarget `on-request-error.test.ts`
2738
+
2739
+ **Files:**
2740
+ - Modify: `src/implementations/http/on-request-error.test.ts`
2741
+
2742
+ The current file has three `describe` blocks, one per old builder. Collapse to one `describe('onRequestError — HonoAppBuilder')` covering all kinds.
2743
+
2744
+ - [ ] **Step 1: Read the current file**
2745
+
2746
+ Run: `cat src/implementations/http/on-request-error.test.ts`
2747
+ Note the existing test cases — three describe blocks with similar shapes (rpc, api, stream).
2748
+
2749
+ - [ ] **Step 2: Rewrite imports and `boomApp` helpers**
2750
+
2751
+ Replace the imports section (currently importing from `./hono-rpc/index.js`, `./hono-api/index.js`, `./hono-stream/index.js`) with a single import:
2752
+
2753
+ ```ts
2754
+ import { HonoAppBuilder } from './hono/index.js'
2755
+ ```
2756
+
2757
+ Replace the three `boomApp` helpers with one parameterized helper that mounts a procedure of any kind:
2758
+
2759
+ ```ts
2760
+ function boomApp(
2761
+ kind: 'rpc' | 'rpc-stream' | 'http' | 'http-stream',
2762
+ config: ConstructorParameters<typeof HonoAppBuilder>[0],
2763
+ ) {
2764
+ const P = Procedures<{}, RPCConfig>()
2765
+ switch (kind) {
2766
+ case 'rpc':
2767
+ P.Create('Boom', { scope: 'b', version: 1 }, async () => { throw new Error('boom') })
2768
+ break
2769
+ case 'rpc-stream':
2770
+ P.CreateStream('Boom', { scope: 'b', version: 1 }, async function* () {
2771
+ throw new Error('boom')
2772
+ })
2773
+ break
2774
+ case 'http':
2775
+ P.CreateHttp('Boom', { path: '/boom', method: 'get' }, async () => { throw new Error('boom') })
2776
+ break
2777
+ case 'http-stream':
2778
+ P.CreateHttpStream('Boom', { path: '/boom', method: 'get' }, async () => { throw new Error('boom') })
2779
+ break
2780
+ }
2781
+ return new HonoAppBuilder(config).register(P, () => ({})).build()
2782
+ }
2783
+ ```
2784
+
2785
+ Update each `describe` block to call `boomApp` with its kind parameter. The assertions stay the same — `onRequestError` should fire once per request, observer throws should be swallowed, etc.
2786
+
2787
+ - [ ] **Step 3: Run tests**
2788
+
2789
+ Run: `npx vitest run src/implementations/http/on-request-error.test.ts`
2790
+ Expected: PASS — all converted tests green.
2791
+
2792
+ - [ ] **Step 4: Commit**
2793
+
2794
+ ```bash
2795
+ git add src/implementations/http/on-request-error.test.ts
2796
+ git commit -m "test(http): retarget on-request-error.test.ts at HonoAppBuilder
2797
+
2798
+ Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"
2799
+ ```
2800
+
2801
+ ---
2802
+
2803
+ ### Task 17: Retarget `doc-registry.test.ts`
2804
+
2805
+ **Files:**
2806
+ - Modify: `src/implementations/http/doc-registry.test.ts`
2807
+
2808
+ - [ ] **Step 1: Update imports and instantiations**
2809
+
2810
+ Replace `import { HonoRPCAppBuilder } from './hono-rpc/index.js'` and `import { HonoAPIAppBuilder } from './hono-api/index.js'` with:
2811
+
2812
+ ```ts
2813
+ import { HonoAppBuilder } from './hono/index.js'
2814
+ ```
2815
+
2816
+ Replace every `new HonoRPCAppBuilder()` and `new HonoAPIAppBuilder()` with `new HonoAppBuilder()`. The assertions don't need to change — both produce `DocSource`.
2817
+
2818
+ - [ ] **Step 2: Run tests**
2819
+
2820
+ Run: `npx vitest run src/implementations/http/doc-registry.test.ts`
2821
+ Expected: PASS — all tests still green.
2822
+
2823
+ - [ ] **Step 3: Commit**
2824
+
2825
+ ```bash
2826
+ git add src/implementations/http/doc-registry.test.ts
2827
+ git commit -m "test(http): retarget doc-registry.test.ts at HonoAppBuilder
2828
+
2829
+ Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"
2830
+ ```
2831
+
2832
+ ---
2833
+
2834
+ ### Task 18: Retarget `route-errors.test.ts` and top-level `error-taxonomy.test.ts`
2835
+
2836
+ **Files:**
2837
+ - Modify: `src/implementations/http/route-errors.test.ts`
2838
+ - Modify: `src/implementations/http/error-taxonomy.test.ts`
2839
+
2840
+ - [ ] **Step 1: Inspect both files**
2841
+
2842
+ Run: `grep -nE "Hono(RPC|API|Stream)AppBuilder" src/implementations/http/route-errors.test.ts src/implementations/http/error-taxonomy.test.ts`
2843
+ Note every reference site.
2844
+
2845
+ - [ ] **Step 2: Retarget imports and instantiations**
2846
+
2847
+ In each file, replace old builder imports with `import { HonoAppBuilder } from './hono/index.js'` and update each `new HonoXAppBuilder()` to `new HonoAppBuilder()`. For tests using `onSuccess` flat at the constructor (rpc/api), wrap into `rpc: { onSuccess }` or `api: { onSuccess }` per kind. For tests using `onMidStreamError` flat at the constructor, wrap into `stream: { onMidStreamError }`.
2848
+
2849
+ - [ ] **Step 3: Run tests**
2850
+
2851
+ Run: `npx vitest run src/implementations/http/route-errors.test.ts src/implementations/http/error-taxonomy.test.ts`
2852
+ Expected: PASS.
2853
+
2854
+ - [ ] **Step 4: Commit**
2855
+
2856
+ ```bash
2857
+ git add src/implementations/http/route-errors.test.ts src/implementations/http/error-taxonomy.test.ts
2858
+ git commit -m "test(http): retarget route-errors and error-taxonomy tests
2859
+
2860
+ Wrap flat builder hooks in stratified blocks (rpc.onSuccess,
2861
+ stream.onMidStreamError) to match the converged config shape.
2862
+
2863
+ Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"
2864
+ ```
2865
+
2866
+ ---
2867
+
2868
+ ## Phase 7 — Port Deep Test Suites
2869
+
2870
+ The three old test files (`hono-rpc/index.test.ts` 1129 lines, `hono-api/index.test.ts` 1070 lines, `hono-stream/index.test.ts` 1836 lines) cover hundreds of behaviors that the per-handler test files (Tasks 9–12) don't fully replicate. Port their distinctive cases into the per-handler test files before deletion so coverage is preserved.
2871
+
2872
+ ### Task 19: Port `hono-rpc/index.test.ts` cases into `hono/handlers/rpc.test.ts`
2873
+
2874
+ **Files:**
2875
+ - Modify: `src/implementations/http/hono/handlers/rpc.test.ts`
2876
+ - Reference (read, do not modify): `src/implementations/http/hono-rpc/index.test.ts`
2877
+ - Reference: `src/implementations/http/hono-rpc/error-taxonomy.test.ts`
2878
+
2879
+ - [ ] **Step 1: Inventory cases worth porting**
2880
+
2881
+ Run: `grep -nE "test\(|it\(" src/implementations/http/hono-rpc/index.test.ts src/implementations/http/hono-rpc/error-taxonomy.test.ts | head -80`
2882
+
2883
+ Identify cases not already covered by the four base tests in `hono/handlers/rpc.test.ts`. Look for: pathPrefix variations, factoryContext variations (sync/async/static object), `extendProcedureDoc` callback, error taxonomy with user entries, `unknownError` fallback, observer-then-taxonomy ordering, validation errors, registration of multiple factories, and tests where `kind !== 'rpc'` procedures should be passed-through (no longer skipped under the unified builder).
2884
+
2885
+ - [ ] **Step 2: Port the missing cases**
2886
+
2887
+ For each case not yet covered, add a test in `hono/handlers/rpc.test.ts` using the existing `buildApp` helper. Wrap any constructor-flat hooks (`onSuccess`, `errors`, `onError`, `onRequestError`, `onRequestStart`, `onRequestEnd`) — `onSuccess` goes under `rpc: { onSuccess }`; the rest stay top-level.
2888
+
2889
+ For the "non-rpc procedures get skipped" tests (e.g., registering a stream procedure with the rpc builder), DELETE them — under the unified builder, mixed-kind factories work natively, so the skip behavior no longer applies. The `index.test.ts` mixed-kind dispatch test (Task 14) covers the new behavior.
2890
+
2891
+ For tests asserting on `builder.skippedProcedures`, retarget them to assert that the array is empty for known kinds — only "unknown future kind" entries should appear.
2892
+
2893
+ - [ ] **Step 3: Run the consolidated test file**
2894
+
2895
+ Run: `npx vitest run src/implementations/http/hono/handlers/rpc.test.ts`
2896
+ Expected: PASS — all ported tests green.
2897
+
2898
+ - [ ] **Step 4: Commit**
2899
+
2900
+ ```bash
2901
+ git add src/implementations/http/hono/handlers/rpc.test.ts
2902
+ git commit -m "test(hono): port hono-rpc test coverage into handlers/rpc.test.ts
2903
+
2904
+ Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"
2905
+ ```
2906
+
2907
+ ---
2908
+
2909
+ ### Task 20: Port `hono-api/index.test.ts` cases into `hono/handlers/http.test.ts`
2910
+
2911
+ **Files:**
2912
+ - Modify: `src/implementations/http/hono/handlers/http.test.ts`
2913
+ - Reference: `src/implementations/http/hono-api/index.test.ts`
2914
+ - Reference: `src/implementations/http/hono-api/error-taxonomy.test.ts`
2915
+
2916
+ Same approach as Task 19. The hono-api file also covers the http-stream branch (since hono-api today serves both http and http-stream procedures); split those cases between `handlers/http.test.ts` and `handlers/http-stream.test.ts` based on the procedure kind under test.
2917
+
2918
+ - [ ] **Step 1: Inventory and port to `handlers/http.test.ts`**
2919
+
2920
+ `grep -nE "test\(|it\(" src/implementations/http/hono-api/index.test.ts | head -60` — identify cases for `kind: 'http'` (developer paths, queryParser variants, all six HTTP methods, headers handling, `successStatus` overrides, response shape detection { body, headers } / bare body / headers-only, `extendProcedureDoc`, error taxonomy).
2921
+
2922
+ Wrap flat constructor knobs: `onSuccess` → `api.onSuccess`, `queryParser` → `api.queryParser`.
2923
+
2924
+ - [ ] **Step 2: Inventory and port to `handlers/http-stream.test.ts`**
2925
+
2926
+ In the same `hono-api` file, the http-stream cases are mixed into the same `describe('HonoAPIAppBuilder')`. Identify them by the procedure created via `CreateHttpStream` and port them into `handlers/http-stream.test.ts`.
2927
+
2928
+ - [ ] **Step 3: Run both files**
2929
+
2930
+ Run: `npx vitest run src/implementations/http/hono/handlers/http.test.ts src/implementations/http/hono/handlers/http-stream.test.ts`
2931
+ Expected: PASS.
2932
+
2933
+ - [ ] **Step 4: Commit**
2934
+
2935
+ ```bash
2936
+ git add src/implementations/http/hono/handlers/http.test.ts src/implementations/http/hono/handlers/http-stream.test.ts
2937
+ git commit -m "test(hono): port hono-api test coverage into handlers/http and handlers/http-stream
2938
+
2939
+ Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"
2940
+ ```
2941
+
2942
+ ---
2943
+
2944
+ ### Task 21: Port `hono-stream/index.test.ts` cases into `hono/handlers/stream.test.ts`
2945
+
2946
+ **Files:**
2947
+ - Modify: `src/implementations/http/hono/handlers/stream.test.ts`
2948
+ - Reference: `src/implementations/http/hono-stream/index.test.ts`
2949
+ - Reference: `src/implementations/http/hono-stream/error-taxonomy.test.ts`
2950
+
2951
+ Same approach. Cover: SSE vs text mode, `defaultStreamMode` global default vs per-register override (`register(F, ctx, { streamMode: 'text' })`), `sse(data, options)` helper for custom event/id/retry, mid-stream error variants, `onMidStreamError` returning custom data, abort signal propagation when client disconnects, `validateYields: true` behavior.
2952
+
2953
+ Wrap flat constructor knobs: `onMidStreamError` → `stream.onMidStreamError`, `defaultStreamMode` → `stream.defaultStreamMode`, `onStreamStart`/`onStreamEnd` → `stream.onStreamStart`/`stream.onStreamEnd`.
2954
+
2955
+ - [ ] **Step 1: Inventory and port**
2956
+
2957
+ Run the same inventory as Tasks 19–20.
2958
+
2959
+ - [ ] **Step 2: Add a test for the `register(F, ctx, { streamMode: 'text' })` per-factory override**
2960
+
2961
+ This is the converged shape's replacement for the old `register(F, ctx, { streamMode: 'text' })` on `HonoStreamAppBuilder`. Port the equivalent cases.
2962
+
2963
+ - [ ] **Step 3: Run**
2964
+
2965
+ Run: `npx vitest run src/implementations/http/hono/handlers/stream.test.ts`
2966
+ Expected: PASS.
2967
+
2968
+ - [ ] **Step 4: Commit**
2969
+
2970
+ ```bash
2971
+ git add src/implementations/http/hono/handlers/stream.test.ts
2972
+ git commit -m "test(hono): port hono-stream test coverage into handlers/stream.test.ts
2973
+
2974
+ Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"
2975
+ ```
2976
+
2977
+ ---
2978
+
2979
+ ## Phase 8 — Delete Old + Cleanup
2980
+
2981
+ ### Task 22: Delete the three old builder directories + remove from `package.json#exports`
2982
+
2983
+ **Files:**
2984
+ - Delete: `src/implementations/http/hono-rpc/`
2985
+ - Delete: `src/implementations/http/hono-api/`
2986
+ - Delete: `src/implementations/http/hono-stream/`
2987
+ - Modify: `package.json`
2988
+
2989
+ - [ ] **Step 1: Confirm no remaining references**
2990
+
2991
+ Run: `grep -rEn "hono-rpc|hono-api|hono-stream" src/ docs/ agent_config/ CLAUDE.md README.md package.json 2>/dev/null | grep -v "build/" | grep -v "docs/superpowers/"`
2992
+
2993
+ Expected: only `package.json` (entries we're about to delete), comments in source files (Task 27 fixes those), and docs/agent_config files (Tasks 24–26 fix those). No imports.
2994
+
2995
+ If any imports remain, retarget them at `./hono/index.js` before proceeding.
2996
+
2997
+ - [ ] **Step 2: Delete the directories**
2998
+
2999
+ ```bash
3000
+ rm -rf src/implementations/http/hono-rpc/
3001
+ rm -rf src/implementations/http/hono-api/
3002
+ rm -rf src/implementations/http/hono-stream/
3003
+ ```
3004
+
3005
+ - [ ] **Step 3: Remove old subpath exports from `package.json`**
3006
+
3007
+ Open `package.json` and delete these three blocks from the `exports` object:
3008
+
3009
+ ```json
3010
+ "./hono-rpc": {
3011
+ "types": "./build/implementations/http/hono-rpc/index.d.ts",
3012
+ "import": "./build/implementations/http/hono-rpc/index.js"
3013
+ },
3014
+ "./hono-stream": {
3015
+ "types": "./build/implementations/http/hono-stream/index.d.ts",
3016
+ "import": "./build/implementations/http/hono-stream/index.js"
3017
+ },
3018
+ "./hono-api": {
3019
+ "types": "./build/implementations/http/hono-api/index.d.ts",
3020
+ "import": "./build/implementations/http/hono-api/index.js"
3021
+ },
3022
+ ```
3023
+
3024
+ - [ ] **Step 4: Run full suite + lint + build**
3025
+
3026
+ Run these in parallel:
3027
+
3028
+ ```bash
3029
+ npm run test
3030
+ npm run lint
3031
+ npm run build
3032
+ ```
3033
+
3034
+ Expected: all pass. If anything fails, the most likely cause is a forgotten import that wasn't caught in step 1 — fix it and re-run.
3035
+
3036
+ - [ ] **Step 5: Commit**
3037
+
3038
+ ```bash
3039
+ git add -A
3040
+ git commit -m "$(cat <<'EOF'
3041
+ refactor(hono): remove old hono-rpc, hono-api, hono-stream builders
3042
+
3043
+ The three specialized builders are replaced by HonoAppBuilder which
3044
+ dispatches all four procedure kinds (rpc, rpc-stream, http,
3045
+ http-stream) by procedure.kind from a single register() call.
3046
+
3047
+ Removes ~1,580 lines of near-parallel dispatch/error/lifecycle code
3048
+ from src/implementations/http/.
3049
+
3050
+ Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
3051
+ EOF
3052
+ )"
3053
+ ```
3054
+
3055
+ ---
3056
+
3057
+ ### Task 23: Update source-comment references to old builder names
3058
+
3059
+ **Files:**
3060
+ - Modify: `src/types.ts` (lines around the `ProcedureKind` JSDoc)
3061
+ - Modify: `src/create-stream.ts` (comments referring to `HonoStreamAppBuilder`)
3062
+ - Modify: `src/implementations/http/doc-registry.ts` (comment about `HonoRPCAppBuilder`)
3063
+
3064
+ - [ ] **Step 1: Update `src/types.ts`**
3065
+
3066
+ In `src/types.ts`, find the `ProcedureKind` JSDoc block (around line 17–28) and replace the four "served by Hono*AppBuilder" lines with a single line:
3067
+
3068
+ ```ts
3069
+ /**
3070
+ * Discriminant on every procedure registration that drives builder routing.
3071
+ *
3072
+ * - `'rpc'` — Create() registration
3073
+ * - `'rpc-stream'` — CreateStream() registration
3074
+ * - `'http'` — CreateHttp() registration
3075
+ * - `'http-stream'` — CreateHttpStream() registration
3076
+ *
3077
+ * `HonoAppBuilder` dispatches all four kinds from a single registration.
3078
+ * Use `kind` for any logic that needs to distinguish procedure shape; the
3079
+ * legacy `isStream` field on TStreamProcedureRegistration is kept for
3080
+ * back-compat but new code should branch on `kind`.
3081
+ */
3082
+ ```
3083
+
3084
+ - [ ] **Step 2: Update `src/create-stream.ts`**
3085
+
3086
+ In `src/create-stream.ts`, find the two comments referencing `HonoStreamAppBuilder` (around lines 97 and 160) and replace `HonoStreamAppBuilder` with `HonoAppBuilder`.
3087
+
3088
+ - [ ] **Step 3: Update `src/implementations/http/doc-registry.ts`**
3089
+
3090
+ In `src/implementations/http/doc-registry.ts`, find the comment around line 92 (`// streaming procedure registered with HonoRPCAppBuilder`) and replace `HonoRPCAppBuilder` with `HonoAppBuilder`. Update the surrounding sentence to reflect that under the unified builder, mismatches are now "unknown procedure kind" rather than "registered with the wrong builder".
3091
+
3092
+ - [ ] **Step 4: Verify build + lint**
3093
+
3094
+ Run: `npm run lint && npm run build`
3095
+ Expected: PASS.
3096
+
3097
+ - [ ] **Step 5: Commit**
3098
+
3099
+ ```bash
3100
+ git add src/types.ts src/create-stream.ts src/implementations/http/doc-registry.ts
3101
+ git commit -m "docs(types): update JSDoc references from old builder names
3102
+
3103
+ Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"
3104
+ ```
3105
+
3106
+ ---
3107
+
3108
+ ### Task 24: Update `src/implementations/http/README.md`
3109
+
3110
+ **Files:**
3111
+ - Modify: `src/implementations/http/README.md`
3112
+
3113
+ This README currently documents three separate builders. Rewrite around the single `HonoAppBuilder`.
3114
+
3115
+ - [ ] **Step 1: Read the current README**
3116
+
3117
+ Run: `cat src/implementations/http/README.md`
3118
+ Reference structure (sections to keep, sections to drop, sections to rewrite).
3119
+
3120
+ - [ ] **Step 2: Rewrite the README**
3121
+
3122
+ Replace the file's content with the structure below. The sections "Procedure Types," "Path Generation," "Context Resolution," "Abort Signal," "Error Handling," "Lifecycle Hooks," "Route Documentation," "DocRegistry," and "Client Code Generation" stay (with internal references updated). The sections "Available Implementations" and "Builder Pattern" (which currently enumerate three builders) collapse into one "HonoAppBuilder" section. The "One Hono Server, Multiple Builders" reference at the bottom is removed.
3123
+
3124
+ The Hono section's example should mirror the spec's Public API code block.
3125
+
3126
+ Update the table at the bottom mapping kind → params source → generated callable so it lists all four kinds (rpc, rpc-stream, http, http-stream) under HonoAppBuilder.
3127
+
3128
+ - [ ] **Step 3: Run docs-consistency check (if it exists)**
3129
+
3130
+ Run: `npm run check-docs`
3131
+ Expected: PASS — or note any issues for follow-up.
3132
+
3133
+ - [ ] **Step 4: Commit**
3134
+
3135
+ ```bash
3136
+ git add src/implementations/http/README.md
3137
+ git commit -m "docs(http): rewrite README around HonoAppBuilder
3138
+
3139
+ Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"
3140
+ ```
3141
+
3142
+ ---
3143
+
3144
+ ### Task 25: Update `docs/http-integrations.md`
3145
+
3146
+ **Files:**
3147
+ - Modify: `docs/http-integrations.md`
3148
+
3149
+ - [ ] **Step 1: Inspect the current doc**
3150
+
3151
+ Run: `head -200 docs/http-integrations.md` and `grep -nE "Hono(RPC|API|Stream)AppBuilder|One Hono Server" docs/http-integrations.md`
3152
+
3153
+ - [ ] **Step 2: Rewrite the doc**
3154
+
3155
+ - Replace every `HonoRPCAppBuilder`, `HonoAPIAppBuilder`, `HonoStreamAppBuilder` reference with `HonoAppBuilder`.
3156
+ - Rewrite all example code to use the unified builder + stratified config.
3157
+ - Delete the section `## One Hono Server, Multiple Builders` entirely (it's no longer relevant).
3158
+ - Update the error-handling section: keep the spec narrative (declarative + imperative peers, observer); update the "Hono-stream coverage: pre-stream path only" note to reflect that mid-stream errors now also dispatch through the taxonomy in http-stream (not just rpc-stream).
3159
+
3160
+ - [ ] **Step 3: Run docs-consistency check**
3161
+
3162
+ Run: `npm run check-docs`
3163
+ Expected: PASS.
3164
+
3165
+ - [ ] **Step 4: Commit**
3166
+
3167
+ ```bash
3168
+ git add docs/http-integrations.md
3169
+ git commit -m "docs: rewrite http-integrations around HonoAppBuilder
3170
+
3171
+ Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"
3172
+ ```
3173
+
3174
+ ---
3175
+
3176
+ ### Task 26: Update agent_config templates and skills
3177
+
3178
+ **Files:**
3179
+ - Delete: `agent_config/claude-code/skills/ts-procedures-scaffold/templates/hono-rpc.ts`
3180
+ - Delete: `agent_config/claude-code/skills/ts-procedures-scaffold/templates/hono-stream.ts`
3181
+ - Modify (rename): `agent_config/claude-code/skills/ts-procedures-scaffold/templates/hono-api.ts` → `hono.ts`
3182
+ - Modify: `agent_config/claude-code/skills/ts-procedures-scaffold/SKILL.md`
3183
+ - Modify: `agent_config/claude-code/skills/ts-procedures/SKILL.md`
3184
+ - Modify: `agent_config/claude-code/skills/ts-procedures/patterns.md`
3185
+ - Modify: `agent_config/claude-code/skills/ts-procedures/anti-patterns.md`
3186
+ - Modify: `agent_config/claude-code/skills/ts-procedures/api-reference.md`
3187
+ - Modify: `agent_config/copilot/copilot-instructions.md`
3188
+ - Modify: `agent_config/cursor/cursorrules`
3189
+
3190
+ - [ ] **Step 1: Inspect existing templates and skills**
3191
+
3192
+ ```bash
3193
+ ls agent_config/claude-code/skills/ts-procedures-scaffold/templates/
3194
+ grep -lE "Hono(RPC|API|Stream)AppBuilder" agent_config/
3195
+ ```
3196
+
3197
+ - [ ] **Step 2: Replace the templates**
3198
+
3199
+ Delete `hono-rpc.ts` and `hono-stream.ts`. Rename `hono-api.ts` to `hono.ts` and rewrite it to demonstrate one builder serving all four kinds — pattern from the spec's Public API block. Keep the `Procedures<Ctx>()` factory + `Create`/`CreateStream`/`CreateHttp`/`CreateHttpStream` examples + a single `new HonoAppBuilder(...)` registration.
3200
+
3201
+ - [ ] **Step 3: Update skill docs**
3202
+
3203
+ In each of the listed skill / instruction files, replace every reference to `HonoRPCAppBuilder` / `HonoAPIAppBuilder` / `HonoStreamAppBuilder` with `HonoAppBuilder`, and rewrite any code examples to use the stratified config shape. Remove any "use this builder for this kind" stratification — replace with a "which procedure kind to use" decision tree.
3204
+
3205
+ - [ ] **Step 4: Verify scaffolding still works**
3206
+
3207
+ Run: `node ./agent_config/bin/setup.mjs --dry-run`
3208
+ Expected: PASS — dry run reports the new file structure with no missing template references.
3209
+
3210
+ - [ ] **Step 5: Commit**
3211
+
3212
+ ```bash
3213
+ git add agent_config/
3214
+ git commit -m "$(cat <<'EOF'
3215
+ docs(agent_config): consolidate hono templates around HonoAppBuilder
3216
+
3217
+ Delete hono-rpc.ts and hono-stream.ts templates; rename hono-api.ts to
3218
+ hono.ts and rewrite around one builder serving all four kinds. Update
3219
+ skills, patterns, copilot instructions, and cursor rules to match.
3220
+
3221
+ Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
3222
+ EOF
3223
+ )"
3224
+ ```
3225
+
3226
+ ---
3227
+
3228
+ ### Task 27: Update `CLAUDE.md` and `README.md`
3229
+
3230
+ **Files:**
3231
+ - Modify: `CLAUDE.md`
3232
+ - Modify: `README.md`
3233
+
3234
+ - [ ] **Step 1: Update `CLAUDE.md`**
3235
+
3236
+ In the "Key Files" section (around the `src/implementations/http/` entries), replace the three separate hono builder lines with a single line:
3237
+
3238
+ ```
3239
+ - `src/implementations/http/hono/` - Unified Hono builder (`HonoAppBuilder`) — dispatches all four procedure kinds from one register() call. Subpath: `ts-procedures/hono`.
3240
+ - `src/implementations/http/error-dispatch.ts` - Shared dispatchPreStreamError + dispatchMidStreamError helpers used by HonoAppBuilder.
3241
+ ```
3242
+
3243
+ In the "Builder Pattern" section, update text to describe one builder; remove the "Single Hono server across builders" callout (now built-in).
3244
+
3245
+ In the "Generated client code" sections, update any references to old builder names.
3246
+
3247
+ - [ ] **Step 2: Update `README.md`**
3248
+
3249
+ Find the v8 changelog/migration section and add an entry summarizing the convergence:
3250
+
3251
+ ```
3252
+ ### Hono builders converged into HonoAppBuilder (v8)
3253
+
3254
+ The three Hono builders — `HonoRPCAppBuilder`, `HonoAPIAppBuilder`,
3255
+ `HonoStreamAppBuilder` — are replaced by a single `HonoAppBuilder` that
3256
+ accepts any `Procedures<>()` factory and dispatches all four procedure
3257
+ kinds (`rpc`, `rpc-stream`, `http`, `http-stream`) from one
3258
+ registration call.
3259
+
3260
+ Migration:
3261
+ - `import { HonoAppBuilder } from 'ts-procedures/hono'`
3262
+ - Wrap kind-specific knobs in stratified blocks: `rpc.onSuccess`,
3263
+ `api.queryParser`, `api.onSuccess`, `stream.defaultStreamMode`,
3264
+ `stream.onMidStreamError`, `stream.onStreamStart`, `stream.onStreamEnd`.
3265
+ - Single-app users can call `builder.toDocEnvelope()` instead of going
3266
+ through `DocRegistry`. Multi-app users keep using `DocRegistry`.
3267
+ ```
3268
+
3269
+ - [ ] **Step 3: Verify check-docs**
3270
+
3271
+ Run: `npm run check-docs`
3272
+ Expected: PASS.
3273
+
3274
+ - [ ] **Step 4: Commit**
3275
+
3276
+ ```bash
3277
+ git add CLAUDE.md README.md
3278
+ git commit -m "docs: update CLAUDE.md and README.md for HonoAppBuilder convergence
3279
+
3280
+ Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"
3281
+ ```
3282
+
3283
+ ---
3284
+
3285
+ ## Phase 9 — Final Verification
3286
+
3287
+ ### Task 28: Codegen end-to-end + full suite
3288
+
3289
+ **Files:** none — verification only
3290
+
3291
+ - [ ] **Step 1: Run full test suite, lint, build**
3292
+
3293
+ ```bash
3294
+ npm run test
3295
+ npm run lint
3296
+ npm run build
3297
+ ```
3298
+
3299
+ Expected: all PASS.
3300
+
3301
+ - [ ] **Step 2: Verify codegen against the new builder**
3302
+
3303
+ The codegen e2e fixtures consume `DocEnvelope` JSON. Build a small inline harness that:
3304
+ 1. Sets up a `HonoAppBuilder` with one factory containing all four kinds.
3305
+ 2. Calls `builder.toDocEnvelope({ basePath: '/api' })`.
3306
+ 3. Passes the envelope to `generateClient({ envelope, outDir, dryRun: true, target: 'ts' })`.
3307
+ 4. Asserts the dry-run output includes scope files for each kind.
3308
+
3309
+ If a similar test already exists in `src/codegen/`, run it: `npx vitest run src/codegen/` and confirm all green.
3310
+
3311
+ - [ ] **Step 3: Build typecheck of generated output (optional sanity check)**
3312
+
3313
+ If the existing codegen e2e test (e.g., `src/codegen/e2e-compile.test.ts`) compiles generated TypeScript via `tsc`, run it and confirm green. This validates that the new builder's `toDocEnvelope()` produces a codegen-compatible envelope.
3314
+
3315
+ ```bash
3316
+ grep -lE "e2e|compile" src/codegen/*.test.ts
3317
+ ```
3318
+
3319
+ If found: `npx vitest run <path>` and confirm pass.
3320
+
3321
+ - [ ] **Step 4: Final smoke summary**
3322
+
3323
+ Verify the work is complete by checking:
3324
+ - `src/implementations/http/hono-rpc/`, `hono-api/`, `hono-stream/` no longer exist
3325
+ - `src/implementations/http/hono/` exists with the layout from the File Structure section
3326
+ - `src/implementations/http/error-dispatch.ts` exists with both dispatchers
3327
+ - `package.json#exports` has `./hono` and lacks `./hono-rpc`, `./hono-api`, `./hono-stream`
3328
+ - `npm run test` passes
3329
+ - `npm run lint` passes
3330
+ - `npm run build` passes
3331
+ - `npm run check-docs` passes (if available)
3332
+ - `grep -rEn "Hono(RPC|API|Stream)AppBuilder" src/ docs/ agent_config/ CLAUDE.md README.md` returns zero hits in non-build files
3333
+
3334
+ If all eight checks pass, the convergence is complete.
3335
+
3336
+ - [ ] **Step 5: Optional — final summary commit (only if there are stray artifacts)**
3337
+
3338
+ If the verification surfaced any small fixes (typo, leftover reference), commit them in a single `chore(convergence): final cleanup` commit. Otherwise, no commit needed for verification.
3339
+
3340
+ ---
3341
+
3342
+ ## Self-Review Notes
3343
+
3344
+ **Spec coverage check (run before submitting):**
3345
+
3346
+ | Spec section | Plan task(s) |
3347
+ |---|---|
3348
+ | Public API surface (HonoAppBuilder, register, build, docs, toDocEnvelope) | Tasks 13–14 |
3349
+ | Stratified config (rpc, api, stream blocks) | Tasks 8, 13 |
3350
+ | Internal architecture (handler/doc layout) | Tasks 4–12 |
3351
+ | Shared error-dispatch | Tasks 2–3 |
3352
+ | Lazy doc computation | Task 13 |
3353
+ | Mixed-kind factories work natively | Task 14 (mixed-kind dispatch test) |
3354
+ | `pathPrefix` global | Task 1 (path test cases) |
3355
+ | `extendProcedureDoc` discriminated union | Task 8 (types), Task 19 (port test) |
3356
+ | Per-factory `streamMode` override | Task 21 (stream test cases) |
3357
+ | `AbortSignal` injection preserved | Task 9 (rpc test) — applies to all handlers |
3358
+ | `event: 'return'` SSE return value | Tasks 11, 12 (stream + http-stream tests) |
3359
+ | `isPrevalidated` short-circuit | Task 11 (stream handler) |
3360
+ | `skippedProcedures` near-empty after convergence | Task 13 + Task 19 (retargeted test) |
3361
+ | Tests consolidated under `hono/` | Tasks 19–21 |
3362
+ | Templates/docs/CLAUDE.md/README.md updated | Tasks 24–27 |
3363
+ | Codegen e2e verification | Task 28 |
3364
+
3365
+ No placeholders, no TBDs. Code blocks are concrete. Each task ends in a commit.