@xoxoai/checkmate 0.6.7 → 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,868 @@
1
+ import type { Gap, Plan, Result } from './types.ts'
2
+ import type { Store } from './store.ts'
3
+ import { all, canonical, completed, digest, execute, one, removeTemporaryDirectory, ulid, write } from './store.ts'
4
+ import type { AssertionResult, ResultTest, Test } from './contracts.ts'
5
+ import {
6
+ approvalPolicy,
7
+ expectDigest,
8
+ requireChoice,
9
+ requireCondition,
10
+ requireIdentifier,
11
+ requireText,
12
+ resultCoverage,
13
+ testOutcome,
14
+ testReference,
15
+ validate,
16
+ } from './contracts.ts'
17
+ import { approvalState, dependencyBlock, gate, latestPlans, recordApproval } from './plans.ts'
18
+ import { targetRevision } from './targets.ts'
19
+ import { mkdirSync, readFileSync, realpathSync, rmSync, statSync } from 'node:fs'
20
+ import { basename, extname, isAbsolute, join, resolve, sep } from 'node:path'
21
+ import { randomUUID } from 'node:crypto'
22
+
23
+ export type RunRow = {
24
+ run_id: string
25
+ plan_id: string
26
+ plan_revision: number
27
+ plan_sha256: string
28
+ target_id: string
29
+ target_revision: number
30
+ application_build: string
31
+ request: string
32
+ started_at: string
33
+ browser_session: string
34
+ deadline_seconds: number
35
+ }
36
+ export type RunRequest = {
37
+ selectedTestIds: string[]
38
+ partialExecution: string
39
+ onNewBlocker: string
40
+ timeoutSeconds: number
41
+ retestOf: string | null
42
+ }
43
+ export type RunState = { row: RunRow; request: RunRequest; plan: Plan; finished: boolean }
44
+ export type OutcomeRow = { test_id: string; attempt: number; outcome: ResultTest['outcome']; reason: string | null }
45
+ export type AssertionRow = {
46
+ test_id: string
47
+ assertion_id: string
48
+ attempt: number
49
+ result: AssertionResult
50
+ observed: string | null
51
+ reason: string | null
52
+ }
53
+ export type EvidenceRow = {
54
+ id: string
55
+ test_id: string
56
+ assertion_id: string | null
57
+ kind: Result['evidence'][number]['kind']
58
+ name: string
59
+ media_type: string
60
+ sha256: string
61
+ size: number
62
+ }
63
+ export type BeginInput = {
64
+ runId: string
65
+ reference: string
66
+ tests?: string[]
67
+ build: string
68
+ partial?: string
69
+ onNewBlocker?: string
70
+ timeoutSeconds?: number
71
+ retestOf?: string | null
72
+ }
73
+ export type EvidenceInput = string | { text: string; kind?: string }
74
+ export type CheckInput = {
75
+ runId: string
76
+ testId: string
77
+ assertionId: string
78
+ result: string
79
+ observed?: string
80
+ reason?: string
81
+ evidence?: EvidenceInput[]
82
+ }
83
+ export type CloseInput = {
84
+ runId: string
85
+ testId: string
86
+ outcome?: string
87
+ reason?: string
88
+ evidence?: EvidenceInput[]
89
+ }
90
+ export type FinishInput = { runId: string; completion: string; cleanup: string; cleanupReason: string }
91
+ export type ResumeInput = { runId: string; error?: string[]; reason?: string }
92
+ export type ResultVerdict = { runId: string; rationale: string; expectSha256?: string }
93
+
94
+ export const maxEvidenceBytes = 32 * 1024 * 1024
95
+ export const stoppedReason = 'Run stopped before the test closed. Actions may have occurred.'
96
+ const kindByExtension: Record<string, EvidenceRow['kind']> = {
97
+ '.png': 'screenshot',
98
+ '.jpg': 'screenshot',
99
+ '.jpeg': 'screenshot',
100
+ '.webp': 'screenshot',
101
+ '.json': 'http_exchange',
102
+ '.zip': 'trace',
103
+ '.trace': 'trace',
104
+ }
105
+ const mediaTypes: Record<string, string> = {
106
+ '.png': 'image/png',
107
+ '.jpg': 'image/jpeg',
108
+ '.jpeg': 'image/jpeg',
109
+ '.webp': 'image/webp',
110
+ '.gif': 'image/gif',
111
+ '.json': 'application/json',
112
+ '.txt': 'text/plain',
113
+ '.log': 'text/plain',
114
+ '.md': 'text/markdown',
115
+ '.html': 'text/html',
116
+ '.zip': 'application/zip',
117
+ }
118
+
119
+ export function evidenceDirectory(runId: string) {
120
+ return `.checkmate/tmp/${runId}/`
121
+ }
122
+
123
+ export function runState(store: Store, runId: string): RunState {
124
+ const row = one<RunRow>(store, 'SELECT * FROM runs WHERE run_id = ?', runId)
125
+ requireCondition(row, `Unknown run: ${runId}`)
126
+ const plan = one<{ body: string }>(
127
+ store,
128
+ 'SELECT body FROM plan_revisions WHERE plan_id = ? AND revision = ?',
129
+ row.plan_id,
130
+ row.plan_revision
131
+ )!
132
+ return {
133
+ row,
134
+ request: JSON.parse(row.request),
135
+ plan: JSON.parse(plan.body),
136
+ finished: Boolean(one(store, 'SELECT 1 FROM results WHERE run_id = ?', runId)),
137
+ }
138
+ }
139
+
140
+ function startedRun(store: Store, runId: string) {
141
+ const run = runState(store, runId)
142
+ requireCondition(!run.finished, `Run ${runId} is finished. A finished run does not change.`)
143
+ return run
144
+ }
145
+
146
+ function selectedTest(run: RunState, testId: string): Test {
147
+ requireCondition(
148
+ run.request.selectedTestIds.includes(testId),
149
+ `Test ${testId} is not selected in run ${run.row.run_id}`
150
+ )
151
+ return run.plan.tests.find((test) => test.id === testId)!
152
+ }
153
+
154
+ export function currentAttempt(store: Store, runId: string) {
155
+ return one<{ attempt: number }>(store, 'SELECT max(attempt) AS attempt FROM run_attempts WHERE run_id = ?', runId)!
156
+ .attempt
157
+ }
158
+
159
+ export function runOutcomes(store: Store, runId: string) {
160
+ const rows = all<OutcomeRow>(store, 'SELECT * FROM outcomes WHERE run_id = ? ORDER BY rowid', runId)
161
+ return new Map(rows.map((row) => [row.test_id, row]))
162
+ }
163
+
164
+ export function runAssertions(store: Store, runId: string) {
165
+ const rows = all<AssertionRow>(store, 'SELECT * FROM assertion_results WHERE run_id = ? ORDER BY rowid', runId)
166
+ return new Map(rows.map((row) => [`${row.test_id}.${row.assertion_id}`, row]))
167
+ }
168
+
169
+ export function runEvidence(store: Store, runId: string) {
170
+ return all<EvidenceRow>(
171
+ store,
172
+ 'SELECT e.id, e.test_id, e.assertion_id, e.kind, e.name, e.media_type, e.sha256, b.size FROM evidence e JOIN blobs b ON b.sha256 = e.sha256 WHERE e.run_id = ? ORDER BY e.rowid',
173
+ runId
174
+ )
175
+ }
176
+
177
+ export function runFindings(store: Store, runId: string): Result['findings'] {
178
+ return all<{ body: string }>(store, 'SELECT body FROM findings WHERE run_id = ? ORDER BY rowid', runId).map((row) =>
179
+ JSON.parse(row.body)
180
+ )
181
+ }
182
+
183
+ export function runGaps(store: Store, runId: string): Gap[] {
184
+ return all<{ body: string }>(store, 'SELECT body FROM run_gaps WHERE run_id = ? ORDER BY rowid', runId).map((row) =>
185
+ JSON.parse(row.body)
186
+ )
187
+ }
188
+
189
+ export function beginRun(store: Store, actor: string | undefined, input: BeginInput) {
190
+ const reply = write(
191
+ store,
192
+ { actor, command: 'run begin', subject: input.runId, input },
193
+ (operationId, at, actor) => {
194
+ const runId = requireIdentifier(input.runId, 'run ID')
195
+ requireCondition(
196
+ !one(store, 'SELECT 1 FROM runs WHERE run_id = ?', runId),
197
+ `Run ${runId} exists. Use a new run ID.`
198
+ )
199
+ const onNewBlocker = requireChoice(
200
+ input.onNewBlocker ?? 'return_to_oracle',
201
+ ['return_to_oracle', 'continue_independent'],
202
+ 'new blocker policy (--on-new-blocker)'
203
+ )
204
+ const timeoutSeconds = input.timeoutSeconds ?? 600
205
+ requireCondition(
206
+ Number.isInteger(timeoutSeconds) && timeoutSeconds > 0 && timeoutSeconds <= 3600,
207
+ `Invalid timeout: ${timeoutSeconds}. Use 1 to 3600 seconds.`
208
+ )
209
+ const checked = gate(store, input)
210
+ if (checked.reply.status !== 'completed')
211
+ return { reply: { ...checked.reply, subject: runId }, subjects: [] }
212
+ const { plan, sha256 } = checked.revision
213
+ const request: RunRequest = {
214
+ selectedTestIds: checked.tests,
215
+ partialExecution: checked.partial,
216
+ onNewBlocker,
217
+ timeoutSeconds,
218
+ retestOf: input.retestOf ?? null,
219
+ }
220
+ const browserSession = `cm-${randomUUID()}`
221
+ execute(
222
+ store,
223
+ 'INSERT INTO runs (run_id, plan_id, plan_revision, plan_sha256, target_id, target_revision, application_build, request, started_at, browser_session, deadline_seconds, operation_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
224
+ runId,
225
+ plan.planId,
226
+ plan.revision,
227
+ sha256,
228
+ plan.target.targetId,
229
+ plan.target.revision,
230
+ checked.build,
231
+ canonical(request),
232
+ at,
233
+ browserSession,
234
+ timeoutSeconds,
235
+ operationId
236
+ )
237
+ execute(
238
+ store,
239
+ 'INSERT INTO run_attempts (run_id, attempt, started_at, actor) VALUES (?, 1, ?, ?)',
240
+ runId,
241
+ at,
242
+ actor
243
+ )
244
+ return {
245
+ reply: completed(runId, {
246
+ runId,
247
+ plan: `${plan.planId}@${plan.revision}`,
248
+ tests: checked.tests,
249
+ leftOut: checked.reply.data.leftOut,
250
+ startedAt: at,
251
+ browserSession,
252
+ deadlineSeconds: timeoutSeconds,
253
+ evidenceDirectory: evidenceDirectory(runId),
254
+ }),
255
+ subjects: [`run:${runId}`, `plan:${plan.planId}`],
256
+ }
257
+ }
258
+ )
259
+ if (reply.status === 'completed')
260
+ mkdirSync(join(store.workspace, evidenceDirectory(input.runId)), { recursive: true })
261
+ return reply
262
+ }
263
+
264
+ type EvidenceFile = { path: string | null; name: string; kind: string; mediaType: string; bytes: Buffer }
265
+
266
+ const evidenceKinds = ['observation', 'http_exchange', 'screenshot', 'trace'] as const
267
+
268
+ function textEvidence(input: { text: string; kind?: string }, name: string): EvidenceFile {
269
+ requireCondition(
270
+ typeof input.text === 'string' && input.text.trim().length > 0,
271
+ 'Missing evidence text on standard input'
272
+ )
273
+ const bytes = Buffer.from(input.text, 'utf8')
274
+ requireCondition(bytes.length <= maxEvidenceBytes, 'Evidence text exceeds 32 MiB')
275
+ let json = true
276
+ try {
277
+ JSON.parse(input.text)
278
+ } catch {
279
+ json = false
280
+ }
281
+ return {
282
+ path: null,
283
+ name: `${name}.${json ? 'json' : 'txt'}`,
284
+ kind: requireChoice(input.kind ?? 'observation', evidenceKinds, 'evidence kind'),
285
+ mediaType: json ? 'application/json' : 'text/plain',
286
+ bytes,
287
+ }
288
+ }
289
+
290
+ function readEvidence(store: Store, runId: string, spec: string): EvidenceFile {
291
+ const match = /^(.*):(observation|http_exchange|screenshot|trace)$/.exec(spec)
292
+ const name = match ? match[1] : spec
293
+ requireCondition(name && !isAbsolute(name), `Evidence path must be relative to the workspace: ${name}`)
294
+ const directory = evidenceDirectory(runId)
295
+ let root: string
296
+ let real: string
297
+ try {
298
+ root = realpathSync(join(store.workspace, directory))
299
+ } catch {
300
+ throw new Error(`Evidence directory ${directory} does not exist`)
301
+ }
302
+ const path = resolve(store.workspace, name)
303
+ try {
304
+ real = realpathSync(path)
305
+ } catch {
306
+ throw new Error(`Evidence file not found: ${name}`)
307
+ }
308
+ requireCondition(real.startsWith(root + sep), `Evidence file must be inside ${directory}: ${name}`)
309
+ const stats = statSync(real)
310
+ requireCondition(stats.isFile(), `Evidence must be a file: ${name}`)
311
+ requireCondition(stats.size <= maxEvidenceBytes, `Evidence file exceeds 32 MiB: ${name}`)
312
+ const extension = extname(name).toLowerCase()
313
+ return {
314
+ path,
315
+ name: basename(name),
316
+ kind: match ? match[2] : (kindByExtension[extension] ?? 'observation'),
317
+ mediaType: mediaTypes[extension] ?? 'application/octet-stream',
318
+ bytes: readFileSync(real),
319
+ }
320
+ }
321
+
322
+ function storeEvidence(
323
+ store: Store,
324
+ runId: string,
325
+ testId: string,
326
+ assertionId: string | null,
327
+ files: EvidenceFile[],
328
+ operationId: string
329
+ ) {
330
+ return files.map((file) => {
331
+ const sha256 = digest(file.bytes)
332
+ execute(
333
+ store,
334
+ 'INSERT OR IGNORE INTO blobs (sha256, size, bytes) VALUES (?, ?, ?)',
335
+ sha256,
336
+ file.bytes.length,
337
+ file.bytes
338
+ )
339
+ const id = ulid()
340
+ execute(
341
+ store,
342
+ 'INSERT INTO evidence (id, run_id, test_id, assertion_id, kind, name, media_type, sha256, operation_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)',
343
+ id,
344
+ runId,
345
+ testId,
346
+ assertionId,
347
+ file.kind,
348
+ file.name,
349
+ file.mediaType,
350
+ sha256,
351
+ operationId
352
+ )
353
+ return id
354
+ })
355
+ }
356
+
357
+ function evidenceFiles(store: Store, runId: string, name: string, specs: EvidenceInput[] = []) {
358
+ const files = specs.map((spec) =>
359
+ typeof spec === 'string' ? readEvidence(store, runId, spec) : textEvidence(spec, name)
360
+ )
361
+ const paths = files.flatMap((file) => file.path ?? [])
362
+ requireCondition(new Set(paths).size === paths.length, 'An evidence file is listed twice')
363
+ requireCondition(files.length - paths.length <= 1, 'Give one evidence text on standard input')
364
+ return files
365
+ }
366
+
367
+ function removeFiles(files: EvidenceFile[]) {
368
+ for (const file of files) if (file.path) rmSync(file.path, { force: true })
369
+ }
370
+
371
+ function optionalText(value: string | undefined, name: string) {
372
+ return value === undefined ? null : requireText(value, name)
373
+ }
374
+
375
+ function requireOpen(store: Store, runId: string, testId: string) {
376
+ const closed = runOutcomes(store, runId).get(testId)
377
+ requireCondition(!closed, `Test ${testId} is closed as ${closed?.outcome}. A test outcome is final.`)
378
+ }
379
+
380
+ export function checkAssertion(store: Store, actor: string | undefined, input: CheckInput) {
381
+ let files: EvidenceFile[] = []
382
+ const reply = write(store, { actor, command: 'run check', subject: input.runId, input }, (operationId, at) => {
383
+ const run = startedRun(store, input.runId)
384
+ const test = selectedTest(run, input.testId)
385
+ const assertion = test.assertions.find((assertion) => assertion.id === input.assertionId)
386
+ requireCondition(assertion, `Unknown assertion ${test.id}.${input.assertionId}`)
387
+ requireOpen(store, input.runId, test.id)
388
+ requireCondition(
389
+ !runAssertions(store, input.runId).has(`${test.id}.${assertion.id}`),
390
+ `Assertion ${test.id}.${assertion.id} has a result. An assertion result is final.`
391
+ )
392
+ const outcomes = runOutcomes(store, input.runId)
393
+ for (const dependency of test.dependsOn.filter((id) => !id.includes(':')))
394
+ requireCondition(
395
+ outcomes.get(dependency)?.outcome === 'passed',
396
+ `Prerequisite ${dependency} has not passed. Close ${dependency} as passed before you check ${test.id}.`
397
+ )
398
+ const result = requireChoice(
399
+ input.result,
400
+ ['passed', 'failed', 'inconclusive', 'not_checked'] as const,
401
+ 'result (--result)'
402
+ )
403
+ const observed = optionalText(input.observed, 'observation (--observed)')
404
+ const reason = optionalText(input.reason, 'reason (--reason)')
405
+ files = evidenceFiles(store, input.runId, `${test.id}-${assertion.id}`, input.evidence)
406
+ if (result !== 'not_checked')
407
+ requireCondition(
408
+ observed && files.length > 0,
409
+ `${result === 'inconclusive' ? 'An' : 'A'} ${result} result needs an observation (--observed) and one or more evidence items (--evidence)`
410
+ )
411
+ if (result === 'inconclusive')
412
+ requireCondition(
413
+ reason,
414
+ 'An inconclusive result needs a reason (--reason) that says what the evidence cannot show'
415
+ )
416
+ if (result === 'not_checked')
417
+ requireCondition(reason, 'A not_checked result needs a reason (--reason) that says why it was not checked')
418
+ execute(
419
+ store,
420
+ 'INSERT INTO assertion_results (run_id, test_id, assertion_id, attempt, result, observed, reason, recorded_at, operation_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)',
421
+ input.runId,
422
+ test.id,
423
+ assertion.id,
424
+ currentAttempt(store, input.runId),
425
+ result,
426
+ observed,
427
+ reason,
428
+ at,
429
+ operationId
430
+ )
431
+ const evidenceIds = storeEvidence(store, input.runId, test.id, assertion.id, files, operationId)
432
+ return {
433
+ reply: completed(input.runId, {
434
+ runId: input.runId,
435
+ testId: test.id,
436
+ assertionId: assertion.id,
437
+ result,
438
+ evidenceIds,
439
+ }),
440
+ subjects: [`run:${input.runId}`],
441
+ }
442
+ })
443
+ removeFiles(files)
444
+ return reply
445
+ }
446
+
447
+ export function closeTest(store: Store, actor: string | undefined, input: CloseInput) {
448
+ let files: EvidenceFile[] = []
449
+ const reply = write(store, { actor, command: 'run close', subject: input.runId, input }, (operationId, at) => {
450
+ const run = startedRun(store, input.runId)
451
+ const test = selectedTest(run, input.testId)
452
+ requireOpen(store, input.runId, test.id)
453
+ const results = runAssertions(store, input.runId)
454
+ const recorded = test.assertions.flatMap((assertion) => results.get(`${test.id}.${assertion.id}`) ?? [])
455
+ let outcome: ResultTest['outcome']
456
+ let reason: string | null
457
+ if (input.outcome === undefined) {
458
+ requireCondition(input.reason === undefined, '--reason applies only with --outcome')
459
+ requireCondition(!input.evidence?.length, '--evidence applies only with --outcome error')
460
+ const missing = test.assertions.filter((assertion) => !results.has(`${test.id}.${assertion.id}`))
461
+ requireCondition(
462
+ !missing.length,
463
+ `Assertions without a result: ${missing.map((assertion) => `${test.id}.${assertion.id}`).join(', ')}`
464
+ )
465
+ outcome = testOutcome(recorded.map((row) => row.result))
466
+ const unchecked = recorded.filter((row) => row.result === 'not_checked')
467
+ reason =
468
+ outcome === 'error'
469
+ ? `Assertions not checked: ${unchecked.map((row) => `${test.id}.${row.assertion_id}`).join(', ')}`
470
+ : null
471
+ } else {
472
+ outcome = requireChoice(input.outcome, ['blocked', 'not_run', 'error'] as const, 'outcome (--outcome)')
473
+ reason = requireText(input.reason, `reason (--reason) for outcome ${outcome}`)
474
+ requireCondition(
475
+ outcome === 'error' || !recorded.length,
476
+ `Test ${test.id} has assertion results. Record the remaining assertions, or close it as error.`
477
+ )
478
+ requireCondition(
479
+ outcome === 'error' || !input.evidence?.length,
480
+ '--evidence applies only with --outcome error'
481
+ )
482
+ files = evidenceFiles(store, input.runId, test.id, input.evidence)
483
+ }
484
+ execute(
485
+ store,
486
+ 'INSERT INTO outcomes (run_id, test_id, attempt, outcome, reason, recorded_at, operation_id) VALUES (?, ?, ?, ?, ?, ?, ?)',
487
+ input.runId,
488
+ test.id,
489
+ currentAttempt(store, input.runId),
490
+ outcome,
491
+ reason,
492
+ at,
493
+ operationId
494
+ )
495
+ const evidenceIds = storeEvidence(store, input.runId, test.id, null, files, operationId)
496
+ return {
497
+ reply: completed(input.runId, { runId: input.runId, testId: test.id, outcome, reason, evidenceIds }),
498
+ subjects: [`run:${input.runId}`],
499
+ }
500
+ })
501
+ removeFiles(files)
502
+ return reply
503
+ }
504
+
505
+ export function recordFinding(store: Store, actor: string | undefined, input: { runId: string; finding: unknown }) {
506
+ return write(store, { actor, command: 'run finding', subject: input.runId, input }, (operationId) => {
507
+ const run = startedRun(store, input.runId)
508
+ const finding = validate('finding', input.finding)
509
+ requireCondition(
510
+ !one(store, 'SELECT 1 FROM findings WHERE run_id = ? AND id = ?', input.runId, finding.id),
511
+ `Finding ${finding.id} exists in run ${input.runId}`
512
+ )
513
+ const results = runAssertions(store, input.runId)
514
+ const named = finding.assertionIds.map((reference) => {
515
+ const [testId, assertionId] = reference.split('.')
516
+ const test = selectedTest(run, testId)
517
+ requireCondition(
518
+ test.assertions.some((assertion) => assertion.id === assertionId),
519
+ `Unknown assertion in finding ${finding.id}: ${reference}`
520
+ )
521
+ if (finding.type === 'defect')
522
+ requireCondition(
523
+ results.get(reference)?.result === 'failed',
524
+ `Defect ${finding.id} names ${reference}, which is not failed. A defect names only failed assertions.`
525
+ )
526
+ return { test, assertionId }
527
+ })
528
+ const testIds = run.plan.tests
529
+ .filter((test) => named.some((item) => item.test.id === test.id))
530
+ .map((test) => test.id)
531
+ for (const requirementId of finding.requirementIds)
532
+ for (const { test } of named)
533
+ requireCondition(
534
+ test.requirementIds.includes(requirementId),
535
+ `Finding ${finding.id} names requirement ${requirementId}, which test ${test.id} does not cover`
536
+ )
537
+ const evidence = new Map(runEvidence(store, input.runId).map((row) => [row.id, row]))
538
+ for (const id of finding.evidenceIds) {
539
+ const row = evidence.get(id)
540
+ requireCondition(row, `Unknown evidence in finding ${finding.id}: ${id}`)
541
+ requireCondition(
542
+ testIds.includes(row.test_id) &&
543
+ (row.assertion_id === null || finding.assertionIds.includes(`${row.test_id}.${row.assertion_id}`)),
544
+ `Evidence ${id} does not belong to the assertions or tests of finding ${finding.id}`
545
+ )
546
+ }
547
+ execute(
548
+ store,
549
+ 'INSERT INTO findings (id, run_id, body, operation_id) VALUES (?, ?, ?, ?)',
550
+ finding.id,
551
+ input.runId,
552
+ canonical({ ...finding, testIds }),
553
+ operationId
554
+ )
555
+ return {
556
+ reply: completed(input.runId, { runId: input.runId, findingId: finding.id, testIds }),
557
+ subjects: [`run:${input.runId}`],
558
+ }
559
+ })
560
+ }
561
+
562
+ export function recordGap(store: Store, actor: string | undefined, input: { runId: string; gap: unknown }) {
563
+ return write(store, { actor, command: 'run gap', subject: input.runId, input }, (operationId) => {
564
+ const run = startedRun(store, input.runId)
565
+ const gap = validate('gap', input.gap)
566
+ requireCondition(
567
+ gap.status === 'open' && gap.decisionId === null,
568
+ `Runtime gap ${gap.id} must be open and have no decision`
569
+ )
570
+ requireCondition(
571
+ !run.plan.gaps.some((old) => old.id === gap.id),
572
+ `Gap ${gap.id} exists in ${run.plan.planId}@${run.plan.revision}. Use a new gap ID.`
573
+ )
574
+ requireCondition(
575
+ !one(store, 'SELECT 1 FROM run_gaps WHERE run_id = ? AND id = ?', input.runId, gap.id),
576
+ `Gap ${gap.id} exists in run ${input.runId}. Use a new gap ID.`
577
+ )
578
+ for (const id of gap.requirementIds)
579
+ requireCondition(
580
+ run.plan.requirements.some((requirement) => requirement.id === id),
581
+ `Unknown requirement in gap ${gap.id}: ${id}`
582
+ )
583
+ const plans = latestPlans(store)
584
+ for (const reference of gap.testIds) {
585
+ const { planId, testId } = testReference(reference)
586
+ const plan = planId === null ? run.plan : plans.get(planId)
587
+ requireCondition(
588
+ plan?.tests.some((test) => test.id === testId),
589
+ `Unknown test in gap ${gap.id}: ${reference}`
590
+ )
591
+ }
592
+ execute(
593
+ store,
594
+ 'INSERT INTO run_gaps (id, run_id, body, operation_id) VALUES (?, ?, ?, ?)',
595
+ gap.id,
596
+ input.runId,
597
+ canonical(gap),
598
+ operationId
599
+ )
600
+ return {
601
+ reply: completed(input.runId, { runId: input.runId, gapId: gap.id }),
602
+ subjects: [`run:${input.runId}`],
603
+ }
604
+ })
605
+ }
606
+
607
+ export function finishRun(store: Store, actor: string | undefined, input: FinishInput) {
608
+ const reply = finish(store, actor, input)
609
+ removeTemporaryDirectory(store, input.runId)
610
+ return reply
611
+ }
612
+
613
+ function finish(store: Store, actor: string | undefined, input: FinishInput) {
614
+ return write(store, { actor, command: 'run finish', subject: input.runId, input }, (operationId, at) => {
615
+ const run = startedRun(store, input.runId)
616
+ const completion = requireChoice(
617
+ input.completion,
618
+ ['completed', 'interrupted', 'operational_error'] as const,
619
+ 'completion (--completion)'
620
+ )
621
+ const cleanup = {
622
+ status: requireChoice(
623
+ input.cleanup,
624
+ ['completed', 'failed', 'not_applicable'] as const,
625
+ 'cleanup (--cleanup)'
626
+ ),
627
+ reason: requireText(input.cleanupReason, 'cleanup reason (--cleanup-reason)'),
628
+ }
629
+ const selected = run.request.selectedTestIds
630
+ const attempt = currentAttempt(store, input.runId)
631
+ const unclosed = selected.filter((id) => !runOutcomes(store, input.runId).has(id))
632
+ if (completion === 'completed')
633
+ requireCondition(
634
+ !unclosed.length,
635
+ `Selected tests are not closed: ${unclosed.join(', ')}. Close them, or finish as interrupted.`
636
+ )
637
+ for (const testId of unclosed)
638
+ execute(
639
+ store,
640
+ 'INSERT INTO outcomes (run_id, test_id, attempt, outcome, reason, recorded_at, operation_id) VALUES (?, ?, ?, ?, ?, ?, ?)',
641
+ input.runId,
642
+ testId,
643
+ attempt,
644
+ 'error',
645
+ stoppedReason,
646
+ at,
647
+ operationId
648
+ )
649
+ const outcomes = runOutcomes(store, input.runId)
650
+ const results = runAssertions(store, input.runId)
651
+ const findings = runFindings(store, input.runId)
652
+ const newGaps = runGaps(store, input.runId)
653
+ for (const row of results.values())
654
+ if (row.result === 'failed')
655
+ requireCondition(
656
+ findings.some(
657
+ (finding) =>
658
+ finding.type === 'defect' &&
659
+ finding.assertionIds.includes(`${row.test_id}.${row.assertion_id}`)
660
+ ),
661
+ `Failed assertion ${row.test_id}.${row.assertion_id} needs a defect finding. Record it with run finding.`
662
+ )
663
+ if (completion === 'completed') {
664
+ const failures = selected.filter((id) => outcomes.get(id)!.outcome === 'error')
665
+ requireCondition(
666
+ !failures.length,
667
+ `Operational failure cannot be completed: ${failures.join(', ')} closed as error`
668
+ )
669
+ requireCondition(cleanup.status !== 'failed', 'Cleanup failed, so the run cannot be completed')
670
+ for (const test of run.plan.tests.filter((test) => selected.includes(test.id))) {
671
+ const outcome = outcomes.get(test.id)!.outcome
672
+ if (outcome !== 'not_run' && outcome !== 'blocked') continue
673
+ const cause =
674
+ test.dependsOn.some((id) =>
675
+ id.includes(':')
676
+ ? dependencyBlock(store, id, run.row.application_build) !== null
677
+ : outcomes.get(id)?.outcome !== 'passed'
678
+ ) || newGaps.some((gap) => gap.severity === 'blocking')
679
+ requireCondition(
680
+ cause,
681
+ `Selected test ${test.id} is ${outcome} without a cause. A completed run needs a failed prerequisite or a new blocking gap.`
682
+ )
683
+ }
684
+ }
685
+ const evidence = runEvidence(store, input.runId)
686
+ const tests: ResultTest[] = run.plan.tests.map((test) => {
687
+ const closed = outcomes.get(test.id)
688
+ if (!closed)
689
+ return { testId: test.id, outcome: 'not_run', reason: 'Test was not selected.', assertions: [] }
690
+ return {
691
+ testId: test.id,
692
+ outcome: closed.outcome,
693
+ reason: closed.reason,
694
+ assertions: test.assertions.flatMap((assertion) => {
695
+ const row = results.get(`${test.id}.${assertion.id}`)
696
+ if (!row) return []
697
+ return {
698
+ assertionId: assertion.id,
699
+ check: assertion.check,
700
+ checkKind: assertion.checkKind,
701
+ result: row.result,
702
+ observed: row.observed,
703
+ reason: row.reason,
704
+ evidenceIds: evidence
705
+ .filter((item) => item.test_id === test.id && item.assertion_id === assertion.id)
706
+ .map((item) => item.id),
707
+ }
708
+ }),
709
+ }
710
+ })
711
+ const coverage = resultCoverage(run.plan, tests, newGaps)
712
+ const result: Result = validate('result', {
713
+ contractVersion: 'toolkit-2',
714
+ runId: input.runId,
715
+ plan: { planId: run.plan.planId, revision: run.plan.revision, sha256: run.row.plan_sha256 },
716
+ target: run.plan.target,
717
+ applicationBuild: run.row.application_build,
718
+ selectedTestIds: selected,
719
+ retestOf: run.request.retestOf,
720
+ startedAt: run.row.started_at,
721
+ finishedAt: at,
722
+ attempts: attempt,
723
+ completion,
724
+ tests,
725
+ findings,
726
+ newGaps,
727
+ evidence: evidence.map((row) => ({
728
+ id: row.id,
729
+ testId: row.test_id,
730
+ assertionId: row.assertion_id,
731
+ kind: row.kind,
732
+ name: row.name,
733
+ mediaType: row.media_type,
734
+ sha256: row.sha256,
735
+ size: row.size,
736
+ })),
737
+ cleanup,
738
+ coverage,
739
+ })
740
+ const body = canonical(result)
741
+ const sha256 = digest(body)
742
+ execute(
743
+ store,
744
+ 'INSERT INTO results (run_id, completion, coverage, finished_at, sha256, body, operation_id) VALUES (?, ?, ?, ?, ?, ?, ?)',
745
+ input.runId,
746
+ completion,
747
+ coverage.status,
748
+ at,
749
+ sha256,
750
+ body,
751
+ operationId
752
+ )
753
+ return {
754
+ reply: {
755
+ status: completion,
756
+ subject: input.runId,
757
+ reasons: [],
758
+ data: {
759
+ runId: input.runId,
760
+ completion,
761
+ coverage: coverage.status,
762
+ sha256,
763
+ counts: outcomeCounts(tests.filter((test) => selected.includes(test.testId))),
764
+ },
765
+ },
766
+ subjects: [`run:${input.runId}`, `plan:${run.plan.planId}`, 'inbox'],
767
+ }
768
+ })
769
+ }
770
+
771
+ export function outcomeCounts(tests: ResultTest[]) {
772
+ const counts: Record<string, number> = {}
773
+ for (const test of tests) counts[test.outcome] = (counts[test.outcome] ?? 0) + 1
774
+ return counts
775
+ }
776
+
777
+ export function resumeRun(store: Store, actor: string | undefined, input: ResumeInput) {
778
+ return write(store, { actor, command: 'run resume', subject: input.runId, input }, (operationId, at, actor) => {
779
+ const run = startedRun(store, input.runId)
780
+ const stalled = input.error ?? []
781
+ requireCondition(stalled.length > 0 || input.reason === undefined, '--reason applies only with --error')
782
+ const reason = stalled.length ? requireText(input.reason, 'reason (--reason) for --error') : null
783
+ const attempt = currentAttempt(store, input.runId) + 1
784
+ execute(
785
+ store,
786
+ 'INSERT INTO run_attempts (run_id, attempt, started_at, actor) VALUES (?, ?, ?, ?)',
787
+ input.runId,
788
+ attempt,
789
+ at,
790
+ actor
791
+ )
792
+ for (const testId of stalled) {
793
+ selectedTest(run, testId)
794
+ requireOpen(store, input.runId, testId)
795
+ execute(
796
+ store,
797
+ 'INSERT INTO outcomes (run_id, test_id, attempt, outcome, reason, recorded_at, operation_id) VALUES (?, ?, ?, ?, ?, ?, ?)',
798
+ input.runId,
799
+ testId,
800
+ attempt,
801
+ 'error',
802
+ reason,
803
+ at,
804
+ operationId
805
+ )
806
+ }
807
+ const outcomes = runOutcomes(store, input.runId)
808
+ const results = runAssertions(store, input.runId)
809
+ const tests = run.plan.tests
810
+ .filter((test) => run.request.selectedTestIds.includes(test.id) && !outcomes.has(test.id))
811
+ .map((test) => ({
812
+ testId: test.id,
813
+ warning: 'Actions may have occurred',
814
+ recorded: test.assertions.filter((assertion) => results.has(`${test.id}.${assertion.id}`)).length,
815
+ assertions: test.assertions.filter((assertion) => !results.has(`${test.id}.${assertion.id}`)),
816
+ }))
817
+ return {
818
+ reply: completed(input.runId, {
819
+ runId: input.runId,
820
+ attempt,
821
+ browserSession: run.row.browser_session,
822
+ deadlineSeconds: run.row.deadline_seconds,
823
+ evidenceDirectory: evidenceDirectory(input.runId),
824
+ closed: stalled,
825
+ tests,
826
+ }),
827
+ subjects: [`run:${input.runId}`],
828
+ }
829
+ })
830
+ }
831
+
832
+ export function acceptResult(store: Store, actor: string | undefined, input: ResultVerdict) {
833
+ return decideResult(store, actor, input, 'approved')
834
+ }
835
+
836
+ export function rejectResult(store: Store, actor: string | undefined, input: ResultVerdict) {
837
+ return decideResult(store, actor, input, 'rejected')
838
+ }
839
+
840
+ function decideResult(store: Store, actor: string | undefined, input: ResultVerdict, verdict: 'approved' | 'rejected') {
841
+ const command = verdict === 'approved' ? 'result accept' : 'result reject'
842
+ return write(store, { actor, command, subject: input.runId, input }, (operationId, at, actor) => {
843
+ const run = runState(store, input.runId)
844
+ const result = runResult(store, input.runId)
845
+ requireCondition(result, `Run ${input.runId} is not finished. Finish it before you decide on its result.`)
846
+ expectDigest(result.sha256, input.expectSha256, input.runId)
847
+ return {
848
+ reply: recordApproval(store, actor, operationId, {
849
+ kind: 'result',
850
+ subject: input.runId,
851
+ sha256: result.sha256,
852
+ verdict,
853
+ rationale: input.rationale,
854
+ }),
855
+ subjects: [`run:${input.runId}`, `plan:${run.plan.planId}`, 'inbox'],
856
+ }
857
+ })
858
+ }
859
+
860
+ export function runResult(store: Store, runId: string) {
861
+ const row = one<{ sha256: string; body: string }>(store, 'SELECT sha256, body FROM results WHERE run_id = ?', runId)
862
+ return row ? { sha256: row.sha256, result: JSON.parse(row.body) as Result } : null
863
+ }
864
+
865
+ export function resultAcceptance(store: Store, run: RunRow, sha256: string) {
866
+ const policy = approvalPolicy(targetRevision(store, run.target_id, run.target_revision).profile).result
867
+ return approvalState(store, 'result', run.run_id, sha256, policy)
868
+ }