@winspan/claude-forge 8.50.6 → 8.51.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 (361) hide show
  1. package/CLAUDE.md +7 -7
  2. package/dist/claudemd/claudemd-generator.d.ts.map +1 -1
  3. package/dist/claudemd/claudemd-generator.js +27 -237
  4. package/dist/claudemd/claudemd-generator.js.map +1 -1
  5. package/dist/claudemd/resume-manager.js +1 -1
  6. package/dist/claudemd/resume-manager.js.map +1 -1
  7. package/dist/claudemd/templates/swarm-protocol.md +222 -0
  8. package/dist/cli/commands/daemon.js +6 -6
  9. package/dist/cli/commands/daemon.js.map +1 -1
  10. package/dist/cli/commands/executions.d.ts.map +1 -1
  11. package/dist/cli/commands/executions.js +4 -3
  12. package/dist/cli/commands/executions.js.map +1 -1
  13. package/dist/cli/commands/init.js +2 -2
  14. package/dist/cli/commands/init.js.map +1 -1
  15. package/dist/cli/commands/logs.js.map +1 -1
  16. package/dist/cli/commands/mcp.d.ts.map +1 -1
  17. package/dist/cli/commands/mcp.js +3 -5
  18. package/dist/cli/commands/mcp.js.map +1 -1
  19. package/dist/cli/commands/menu.d.ts.map +1 -1
  20. package/dist/cli/commands/menu.js +4 -3
  21. package/dist/cli/commands/menu.js.map +1 -1
  22. package/dist/cli/commands/stats.d.ts.map +1 -1
  23. package/dist/cli/commands/stats.js +2 -3
  24. package/dist/cli/commands/stats.js.map +1 -1
  25. package/dist/cli/commands/status.js +2 -2
  26. package/dist/cli/commands/status.js.map +1 -1
  27. package/dist/cli/commands/trace.d.ts.map +1 -1
  28. package/dist/cli/commands/trace.js +11 -23
  29. package/dist/cli/commands/trace.js.map +1 -1
  30. package/dist/cli/init/hook-manager.d.ts.map +1 -1
  31. package/dist/cli/init/hook-manager.js +2 -2
  32. package/dist/cli/init/hook-manager.js.map +1 -1
  33. package/dist/core/ai/provider.js +2 -2
  34. package/dist/core/ai/provider.js.map +1 -1
  35. package/dist/core/constants.d.ts +12 -1
  36. package/dist/core/constants.d.ts.map +1 -1
  37. package/dist/core/constants.js +15 -1
  38. package/dist/core/constants.js.map +1 -1
  39. package/dist/core/event-fields.d.ts +16 -0
  40. package/dist/core/event-fields.d.ts.map +1 -0
  41. package/dist/core/event-fields.js +19 -0
  42. package/dist/core/event-fields.js.map +1 -0
  43. package/dist/core/queue/index.d.ts.map +1 -1
  44. package/dist/core/queue/index.js +3 -4
  45. package/dist/core/queue/index.js.map +1 -1
  46. package/dist/core/storage/base.d.ts +36 -3
  47. package/dist/core/storage/base.d.ts.map +1 -1
  48. package/dist/core/storage/base.js +101 -58
  49. package/dist/core/storage/base.js.map +1 -1
  50. package/dist/core/storage/events.d.ts +92 -3
  51. package/dist/core/storage/events.d.ts.map +1 -1
  52. package/dist/core/storage/events.js +147 -0
  53. package/dist/core/storage/events.js.map +1 -1
  54. package/dist/core/storage/routing.d.ts +54 -1
  55. package/dist/core/storage/routing.d.ts.map +1 -1
  56. package/dist/core/storage/routing.js +99 -1
  57. package/dist/core/storage/routing.js.map +1 -1
  58. package/dist/core/storage/schema.sql +12 -2
  59. package/dist/core/storage/sessions.d.ts +20 -0
  60. package/dist/core/storage/sessions.d.ts.map +1 -1
  61. package/dist/core/storage/sessions.js +59 -0
  62. package/dist/core/storage/sessions.js.map +1 -1
  63. package/dist/core/storage/skills.d.ts +23 -0
  64. package/dist/core/storage/skills.d.ts.map +1 -1
  65. package/dist/core/storage/skills.js +47 -0
  66. package/dist/core/storage/skills.js.map +1 -1
  67. package/dist/core/storage/sqlite.d.ts +35 -2
  68. package/dist/core/storage/sqlite.d.ts.map +1 -1
  69. package/dist/core/storage/sqlite.js +93 -4
  70. package/dist/core/storage/sqlite.js.map +1 -1
  71. package/dist/core/storage/tasks.d.ts +49 -0
  72. package/dist/core/storage/tasks.d.ts.map +1 -1
  73. package/dist/core/storage/tasks.js +143 -1
  74. package/dist/core/storage/tasks.js.map +1 -1
  75. package/dist/core/storage/token-usage.d.ts +1 -1
  76. package/dist/core/storage/token-usage.d.ts.map +1 -1
  77. package/dist/core/storage/token-usage.js +1 -1
  78. package/dist/core/storage/token-usage.js.map +1 -1
  79. package/dist/core/types.d.ts +24 -3
  80. package/dist/core/types.d.ts.map +1 -1
  81. package/dist/core/types.js.map +1 -1
  82. package/dist/core/utils/error-handler.d.ts.map +1 -1
  83. package/dist/core/utils/error-handler.js +3 -2
  84. package/dist/core/utils/error-handler.js.map +1 -1
  85. package/dist/core/utils/git.d.ts +10 -0
  86. package/dist/core/utils/git.d.ts.map +1 -0
  87. package/dist/core/utils/git.js +24 -0
  88. package/dist/core/utils/git.js.map +1 -0
  89. package/dist/core/utils/logger.d.ts.map +1 -1
  90. package/dist/core/utils/logger.js +15 -1
  91. package/dist/core/utils/logger.js.map +1 -1
  92. package/dist/core/utils/lru-cache.d.ts +1 -0
  93. package/dist/core/utils/lru-cache.d.ts.map +1 -1
  94. package/dist/core/utils/lru-cache.js +3 -0
  95. package/dist/core/utils/lru-cache.js.map +1 -1
  96. package/dist/core/utils/token-tracker.js +1 -1
  97. package/dist/core/utils/token-tracker.js.map +1 -1
  98. package/dist/daemon/event-parser.d.ts.map +1 -1
  99. package/dist/daemon/event-parser.js +2 -1
  100. package/dist/daemon/event-parser.js.map +1 -1
  101. package/dist/daemon/handlers/history-exporter.js.map +1 -1
  102. package/dist/daemon/handlers/post-tool-use.d.ts.map +1 -1
  103. package/dist/daemon/handlers/post-tool-use.js +7 -3
  104. package/dist/daemon/handlers/post-tool-use.js.map +1 -1
  105. package/dist/daemon/handlers/stop.d.ts +4 -0
  106. package/dist/daemon/handlers/stop.d.ts.map +1 -1
  107. package/dist/daemon/handlers/stop.js +23 -35
  108. package/dist/daemon/handlers/stop.js.map +1 -1
  109. package/dist/daemon/handlers/user-prompt.d.ts +3 -3
  110. package/dist/daemon/handlers/user-prompt.d.ts.map +1 -1
  111. package/dist/daemon/handlers/user-prompt.js +12 -22
  112. package/dist/daemon/handlers/user-prompt.js.map +1 -1
  113. package/dist/daemon/index.d.ts.map +1 -1
  114. package/dist/daemon/index.js +23 -9
  115. package/dist/daemon/index.js.map +1 -1
  116. package/dist/daemon/lifecycle.js +3 -4
  117. package/dist/daemon/lifecycle.js.map +1 -1
  118. package/dist/daemon/server.d.ts +6 -4
  119. package/dist/daemon/server.d.ts.map +1 -1
  120. package/dist/daemon/server.js +76 -85
  121. package/dist/daemon/server.js.map +1 -1
  122. package/dist/daemon/services/task-segmenter.js +1 -1
  123. package/dist/daemon/services/task-segmenter.js.map +1 -1
  124. package/dist/hooks/hook-lib.sh +37 -0
  125. package/dist/hooks/notification.sh +2 -2
  126. package/dist/hooks/post-tool-use.sh +2 -2
  127. package/dist/hooks/pre-tool-use.sh +2 -2
  128. package/dist/hooks/stop.sh +9 -6
  129. package/dist/hooks/user-prompt-submit.sh +2 -2
  130. package/dist/{daemon/services → web/analytics}/anti-pattern-detector.d.ts +3 -4
  131. package/dist/web/analytics/anti-pattern-detector.d.ts.map +1 -0
  132. package/dist/{daemon/services → web/analytics}/anti-pattern-detector.js +7 -46
  133. package/dist/{daemon/services → web/analytics}/anti-pattern-detector.js.map +1 -1
  134. package/dist/web/analytics/drift-detector.d.ts.map +1 -0
  135. package/dist/{daemon/services → web/analytics}/drift-detector.js +10 -13
  136. package/dist/web/analytics/drift-detector.js.map +1 -0
  137. package/dist/web/analytics/weekly-report.d.ts.map +1 -0
  138. package/dist/{daemon/services → web/analytics}/weekly-report.js +51 -50
  139. package/dist/web/analytics/weekly-report.js.map +1 -0
  140. package/dist/web/auth-middleware.d.ts.map +1 -1
  141. package/dist/web/auth-middleware.js +1 -2
  142. package/dist/web/auth-middleware.js.map +1 -1
  143. package/dist/web/routes/_helpers.d.ts +16 -0
  144. package/dist/web/routes/_helpers.d.ts.map +1 -0
  145. package/dist/web/routes/_helpers.js +32 -0
  146. package/dist/web/routes/_helpers.js.map +1 -0
  147. package/dist/web/routes/drift.js +1 -1
  148. package/dist/web/routes/drift.js.map +1 -1
  149. package/dist/web/routes/insights.js +1 -1
  150. package/dist/web/routes/insights.js.map +1 -1
  151. package/dist/web/routes/reports.js +1 -1
  152. package/dist/web/routes/reports.js.map +1 -1
  153. package/dist/web/routes/rules.d.ts +3 -0
  154. package/dist/web/routes/rules.d.ts.map +1 -1
  155. package/dist/web/routes/rules.js +28 -52
  156. package/dist/web/routes/rules.js.map +1 -1
  157. package/dist/web/routes/sessions.d.ts.map +1 -1
  158. package/dist/web/routes/sessions.js +16 -30
  159. package/dist/web/routes/sessions.js.map +1 -1
  160. package/dist/web/routes/skill-stats.d.ts +2 -0
  161. package/dist/web/routes/skill-stats.d.ts.map +1 -1
  162. package/dist/web/routes/skill-stats.js +28 -64
  163. package/dist/web/routes/skill-stats.js.map +1 -1
  164. package/dist/web/routes/skills.d.ts.map +1 -1
  165. package/dist/web/routes/skills.js +5 -4
  166. package/dist/web/routes/skills.js.map +1 -1
  167. package/dist/web/routes/stats.d.ts +4 -0
  168. package/dist/web/routes/stats.d.ts.map +1 -1
  169. package/dist/web/routes/stats.js +19 -21
  170. package/dist/web/routes/stats.js.map +1 -1
  171. package/dist/web/routes/tasks.d.ts.map +1 -1
  172. package/dist/web/routes/tasks.js +17 -42
  173. package/dist/web/routes/tasks.js.map +1 -1
  174. package/dist/web/routes/trace.d.ts.map +1 -1
  175. package/dist/web/routes/trace.js +7 -17
  176. package/dist/web/routes/trace.js.map +1 -1
  177. package/dist/web/routes/types.d.ts.map +1 -1
  178. package/dist/web/routes/types.js +4 -3
  179. package/dist/web/routes/types.js.map +1 -1
  180. package/dist/web/static/assets/{AIConfig-BQCAQE9D.js → AIConfig-CdDWzJyO.js} +2 -2
  181. package/dist/web/static/assets/{AIConfig-BQCAQE9D.js.map → AIConfig-CdDWzJyO.js.map} +1 -1
  182. package/dist/web/static/assets/{Dashboard-D7Bo6Kan.js → Dashboard-CoEmmIDt.js} +2 -2
  183. package/dist/web/static/assets/{Dashboard-D7Bo6Kan.js.map → Dashboard-CoEmmIDt.js.map} +1 -1
  184. package/dist/web/static/assets/{Drawer-BeHRQxUS.js → Drawer-DdRTzlLB.js} +2 -2
  185. package/dist/web/static/assets/{Drawer-BeHRQxUS.js.map → Drawer-DdRTzlLB.js.map} +1 -1
  186. package/dist/web/static/assets/{Events-K_tCY2ti.js → Events-DrIq1SUS.js} +2 -2
  187. package/dist/web/static/assets/{Events-K_tCY2ti.js.map → Events-DrIq1SUS.js.map} +1 -1
  188. package/dist/web/static/assets/{Reports-BJCmBnc_.js → Reports-DFBM3MDK.js} +2 -2
  189. package/dist/web/static/assets/{Reports-BJCmBnc_.js.map → Reports-DFBM3MDK.js.map} +1 -1
  190. package/dist/web/static/assets/{SearchInput-BX2KhMkw.js → SearchInput-qCj_jAcf.js} +2 -2
  191. package/dist/web/static/assets/{SearchInput-BX2KhMkw.js.map → SearchInput-qCj_jAcf.js.map} +1 -1
  192. package/dist/web/static/assets/{SessionDetail-Bkr-kC7V.js → SessionDetail-CCzwdoT7.js} +2 -2
  193. package/dist/web/static/assets/{SessionDetail-Bkr-kC7V.js.map → SessionDetail-CCzwdoT7.js.map} +1 -1
  194. package/dist/web/static/assets/{Sessions-Chx9OCLH.js → Sessions-FfLYkAw9.js} +2 -2
  195. package/dist/web/static/assets/{Sessions-Chx9OCLH.js.map → Sessions-FfLYkAw9.js.map} +1 -1
  196. package/dist/web/static/assets/{Skills-O0GT1i7m.js → Skills-C8Gvs3Qa.js} +2 -2
  197. package/dist/web/static/assets/{Skills-O0GT1i7m.js.map → Skills-C8Gvs3Qa.js.map} +1 -1
  198. package/dist/web/static/assets/TaskDetail-BS8pYhaR.js +2 -0
  199. package/dist/web/static/assets/TaskDetail-BS8pYhaR.js.map +1 -0
  200. package/dist/web/static/assets/Tasks-CyuhizG8.js +2 -0
  201. package/dist/web/static/assets/Tasks-CyuhizG8.js.map +1 -0
  202. package/dist/web/static/assets/index-CBX47X8l.js +3 -0
  203. package/dist/web/static/assets/{index-DxIbmNmr.js.map → index-CBX47X8l.js.map} +1 -1
  204. package/dist/web/static/assets/index-DjIoMdoR.css +1 -0
  205. package/dist/web/static/assets/{lucide-fJlPI3H7.js → lucide-Bs_edTLa.js} +44 -39
  206. package/dist/web/static/assets/lucide-Bs_edTLa.js.map +1 -0
  207. package/dist/web/static/assets/react-router-r79dBVy4.js +20 -0
  208. package/dist/web/static/assets/{react-router-I-HqunH7.js.map → react-router-r79dBVy4.js.map} +1 -1
  209. package/dist/web/static/assets/task-title-BhOcemuR.js +2 -0
  210. package/dist/web/static/assets/task-title-BhOcemuR.js.map +1 -0
  211. package/dist/web/static/index.html +4 -4
  212. package/docs/design/h1-storage-aggregation-spec-20260518-1121.md +299 -0
  213. package/docs/design/h2-getdatabase-encapsulation-spec-20260518-1450.md +191 -0
  214. package/docs/design/h3-fallback-removal-spec-20260518-1245.md +76 -0
  215. package/docs/design/h4-index-dedup-spec-20260518-1230.md +109 -0
  216. package/docs/design/h6-services-migration-spec-20260518-1355.md +82 -0
  217. package/docs/design/l1-swarm-protocol-extract-spec-20260518-1605.md +106 -0
  218. package/docs/design/m10-forge-paths-spec-20260518-1320.md +121 -0
  219. package/docs/design/m2-m3-tool-input-spec-20260518-1425.md +131 -0
  220. package/docs/design/m7-routing-event-association-spec-20260518-1545.md +103 -0
  221. package/docs/design/project-path-gitroot-spec-20260518-1715.md +134 -0
  222. package/docs/design/task-active-gc-spec-20260518-1745.md +146 -0
  223. package/docs/implementation/h1-storage-aggregation-changelog-20260518-1121.md +82 -0
  224. package/docs/implementation/h2-final-changelog-20260518-1530.md +61 -0
  225. package/docs/implementation/h2-phase1-safety-net-changelog-20260518-1450.md +70 -0
  226. package/docs/implementation/h2-phase2-operations-changelog-20260518-1450.md +120 -0
  227. package/docs/implementation/h2-phase3-callsites-changelog-20260518-1450.md +71 -0
  228. package/docs/implementation/h3-fallback-removal-changelog-20260518-1245.md +71 -0
  229. package/docs/implementation/h4-index-dedup-changelog-20260518-1230.md +60 -0
  230. package/docs/implementation/h6-services-migration-changelog-20260518-1355.md +46 -0
  231. package/docs/implementation/h7-m9-defaults-changelog-20260518-1300.md +46 -0
  232. package/docs/implementation/l1-swarm-protocol-extract-changelog-20260518-1605.md +45 -0
  233. package/docs/implementation/l3-l4-daemon-perf-changelog-20260518-1410.md +63 -0
  234. package/docs/implementation/l6-l8-final-cleanup-changelog-20260518-1640.md +38 -0
  235. package/docs/implementation/m1-m4-m5-l7-cleanup-changelog-20260518-1310.md +58 -0
  236. package/docs/implementation/m10-forge-paths-changelog-20260518-1320.md +60 -0
  237. package/docs/implementation/m2-m3-tool-input-changelog-20260518-1425.md +43 -0
  238. package/docs/implementation/m6-m8-naming-shutdown-changelog-20260518-1340.md +56 -0
  239. package/docs/implementation/m7-routing-association-changelog-20260518-1545.md +69 -0
  240. package/docs/implementation/project-path-gitroot-changelog-20260518-1715.md +63 -0
  241. package/docs/implementation/task-active-gc-changelog-20260518-1745.md +35 -0
  242. package/docs/implementation/task-title-summary-changelog-20260518-1130.md +39 -0
  243. package/docs/implementation/tasks-detail-back-loses-filters-changelog-20260518-1100.md +22 -0
  244. package/docs/implementation/tasks-page-white-screen-hotfix-changelog-20260518-1015.md +56 -0
  245. package/docs/reviews/task-title-summary.md +92 -0
  246. package/docs/reviews/tasks-detail-back-loses-filters.md +58 -0
  247. package/docs/reviews/tasks-page-white-screen-hotfix.md +126 -0
  248. package/package.json +2 -2
  249. package/src/claudemd/claudemd-generator.ts +29 -238
  250. package/src/claudemd/resume-manager.ts +1 -1
  251. package/src/claudemd/templates/swarm-protocol.md +222 -0
  252. package/src/cli/commands/daemon.ts +6 -6
  253. package/src/cli/commands/executions.ts +4 -3
  254. package/src/cli/commands/init.ts +2 -2
  255. package/src/cli/commands/logs.ts +1 -1
  256. package/src/cli/commands/mcp.ts +3 -5
  257. package/src/cli/commands/menu.ts +4 -3
  258. package/src/cli/commands/stats.ts +2 -3
  259. package/src/cli/commands/status.ts +2 -2
  260. package/src/cli/commands/trace.ts +10 -26
  261. package/src/cli/init/hook-manager.ts +2 -2
  262. package/src/core/ai/provider.ts +2 -2
  263. package/src/core/constants.ts +18 -1
  264. package/src/core/event-fields.ts +32 -0
  265. package/src/core/queue/index.ts +3 -4
  266. package/src/core/storage/base.ts +132 -56
  267. package/src/core/storage/events.ts +183 -4
  268. package/src/core/storage/routing.ts +129 -1
  269. package/src/core/storage/schema.sql +12 -2
  270. package/src/core/storage/sessions.ts +64 -0
  271. package/src/core/storage/skills.ts +69 -0
  272. package/src/core/storage/sqlite.ts +103 -4
  273. package/src/core/storage/tasks.ts +149 -1
  274. package/src/core/storage/token-usage.ts +1 -1
  275. package/src/core/types.ts +30 -3
  276. package/src/core/utils/error-handler.ts +3 -2
  277. package/src/core/utils/git.ts +23 -0
  278. package/src/core/utils/logger.ts +16 -1
  279. package/src/core/utils/lru-cache.ts +4 -0
  280. package/src/core/utils/token-tracker.ts +1 -1
  281. package/src/daemon/event-parser.ts +4 -3
  282. package/src/daemon/handlers/history-exporter.ts +1 -1
  283. package/src/daemon/handlers/post-tool-use.ts +7 -3
  284. package/src/daemon/handlers/stop.ts +32 -39
  285. package/src/daemon/handlers/user-prompt.ts +12 -22
  286. package/src/daemon/index.ts +24 -10
  287. package/src/daemon/lifecycle.ts +3 -3
  288. package/src/daemon/server.ts +76 -89
  289. package/src/daemon/services/task-segmenter.ts +1 -1
  290. package/src/hooks/hook-lib.sh +37 -0
  291. package/src/hooks/notification.sh +2 -2
  292. package/src/hooks/post-tool-use.sh +2 -2
  293. package/src/hooks/pre-tool-use.sh +2 -2
  294. package/src/hooks/stop.sh +9 -6
  295. package/src/hooks/user-prompt-submit.sh +2 -2
  296. package/src/{daemon/services → web/analytics}/anti-pattern-detector.ts +9 -54
  297. package/src/{daemon/services → web/analytics}/drift-detector.ts +10 -23
  298. package/src/{daemon/services → web/analytics}/weekly-report.ts +52 -75
  299. package/src/web/auth-middleware.ts +1 -2
  300. package/src/web/routes/_helpers.ts +34 -0
  301. package/src/web/routes/drift.ts +1 -1
  302. package/src/web/routes/insights.ts +1 -1
  303. package/src/web/routes/reports.ts +1 -1
  304. package/src/web/routes/rules.ts +31 -56
  305. package/src/web/routes/sessions.ts +18 -30
  306. package/src/web/routes/skill-stats.ts +29 -69
  307. package/src/web/routes/skills.ts +5 -4
  308. package/src/web/routes/stats.ts +19 -29
  309. package/src/web/routes/tasks.ts +17 -42
  310. package/src/web/routes/trace.ts +7 -19
  311. package/src/web/routes/types.ts +4 -3
  312. package/tests/integration/claudemd-generator.test.ts +90 -0
  313. package/tests/integration/web-analytics.integration.test.ts +133 -0
  314. package/tests/integration/web-stats.integration.test.ts +135 -0
  315. package/tests/integration/web-trace.integration.test.ts +175 -0
  316. package/tests/unit/core/forge-paths.test.ts +99 -0
  317. package/tests/unit/daemon/post-tool-use.test.ts +121 -0
  318. package/tests/unit/daemon/stop-handler-behavior-summary.test.ts +202 -0
  319. package/tests/unit/daemon/task-segmenter-recover.test.ts +84 -0
  320. package/tests/unit/event-fields.test.ts +88 -0
  321. package/tests/unit/event-parser.test.ts +55 -0
  322. package/tests/unit/hooks/resolve-project-path.test.ts +122 -0
  323. package/tests/unit/socket-server.test.ts +183 -0
  324. package/tests/unit/storage/event-operations-aggregates.test.ts +342 -0
  325. package/tests/unit/storage/migration-idempotent.test.ts +304 -0
  326. package/tests/unit/storage/routing-aggregates.test.ts +276 -0
  327. package/tests/unit/storage/routing.test.ts +117 -0
  328. package/tests/unit/storage/schema-missing.test.ts +81 -0
  329. package/tests/unit/storage/session-operations-aggregates.test.ts +120 -0
  330. package/tests/unit/storage/skill-operations-counts.test.ts +106 -0
  331. package/tests/unit/storage/skills-aggregates.test.ts +104 -0
  332. package/tests/unit/storage/sqlite-refactor-harness.test.ts +3 -3
  333. package/tests/unit/storage/task-operations-counts.test.ts +46 -0
  334. package/tests/unit/storage/tasks-getById.test.ts +343 -0
  335. package/tests/unit/storage/tasks-stale-gc.test.ts +86 -0
  336. package/tests/unit/token-usage.test.ts +6 -6
  337. package/tests/unit/web/navigation-back-contract.test.ts +134 -0
  338. package/tests/unit/web/routes-rules.test.ts +182 -0
  339. package/tests/unit/web/routes-tasks.test.ts +34 -0
  340. package/tests/unit/web/task-title-contract.test.ts +210 -0
  341. package/tests/unit/web/tasks-component-contract.test.ts +179 -0
  342. package/vitest.config.ts +1 -1
  343. package/web/src/pages/TaskDetail.tsx +9 -5
  344. package/web/src/pages/Tasks.tsx +315 -50
  345. package/web/src/utils/navigation.ts +25 -0
  346. package/web/src/utils/task-title.ts +49 -0
  347. package/dist/daemon/services/anti-pattern-detector.d.ts.map +0 -1
  348. package/dist/daemon/services/drift-detector.d.ts.map +0 -1
  349. package/dist/daemon/services/drift-detector.js.map +0 -1
  350. package/dist/daemon/services/weekly-report.d.ts.map +0 -1
  351. package/dist/daemon/services/weekly-report.js.map +0 -1
  352. package/dist/web/static/assets/TaskDetail-5SR8zGzv.js +0 -2
  353. package/dist/web/static/assets/TaskDetail-5SR8zGzv.js.map +0 -1
  354. package/dist/web/static/assets/Tasks-DCgDqvOZ.js +0 -2
  355. package/dist/web/static/assets/Tasks-DCgDqvOZ.js.map +0 -1
  356. package/dist/web/static/assets/index-D8AKj26b.css +0 -1
  357. package/dist/web/static/assets/index-DxIbmNmr.js +0 -3
  358. package/dist/web/static/assets/lucide-fJlPI3H7.js.map +0 -1
  359. package/dist/web/static/assets/react-router-I-HqunH7.js +0 -20
  360. /package/dist/{daemon/services → web/analytics}/drift-detector.d.ts +0 -0
  361. /package/dist/{daemon/services → web/analytics}/weekly-report.d.ts +0 -0
@@ -0,0 +1,183 @@
1
+ /**
2
+ * SocketServer behavior tests
3
+ *
4
+ * 重点验证 L3 性能修复:buffer 分片到达时,仅在遇到换行符后才解析,
5
+ * 避免大事件每个 chunk 都跑一次完整 JSON.parse 的 N² 行为。
6
+ */
7
+ import { describe, it, expect, afterEach, vi } from 'vitest';
8
+ import net from 'node:net';
9
+ import os from 'node:os';
10
+ import path from 'node:path';
11
+ import fs from 'node:fs';
12
+ import { SocketServer } from '../../src/daemon/server.js';
13
+ import { EventParser } from '../../src/daemon/event-parser.js';
14
+
15
+ function makeSockPath(): string {
16
+ const name = `forge-test-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}.sock`;
17
+ return path.join(os.tmpdir(), name);
18
+ }
19
+
20
+ function makeEvent(overrides: Record<string, unknown> = {}): Record<string, unknown> {
21
+ return {
22
+ hook_type: 'PostToolUse',
23
+ timestamp: new Date().toISOString(),
24
+ session_id: 'test-session',
25
+ project_path: '/tmp/test',
26
+ tool_name: 'Read',
27
+ ...overrides,
28
+ };
29
+ }
30
+
31
+ function sendOnSocket(sockPath: string, chunks: string[], delayMs = 5): Promise<string> {
32
+ return new Promise((resolve, reject) => {
33
+ const client = net.createConnection(sockPath);
34
+ let response = '';
35
+ client.on('connect', async () => {
36
+ for (const chunk of chunks) {
37
+ client.write(chunk);
38
+ if (delayMs > 0) await new Promise((r) => setTimeout(r, delayMs));
39
+ }
40
+ });
41
+ client.on('data', (data) => {
42
+ response += data.toString();
43
+ });
44
+ client.on('end', () => resolve(response));
45
+ client.on('close', () => resolve(response));
46
+ client.on('error', reject);
47
+ });
48
+ }
49
+
50
+ describe('SocketServer — L3 chunked-parse behavior', () => {
51
+ const servers: SocketServer[] = [];
52
+ const sockPaths: string[] = [];
53
+
54
+ afterEach(async () => {
55
+ for (const s of servers) await s.close();
56
+ servers.length = 0;
57
+ for (const p of sockPaths) {
58
+ try { fs.unlinkSync(p); } catch { /* ignore */ }
59
+ }
60
+ sockPaths.length = 0;
61
+ });
62
+
63
+ it('parses a complete single-chunk message (with trailing newline)', async () => {
64
+ const sockPath = makeSockPath();
65
+ sockPaths.push(sockPath);
66
+
67
+ const handler = vi.fn().mockReturnValue({ allow: true });
68
+ const server = new SocketServer(sockPath, handler);
69
+ servers.push(server);
70
+ await new Promise((r) => setTimeout(r, 30));
71
+
72
+ const event = makeEvent();
73
+ const payload = JSON.stringify(event) + '\n';
74
+ await sendOnSocket(sockPath, [payload]);
75
+
76
+ expect(handler).toHaveBeenCalledTimes(1);
77
+ const passed = handler.mock.calls[0][0];
78
+ expect(passed.hook_type).toBe('PostToolUse');
79
+ expect(passed.session_id).toBe('test-session');
80
+ });
81
+
82
+ it('parses a large message split across many chunks WITHOUT per-chunk JSON.parse', async () => {
83
+ const sockPath = makeSockPath();
84
+ sockPaths.push(sockPath);
85
+
86
+ // 监视 JSON.parse —— 用于断言不再 N²(不能为每个 chunk 都 parse 一次)
87
+ const parseSpy = vi.spyOn(JSON, 'parse');
88
+
89
+ const handler = vi.fn().mockReturnValue({ allow: true });
90
+ const server = new SocketServer(sockPath, handler);
91
+ servers.push(server);
92
+ await new Promise((r) => setTimeout(r, 30));
93
+
94
+ // 构造大事件(约 50KB tool_output),分成 ~20 个 chunk 喂入
95
+ const bigOutput = 'x'.repeat(50_000);
96
+ const event = makeEvent({ tool_output: { data: bigOutput } });
97
+ const payload = JSON.stringify(event) + '\n';
98
+ const chunkSize = Math.ceil(payload.length / 20);
99
+ const chunks: string[] = [];
100
+ for (let i = 0; i < payload.length; i += chunkSize) {
101
+ chunks.push(payload.slice(i, i + chunkSize));
102
+ }
103
+ expect(chunks.length).toBeGreaterThan(10);
104
+
105
+ const callsBefore = parseSpy.mock.calls.length;
106
+ await sendOnSocket(sockPath, chunks, 2);
107
+ const callsAfter = parseSpy.mock.calls.length;
108
+
109
+ // 关键断言:handler 必须收到完整事件
110
+ expect(handler).toHaveBeenCalledTimes(1);
111
+
112
+ // 关键断言:JSON.parse 调用次数远少于 chunk 数(旧代码会 ≈ chunks.length 次)
113
+ // 现在应该只有:可能 1 次(无 auth 时)或 2 次(有 auth 时——auth 检查 + EventParser)
114
+ const parseDelta = callsAfter - callsBefore;
115
+ expect(parseDelta).toBeLessThanOrEqual(3);
116
+ expect(parseDelta).toBeLessThan(chunks.length); // 强对比旧 N² 行为
117
+
118
+ parseSpy.mockRestore();
119
+ });
120
+
121
+ it('rejects oversized buffer that never sees a newline', async () => {
122
+ const sockPath = makeSockPath();
123
+ sockPaths.push(sockPath);
124
+
125
+ const handler = vi.fn();
126
+ const server = new SocketServer(sockPath, handler);
127
+ servers.push(server);
128
+ await new Promise((r) => setTimeout(r, 30));
129
+
130
+ // 600KB 无换行 —— 必须超过 MAX_BUFFER_SIZE (512KB) 并被丢弃
131
+ // 注意:服务端 destroy 后客户端继续 write 会触发 EPIPE,要容错
132
+ const oversized = 'a'.repeat(600 * 1024);
133
+ await new Promise<void>((resolve) => {
134
+ const client = net.createConnection(sockPath);
135
+ client.on('error', () => { /* EPIPE 预期 */ });
136
+ client.on('close', () => resolve());
137
+ client.on('connect', () => {
138
+ client.write(oversized, () => {
139
+ // 给服务端处理时间
140
+ setTimeout(() => client.destroy(), 80);
141
+ });
142
+ });
143
+ });
144
+
145
+ expect(handler).not.toHaveBeenCalled();
146
+ });
147
+
148
+ it('uses fallback parse on socket end() for messages without trailing newline', async () => {
149
+ const sockPath = makeSockPath();
150
+ sockPaths.push(sockPath);
151
+
152
+ const handler = vi.fn().mockReturnValue({ allow: true });
153
+ const server = new SocketServer(sockPath, handler);
154
+ servers.push(server);
155
+ await new Promise((r) => setTimeout(r, 30));
156
+
157
+ // 无换行结尾 —— 走 socket.end() 兜底
158
+ const event = makeEvent();
159
+ const payload = JSON.stringify(event);
160
+ await new Promise<void>((resolve, reject) => {
161
+ const client = net.createConnection(sockPath);
162
+ client.on('connect', () => {
163
+ client.end(payload); // 写完即关闭:触发 server 的 'end' 事件
164
+ });
165
+ client.on('close', () => resolve());
166
+ client.on('error', reject);
167
+ });
168
+ // 等 server 端处理完
169
+ await new Promise((r) => setTimeout(r, 50));
170
+
171
+ expect(handler).toHaveBeenCalledTimes(1);
172
+ });
173
+ });
174
+
175
+ describe('EventParser — sanity', () => {
176
+ it('parses a valid event JSON', () => {
177
+ const parser = new EventParser();
178
+ const raw = JSON.stringify(makeEvent());
179
+ const out = parser.parse(raw);
180
+ expect(out.session_id).toBe('test-session');
181
+ expect(out.hook_type).toBe('PostToolUse');
182
+ });
183
+ });
@@ -0,0 +1,342 @@
1
+ /**
2
+ * H2: EventOperations 新增 aggregate / count / query 方法测试
3
+ *
4
+ * 覆盖 11 个方法:
5
+ * countAllEvents
6
+ * aggregateToolUsage(含 hook_type 过滤)
7
+ * aggregateDailyEventCounts
8
+ * aggregateHookTypeBySession
9
+ * aggregateAgentTypeBySession
10
+ * aggregateToolUsageBySession
11
+ * countActiveDays
12
+ * aggregateOverviewByRange
13
+ * queryDistinctProjects
14
+ * aggregateToolFailureRate
15
+ * queryFileEditInputs
16
+ * queryEventsByTimeRange
17
+ */
18
+
19
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
20
+ import { mkdtempSync, rmSync } from 'node:fs';
21
+ import { tmpdir } from 'node:os';
22
+ import { join } from 'node:path';
23
+ import { SQLiteStorage } from '../../../src/core/storage/sqlite.js';
24
+ import type { ForgeEvent } from '../../../src/core/types.js';
25
+
26
+ describe('EventOperations H2 aggregates', () => {
27
+ let tmp: string;
28
+ let storage: SQLiteStorage;
29
+
30
+ beforeEach(() => {
31
+ tmp = mkdtempSync(join(tmpdir(), 'forge-h2-events-'));
32
+ storage = new SQLiteStorage(join(tmp, 'data.db'));
33
+ });
34
+
35
+ afterEach(() => {
36
+ try { storage.close(); } catch { /* ignore */ }
37
+ rmSync(tmp, { recursive: true, force: true });
38
+ });
39
+
40
+ function makeEvent(overrides: Partial<ForgeEvent>): ForgeEvent {
41
+ return {
42
+ session_id: 's1',
43
+ project_path: '/tmp/proj',
44
+ timestamp: '2026-05-10T10:00:00.000Z',
45
+ hook_type: 'PreToolUse',
46
+ ...overrides,
47
+ };
48
+ }
49
+
50
+ // ── countAllEvents ─────────────────────────────────────────────────
51
+ describe('countAllEvents', () => {
52
+ it('空表返回 0', () => {
53
+ expect(storage.countAllEvents()).toBe(0);
54
+ });
55
+ it('多条事件返回总数', () => {
56
+ storage.writeEvent(makeEvent({ session_id: 'a' }));
57
+ storage.writeEvent(makeEvent({ session_id: 'b' }));
58
+ storage.writeEvent(makeEvent({ session_id: 'c' }));
59
+ expect(storage.countAllEvents()).toBe(3);
60
+ });
61
+ });
62
+
63
+ // ── aggregateToolUsage ─────────────────────────────────────────────
64
+ describe('aggregateToolUsage', () => {
65
+ it('空表返回空数组', () => {
66
+ expect(storage.aggregateToolUsage()).toEqual([]);
67
+ });
68
+ it('按 count 降序,跳过空 tool_name', () => {
69
+ storage.writeEvent(makeEvent({ tool_name: 'Bash' }));
70
+ storage.writeEvent(makeEvent({ tool_name: 'Bash' }));
71
+ storage.writeEvent(makeEvent({ tool_name: 'Read' }));
72
+ storage.writeEvent(makeEvent({ hook_type: 'UserPromptSubmit' })); // null tool_name
73
+ const r = storage.aggregateToolUsage();
74
+ expect(r).toEqual([
75
+ { tool_name: 'Bash', count: 2 },
76
+ { tool_name: 'Read', count: 1 },
77
+ ]);
78
+ });
79
+ it('hook_type 过滤只统计指定类型', () => {
80
+ storage.writeEvent(makeEvent({ tool_name: 'Bash', hook_type: 'PreToolUse' }));
81
+ storage.writeEvent(makeEvent({ tool_name: 'Bash', hook_type: 'PostToolUse' }));
82
+ storage.writeEvent(makeEvent({ tool_name: 'Read', hook_type: 'PreToolUse' }));
83
+ const r = storage.aggregateToolUsage({ hook_type: 'PreToolUse' });
84
+ expect(r.find(x => x.tool_name === 'Bash')?.count).toBe(1);
85
+ expect(r.find(x => x.tool_name === 'Read')?.count).toBe(1);
86
+ });
87
+ it('since 过滤排除早期事件', () => {
88
+ storage.writeEvent(makeEvent({ tool_name: 'Bash', timestamp: '2026-01-01T00:00:00.000Z' }));
89
+ storage.writeEvent(makeEvent({ tool_name: 'Bash', timestamp: '2026-05-10T00:00:00.000Z', session_id: 's2' }));
90
+ const r = storage.aggregateToolUsage({ since: '2026-03-01T00:00:00.000Z' });
91
+ expect(r).toEqual([{ tool_name: 'Bash', count: 1 }]);
92
+ });
93
+ });
94
+
95
+ // ── aggregateDailyEventCounts ──────────────────────────────────────
96
+ describe('aggregateDailyEventCounts', () => {
97
+ it('按 date 分组并升序', () => {
98
+ storage.writeEvent(makeEvent({ timestamp: '2026-05-10T10:00:00.000Z' }));
99
+ storage.writeEvent(makeEvent({ timestamp: '2026-05-10T15:00:00.000Z', session_id: 's2' }));
100
+ storage.writeEvent(makeEvent({ timestamp: '2026-05-11T10:00:00.000Z', session_id: 's3' }));
101
+ const r = storage.aggregateDailyEventCounts({ since: '2026-05-01T00:00:00.000Z' });
102
+ expect(r).toEqual([
103
+ { date: '2026-05-10', count: 2 },
104
+ { date: '2026-05-11', count: 1 },
105
+ ]);
106
+ });
107
+ it('until 过滤排除上界', () => {
108
+ storage.writeEvent(makeEvent({ timestamp: '2026-05-10T10:00:00.000Z' }));
109
+ storage.writeEvent(makeEvent({ timestamp: '2026-05-15T10:00:00.000Z', session_id: 's2' }));
110
+ const r = storage.aggregateDailyEventCounts({
111
+ since: '2026-05-01T00:00:00.000Z',
112
+ until: '2026-05-12T00:00:00.000Z',
113
+ });
114
+ expect(r).toEqual([{ date: '2026-05-10', count: 1 }]);
115
+ });
116
+ });
117
+
118
+ // ── aggregateHookTypeBySession ─────────────────────────────────────
119
+ describe('aggregateHookTypeBySession', () => {
120
+ it('空表返回空', () => {
121
+ expect(storage.aggregateHookTypeBySession('nope')).toEqual([]);
122
+ });
123
+ it('只统计指定 session 的 hook_type', () => {
124
+ storage.writeEvent(makeEvent({ session_id: 'a', hook_type: 'PreToolUse' }));
125
+ storage.writeEvent(makeEvent({ session_id: 'a', hook_type: 'PostToolUse' }));
126
+ storage.writeEvent(makeEvent({ session_id: 'a', hook_type: 'PreToolUse' }));
127
+ storage.writeEvent(makeEvent({ session_id: 'b', hook_type: 'PreToolUse' }));
128
+ const r = storage.aggregateHookTypeBySession('a');
129
+ const map = Object.fromEntries(r.map(x => [x.hook_type, x.count]));
130
+ expect(map).toEqual({ PreToolUse: 2, PostToolUse: 1 });
131
+ });
132
+ });
133
+
134
+ // ── aggregateAgentTypeBySession ────────────────────────────────────
135
+ describe('aggregateAgentTypeBySession', () => {
136
+ it('json_extract subagent_type 并分组', () => {
137
+ storage.writeEvent(makeEvent({
138
+ session_id: 'a',
139
+ tool_name: 'Task',
140
+ tool_input: { subagent_type: 'researcher' },
141
+ }));
142
+ storage.writeEvent(makeEvent({
143
+ session_id: 'a',
144
+ tool_name: 'Task',
145
+ tool_input: { subagent_type: 'researcher' },
146
+ }));
147
+ storage.writeEvent(makeEvent({
148
+ session_id: 'a',
149
+ tool_name: 'Agent',
150
+ tool_input: { subagent_type: 'coder' },
151
+ }));
152
+ // 非 Agent/Task 不应被纳入
153
+ storage.writeEvent(makeEvent({
154
+ session_id: 'a',
155
+ tool_name: 'Bash',
156
+ tool_input: { foo: 'bar' },
157
+ }));
158
+ const r = storage.aggregateAgentTypeBySession('a');
159
+ const map = Object.fromEntries(r.map(x => [x.agent_type, x.count]));
160
+ expect(map.researcher).toBe(2);
161
+ expect(map.coder).toBe(1);
162
+ });
163
+ });
164
+
165
+ // ── aggregateToolUsageBySession ────────────────────────────────────
166
+ describe('aggregateToolUsageBySession', () => {
167
+ it('按 session_id 统计 tool_name', () => {
168
+ storage.writeEvent(makeEvent({ session_id: 'a', tool_name: 'Bash' }));
169
+ storage.writeEvent(makeEvent({ session_id: 'a', tool_name: 'Bash' }));
170
+ storage.writeEvent(makeEvent({ session_id: 'a', tool_name: 'Read' }));
171
+ storage.writeEvent(makeEvent({ session_id: 'b', tool_name: 'Bash' }));
172
+ const r = storage.aggregateToolUsageBySession('a');
173
+ expect(r).toEqual([
174
+ { tool_name: 'Bash', count: 2 },
175
+ { tool_name: 'Read', count: 1 },
176
+ ]);
177
+ });
178
+ });
179
+
180
+ // ── countActiveDays ────────────────────────────────────────────────
181
+ describe('countActiveDays', () => {
182
+ it('distinct date 计数', () => {
183
+ storage.writeEvent(makeEvent({ timestamp: '2026-05-10T10:00:00.000Z' }));
184
+ storage.writeEvent(makeEvent({ timestamp: '2026-05-10T11:00:00.000Z', session_id: 's2' }));
185
+ storage.writeEvent(makeEvent({ timestamp: '2026-05-11T10:00:00.000Z', session_id: 's3' }));
186
+ storage.writeEvent(makeEvent({ timestamp: '2026-05-12T10:00:00.000Z', session_id: 's4' }));
187
+ expect(storage.countActiveDays({ since: '2026-05-01T00:00:00.000Z' })).toBe(3);
188
+ });
189
+ it('空范围返回 0', () => {
190
+ expect(storage.countActiveDays({ since: '2026-05-01T00:00:00.000Z' })).toBe(0);
191
+ });
192
+ });
193
+
194
+ // ── aggregateOverviewByRange ───────────────────────────────────────
195
+ describe('aggregateOverviewByRange', () => {
196
+ it('返回 event/session/day 复合计数', () => {
197
+ storage.writeEvent(makeEvent({ session_id: 'a', timestamp: '2026-05-10T10:00:00.000Z' }));
198
+ storage.writeEvent(makeEvent({ session_id: 'a', timestamp: '2026-05-10T11:00:00.000Z' }));
199
+ storage.writeEvent(makeEvent({ session_id: 'b', timestamp: '2026-05-11T10:00:00.000Z' }));
200
+ const r = storage.aggregateOverviewByRange({
201
+ since: '2026-05-01T00:00:00.000Z',
202
+ until: '2026-06-01T00:00:00.000Z',
203
+ });
204
+ expect(r.event_count).toBe(3);
205
+ expect(r.session_count).toBe(2);
206
+ expect(r.day_count).toBe(2);
207
+ });
208
+ it('空表返回全 0', () => {
209
+ const r = storage.aggregateOverviewByRange({
210
+ since: '2026-05-01T00:00:00.000Z',
211
+ until: '2026-06-01T00:00:00.000Z',
212
+ });
213
+ expect(r).toEqual({ event_count: 0, session_count: 0, day_count: 0 });
214
+ });
215
+ });
216
+
217
+ // ── queryDistinctProjects ──────────────────────────────────────────
218
+ describe('queryDistinctProjects', () => {
219
+ it('返回 distinct project_path', () => {
220
+ storage.writeEvent(makeEvent({ project_path: '/a' }));
221
+ storage.writeEvent(makeEvent({ project_path: '/a', session_id: 's2' }));
222
+ storage.writeEvent(makeEvent({ project_path: '/b', session_id: 's3' }));
223
+ const r = storage.queryDistinctProjects({
224
+ since: '2026-05-01T00:00:00.000Z',
225
+ until: '2026-06-01T00:00:00.000Z',
226
+ });
227
+ expect(r.sort()).toEqual(['/a', '/b']);
228
+ });
229
+ });
230
+
231
+ // ── aggregateToolFailureRate ───────────────────────────────────────
232
+ describe('aggregateToolFailureRate', () => {
233
+ it('LIKE %error% 三个分支正确识别失败', () => {
234
+ // PostToolUse with error
235
+ storage.writeEvent(makeEvent({
236
+ hook_type: 'PostToolUse',
237
+ tool_name: 'Bash',
238
+ tool_output: { error: 'oops' }, // becomes '"error":"oops"' in JSON
239
+ }));
240
+ storage.writeEvent(makeEvent({
241
+ hook_type: 'PostToolUse',
242
+ tool_name: 'Bash',
243
+ tool_output: { is_error: true },
244
+ session_id: 's2',
245
+ }));
246
+ storage.writeEvent(makeEvent({
247
+ hook_type: 'PostToolUse',
248
+ tool_name: 'Bash',
249
+ tool_output: { isError: true },
250
+ session_id: 's3',
251
+ }));
252
+ // PostToolUse OK
253
+ storage.writeEvent(makeEvent({
254
+ hook_type: 'PostToolUse',
255
+ tool_name: 'Bash',
256
+ tool_output: { result: 'ok' },
257
+ session_id: 's4',
258
+ }));
259
+ // PreToolUse should not count
260
+ storage.writeEvent(makeEvent({
261
+ hook_type: 'PreToolUse',
262
+ tool_name: 'Bash',
263
+ tool_output: { error: 'should not count' },
264
+ session_id: 's5',
265
+ }));
266
+
267
+ const r = storage.aggregateToolFailureRate({
268
+ since: '2026-05-01T00:00:00.000Z',
269
+ until: '2026-06-01T00:00:00.000Z',
270
+ });
271
+ expect(r.post_total).toBe(4);
272
+ expect(r.failed).toBe(3);
273
+ });
274
+ it('空范围返回 0/0', () => {
275
+ const r = storage.aggregateToolFailureRate({
276
+ since: '2026-05-01T00:00:00.000Z',
277
+ until: '2026-06-01T00:00:00.000Z',
278
+ });
279
+ expect(r).toEqual({ post_total: 0, failed: 0 });
280
+ });
281
+ });
282
+
283
+ // ── queryFileEditInputs ────────────────────────────────────────────
284
+ describe('queryFileEditInputs', () => {
285
+ it('IN 展开多个 tool_name', () => {
286
+ storage.writeEvent(makeEvent({
287
+ hook_type: 'PreToolUse',
288
+ tool_name: 'Edit',
289
+ tool_input: { file_path: '/a.ts' },
290
+ }));
291
+ storage.writeEvent(makeEvent({
292
+ hook_type: 'PreToolUse',
293
+ tool_name: 'Write',
294
+ tool_input: { file_path: '/b.ts' },
295
+ session_id: 's2',
296
+ }));
297
+ storage.writeEvent(makeEvent({
298
+ hook_type: 'PreToolUse',
299
+ tool_name: 'Bash', // not file edit
300
+ tool_input: { command: 'ls' },
301
+ session_id: 's3',
302
+ }));
303
+ const r = storage.queryFileEditInputs({
304
+ since: '2026-05-01T00:00:00.000Z',
305
+ until: '2026-06-01T00:00:00.000Z',
306
+ tool_names: ['Edit', 'Write', 'MultiEdit'],
307
+ });
308
+ expect(r.length).toBe(2);
309
+ expect(r.map(x => JSON.parse(x.tool_input).file_path).sort()).toEqual(['/a.ts', '/b.ts']);
310
+ });
311
+ it('空 tool_names 返回空', () => {
312
+ storage.writeEvent(makeEvent({ tool_name: 'Edit', tool_input: { file_path: '/a.ts' } }));
313
+ expect(storage.queryFileEditInputs({
314
+ since: '2026-05-01T00:00:00.000Z',
315
+ until: '2026-06-01T00:00:00.000Z',
316
+ tool_names: [],
317
+ })).toEqual([]);
318
+ });
319
+ });
320
+
321
+ // ── queryEventsByTimeRange ─────────────────────────────────────────
322
+ describe('queryEventsByTimeRange', () => {
323
+ it('返回 [since, until) 区间事件,升序', () => {
324
+ storage.writeEvent(makeEvent({ timestamp: '2026-05-10T10:00:00.000Z', tool_name: 'A' }));
325
+ storage.writeEvent(makeEvent({ timestamp: '2026-05-11T10:00:00.000Z', tool_name: 'B', session_id: 's2' }));
326
+ storage.writeEvent(makeEvent({ timestamp: '2026-05-12T10:00:00.000Z', tool_name: 'C', session_id: 's3' }));
327
+ const r = storage.queryEventsByTimeRange({
328
+ since: '2026-05-10T00:00:00.000Z',
329
+ until: '2026-05-12T00:00:00.000Z',
330
+ });
331
+ expect(r.length).toBe(2);
332
+ expect(r[0].tool_name).toBe('A');
333
+ expect(r[1].tool_name).toBe('B');
334
+ });
335
+ it('未指定 until 返回 since 起始的全部', () => {
336
+ storage.writeEvent(makeEvent({ timestamp: '2026-05-10T10:00:00.000Z' }));
337
+ storage.writeEvent(makeEvent({ timestamp: '2026-05-20T10:00:00.000Z', session_id: 's2' }));
338
+ const r = storage.queryEventsByTimeRange({ since: '2026-05-15T00:00:00.000Z' });
339
+ expect(r.length).toBe(1);
340
+ });
341
+ });
342
+ });