@xoxoai/checkmate 0.6.6 → 0.7.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 (382) hide show
  1. package/dist/checkmate.js +13375 -0
  2. package/package.json +14 -100
  3. package/plugin/.claude-plugin/plugin.json +10 -0
  4. package/plugin/bin/checkmate.ts +3 -0
  5. package/plugin/contracts/finding.schema.json +78 -0
  6. package/plugin/contracts/gap.schema.json +56 -0
  7. package/plugin/contracts/plan-candidate.schema.json +103 -0
  8. package/plugin/contracts/plan-delta.schema.json +63 -0
  9. package/plugin/contracts/plan-patch.schema.json +116 -0
  10. package/plugin/contracts/plan-request.schema.json +108 -0
  11. package/plugin/contracts/plan.schema.json +313 -0
  12. package/plugin/contracts/profile.schema.json +154 -0
  13. package/plugin/contracts/response.schema.json +25 -0
  14. package/plugin/contracts/result.schema.json +410 -0
  15. package/plugin/dist/checkmate.js +13012 -0
  16. package/plugin/package.json +4 -0
  17. package/plugin/references/browser.md +35 -0
  18. package/plugin/references/http.md +30 -0
  19. package/plugin/references/workflow.md +122 -0
  20. package/plugin/skills/execute/SKILL.md +92 -0
  21. package/plugin/skills/plan/SKILL.md +94 -0
  22. package/plugin/skills/report/SKILL.md +28 -0
  23. package/plugin/skills/revise/SKILL.md +43 -0
  24. package/plugin/skills/setup/SKILL.md +82 -0
  25. package/plugin/skills/status/SKILL.md +20 -0
  26. package/plugin/skills/ui/SKILL.md +26 -0
  27. package/plugin/src/actor.ts +21 -0
  28. package/plugin/src/cli.ts +1133 -0
  29. package/plugin/src/contracts.ts +342 -0
  30. package/plugin/src/index.ts +52 -0
  31. package/plugin/src/plans.ts +1028 -0
  32. package/plugin/src/render.ts +544 -0
  33. package/plugin/src/runs.ts +868 -0
  34. package/plugin/src/store.ts +384 -0
  35. package/plugin/src/targets.ts +205 -0
  36. package/plugin/src/types.ts +319 -0
  37. package/plugin/src/ui/assets/client.js +501 -0
  38. package/plugin/src/ui/assets/favicon.svg +1 -0
  39. package/plugin/src/ui/assets/fonts/inter-OFL.txt +93 -0
  40. package/plugin/src/ui/assets/fonts/inter.woff2 +0 -0
  41. package/plugin/src/ui/assets/fonts/jetbrains-mono-OFL.txt +93 -0
  42. package/plugin/src/ui/assets/fonts/jetbrains-mono.woff2 +0 -0
  43. package/plugin/src/ui/assets/fonts/space-grotesk-OFL.txt +93 -0
  44. package/plugin/src/ui/assets/fonts/space-grotesk.woff2 +0 -0
  45. package/plugin/src/ui/assets/style.css +1926 -0
  46. package/plugin/src/ui/edit.ts +305 -0
  47. package/plugin/src/ui/forms.ts +209 -0
  48. package/plugin/src/ui/html.ts +130 -0
  49. package/plugin/src/ui/layout.ts +198 -0
  50. package/plugin/src/ui/lists.ts +645 -0
  51. package/plugin/src/ui/plan.ts +577 -0
  52. package/plugin/src/ui/run.ts +489 -0
  53. package/plugin/src/ui/server.ts +684 -0
  54. package/plugin/src/validators.d.ts +13 -0
  55. package/plugin/src/validators.js +1 -0
  56. package/plugin/src/views.ts +839 -0
  57. package/LICENSE +0 -21
  58. package/README.md +0 -294
  59. package/bin/checkmate.js +0 -13
  60. package/dist/ai/client.d.ts +0 -64
  61. package/dist/ai/client.d.ts.map +0 -1
  62. package/dist/ai/client.js +0 -313
  63. package/dist/ai/client.js.map +0 -1
  64. package/dist/ai/message-handler.d.ts +0 -9
  65. package/dist/ai/message-handler.d.ts.map +0 -1
  66. package/dist/ai/message-handler.js +0 -50
  67. package/dist/ai/message-handler.js.map +0 -1
  68. package/dist/ai/message-history.d.ts +0 -17
  69. package/dist/ai/message-history.d.ts.map +0 -1
  70. package/dist/ai/message-history.js +0 -45
  71. package/dist/ai/message-history.js.map +0 -1
  72. package/dist/ai/prompts.d.ts +0 -4
  73. package/dist/ai/prompts.d.ts.map +0 -1
  74. package/dist/ai/prompts.js +0 -24
  75. package/dist/ai/prompts.js.map +0 -1
  76. package/dist/ai/rate-limit-policy.d.ts +0 -9
  77. package/dist/ai/rate-limit-policy.d.ts.map +0 -1
  78. package/dist/ai/rate-limit-policy.js +0 -34
  79. package/dist/ai/rate-limit-policy.js.map +0 -1
  80. package/dist/ai/tool-response-handler.d.ts +0 -16
  81. package/dist/ai/tool-response-handler.d.ts.map +0 -1
  82. package/dist/ai/tool-response-handler.js +0 -85
  83. package/dist/ai/tool-response-handler.js.map +0 -1
  84. package/dist/ai/turn-processor.d.ts +0 -34
  85. package/dist/ai/turn-processor.d.ts.map +0 -1
  86. package/dist/ai/turn-processor.js +0 -107
  87. package/dist/ai/turn-processor.js.map +0 -1
  88. package/dist/api/allocate-run-identity.d.ts +0 -8
  89. package/dist/api/allocate-run-identity.d.ts.map +0 -1
  90. package/dist/api/allocate-run-identity.js +0 -10
  91. package/dist/api/allocate-run-identity.js.map +0 -1
  92. package/dist/api/execute-prepared-run.d.ts +0 -22
  93. package/dist/api/execute-prepared-run.d.ts.map +0 -1
  94. package/dist/api/execute-prepared-run.js +0 -184
  95. package/dist/api/execute-prepared-run.js.map +0 -1
  96. package/dist/api/index.d.ts +0 -25
  97. package/dist/api/index.d.ts.map +0 -1
  98. package/dist/api/index.js +0 -69
  99. package/dist/api/index.js.map +0 -1
  100. package/dist/api/prepare-run.d.ts +0 -44
  101. package/dist/api/prepare-run.d.ts.map +0 -1
  102. package/dist/api/prepare-run.js +0 -261
  103. package/dist/api/prepare-run.js.map +0 -1
  104. package/dist/cli/diagnostic-relay.d.ts +0 -6
  105. package/dist/cli/diagnostic-relay.d.ts.map +0 -1
  106. package/dist/cli/diagnostic-relay.js +0 -6
  107. package/dist/cli/diagnostic-relay.js.map +0 -1
  108. package/dist/cli/main.d.ts +0 -21
  109. package/dist/cli/main.d.ts.map +0 -1
  110. package/dist/cli/main.js +0 -166
  111. package/dist/cli/main.js.map +0 -1
  112. package/dist/cli/protocol.d.ts +0 -57
  113. package/dist/cli/protocol.d.ts.map +0 -1
  114. package/dist/cli/protocol.js +0 -116
  115. package/dist/cli/protocol.js.map +0 -1
  116. package/dist/cli/reconcile-result.d.ts +0 -13
  117. package/dist/cli/reconcile-result.d.ts.map +0 -1
  118. package/dist/cli/reconcile-result.js +0 -300
  119. package/dist/cli/reconcile-result.js.map +0 -1
  120. package/dist/cli/run-parent.d.ts +0 -37
  121. package/dist/cli/run-parent.d.ts.map +0 -1
  122. package/dist/cli/run-parent.js +0 -491
  123. package/dist/cli/run-parent.js.map +0 -1
  124. package/dist/cli/signals.d.ts +0 -36
  125. package/dist/cli/signals.d.ts.map +0 -1
  126. package/dist/cli/signals.js +0 -85
  127. package/dist/cli/signals.js.map +0 -1
  128. package/dist/cli/static-commands.d.ts +0 -11
  129. package/dist/cli/static-commands.d.ts.map +0 -1
  130. package/dist/cli/static-commands.js +0 -83
  131. package/dist/cli/static-commands.js.map +0 -1
  132. package/dist/cli/worker-stream-guard.d.ts +0 -20
  133. package/dist/cli/worker-stream-guard.d.ts.map +0 -1
  134. package/dist/cli/worker-stream-guard.js +0 -41
  135. package/dist/cli/worker-stream-guard.js.map +0 -1
  136. package/dist/cli/worker.d.ts +0 -3
  137. package/dist/cli/worker.d.ts.map +0 -1
  138. package/dist/cli/worker.js +0 -111
  139. package/dist/cli/worker.js.map +0 -1
  140. package/dist/config/ingestion.d.ts +0 -58
  141. package/dist/config/ingestion.d.ts.map +0 -1
  142. package/dist/config/ingestion.js +0 -283
  143. package/dist/config/ingestion.js.map +0 -1
  144. package/dist/config/manifest.d.ts +0 -20
  145. package/dist/config/manifest.d.ts.map +0 -1
  146. package/dist/config/manifest.js +0 -91
  147. package/dist/config/manifest.js.map +0 -1
  148. package/dist/config/model-egress.d.ts +0 -10
  149. package/dist/config/model-egress.d.ts.map +0 -1
  150. package/dist/config/model-egress.js +0 -62
  151. package/dist/config/model-egress.js.map +0 -1
  152. package/dist/config/package-resolution.d.ts +0 -4
  153. package/dist/config/package-resolution.d.ts.map +0 -1
  154. package/dist/config/package-resolution.js +0 -24
  155. package/dist/config/package-resolution.js.map +0 -1
  156. package/dist/config/policy.d.ts +0 -44
  157. package/dist/config/policy.d.ts.map +0 -1
  158. package/dist/config/policy.js +0 -141
  159. package/dist/config/policy.js.map +0 -1
  160. package/dist/config/record.d.ts +0 -4
  161. package/dist/config/record.d.ts.map +0 -1
  162. package/dist/config/record.js +0 -10
  163. package/dist/config/record.js.map +0 -1
  164. package/dist/config/secrets.d.ts +0 -11
  165. package/dist/config/secrets.d.ts.map +0 -1
  166. package/dist/config/secrets.js +0 -42
  167. package/dist/config/secrets.js.map +0 -1
  168. package/dist/contracts/diagnostics.d.ts +0 -4
  169. package/dist/contracts/diagnostics.d.ts.map +0 -1
  170. package/dist/contracts/diagnostics.js +0 -31
  171. package/dist/contracts/diagnostics.js.map +0 -1
  172. package/dist/contracts/serialize.d.ts +0 -2
  173. package/dist/contracts/serialize.d.ts.map +0 -1
  174. package/dist/contracts/serialize.js +0 -90
  175. package/dist/contracts/serialize.js.map +0 -1
  176. package/dist/contracts/types.d.ts +0 -316
  177. package/dist/contracts/types.d.ts.map +0 -1
  178. package/dist/contracts/types.js +0 -74
  179. package/dist/contracts/types.js.map +0 -1
  180. package/dist/contracts/validator.d.ts +0 -8
  181. package/dist/contracts/validator.d.ts.map +0 -1
  182. package/dist/contracts/validator.js +0 -71
  183. package/dist/contracts/validator.js.map +0 -1
  184. package/dist/driver.d.ts +0 -97
  185. package/dist/driver.d.ts.map +0 -1
  186. package/dist/driver.js +0 -24
  187. package/dist/driver.js.map +0 -1
  188. package/dist/drivers/descriptor.d.ts +0 -15
  189. package/dist/drivers/descriptor.d.ts.map +0 -1
  190. package/dist/drivers/descriptor.js +0 -122
  191. package/dist/drivers/descriptor.js.map +0 -1
  192. package/dist/drivers/loader.d.ts +0 -18
  193. package/dist/drivers/loader.d.ts.map +0 -1
  194. package/dist/drivers/loader.js +0 -97
  195. package/dist/drivers/loader.js.map +0 -1
  196. package/dist/drivers/web/checkmate-driver.json +0 -47
  197. package/dist/drivers/web/index.d.ts +0 -6
  198. package/dist/drivers/web/index.d.ts.map +0 -1
  199. package/dist/drivers/web/index.js +0 -27
  200. package/dist/drivers/web/index.js.map +0 -1
  201. package/dist/drivers/web/session.d.ts +0 -25
  202. package/dist/drivers/web/session.d.ts.map +0 -1
  203. package/dist/drivers/web/session.js +0 -132
  204. package/dist/drivers/web/session.js.map +0 -1
  205. package/dist/drivers/web/tools/network-request-recorder.d.ts +0 -46
  206. package/dist/drivers/web/tools/network-request-recorder.d.ts.map +0 -1
  207. package/dist/drivers/web/tools/network-request-recorder.js +0 -164
  208. package/dist/drivers/web/tools/network-request-recorder.js.map +0 -1
  209. package/dist/drivers/web/tools/screenshot-service.d.ts +0 -10
  210. package/dist/drivers/web/tools/screenshot-service.d.ts.map +0 -1
  211. package/dist/drivers/web/tools/screenshot-service.js +0 -23
  212. package/dist/drivers/web/tools/screenshot-service.js.map +0 -1
  213. package/dist/drivers/web/tools/snapshot-filter/index.d.ts +0 -4
  214. package/dist/drivers/web/tools/snapshot-filter/index.d.ts.map +0 -1
  215. package/dist/drivers/web/tools/snapshot-filter/index.js +0 -4
  216. package/dist/drivers/web/tools/snapshot-filter/index.js.map +0 -1
  217. package/dist/drivers/web/tools/snapshot-filter/semantic-scorer.d.ts +0 -15
  218. package/dist/drivers/web/tools/snapshot-filter/semantic-scorer.d.ts.map +0 -1
  219. package/dist/drivers/web/tools/snapshot-filter/semantic-scorer.js +0 -99
  220. package/dist/drivers/web/tools/snapshot-filter/semantic-scorer.js.map +0 -1
  221. package/dist/drivers/web/tools/snapshot-filter/snapshot-filter.d.ts +0 -5
  222. package/dist/drivers/web/tools/snapshot-filter/snapshot-filter.d.ts.map +0 -1
  223. package/dist/drivers/web/tools/snapshot-filter/snapshot-filter.js +0 -56
  224. package/dist/drivers/web/tools/snapshot-filter/snapshot-filter.js.map +0 -1
  225. package/dist/drivers/web/tools/snapshot-filter/tree-reconstructor.d.ts +0 -3
  226. package/dist/drivers/web/tools/snapshot-filter/tree-reconstructor.d.ts.map +0 -1
  227. package/dist/drivers/web/tools/snapshot-filter/tree-reconstructor.js +0 -85
  228. package/dist/drivers/web/tools/snapshot-filter/tree-reconstructor.js.map +0 -1
  229. package/dist/drivers/web/tools/snapshot-service.d.ts +0 -31
  230. package/dist/drivers/web/tools/snapshot-service.d.ts.map +0 -1
  231. package/dist/drivers/web/tools/snapshot-service.js +0 -96
  232. package/dist/drivers/web/tools/snapshot-service.js.map +0 -1
  233. package/dist/drivers/web/tools/tool.d.ts +0 -76
  234. package/dist/drivers/web/tools/tool.d.ts.map +0 -1
  235. package/dist/drivers/web/tools/tool.js +0 -562
  236. package/dist/drivers/web/tools/tool.js.map +0 -1
  237. package/dist/drivers/web/tools/transient-state-tracker.d.ts +0 -26
  238. package/dist/drivers/web/tools/transient-state-tracker.d.ts.map +0 -1
  239. package/dist/drivers/web/tools/transient-state-tracker.js +0 -301
  240. package/dist/drivers/web/tools/transient-state-tracker.js.map +0 -1
  241. package/dist/drivers/web/tools/types.d.ts +0 -11
  242. package/dist/drivers/web/tools/types.d.ts.map +0 -1
  243. package/dist/drivers/web/tools/types.js +0 -2
  244. package/dist/drivers/web/tools/types.js.map +0 -1
  245. package/dist/evidence/atomic-file.d.ts +0 -23
  246. package/dist/evidence/atomic-file.d.ts.map +0 -1
  247. package/dist/evidence/atomic-file.js +0 -104
  248. package/dist/evidence/atomic-file.js.map +0 -1
  249. package/dist/evidence/layout.d.ts +0 -30
  250. package/dist/evidence/layout.d.ts.map +0 -1
  251. package/dist/evidence/layout.js +0 -152
  252. package/dist/evidence/layout.js.map +0 -1
  253. package/dist/evidence/retention.d.ts +0 -5
  254. package/dist/evidence/retention.d.ts.map +0 -1
  255. package/dist/evidence/retention.js +0 -10
  256. package/dist/evidence/retention.js.map +0 -1
  257. package/dist/evidence/store.d.ts +0 -101
  258. package/dist/evidence/store.d.ts.map +0 -1
  259. package/dist/evidence/store.js +0 -375
  260. package/dist/evidence/store.js.map +0 -1
  261. package/dist/evidence/terminal-finalizer.d.ts +0 -19
  262. package/dist/evidence/terminal-finalizer.d.ts.map +0 -1
  263. package/dist/evidence/terminal-finalizer.js +0 -56
  264. package/dist/evidence/terminal-finalizer.js.map +0 -1
  265. package/dist/index.d.ts +0 -9
  266. package/dist/index.d.ts.map +0 -1
  267. package/dist/index.js +0 -12
  268. package/dist/index.js.map +0 -1
  269. package/dist/logging/invocation-logger.d.ts +0 -4
  270. package/dist/logging/invocation-logger.d.ts.map +0 -1
  271. package/dist/logging/invocation-logger.js +0 -25
  272. package/dist/logging/invocation-logger.js.map +0 -1
  273. package/dist/logging/level-logger.d.ts +0 -11
  274. package/dist/logging/level-logger.d.ts.map +0 -1
  275. package/dist/logging/level-logger.js +0 -82
  276. package/dist/logging/level-logger.js.map +0 -1
  277. package/dist/logging/types.d.ts +0 -8
  278. package/dist/logging/types.d.ts.map +0 -1
  279. package/dist/logging/types.js +0 -7
  280. package/dist/logging/types.js.map +0 -1
  281. package/dist/redaction/diagnostic-sanitizer.d.ts +0 -7
  282. package/dist/redaction/diagnostic-sanitizer.d.ts.map +0 -1
  283. package/dist/redaction/diagnostic-sanitizer.js +0 -42
  284. package/dist/redaction/diagnostic-sanitizer.js.map +0 -1
  285. package/dist/redaction/redactor.d.ts +0 -40
  286. package/dist/redaction/redactor.d.ts.map +0 -1
  287. package/dist/redaction/redactor.js +0 -176
  288. package/dist/redaction/redactor.js.map +0 -1
  289. package/dist/redaction/scrub.d.ts +0 -29
  290. package/dist/redaction/scrub.d.ts.map +0 -1
  291. package/dist/redaction/scrub.js +0 -54
  292. package/dist/redaction/scrub.js.map +0 -1
  293. package/dist/runtime/config.d.ts +0 -16
  294. package/dist/runtime/config.d.ts.map +0 -1
  295. package/dist/runtime/config.js +0 -2
  296. package/dist/runtime/config.js.map +0 -1
  297. package/dist/runtime/driver-boundary.d.ts +0 -16
  298. package/dist/runtime/driver-boundary.d.ts.map +0 -1
  299. package/dist/runtime/driver-boundary.js +0 -71
  300. package/dist/runtime/driver-boundary.js.map +0 -1
  301. package/dist/runtime/driver-session.d.ts +0 -9
  302. package/dist/runtime/driver-session.d.ts.map +0 -1
  303. package/dist/runtime/driver-session.js +0 -64
  304. package/dist/runtime/driver-session.js.map +0 -1
  305. package/dist/runtime/internal-step-evidence.d.ts +0 -30
  306. package/dist/runtime/internal-step-evidence.d.ts.map +0 -1
  307. package/dist/runtime/internal-step-evidence.js +0 -82
  308. package/dist/runtime/internal-step-evidence.js.map +0 -1
  309. package/dist/runtime/result-adapter.d.ts +0 -9
  310. package/dist/runtime/result-adapter.d.ts.map +0 -1
  311. package/dist/runtime/result-adapter.js +0 -77
  312. package/dist/runtime/result-adapter.js.map +0 -1
  313. package/dist/runtime/runner.d.ts +0 -43
  314. package/dist/runtime/runner.d.ts.map +0 -1
  315. package/dist/runtime/runner.js +0 -72
  316. package/dist/runtime/runner.js.map +0 -1
  317. package/dist/runtime/scenario-control.d.ts +0 -40
  318. package/dist/runtime/scenario-control.d.ts.map +0 -1
  319. package/dist/runtime/scenario-control.js +0 -94
  320. package/dist/runtime/scenario-control.js.map +0 -1
  321. package/dist/runtime/scenario-runner.d.ts +0 -41
  322. package/dist/runtime/scenario-runner.d.ts.map +0 -1
  323. package/dist/runtime/scenario-runner.js +0 -274
  324. package/dist/runtime/scenario-runner.js.map +0 -1
  325. package/dist/runtime/scenario-state.d.ts +0 -55
  326. package/dist/runtime/scenario-state.d.ts.map +0 -1
  327. package/dist/runtime/scenario-state.js +0 -128
  328. package/dist/runtime/scenario-state.js.map +0 -1
  329. package/dist/runtime/step-execution.d.ts +0 -27
  330. package/dist/runtime/step-execution.d.ts.map +0 -1
  331. package/dist/runtime/step-execution.js +0 -143
  332. package/dist/runtime/step-execution.js.map +0 -1
  333. package/dist/runtime/text.d.ts +0 -10
  334. package/dist/runtime/text.d.ts.map +0 -1
  335. package/dist/runtime/text.js +0 -25
  336. package/dist/runtime/text.js.map +0 -1
  337. package/dist/runtime/types.d.ts +0 -53
  338. package/dist/runtime/types.d.ts.map +0 -1
  339. package/dist/runtime/types.js +0 -2
  340. package/dist/runtime/types.js.map +0 -1
  341. package/dist/runtime/usage-tracker.d.ts +0 -37
  342. package/dist/runtime/usage-tracker.d.ts.map +0 -1
  343. package/dist/runtime/usage-tracker.js +0 -110
  344. package/dist/runtime/usage-tracker.js.map +0 -1
  345. package/dist/tools/define-agent-tool.d.ts +0 -20
  346. package/dist/tools/define-agent-tool.d.ts.map +0 -1
  347. package/dist/tools/define-agent-tool.js +0 -30
  348. package/dist/tools/define-agent-tool.js.map +0 -1
  349. package/dist/tools/dispatcher.d.ts +0 -19
  350. package/dist/tools/dispatcher.d.ts.map +0 -1
  351. package/dist/tools/dispatcher.js +0 -91
  352. package/dist/tools/dispatcher.js.map +0 -1
  353. package/dist/tools/loop-detector.d.ts +0 -27
  354. package/dist/tools/loop-detector.d.ts.map +0 -1
  355. package/dist/tools/loop-detector.js +0 -76
  356. package/dist/tools/loop-detector.js.map +0 -1
  357. package/dist/tools/registry.d.ts +0 -19
  358. package/dist/tools/registry.d.ts.map +0 -1
  359. package/dist/tools/registry.js +0 -54
  360. package/dist/tools/registry.js.map +0 -1
  361. package/dist/tools/step/result-tool.d.ts +0 -7
  362. package/dist/tools/step/result-tool.d.ts.map +0 -1
  363. package/dist/tools/step/result-tool.js +0 -34
  364. package/dist/tools/step/result-tool.js.map +0 -1
  365. package/dist/tools/tool-contract.d.ts +0 -3
  366. package/dist/tools/tool-contract.d.ts.map +0 -1
  367. package/dist/tools/tool-contract.js +0 -2
  368. package/dist/tools/tool-contract.js.map +0 -1
  369. package/dist/tools/types.d.ts +0 -210
  370. package/dist/tools/types.d.ts.map +0 -1
  371. package/dist/tools/types.js +0 -4
  372. package/dist/tools/types.js.map +0 -1
  373. package/docs/CLI.md +0 -59
  374. package/docs/CONFIGURATION.md +0 -105
  375. package/docs/DRIVERS.md +0 -79
  376. package/docs/EVIDENCE.md +0 -64
  377. package/schemas/checkmate-config.v1.json +0 -176
  378. package/schemas/describe-result.v1.json +0 -414
  379. package/schemas/driver-descriptor.v1.json +0 -66
  380. package/schemas/run-request.v1.json +0 -67
  381. package/schemas/run-result.v1.json +0 -333
  382. package/schemas/validation-result.v1.json +0 -86
@@ -0,0 +1,839 @@
1
+ import type { Plan, Result } from './types.ts'
2
+ import type { Store } from './store.ts'
3
+ import { all, one } from './store.ts'
4
+ import { requireChoice, requireCondition } from './contracts.ts'
5
+ import {
6
+ approvalState,
7
+ dependencyBlock,
8
+ latestApproval,
9
+ planApproval,
10
+ planChanges,
11
+ planRequest,
12
+ planRevision,
13
+ planRunGaps,
14
+ } from './plans.ts'
15
+ import {
16
+ outcomeCounts,
17
+ resultAcceptance,
18
+ runAssertions,
19
+ runEvidence,
20
+ runFindings,
21
+ runGaps,
22
+ runOutcomes,
23
+ runResult,
24
+ runState,
25
+ } from './runs.ts'
26
+ import type { RunRow } from './runs.ts'
27
+
28
+ type RevisionRow = { plan_id: string; revision: number; sha256: string; body: string; at: string; actor: string }
29
+ type ApprovalState = ReturnType<typeof approvalState>
30
+ export type ApprovalLabel = 'approved' | 'rejected' | 'pending' | 'not_required'
31
+ export type PlanListRow = {
32
+ planId: string
33
+ status: 'requested' | 'drafted'
34
+ revision: number | null
35
+ sha256: string | null
36
+ since: string
37
+ openGaps: string[]
38
+ openBlockingGaps: string[]
39
+ tests: { total: number; ready: number; blocked: number; deferred: number }
40
+ approval: ApprovalLabel | null
41
+ blocked: Array<{ testId: string; dependency: string; cause: string }>
42
+ loop: { state: LoopState; waitsOn: Role }
43
+ }
44
+
45
+ export function approvalLabel(state: ApprovalState): ApprovalLabel {
46
+ if (state.approved) return 'approved'
47
+ if (state.rejected) return 'rejected'
48
+ return state.policy === 'required' ? 'pending' : 'not_required'
49
+ }
50
+
51
+ function latestRevisions(store: Store) {
52
+ return all<RevisionRow>(
53
+ store,
54
+ `SELECT p.plan_id, p.revision, p.sha256, p.body, o.at, o.actor FROM latest_plans p
55
+ JOIN operations o ON o.id = p.operation_id ORDER BY p.plan_id`
56
+ ).map((row) => ({ ...row, plan: JSON.parse(row.body) as Plan }))
57
+ }
58
+
59
+ function requestedPlans(store: Store) {
60
+ return all<{ plan_id: string; target_id: string; target_revision: number; body: string; at: string }>(
61
+ store,
62
+ `SELECT r.plan_id, r.target_id, r.target_revision, r.body, o.at FROM plan_requests r
63
+ JOIN operations o ON o.id = r.operation_id
64
+ WHERE NOT EXISTS (SELECT 1 FROM plan_revisions p WHERE p.plan_id = r.plan_id) ORDER BY r.plan_id`
65
+ ).map((row) => ({
66
+ planId: row.plan_id,
67
+ target: `${row.target_id}@${row.target_revision}`,
68
+ objective: (JSON.parse(row.body) as { objective: string }).objective,
69
+ since: row.at,
70
+ }))
71
+ }
72
+
73
+ function planSummary(plan: Plan) {
74
+ const count = (readiness: string) => plan.tests.filter((test) => test.readiness === readiness).length
75
+ const open = plan.gaps.filter((gap) => gap.status === 'open').length
76
+ return `${plan.tests.length} tests: ${count('ready')} ready, ${count('blocked')} blocked, ${count('deferred')} deferred; ${open} open gaps`
77
+ }
78
+
79
+ export function inbox(store: Store) {
80
+ const pending = new Set(
81
+ all<{ plan_id: string; gap_id: string }>(
82
+ store,
83
+ 'SELECT plan_id, gap_id FROM decisions WHERE applied_revision IS NULL'
84
+ ).map((row) => `${row.plan_id}:${row.gap_id}`)
85
+ )
86
+ const revisions = latestRevisions(store)
87
+ const gaps = revisions
88
+ .flatMap((row) => [
89
+ ...row.plan.gaps
90
+ .filter((gap) => gap.status === 'open' && !pending.has(`${row.plan_id}:${gap.id}`))
91
+ .map((gap) => ({
92
+ planId: row.plan_id,
93
+ revision: row.revision,
94
+ gapId: gap.id,
95
+ question: gap.question,
96
+ severity: gap.severity,
97
+ since: row.at,
98
+ runId: null as string | null,
99
+ })),
100
+ ...planRunGaps(store, row.plan_id)
101
+ .filter((item) => !item.adopted)
102
+ .map((item) => ({
103
+ planId: row.plan_id,
104
+ revision: row.revision,
105
+ gapId: item.gap.id,
106
+ question: item.gap.question,
107
+ severity: item.gap.severity,
108
+ since: item.at,
109
+ runId: item.runId,
110
+ })),
111
+ ])
112
+ .sort(bySince)
113
+ const plans = revisions
114
+ .filter((row) => {
115
+ const state = planApproval(store, row.plan, row.sha256)
116
+ return state.policy === 'required' && !state.approved && !state.rejected
117
+ })
118
+ .map((row) => ({
119
+ planId: row.plan_id,
120
+ revision: row.revision,
121
+ sha256: row.sha256,
122
+ since: row.at,
123
+ summary: planSummary(row.plan),
124
+ }))
125
+ .sort(bySince)
126
+ const superseded = retestedRuns(store)
127
+ const results = all<RunRow & { finished_at: string; sha256: string; coverage: string }>(
128
+ store,
129
+ 'SELECT r.*, s.finished_at, s.sha256, s.coverage FROM runs r JOIN results s ON s.run_id = r.run_id ORDER BY s.rowid'
130
+ )
131
+ .filter((row) => {
132
+ const state = resultAcceptance(store, row, row.sha256)
133
+ return state.policy === 'required' && !state.latest && !superseded.has(row.run_id)
134
+ })
135
+ .map((row) => ({
136
+ runId: row.run_id,
137
+ planId: row.plan_id,
138
+ revision: row.plan_revision,
139
+ build: row.application_build,
140
+ counts: selectedCounts(runResult(store, row.run_id)!.result),
141
+ coverage: row.coverage,
142
+ sha256: row.sha256,
143
+ since: row.finished_at,
144
+ }))
145
+ return { gaps, plans, results, requests: requestedPlans(store).sort(bySince) }
146
+ }
147
+
148
+ function selectedCounts(result: Result) {
149
+ return outcomeCounts(result.tests.filter((test) => result.selectedTestIds.includes(test.testId)))
150
+ }
151
+
152
+ function bySince(a: { since: string }, b: { since: string }) {
153
+ return a.since.localeCompare(b.since)
154
+ }
155
+
156
+ function testCounts(plan: Plan) {
157
+ const count = (readiness: string) => plan.tests.filter((test) => test.readiness === readiness).length
158
+ return { total: plan.tests.length, ready: count('ready'), blocked: count('blocked'), deferred: count('deferred') }
159
+ }
160
+
161
+ export function planList(store: Store): PlanListRow[] {
162
+ const drafted = latestRevisions(store).map((row): PlanListRow => {
163
+ const open = [
164
+ ...row.plan.gaps.filter((gap) => gap.status === 'open'),
165
+ ...planRunGaps(store, row.plan_id)
166
+ .filter((item) => !item.adopted)
167
+ .map((item) => item.gap),
168
+ ]
169
+ return {
170
+ planId: row.plan_id,
171
+ status: 'drafted',
172
+ revision: row.revision,
173
+ sha256: row.sha256,
174
+ since: row.at,
175
+ openGaps: open.map((gap) => gap.id),
176
+ openBlockingGaps: open.filter((gap) => gap.severity === 'blocking').map((gap) => gap.id),
177
+ tests: testCounts(row.plan),
178
+ approval: approvalLabel(planApproval(store, row.plan, row.sha256)),
179
+ blocked: row.plan.tests.flatMap((test) =>
180
+ test.dependsOn
181
+ .filter((dependency) => dependency.includes(':'))
182
+ .flatMap((dependency) => {
183
+ const cause = dependencyBlock(store, dependency, null)
184
+ return cause ? [{ testId: test.id, dependency, cause }] : []
185
+ })
186
+ ),
187
+ loop: loopOf(store, row.plan_id),
188
+ }
189
+ })
190
+ const requested = requestedPlans(store).map((request): PlanListRow => ({
191
+ planId: request.planId,
192
+ status: 'requested',
193
+ revision: null,
194
+ sha256: null,
195
+ since: request.since,
196
+ openGaps: [],
197
+ openBlockingGaps: [],
198
+ tests: { total: 0, ready: 0, blocked: 0, deferred: 0 },
199
+ approval: null,
200
+ blocked: [],
201
+ loop: { state: 'needs_plan', waitsOn: 'planner' },
202
+ }))
203
+ return [...drafted, ...requested].sort((a, b) => a.planId.localeCompare(b.planId))
204
+ }
205
+
206
+ export function planDetail(store: Store, planId: string, revision: number | null = null) {
207
+ const selected = planRevision(store, planId, revision)
208
+ const latest = planRevision(store, planId)
209
+ const approval = planApproval(store, selected.plan, selected.sha256)
210
+ const pending =
211
+ latest.plan.revision === selected.plan.revision
212
+ ? all<{ id: string; gap_id: string; kind: string; value: string; actor: string }>(
213
+ store,
214
+ 'SELECT id, gap_id, kind, value, actor FROM decisions WHERE plan_id = ? AND applied_revision IS NULL ORDER BY rowid',
215
+ planId
216
+ ).map((row) => ({ id: row.id, gapId: row.gap_id, kind: row.kind, value: row.value, actor: row.actor }))
217
+ : []
218
+ return {
219
+ planId,
220
+ revision: selected.plan.revision,
221
+ latestRevision: latest.plan.revision,
222
+ sha256: selected.sha256,
223
+ approval: { state: approvalLabel(approval), policy: approval.policy, latest: approval.latest },
224
+ pendingDecisions: pending,
225
+ plan: selected.plan,
226
+ runs: runList(store, { planId }).map((run) => ({
227
+ runId: run.runId,
228
+ revision: run.revision,
229
+ build: run.build,
230
+ startedAt: run.startedAt,
231
+ state: run.state,
232
+ completion: run.completion,
233
+ coverage: run.coverage,
234
+ counts: run.counts,
235
+ acceptance: run.acceptance,
236
+ })),
237
+ }
238
+ }
239
+
240
+ export function planHistory(store: Store, planId: string) {
241
+ planRevision(store, planId)
242
+ const rows = all<RevisionRow & { operation_id: string }>(
243
+ store,
244
+ `SELECT p.plan_id, p.revision, p.sha256, p.body, p.operation_id, o.at, o.actor FROM plan_revisions p
245
+ JOIN operations o ON o.id = p.operation_id WHERE p.plan_id = ? ORDER BY p.revision`,
246
+ planId
247
+ )
248
+ const plans = rows.map((row) => JSON.parse(row.body) as Plan)
249
+ return rows.map((row, i) => ({
250
+ revision: row.revision,
251
+ sha256: row.sha256,
252
+ operationId: row.operation_id,
253
+ actor: row.actor,
254
+ at: row.at,
255
+ changes: i === 0 ? [] : planChanges(plans[i - 1], plans[i]),
256
+ }))
257
+ }
258
+
259
+ export type LoopState =
260
+ | 'needs_plan'
261
+ | 'run_in_progress'
262
+ | 'needs_revision'
263
+ | 'plan_rejected'
264
+ | 'needs_answers'
265
+ | 'needs_approval'
266
+ | 'ready_to_run'
267
+ | 'nothing_to_run'
268
+ | 'defects_open'
269
+ | 'needs_retest'
270
+ | 'awaits_acceptance'
271
+ | 'accepted'
272
+ | 'passed'
273
+ export type Role = 'planner' | 'oracle' | 'qa' | 'factory' | 'none'
274
+ export const roles = ['planner', 'oracle', 'qa', 'factory', 'none'] as const
275
+
276
+ export function planStatus(store: Store, planId: string) {
277
+ if (!one(store, 'SELECT 1 FROM plan_revisions WHERE plan_id = ?', planId)) {
278
+ planRequest(store, planId)
279
+ return {
280
+ planId,
281
+ revision: null,
282
+ ...loop('needs_plan', 'planner', [], `checkmate delegate plan ${planId}`),
283
+ approval: null,
284
+ gaps: [],
285
+ pendingDecisions: [],
286
+ latestRun: null,
287
+ defects: [],
288
+ }
289
+ }
290
+ const { plan, sha256 } = planRevision(store, planId)
291
+ const subject = `${planId}@${plan.revision}`
292
+ const pendingDecisions = all<{ id: string; gap_id: string; kind: string }>(
293
+ store,
294
+ 'SELECT id, gap_id, kind FROM decisions WHERE plan_id = ? AND applied_revision IS NULL ORDER BY rowid',
295
+ planId
296
+ ).map((row) => ({ id: row.id, gapId: row.gap_id, kind: row.kind }))
297
+ const gaps = [
298
+ ...plan.gaps
299
+ .filter((gap) => gap.status === 'open' && !pendingDecisions.some((item) => item.gapId === gap.id))
300
+ .map((gap) => ({
301
+ gapId: gap.id,
302
+ severity: gap.severity,
303
+ question: gap.question,
304
+ runId: null as string | null,
305
+ })),
306
+ ...planRunGaps(store, planId)
307
+ .filter((item) => !item.adopted)
308
+ .map((item) => ({
309
+ gapId: item.gap.id,
310
+ severity: item.gap.severity,
311
+ question: item.gap.question,
312
+ runId: item.runId as string | null,
313
+ })),
314
+ ]
315
+ const approval = approvalLabel(planApproval(store, plan, sha256))
316
+ const runs = runList(store, { planId })
317
+ const started = runs.find((run) => run.state === 'started')
318
+ const superseded = retestedRuns(store)
319
+ const finished = runs.filter((run) => run.state === 'finalized' && !superseded.has(run.runId)).at(-1)
320
+ const latestRun = started ?? finished ?? null
321
+ const defects = finished
322
+ ? runFindings(store, finished.runId)
323
+ .filter((finding) => finding.type === 'defect')
324
+ .map((finding) => ({
325
+ runId: finished.runId,
326
+ findingId: finding.id,
327
+ severity: finding.severity,
328
+ assertionIds: finding.assertionIds,
329
+ title: finding.title,
330
+ }))
331
+ : []
332
+ const state = (): ReturnType<typeof loop> => {
333
+ const run = `checkmate run begin <runId> ${subject} --build <build>`
334
+ if (started)
335
+ return loop(
336
+ 'run_in_progress',
337
+ 'qa',
338
+ [`${started.runId} is started on build ${started.build}`],
339
+ `checkmate delegate execute ${started.runId}`
340
+ )
341
+ if (pendingDecisions.length)
342
+ return loop(
343
+ 'needs_revision',
344
+ 'planner',
345
+ pendingDecisions.map((item) => `${item.gapId} has a pending ${item.kind} decision`),
346
+ `checkmate delegate revise ${planId}`
347
+ )
348
+ if (approval === 'rejected')
349
+ return loop('plan_rejected', 'oracle', [`${subject} is rejected`], `checkmate plan edit ${subject} --patch`)
350
+ const answer = `checkmate gap answer ${planId} <gapId> --kind <kind> --value "<answer>" --rationale "<why>"`
351
+ if (approval === 'pending')
352
+ return gaps.length
353
+ ? loop(
354
+ 'needs_answers',
355
+ 'oracle',
356
+ [amount(gaps.length, 'open gap'), `${subject} waits for approval`],
357
+ answer
358
+ )
359
+ : loop(
360
+ 'needs_approval',
361
+ 'oracle',
362
+ [`${subject} waits for approval`],
363
+ `checkmate plan approve ${subject} --rationale "<why>"`
364
+ )
365
+ const ready = plan.tests.filter((test) => test.readiness === 'ready').length
366
+ if (!finished) {
367
+ if (ready)
368
+ return loop(
369
+ 'ready_to_run',
370
+ 'qa',
371
+ [`${ready} of ${plan.tests.length} tests are ready`],
372
+ `${run} --partial ready_only`
373
+ )
374
+ return gaps.length
375
+ ? loop('needs_answers', 'oracle', [`No test is ready. ${amount(gaps.length, 'open gap')}`], answer)
376
+ : loop(
377
+ 'nothing_to_run',
378
+ 'oracle',
379
+ ['No test is ready and no gap is open'],
380
+ `checkmate plan edit ${subject} --patch`
381
+ )
382
+ }
383
+ const blocking = gaps.filter((gap) => gap.severity === 'blocking')
384
+ if (blocking.length)
385
+ return loop('needs_answers', 'oracle', [amount(blocking.length, 'open blocking gap')], answer)
386
+ const retest = `checkmate run begin <runId> ${subject} --build <new build> --retest-of ${finished.runId}`
387
+ if (defects.length)
388
+ return loop(
389
+ 'defects_open',
390
+ 'factory',
391
+ [`${finished.runId} has ${amount(defects.length, 'defect')} on build ${finished.build}`],
392
+ `Build a fix, then ${retest}`
393
+ )
394
+ if (gaps.length) return loop('needs_answers', 'oracle', [amount(gaps.length, 'open gap')], answer)
395
+ const result = runResult(store, finished.runId)!.result
396
+ const unpassed = result.tests.filter(
397
+ (test) => result.selectedTestIds.includes(test.testId) && test.outcome !== 'passed'
398
+ )
399
+ const why = [
400
+ ...(finished.revision !== plan.revision
401
+ ? [`${finished.runId} tested ${planId}@${finished.revision}, not ${subject}`]
402
+ : []),
403
+ ...(result.completion !== 'completed' ? [`${finished.runId} is ${result.completion}`] : []),
404
+ ...unpassed.map((test) => `${test.testId} is ${test.outcome} in ${finished.runId}`),
405
+ ...(finished.acceptance === 'rejected' ? [`Result ${finished.runId} is rejected`] : []),
406
+ ]
407
+ if (why.length)
408
+ return loop(
409
+ 'needs_retest',
410
+ 'qa',
411
+ why,
412
+ `checkmate run begin <runId> ${subject} --build ${finished.build} --partial ready_only`
413
+ )
414
+ if (finished.acceptance === 'pending')
415
+ return loop(
416
+ 'awaits_acceptance',
417
+ 'oracle',
418
+ [`${finished.runId} passed on build ${finished.build}`],
419
+ `checkmate result accept ${finished.runId} --rationale "<why>"`
420
+ )
421
+ if (finished.acceptance === 'approved') return loop('accepted', 'none', [`${finished.runId} is accepted`], null)
422
+ return loop('passed', 'none', [`${finished.runId} passed. Result acceptance is not required`], null)
423
+ }
424
+ return {
425
+ planId,
426
+ revision: plan.revision,
427
+ ...state(),
428
+ approval,
429
+ gaps,
430
+ pendingDecisions,
431
+ latestRun,
432
+ defects,
433
+ }
434
+ }
435
+
436
+ function loopOf(store: Store, planId: string) {
437
+ const { state, waitsOn } = planStatus(store, planId)
438
+ return { state, waitsOn }
439
+ }
440
+
441
+ export function retestedRuns(store: Store) {
442
+ return new Set(
443
+ all<{ run_id: string }>(
444
+ store,
445
+ "SELECT DISTINCT request ->> '$.retestOf' AS run_id FROM runs WHERE request ->> '$.retestOf' IS NOT NULL"
446
+ ).map((row) => row.run_id)
447
+ )
448
+ }
449
+
450
+ function amount(count: number, noun: string) {
451
+ return `${count} ${noun}${count === 1 ? '' : 's'}`
452
+ }
453
+
454
+ function loop(state: LoopState, waitsOn: Role, why: string[], next: string | null) {
455
+ return { state, waitsOn, why, next }
456
+ }
457
+
458
+ export function runList(store: Store, filters: { planId?: string; build?: string } = {}) {
459
+ const rows = all<RunRow & { completion: string | null; coverage: string | null; sha256: string | null }>(
460
+ store,
461
+ `SELECT r.*, s.completion, s.coverage, s.sha256 FROM runs r LEFT JOIN results s ON s.run_id = r.run_id
462
+ WHERE (? IS NULL OR r.plan_id = ?) AND (? IS NULL OR r.application_build = ?) ORDER BY r.rowid`,
463
+ filters.planId ?? null,
464
+ filters.planId ?? null,
465
+ filters.build ?? null,
466
+ filters.build ?? null
467
+ )
468
+ return rows.map((row) => ({
469
+ runId: row.run_id,
470
+ planId: row.plan_id,
471
+ revision: row.plan_revision,
472
+ build: row.application_build,
473
+ startedAt: row.started_at,
474
+ state: row.sha256 ? ('finalized' as const) : ('started' as const),
475
+ completion: row.completion,
476
+ coverage: row.coverage,
477
+ sha256: row.sha256,
478
+ acceptance: row.sha256 ? approvalLabel(resultAcceptance(store, row, row.sha256)) : null,
479
+ counts: Object.fromEntries(
480
+ all<{ outcome: string; count: number }>(
481
+ store,
482
+ 'SELECT outcome, count(*) AS count FROM outcomes WHERE run_id = ? GROUP BY outcome ORDER BY outcome',
483
+ row.run_id
484
+ ).map((count) => [count.outcome, count.count])
485
+ ) as Record<string, number>,
486
+ }))
487
+ }
488
+
489
+ export function runDetail(store: Store, runId: string) {
490
+ const run = runState(store, runId)
491
+ const outcomes = runOutcomes(store, runId)
492
+ const results = runAssertions(store, runId)
493
+ const evidence = runEvidence(store, runId).map((row) => ({
494
+ id: row.id,
495
+ testId: row.test_id,
496
+ assertionId: row.assertion_id,
497
+ kind: row.kind,
498
+ name: row.name,
499
+ mediaType: row.media_type,
500
+ sha256: row.sha256,
501
+ size: row.size,
502
+ }))
503
+ const finished = runResult(store, runId)
504
+ const attempts = all<{ attempt: number; started_at: string; actor: string }>(
505
+ store,
506
+ 'SELECT attempt, started_at, actor FROM run_attempts WHERE run_id = ? ORDER BY attempt',
507
+ runId
508
+ ).map((row) => ({ attempt: row.attempt, startedAt: row.started_at, actor: row.actor }))
509
+ const tests = run.plan.tests
510
+ .filter((test) => run.request.selectedTestIds.includes(test.id))
511
+ .map((test) => {
512
+ const closed = outcomes.get(test.id)
513
+ return {
514
+ testId: test.id,
515
+ title: test.title,
516
+ outcome: closed?.outcome ?? null,
517
+ reason: closed?.reason ?? null,
518
+ evidence: evidence.filter((item) => item.testId === test.id && item.assertionId === null),
519
+ assertions: test.assertions.map((assertion) => {
520
+ const row = results.get(`${test.id}.${assertion.id}`)
521
+ return {
522
+ assertionId: assertion.id,
523
+ check: assertion.check,
524
+ checkKind: assertion.checkKind,
525
+ result: row?.result ?? null,
526
+ observed: row?.observed ?? null,
527
+ reason: row?.reason ?? null,
528
+ evidence: evidence.filter(
529
+ (item) => item.testId === test.id && item.assertionId === assertion.id
530
+ ),
531
+ }
532
+ }),
533
+ }
534
+ })
535
+ return {
536
+ runId,
537
+ planId: run.row.plan_id,
538
+ revision: run.row.plan_revision,
539
+ planSha256: run.row.plan_sha256,
540
+ target: `${run.row.target_id}@${run.row.target_revision}`,
541
+ build: run.row.application_build,
542
+ startedAt: run.row.started_at,
543
+ browserSession: run.row.browser_session,
544
+ deadlineSeconds: run.row.deadline_seconds,
545
+ request: run.request,
546
+ state: finished ? ('finalized' as const) : ('started' as const),
547
+ attempts,
548
+ tests,
549
+ findings: runFindings(store, runId),
550
+ newGaps: runGaps(store, runId),
551
+ result: finished
552
+ ? {
553
+ sha256: finished.sha256,
554
+ completion: finished.result.completion,
555
+ coverage: finished.result.coverage,
556
+ cleanup: finished.result.cleanup,
557
+ finishedAt: finished.result.finishedAt,
558
+ acceptance: approvalLabel(resultAcceptance(store, run.row, finished.sha256)),
559
+ latestDecision: latestApproval(store, 'result', runId) ?? null,
560
+ }
561
+ : null,
562
+ }
563
+ }
564
+
565
+ type OperationRow = {
566
+ rowid: number
567
+ id: string
568
+ at: string
569
+ actor: string
570
+ command: string
571
+ arguments: string
572
+ status: string
573
+ response: string
574
+ subjects: string
575
+ }
576
+
577
+ export function operationLog(store: Store, filters: { subject?: string; limit?: number; after?: number } = {}) {
578
+ const limit = filters.limit ?? 50
579
+ requireCondition(Number.isInteger(limit) && limit > 0 && limit <= 1000, `Invalid limit: ${limit}. Use 1 to 1000.`)
580
+ return all<OperationRow>(
581
+ store,
582
+ `SELECT rowid, * FROM operations
583
+ WHERE (? IS NULL OR EXISTS (SELECT 1 FROM json_each(subjects) WHERE value = ?)) AND rowid > ?
584
+ ORDER BY rowid DESC LIMIT ?`,
585
+ filters.subject ?? null,
586
+ filters.subject ?? null,
587
+ filters.after ?? 0,
588
+ limit
589
+ ).map(operationRow)
590
+ }
591
+
592
+ function operationRow(row: OperationRow) {
593
+ return {
594
+ rowid: row.rowid,
595
+ id: row.id,
596
+ at: row.at,
597
+ actor: row.actor,
598
+ command: row.command,
599
+ status: row.status,
600
+ subjects: JSON.parse(row.subjects) as string[],
601
+ arguments: JSON.parse(row.arguments) as unknown,
602
+ response: JSON.parse(row.response) as unknown,
603
+ }
604
+ }
605
+
606
+ export function changesAfter(store: Store, rowid: number) {
607
+ return all<{ rowid: number; id: string; subjects: string }>(
608
+ store,
609
+ 'SELECT rowid, id, subjects FROM operations WHERE rowid > ? ORDER BY rowid',
610
+ rowid
611
+ ).map((row) => ({ rowid: row.rowid, id: row.id, subjects: JSON.parse(row.subjects) as string[] }))
612
+ }
613
+
614
+ export function evidenceBytes(store: Store, id: string) {
615
+ const row = one<{
616
+ id: string
617
+ run_id: string
618
+ test_id: string
619
+ assertion_id: string | null
620
+ kind: string
621
+ name: string
622
+ media_type: string
623
+ sha256: string
624
+ size: number
625
+ bytes: Uint8Array
626
+ }>(
627
+ store,
628
+ 'SELECT e.id, e.run_id, e.test_id, e.assertion_id, e.kind, e.name, e.media_type, e.sha256, b.size, b.bytes FROM evidence e JOIN blobs b ON b.sha256 = e.sha256 WHERE e.id = ?',
629
+ id
630
+ )
631
+ requireCondition(row, `Unknown evidence: ${id}`)
632
+ return {
633
+ id: row.id,
634
+ runId: row.run_id,
635
+ testId: row.test_id,
636
+ assertionId: row.assertion_id,
637
+ kind: row.kind,
638
+ name: row.name,
639
+ mediaType: row.media_type,
640
+ sha256: row.sha256,
641
+ size: row.size,
642
+ bytes: row.bytes,
643
+ }
644
+ }
645
+
646
+ export type ListQuery = {
647
+ limit?: number
648
+ offset?: number
649
+ sort?: string
650
+ direction?: 'asc' | 'desc'
651
+ q?: string
652
+ filters?: Record<string, string | undefined>
653
+ }
654
+ export type ListPage<T> = {
655
+ rows: T[]
656
+ total: number
657
+ limit: number
658
+ offset: number
659
+ sort: string
660
+ direction: 'asc' | 'desc'
661
+ }
662
+ type Sortable = string | number | null
663
+
664
+ const pageSizes = [25, 50, 100]
665
+
666
+ function listQuery(query: ListQuery, columns: string[], defaults: { sort: string; direction: 'asc' | 'desc' }) {
667
+ const limit = query.limit ?? 25
668
+ const offset = query.offset ?? 0
669
+ const sort = query.sort ?? defaults.sort
670
+ const direction = query.direction ?? (query.sort ? 'asc' : defaults.direction)
671
+ requireCondition(pageSizes.includes(limit), `Invalid limit: ${limit}. Use one of: ${pageSizes.join(', ')}.`)
672
+ requireCondition(Number.isInteger(offset) && offset >= 0, `Invalid offset: ${offset}. Use 0 or more.`)
673
+ requireChoice(sort, columns, 'sort column')
674
+ requireChoice(direction, ['asc', 'desc'], 'sort direction')
675
+ return { limit, offset, sort, direction, q: query.q?.trim().toLowerCase() ?? '' }
676
+ }
677
+
678
+ function filterValue(filters: ListQuery['filters'], names: Record<string, readonly string[] | null>) {
679
+ const values: Record<string, string> = {}
680
+ for (const [name, value] of Object.entries(filters ?? {})) {
681
+ if (value === undefined || value === '') continue
682
+ requireCondition(name in names, `Unknown filter: ${name}. Use: ${Object.keys(names).join(', ')}.`)
683
+ const choices = names[name]
684
+ if (choices) requireChoice(value, choices, `${name} filter`)
685
+ values[name] = value
686
+ }
687
+ return values
688
+ }
689
+
690
+ function pageOf<T>(
691
+ rows: T[],
692
+ query: ReturnType<typeof listQuery>,
693
+ columns: Record<string, (row: T) => Sortable>,
694
+ text: (row: T) => unknown[]
695
+ ): ListPage<T> {
696
+ const matched = query.q
697
+ ? rows.filter((row) =>
698
+ text(row).some((field) =>
699
+ String(field ?? '')
700
+ .toLowerCase()
701
+ .includes(query.q)
702
+ )
703
+ )
704
+ : rows
705
+ const key = columns[query.sort]
706
+ const order = query.direction === 'asc' ? 1 : -1
707
+ const position = new Map(matched.map((row, index) => [row, index]))
708
+ const sorted = [...matched].sort((a, b) => {
709
+ const x = key(a)
710
+ const y = key(b)
711
+ if (x === y) return (position.get(a)! - position.get(b)!) * order
712
+ if (x === null) return 1
713
+ if (y === null) return -1
714
+ return (x < y ? -1 : 1) * order
715
+ })
716
+ return {
717
+ rows: sorted.slice(query.offset, query.offset + query.limit),
718
+ total: matched.length,
719
+ limit: query.limit,
720
+ offset: query.offset,
721
+ sort: query.sort,
722
+ direction: query.direction,
723
+ }
724
+ }
725
+
726
+ const planShows = ['all', 'needs_decision', 'awaiting_approval', 'blocked'] as const
727
+ const approvalLabels = ['approved', 'rejected', 'pending', 'not_required'] as const
728
+
729
+ export function pagedPlans(store: Store, query: ListQuery = {}) {
730
+ type Row = ReturnType<typeof planList>[number]
731
+ const columns: Record<string, (row: Row) => Sortable> = {
732
+ plan: (row) => row.planId,
733
+ loop: (row) => row.loop.state,
734
+ tests: (row) => row.tests.total,
735
+ gaps: (row) => row.openGaps.length,
736
+ approval: (row) => row.approval ?? row.status,
737
+ blocked: (row) => row.blocked.length,
738
+ updated: (row) => row.since,
739
+ }
740
+ const options = listQuery(query, Object.keys(columns), { sort: 'plan', direction: 'asc' })
741
+ const filters = filterValue(query.filters, { show: planShows, approval: approvalLabels, waits: roles })
742
+ const rows = planList(store).filter(
743
+ (row) =>
744
+ (filters.approval === undefined || row.approval === filters.approval) &&
745
+ (filters.waits === undefined || row.loop.waitsOn === filters.waits) &&
746
+ (filters.show !== 'needs_decision' || row.openGaps.length > 0) &&
747
+ (filters.show !== 'awaiting_approval' || row.approval === 'pending') &&
748
+ (filters.show !== 'blocked' || row.blocked.length > 0 || row.openBlockingGaps.length > 0)
749
+ )
750
+ return pageOf(rows, options, columns, (row) => [
751
+ row.revision === null ? row.planId : `${row.planId}@${row.revision}`,
752
+ row.status,
753
+ row.loop.state,
754
+ `waits on ${row.loop.waitsOn}`,
755
+ `${row.tests.total} tests ${row.tests.ready} ready ${row.tests.blocked} blocked ${row.tests.deferred} deferred`,
756
+ ...row.openGaps,
757
+ row.approval,
758
+ ...row.blocked.map((block) => `${block.testId} ${block.dependency} ${block.cause}`),
759
+ row.since,
760
+ ])
761
+ }
762
+
763
+ const runStatuses = ['started', 'pending', 'approved', 'rejected', 'not_required'] as const
764
+
765
+ function runStatus(row: ReturnType<typeof runList>[number]) {
766
+ return row.state === 'started' ? 'started' : (row.acceptance ?? 'finalized')
767
+ }
768
+
769
+ export function pagedRuns(store: Store, query: ListQuery = {}) {
770
+ type Row = ReturnType<typeof runList>[number]
771
+ const columns: Record<string, (row: Row) => Sortable> = {
772
+ run: (row) => row.runId,
773
+ plan: (row) => `${row.planId}@${String(row.revision).padStart(9, '0')}`,
774
+ build: (row) => row.build,
775
+ completion: (row) => row.completion,
776
+ coverage: (row) => row.coverage,
777
+ status: (row) => runStatus(row),
778
+ started: (row) => row.startedAt,
779
+ }
780
+ const options = listQuery(query, Object.keys(columns), { sort: 'started', direction: 'desc' })
781
+ const filters = filterValue(query.filters, { plan: null, build: null, status: runStatuses })
782
+ const rows = runList(store, { planId: filters.plan, build: filters.build }).filter(
783
+ (row) => filters.status === undefined || runStatus(row) === filters.status
784
+ )
785
+ return pageOf(rows, options, columns, (row) => [
786
+ row.runId,
787
+ `${row.planId}@${row.revision}`,
788
+ row.build,
789
+ ...Object.entries(row.counts).map(([outcome, count]) => `${count} ${outcome}`),
790
+ row.completion,
791
+ row.coverage,
792
+ runStatus(row),
793
+ row.startedAt,
794
+ ])
795
+ }
796
+
797
+ const logColumns: Record<string, string> = {
798
+ time: 'rowid',
799
+ actor: 'actor',
800
+ command: 'command',
801
+ status: 'status',
802
+ }
803
+
804
+ export function pagedOperations(
805
+ store: Store,
806
+ query: ListQuery = {}
807
+ ): ListPage<ReturnType<typeof operationLog>[number]> {
808
+ const options = listQuery(query, Object.keys(logColumns), { sort: 'time', direction: 'desc' })
809
+ const filters = filterValue(query.filters, { subject: null, actor: null, command: null, status: null })
810
+ const where = `WHERE (:subject IS NULL OR EXISTS (SELECT 1 FROM json_each(subjects) WHERE value = :subject))
811
+ AND (:actor IS NULL OR actor = :actor) AND (:command IS NULL OR command = :command)
812
+ AND (:status IS NULL OR status = :status)
813
+ AND (:q = '' OR instr(lower(at || ' ' || actor || ' ' || command || ' ' || status || ' ' || subjects || ' ' || id
814
+ || ' ' || coalesce(response ->> '$.reasons', '')), :q) > 0)`
815
+ const parameters = {
816
+ subject: filters.subject ?? null,
817
+ actor: filters.actor ?? null,
818
+ command: filters.command ?? null,
819
+ status: filters.status ?? null,
820
+ q: options.q,
821
+ }
822
+ const total = (
823
+ store.db.prepare(`SELECT count(*) AS total FROM operations ${where}`).get(parameters) as { total: number }
824
+ ).total
825
+ const direction = options.direction === 'asc' ? 'ASC' : 'DESC'
826
+ const rows = store.db
827
+ .prepare(
828
+ `SELECT rowid, * FROM operations ${where} ORDER BY ${logColumns[options.sort]} ${direction}, rowid ${direction} LIMIT :limit OFFSET :offset`
829
+ )
830
+ .all({ ...parameters, limit: options.limit, offset: options.offset }) as OperationRow[]
831
+ return {
832
+ rows: rows.map(operationRow),
833
+ total,
834
+ limit: options.limit,
835
+ offset: options.offset,
836
+ sort: options.sort,
837
+ direction: options.direction,
838
+ }
839
+ }