@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,305 @@
1
+ import type { Store } from '../store.ts'
2
+ import type { Html } from './html.ts'
3
+ import type { Context, Page } from './layout.ts'
4
+ import { frozenRevision, planCandidate } from '../plans.ts'
5
+ import { planDetail } from '../views.ts'
6
+ import { html, short } from './html.ts'
7
+ import { banner, heading } from './layout.ts'
8
+
9
+ type Candidate = ReturnType<typeof planCandidate>
10
+ type Requirement = Candidate['requirements'][number]
11
+ type Test = Candidate['tests'][number]
12
+
13
+ export type EditState = {
14
+ mode: 'form' | 'json'
15
+ candidate: Candidate
16
+ json: string
17
+ actor: string
18
+ error: string | null
19
+ stale: Html | null
20
+ }
21
+
22
+ function words(values: string) {
23
+ return values
24
+ .split(/[\s,]+/)
25
+ .map((value) => value.trim())
26
+ .filter(Boolean)
27
+ }
28
+
29
+ function lines(values: string) {
30
+ return values
31
+ .split('\n')
32
+ .map((value) => value.trim())
33
+ .filter(Boolean)
34
+ }
35
+
36
+ export function candidateFromForm(base: Candidate, form: URLSearchParams): Candidate {
37
+ const text = (name: string, fallback: string) => (form.get(name) ?? fallback).trim()
38
+ const covered = new Set(form.getAll('cover'))
39
+ return {
40
+ requirements: base.requirements.map((requirement, i) => ({
41
+ id: requirement.id,
42
+ text: text(`r${i}.text`, requirement.text),
43
+ source: text(`r${i}.source`, requirement.source),
44
+ })),
45
+ tests: base.tests.map((test, i) => ({
46
+ ...test,
47
+ title: text(`t${i}.title`, test.title),
48
+ capability: text(`t${i}.capability`, test.capability),
49
+ operations: words(form.get(`t${i}.operations`) ?? test.operations.join(', ')),
50
+ dependsOn: words(form.get(`t${i}.dependsOn`) ?? test.dependsOn.join(', ')),
51
+ preconditions: lines(form.get(`t${i}.preconditions`) ?? test.preconditions.join('\n')),
52
+ action: text(`t${i}.action`, test.action),
53
+ requirementIds: base.requirements
54
+ .filter((requirement) => covered.has(`${test.id} ${requirement.id}`))
55
+ .map((requirement) => requirement.id),
56
+ assertions: test.assertions.map((assertion, j) => ({
57
+ id: assertion.id,
58
+ check: text(`t${i}.a${j}.check`, assertion.check),
59
+ checkKind: form.get(`t${i}.a${j}.kind`) === 'agent_observation' ? 'agent_observation' : 'deterministic',
60
+ })),
61
+ })),
62
+ gaps: base.gaps,
63
+ }
64
+ }
65
+
66
+ export function candidateJson(candidate: unknown) {
67
+ return JSON.stringify(candidate, null, 2)
68
+ }
69
+
70
+ function field(label: string, name: string, value: string, help = '') {
71
+ return html`<div class="field-group">
72
+ <label class="lab" for="${name}">${label}</label>
73
+ <input id="${name}" name="${name}" value="${value}" />
74
+ ${help ? html`<p class="help">${help}</p>` : ''}
75
+ </div>`
76
+ }
77
+
78
+ function area(label: string, name: string, value: string, rows: number, help = '') {
79
+ return html`<div class="field-group">
80
+ <label class="lab" for="${name}">${label}</label>
81
+ <textarea id="${name}" name="${name}" rows="${rows}">${value}</textarea>
82
+ ${help ? html`<p class="help">${help}</p>` : ''}
83
+ </div>`
84
+ }
85
+
86
+ function requirementFields(requirement: Requirement, i: number) {
87
+ return html`<fieldset class="edit-card" id="edit-${requirement.id}">
88
+ <legend class="mono">${requirement.id}</legend>
89
+ ${area('Text', `r${i}.text`, requirement.text, 2)}
90
+ ${field('Source', `r${i}.source`, requirement.source, 'Where the requirement comes from.')}
91
+ </fieldset>`
92
+ }
93
+
94
+ function testFields(test: Test, i: number) {
95
+ return html`<fieldset class="edit-card" id="edit-${test.id}">
96
+ <legend><span class="mono">${test.id}</span> ${test.title}</legend>
97
+ ${field('Title', `t${i}.title`, test.title)}
98
+ <div class="edit-row">
99
+ ${field('Capability', `t${i}.capability`, test.capability)}
100
+ ${field('Operations', `t${i}.operations`, test.operations.join(', '), 'Separate with commas.')}
101
+ </div>
102
+ ${field(
103
+ 'Depends on',
104
+ `t${i}.dependsOn`,
105
+ test.dependsOn.join(', '),
106
+ 'Test IDs, separated with commas. Use plan:T1 for a test in another plan.'
107
+ )}
108
+ ${area('Preconditions', `t${i}.preconditions`, test.preconditions.join('\n'), 2, 'One per line.')}
109
+ ${area('Action', `t${i}.action`, test.action, 2)}
110
+ <div class="field-group">
111
+ <p class="lab">Assertions</p>
112
+ ${
113
+ test.assertions.length
114
+ ? html`<ul class="edit-asrs">
115
+ ${test.assertions.map(
116
+ (assertion, j) =>
117
+ html`<li>
118
+ <label class="aid" for="t${i}.a${j}.check">${assertion.id}</label>
119
+ <textarea id="t${i}.a${j}.check" name="t${i}.a${j}.check" rows="1">
120
+ ${assertion.check}</textarea>
121
+ <label class="sr-only" for="t${i}.a${j}.kind"
122
+ >Check kind of ${assertion.id}</label
123
+ >
124
+ <select id="t${i}.a${j}.kind" name="t${i}.a${j}.kind">
125
+ ${['deterministic', 'agent_observation'].map(
126
+ (kind) =>
127
+ html`<option
128
+ value="${kind}"
129
+ ${kind === assertion.checkKind ? html`selected` : ''}
130
+ >
131
+ ${kind.replace('_', ' ')}
132
+ </option>`
133
+ )}
134
+ </select>
135
+ </li>`
136
+ )}
137
+ </ul>`
138
+ : html`<p class="help">No assertions. Add them in the JSON editor.</p>`
139
+ }
140
+ </div>
141
+ </fieldset>`
142
+ }
143
+
144
+ function coverageMatrix(candidate: Candidate) {
145
+ return html`<div class="scroll">
146
+ <table class="matrix">
147
+ <thead>
148
+ <tr>
149
+ <th scope="col">Requirement</th>
150
+ ${candidate.tests.map((test) => html`<th scope="col" class="mono">${test.id}</th>`)}
151
+ </tr>
152
+ </thead>
153
+ <tbody>
154
+ ${candidate.requirements.map(
155
+ (requirement) =>
156
+ html`<tr>
157
+ <th scope="row">
158
+ <span class="mono">${requirement.id}</span>
159
+ <span class="muted small matrix-text">${requirement.text}</span>
160
+ </th>
161
+ ${candidate.tests.map(
162
+ (test) =>
163
+ html`<td>
164
+ <input
165
+ type="checkbox"
166
+ name="cover"
167
+ value="${test.id} ${requirement.id}"
168
+ aria-label="${test.id} covers ${requirement.id}"
169
+ ${test.requirementIds.includes(requirement.id) ? html`checked` : ''}
170
+ />
171
+ </td>`
172
+ )}
173
+ </tr>`
174
+ )}
175
+ </tbody>
176
+ </table>
177
+ </div>`
178
+ }
179
+
180
+ function hidden(context: Context, sha256: string, mode: string) {
181
+ return html`<input type="hidden" name="csrf" value="${context.csrf}" />
182
+ <input type="hidden" name="sha" value="${sha256}" />
183
+ <input type="hidden" name="mode" value="${mode}" />`
184
+ }
185
+
186
+ function actorLine(context: Context, state: EditState, path: string, mode: string) {
187
+ if (context.actor)
188
+ return html`<p class="help">
189
+ Saving as <strong>${context.actor}</strong>.
190
+ <a href="/actor?back=${encodeURIComponent(path)}">Change name</a>
191
+ </p>`
192
+ return html`<div class="field-group">
193
+ <label class="lab" for="actor-${mode}">Your name</label>
194
+ <input id="actor-${mode}" name="actor" required autocomplete="name" value="${state.actor}" />
195
+ <p class="help">Checkmate records it with the edit.</p>
196
+ </div>`
197
+ }
198
+
199
+ export function editPage(
200
+ store: Store,
201
+ context: Context,
202
+ planId: string,
203
+ revision: number,
204
+ state: EditState | null = null
205
+ ): Page {
206
+ const detail = planDetail(store, planId, revision)
207
+ const subject = `${planId}@${revision}`
208
+ const path = `/plans/${subject}/edit`
209
+ const title = `Edit ${subject}`
210
+ if (detail.revision !== detail.latestRevision)
211
+ return {
212
+ title,
213
+ section: 'plans',
214
+ subjects: [`plan:${planId}`],
215
+ body: html`${heading(title, detail.plan.objective)}
216
+ ${banner(
217
+ 'info',
218
+ html`Only the latest revision can change.
219
+ <a href="/plans/${planId}@${detail.latestRevision}/edit"
220
+ >Edit ${planId}@${detail.latestRevision}</a
221
+ >`
222
+ )}`,
223
+ }
224
+ const frozen = frozenRevision(store, planId, revision)
225
+ const needsApproval = detail.approval.policy !== 'none'
226
+ const next = revision + 1
227
+ const save = frozen ? `Save as ${planId}@${next}` : 'Save changes'
228
+ const why = frozen === 'run' ? `A run uses ${subject}.` : `${subject} is approved.`
229
+ const notice = frozen
230
+ ? banner(
231
+ 'warn',
232
+ html`<strong
233
+ >${why} Saving creates revision ${next}${needsApproval ? ', which needs approval' : ''}.</strong
234
+ >
235
+ ${subject} stays as it is.`
236
+ )
237
+ : banner(
238
+ 'info',
239
+ html`<strong>Edits change this revision in place.</strong> ${subject} is not approved and no run uses
240
+ it.`
241
+ )
242
+ const shown = state ?? {
243
+ mode: 'form' as const,
244
+ candidate: planCandidate(detail.plan),
245
+ json: candidateJson(planCandidate(detail.plan)),
246
+ actor: '',
247
+ error: null,
248
+ stale: null,
249
+ }
250
+ const messages = html`${shown.stale ? banner('warn', shown.stale) : ''}
251
+ ${shown.error ? html`<p class="form-error" role="alert">${shown.error}</p>` : ''}`
252
+ const buttons = (mode: string) =>
253
+ html`<div class="btns">
254
+ <button class="btn primary">${save}</button>
255
+ <a class="btn" href="/plans/${subject}">Cancel</a>
256
+ </div>
257
+ ${actorLine(context, shown, path, mode)}`
258
+ const body = html`${heading(
259
+ title,
260
+ detail.plan.objective,
261
+ html`<div class="meta">
262
+ <a class="pill" href="/plans/${subject}">${subject}</a>
263
+ <span class="pill" title="${detail.sha256}"
264
+ >sha256 <span class="mono">${short(detail.sha256)}</span></span
265
+ >
266
+ </div>`
267
+ )}
268
+ <div data-region="edit" data-stale-message="${planId} changed. Review it before you save.">
269
+ <div data-stale-slot>${notice} ${messages}</div>
270
+ <form method="post" action="${path}" class="edit-form" data-edit-form>
271
+ ${hidden(context, detail.sha256, 'form')}
272
+ <section aria-labelledby="edit-requirements">
273
+ <h2 id="edit-requirements">
274
+ Requirements <span class="n">${shown.candidate.requirements.length}</span>
275
+ </h2>
276
+ <div class="stack">${shown.candidate.requirements.map(requirementFields)}</div>
277
+ </section>
278
+ <section aria-labelledby="edit-tests">
279
+ <h2 id="edit-tests">Tests <span class="n">${shown.candidate.tests.length}</span></h2>
280
+ <div class="stack">${shown.candidate.tests.map(testFields)}</div>
281
+ </section>
282
+ <section aria-labelledby="edit-coverage">
283
+ <h2 id="edit-coverage">Coverage</h2>
284
+ <p class="help">Mark the requirements that each test covers. Each test covers one or more.</p>
285
+ ${coverageMatrix(shown.candidate)}
286
+ </section>
287
+ <div class="edit-save">${buttons('form')}</div>
288
+ </form>
289
+ <details class="edit-json" ${shown.mode === 'json' ? html`open` : ''}>
290
+ <summary>Edit as JSON</summary>
291
+ <form method="post" action="${path}" class="edit-form" data-edit-form>
292
+ ${hidden(context, detail.sha256, 'json')}
293
+ <p class="help">
294
+ Add or remove requirements, tests, assertions, and gaps. The JSON has the fields of a plan
295
+ candidate:
296
+ <code>requirements</code>, <code>tests</code>, and <code>gaps</code>.
297
+ </p>
298
+ <label class="sr-only" for="json">Plan candidate JSON</label>
299
+ <textarea id="json" name="json" rows="24" class="code" spellcheck="false">${shown.json}</textarea>
300
+ ${buttons('json')}
301
+ </form>
302
+ </details>
303
+ </div>`
304
+ return { title, section: 'plans', subjects: [`plan:${planId}`], body }
305
+ }
@@ -0,0 +1,209 @@
1
+ import type { Html } from './html.ts'
2
+ import type { Context } from './layout.ts'
3
+ import { html, short } from './html.ts'
4
+ import { banner } from './layout.ts'
5
+
6
+ export type FormState = {
7
+ values: Record<string, string>
8
+ error: string | null
9
+ stale: Html | null
10
+ confirm: boolean
11
+ }
12
+
13
+ export type Verdict = {
14
+ action: string
15
+ subject: string
16
+ question: string
17
+ sha256: string
18
+ digestLabel: string
19
+ approve: { value: string; label: string; confirm: string; summary: string }
20
+ reject: { value: string; label: string; confirm: string; summary: string }
21
+ staleMessage: string
22
+ intro?: Html | ''
23
+ }
24
+
25
+ export function emptyState(): FormState {
26
+ return { values: {}, error: null, stale: null, confirm: false }
27
+ }
28
+
29
+ function hidden(context: Context, sha256: string) {
30
+ return html`<input type="hidden" name="csrf" value="${context.csrf}" />
31
+ <input type="hidden" name="sha" value="${sha256}" />`
32
+ }
33
+
34
+ function actorField(context: Context, state: FormState) {
35
+ if (context.actor) return ''
36
+ return html`<div class="field-group">
37
+ <label class="lab" for="actor">Your name</label>
38
+ <input id="actor" name="actor" required autocomplete="name" value="${state.values.actor ?? ''}" />
39
+ <p class="help">Checkmate asks once, keeps it in a cookie, and records it with each decision.</p>
40
+ </div>`
41
+ }
42
+
43
+ function messages(state: FormState) {
44
+ return html`${state.stale ? banner('warn', state.stale) : ''}
45
+ ${state.error ? html`<p class="form-error" role="alert">${state.error}</p>` : ''}`
46
+ }
47
+
48
+ function rationaleField(state: FormState, placeholder: string) {
49
+ return html`<div class="field-group">
50
+ <label class="lab" for="rationale">Rationale</label>
51
+ <textarea id="rationale" name="rationale" rows="4" required data-rationale placeholder="${placeholder}">
52
+ ${state.values.rationale ?? ''}</textarea>
53
+ </div>`
54
+ }
55
+
56
+ function confirmStep(context: Context, verdict: Verdict, state: FormState) {
57
+ const choice = state.values.verdict === verdict.reject.value ? verdict.reject : verdict.approve
58
+ return html`<form method="post" action="${verdict.action}" class="decision-form">
59
+ ${hidden(context, verdict.sha256)}
60
+ <input type="hidden" name="verdict" value="${choice.value}" />
61
+ <input type="hidden" name="rationale" value="${state.values.rationale ?? ''}" />
62
+ ${context.actor ? '' : html`<input type="hidden" name="actor" value="${state.values.actor ?? ''}" />`}
63
+ <div class="confirm-box">
64
+ <p class="lab">Confirm</p>
65
+ <p>${choice.summary}</p>
66
+ <blockquote>${state.values.rationale ?? ''}</blockquote>
67
+ </div>
68
+ <div class="btns">
69
+ <button class="btn primary" name="confirmed" value="1">${choice.confirm}</button>
70
+ <button class="btn" name="back" value="1" formnovalidate>Back</button>
71
+ </div>
72
+ </form>`
73
+ }
74
+
75
+ function chooseStep(context: Context, verdict: Verdict, state: FormState) {
76
+ const button = (choice: Verdict['approve'], primary: boolean) =>
77
+ html`<button
78
+ class="btn${primary ? ' primary' : ''}"
79
+ name="verdict"
80
+ value="${choice.value}"
81
+ data-needs-rationale
82
+ data-confirm-label="${choice.confirm}"
83
+ data-summary="${choice.summary}"
84
+ >
85
+ ${choice.label}
86
+ </button>`
87
+ return html`<form method="post" action="${verdict.action}" class="decision-form" data-verdict-form>
88
+ ${hidden(context, verdict.sha256)} ${actorField(context, state)}
89
+ ${rationaleField(state, 'Why do you decide this?')}
90
+ <div class="confirm-box" data-confirm hidden>
91
+ <p class="lab">Confirm</p>
92
+ <p data-confirm-summary></p>
93
+ </div>
94
+ <div class="btns" data-choose>${button(verdict.approve, true)} ${button(verdict.reject, false)}</div>
95
+ <div class="btns" data-confirm-buttons hidden>
96
+ <button class="btn primary" name="confirmed" value="1" data-confirm-submit>Confirm</button>
97
+ <button class="btn" type="button" data-confirm-back>Back</button>
98
+ </div>
99
+ </form>`
100
+ }
101
+
102
+ export function verdictPanel(context: Context, verdict: Verdict, state: FormState) {
103
+ return html`<section
104
+ class="panel decision"
105
+ id="decide"
106
+ aria-labelledby="decide-title"
107
+ data-region="decision"
108
+ data-stale-message="${verdict.staleMessage}"
109
+ >
110
+ <h2 class="lab" id="decide-title">Decision</h2>
111
+ <p class="subj">${verdict.question}</p>
112
+ <p class="help">
113
+ ${verdict.digestLabel} sha256 <code title="${verdict.sha256}">${short(verdict.sha256)}</code>. Your decision
114
+ is recorded in the log.
115
+ </p>
116
+ <div data-stale-slot>${messages(state)}</div>
117
+ ${verdict.intro ?? ''}
118
+ ${state.confirm ? confirmStep(context, verdict, state) : chooseStep(context, verdict, state)}
119
+ </section>`
120
+ }
121
+
122
+ const kinds = [
123
+ {
124
+ value: 'clarify',
125
+ label: 'Clarify',
126
+ help: 'Clarify gives the expected behaviour. The agent updates the affected assertions in the next revision.',
127
+ },
128
+ {
129
+ value: 'accept_assumption',
130
+ label: 'Accept assumption',
131
+ help: 'Accept assumption records your answer as an assumption. Tests run on it, and reports list it.',
132
+ },
133
+ {
134
+ value: 'defer',
135
+ label: 'Defer',
136
+ help: 'Defer moves the gap out of this scope. The affected tests become deferred and do not run.',
137
+ },
138
+ ]
139
+
140
+ export type GapForm = {
141
+ action: string
142
+ cancel: string
143
+ sha256: string
144
+ title: Html
145
+ question: string
146
+ staleMessage: string
147
+ }
148
+
149
+ export function gapPanel(context: Context, form: GapForm, state: FormState) {
150
+ const selected = state.values.kind ?? 'clarify'
151
+ return html`<section
152
+ class="panel decision"
153
+ id="answer"
154
+ aria-labelledby="answer-title"
155
+ data-region="decision"
156
+ data-stale-message="${form.staleMessage}"
157
+ >
158
+ <h2 class="lab" id="answer-title">Answer gap</h2>
159
+ <p class="subj">${form.title}</p>
160
+ <p class="help">${form.question}</p>
161
+ <div data-stale-slot>${messages(state)}</div>
162
+ <form method="post" action="${form.action}" class="decision-form gap-form" data-gap-form>
163
+ ${hidden(context, form.sha256)}
164
+ <fieldset class="seg-field">
165
+ <legend class="lab">Kind of answer</legend>
166
+ <div class="seg">
167
+ ${kinds.map(
168
+ (kind) =>
169
+ html`<input
170
+ type="radio"
171
+ id="kind-${kind.value}"
172
+ name="kind"
173
+ value="${kind.value}"
174
+ ${selected === kind.value ? html`checked` : ''}
175
+ /><label for="kind-${kind.value}">${kind.label}</label>`
176
+ )}
177
+ </div>
178
+ </fieldset>
179
+ ${kinds.map((kind) => html`<p class="help kind-help" data-kind="${kind.value}">${kind.help}</p>`)}
180
+ <div class="field-group">
181
+ <label class="lab" for="value">Answer</label>
182
+ <textarea id="value" name="value" rows="3" required>${state.values.value ?? ''}</textarea>
183
+ </div>
184
+ <div class="field-group source-field">
185
+ <label class="lab" for="source">Source <span class="req">required for Clarify</span></label>
186
+ <input
187
+ id="source"
188
+ name="source"
189
+ value="${state.values.source ?? ''}"
190
+ placeholder="oracle://refund-policy#window"
191
+ data-source
192
+ />
193
+ <p class="help">Where the answer comes from: a document, a ticket, or a person.</p>
194
+ </div>
195
+ ${rationaleField(state, 'Why is this the right answer?')} ${actorField(context, state)}
196
+ <div class="btns">
197
+ <button class="btn primary" data-needs-rationale>Record answer</button>
198
+ <a class="btn" href="${form.cancel}">Cancel</a>
199
+ </div>
200
+ </form>
201
+ </section>`
202
+ }
203
+
204
+ export function recordPanel(title: string, content: Html) {
205
+ return html`<section class="panel decision" aria-labelledby="decide-title" data-region="decision">
206
+ <h2 class="lab" id="decide-title">${title}</h2>
207
+ ${content}
208
+ </section>`
209
+ }
@@ -0,0 +1,130 @@
1
+ export type Html = { readonly html: string }
2
+
3
+ const entities: Record<string, string> = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }
4
+
5
+ export function escape(text: string) {
6
+ return text.replace(/[&<>"']/g, (character) => entities[character])
7
+ }
8
+
9
+ function isHtml(value: unknown): value is Html {
10
+ return typeof value === 'object' && value !== null && 'html' in value
11
+ }
12
+
13
+ function piece(value: unknown): string {
14
+ if (value === null || value === undefined || value === false) return ''
15
+ if (Array.isArray(value)) return value.map(piece).join('')
16
+ if (isHtml(value)) return value.html
17
+ return escape(String(value))
18
+ }
19
+
20
+ export function html(strings: TemplateStringsArray, ...values: unknown[]): Html {
21
+ let text = strings[0]
22
+ for (let i = 0; i < values.length; i++) text += piece(values[i]) + strings[i + 1]
23
+ return { html: text }
24
+ }
25
+
26
+ export function join(items: Html[], separator: Html | string) {
27
+ return html`${items.flatMap((item, i) => (i ? [separator, item] : [item]))}`
28
+ }
29
+
30
+ export type Tone = 'pass' | 'fail' | 'block' | 'warn' | 'unsure' | 'acc' | 'muted'
31
+
32
+ const statusLook: Record<string, [string, Tone]> = {
33
+ passed: ['✓', 'pass'],
34
+ failed: ['✗', 'fail'],
35
+ inconclusive: ['?', 'unsure'],
36
+ not_checked: ['!', 'warn'],
37
+ blocked: ['–', 'block'],
38
+ not_run: ['–', 'block'],
39
+ error: ['!', 'warn'],
40
+ ready: ['✓', 'pass'],
41
+ deferred: ['–', 'block'],
42
+ planned: ['✓', 'pass'],
43
+ tested: ['✓', 'pass'],
44
+ uncovered: ['–', 'block'],
45
+ complete: ['✓', 'pass'],
46
+ partial: ['◐', 'warn'],
47
+ not_executed: ['–', 'block'],
48
+ completed: ['✓', 'pass'],
49
+ interrupted: ['!', 'warn'],
50
+ operational_error: ['!', 'fail'],
51
+ not_applicable: ['–', 'block'],
52
+ approved: ['✓', 'pass'],
53
+ rejected: ['✗', 'fail'],
54
+ pending: ['○', 'acc'],
55
+ not_required: ['–', 'block'],
56
+ started: ['●', 'acc'],
57
+ finalized: ['✓', 'pass'],
58
+ open: ['○', 'warn'],
59
+ resolved: ['✓', 'pass'],
60
+ blocking: ['●', 'fail'],
61
+ non_blocking: ['○', 'warn'],
62
+ clarify: ['✓', 'acc'],
63
+ accept_assumption: ['≈', 'acc'],
64
+ defer: ['–', 'block'],
65
+ defect: ['✗', 'fail'],
66
+ potential_issue: ['?', 'warn'],
67
+ low: ['·', 'block'],
68
+ medium: ['!', 'warn'],
69
+ high: ['!', 'fail'],
70
+ critical: ['!!', 'fail'],
71
+ rejected_operation: ['✗', 'fail'],
72
+ needs_decision: ['?', 'warn'],
73
+ invalid_request: ['✗', 'fail'],
74
+ needs_plan: ['○', 'acc'],
75
+ run_in_progress: ['●', 'acc'],
76
+ needs_revision: ['○', 'acc'],
77
+ plan_rejected: ['✗', 'fail'],
78
+ needs_answers: ['?', 'warn'],
79
+ needs_approval: ['○', 'acc'],
80
+ ready_to_run: ['○', 'acc'],
81
+ nothing_to_run: ['–', 'block'],
82
+ defects_open: ['✗', 'fail'],
83
+ needs_retest: ['!', 'warn'],
84
+ awaits_acceptance: ['○', 'acc'],
85
+ accepted: ['✓', 'pass'],
86
+ }
87
+
88
+ export function words(status: string) {
89
+ return status.replace(/_/g, ' ')
90
+ }
91
+
92
+ export function pill(status: string, label: string = words(status)) {
93
+ const [icon, tone] = statusLook[status] ?? ['·', 'muted']
94
+ return html`<span class="st ${tone}"><span aria-hidden="true">${icon}</span> ${label}</span>`
95
+ }
96
+
97
+ export function icon(status: string | null) {
98
+ const [symbol, tone] = status ? (statusLook[status] ?? ['·', 'muted']) : ['·', 'muted']
99
+ return html`<span class="ic ${tone}" role="img" aria-label="${status ? words(status) : 'no result yet'}"
100
+ >${symbol}</span
101
+ >`
102
+ }
103
+
104
+ export function short(sha256: string) {
105
+ return `${sha256.slice(0, 4)}…${sha256.slice(-4)}`
106
+ }
107
+
108
+ export function age(iso: string, now = Date.now()) {
109
+ const seconds = Math.max(0, Math.round((now - Date.parse(iso)) / 1000))
110
+ if (seconds < 45) return 'now'
111
+ const minutes = Math.round(seconds / 60)
112
+ if (minutes < 60) return `${minutes} min`
113
+ const hours = Math.round(minutes / 60)
114
+ if (hours < 48) return `${hours} h`
115
+ return `${Math.round(hours / 24)} d`
116
+ }
117
+
118
+ export function clock(iso: string) {
119
+ const date = new Date(iso)
120
+ const pad = (value: number) => String(value).padStart(2, '0')
121
+ return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`
122
+ }
123
+
124
+ export function time(iso: string, text = age(iso)) {
125
+ return html`<time datetime="${iso}" title="${clock(iso)}">${text}</time>`
126
+ }
127
+
128
+ export function plural(count: number, one: string, many = `${one}s`) {
129
+ return `${count} ${count === 1 ? one : many}`
130
+ }