@xoxoai/checkmate 0.6.7 → 0.7.1

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 (381) hide show
  1. package/README.md +54 -257
  2. package/dist/checkmate.js +13375 -0
  3. package/package.json +30 -94
  4. package/plugin/.claude-plugin/plugin.json +10 -0
  5. package/plugin/bin/checkmate.ts +3 -0
  6. package/plugin/contracts/finding.schema.json +78 -0
  7. package/plugin/contracts/gap.schema.json +56 -0
  8. package/plugin/contracts/plan-candidate.schema.json +103 -0
  9. package/plugin/contracts/plan-delta.schema.json +63 -0
  10. package/plugin/contracts/plan-patch.schema.json +116 -0
  11. package/plugin/contracts/plan-request.schema.json +108 -0
  12. package/plugin/contracts/plan.schema.json +313 -0
  13. package/plugin/contracts/profile.schema.json +154 -0
  14. package/plugin/contracts/response.schema.json +25 -0
  15. package/plugin/contracts/result.schema.json +410 -0
  16. package/plugin/dist/checkmate.js +13012 -0
  17. package/plugin/package.json +4 -0
  18. package/plugin/references/browser.md +35 -0
  19. package/plugin/references/http.md +30 -0
  20. package/plugin/references/workflow.md +122 -0
  21. package/plugin/skills/execute/SKILL.md +92 -0
  22. package/plugin/skills/plan/SKILL.md +94 -0
  23. package/plugin/skills/report/SKILL.md +28 -0
  24. package/plugin/skills/revise/SKILL.md +43 -0
  25. package/plugin/skills/setup/SKILL.md +82 -0
  26. package/plugin/skills/status/SKILL.md +20 -0
  27. package/plugin/skills/ui/SKILL.md +26 -0
  28. package/plugin/src/actor.ts +21 -0
  29. package/plugin/src/cli.ts +1133 -0
  30. package/plugin/src/contracts.ts +342 -0
  31. package/plugin/src/index.ts +52 -0
  32. package/plugin/src/plans.ts +1028 -0
  33. package/plugin/src/render.ts +544 -0
  34. package/plugin/src/runs.ts +868 -0
  35. package/plugin/src/store.ts +384 -0
  36. package/plugin/src/targets.ts +205 -0
  37. package/plugin/src/types.ts +319 -0
  38. package/plugin/src/ui/assets/client.js +501 -0
  39. package/plugin/src/ui/assets/favicon.svg +1 -0
  40. package/plugin/src/ui/assets/fonts/inter-OFL.txt +93 -0
  41. package/plugin/src/ui/assets/fonts/inter.woff2 +0 -0
  42. package/plugin/src/ui/assets/fonts/jetbrains-mono-OFL.txt +93 -0
  43. package/plugin/src/ui/assets/fonts/jetbrains-mono.woff2 +0 -0
  44. package/plugin/src/ui/assets/fonts/space-grotesk-OFL.txt +93 -0
  45. package/plugin/src/ui/assets/fonts/space-grotesk.woff2 +0 -0
  46. package/plugin/src/ui/assets/style.css +1926 -0
  47. package/plugin/src/ui/edit.ts +305 -0
  48. package/plugin/src/ui/forms.ts +209 -0
  49. package/plugin/src/ui/html.ts +130 -0
  50. package/plugin/src/ui/layout.ts +198 -0
  51. package/plugin/src/ui/lists.ts +645 -0
  52. package/plugin/src/ui/plan.ts +577 -0
  53. package/plugin/src/ui/run.ts +489 -0
  54. package/plugin/src/ui/server.ts +684 -0
  55. package/plugin/src/validators.d.ts +13 -0
  56. package/plugin/src/validators.js +1 -0
  57. package/plugin/src/views.ts +839 -0
  58. package/bin/checkmate.js +0 -13
  59. package/dist/ai/client.d.ts +0 -64
  60. package/dist/ai/client.d.ts.map +0 -1
  61. package/dist/ai/client.js +0 -313
  62. package/dist/ai/client.js.map +0 -1
  63. package/dist/ai/message-handler.d.ts +0 -9
  64. package/dist/ai/message-handler.d.ts.map +0 -1
  65. package/dist/ai/message-handler.js +0 -50
  66. package/dist/ai/message-handler.js.map +0 -1
  67. package/dist/ai/message-history.d.ts +0 -17
  68. package/dist/ai/message-history.d.ts.map +0 -1
  69. package/dist/ai/message-history.js +0 -45
  70. package/dist/ai/message-history.js.map +0 -1
  71. package/dist/ai/prompts.d.ts +0 -4
  72. package/dist/ai/prompts.d.ts.map +0 -1
  73. package/dist/ai/prompts.js +0 -24
  74. package/dist/ai/prompts.js.map +0 -1
  75. package/dist/ai/rate-limit-policy.d.ts +0 -9
  76. package/dist/ai/rate-limit-policy.d.ts.map +0 -1
  77. package/dist/ai/rate-limit-policy.js +0 -34
  78. package/dist/ai/rate-limit-policy.js.map +0 -1
  79. package/dist/ai/tool-response-handler.d.ts +0 -16
  80. package/dist/ai/tool-response-handler.d.ts.map +0 -1
  81. package/dist/ai/tool-response-handler.js +0 -85
  82. package/dist/ai/tool-response-handler.js.map +0 -1
  83. package/dist/ai/turn-processor.d.ts +0 -34
  84. package/dist/ai/turn-processor.d.ts.map +0 -1
  85. package/dist/ai/turn-processor.js +0 -107
  86. package/dist/ai/turn-processor.js.map +0 -1
  87. package/dist/api/allocate-run-identity.d.ts +0 -8
  88. package/dist/api/allocate-run-identity.d.ts.map +0 -1
  89. package/dist/api/allocate-run-identity.js +0 -10
  90. package/dist/api/allocate-run-identity.js.map +0 -1
  91. package/dist/api/execute-prepared-run.d.ts +0 -22
  92. package/dist/api/execute-prepared-run.d.ts.map +0 -1
  93. package/dist/api/execute-prepared-run.js +0 -184
  94. package/dist/api/execute-prepared-run.js.map +0 -1
  95. package/dist/api/index.d.ts +0 -25
  96. package/dist/api/index.d.ts.map +0 -1
  97. package/dist/api/index.js +0 -69
  98. package/dist/api/index.js.map +0 -1
  99. package/dist/api/prepare-run.d.ts +0 -44
  100. package/dist/api/prepare-run.d.ts.map +0 -1
  101. package/dist/api/prepare-run.js +0 -261
  102. package/dist/api/prepare-run.js.map +0 -1
  103. package/dist/cli/diagnostic-relay.d.ts +0 -6
  104. package/dist/cli/diagnostic-relay.d.ts.map +0 -1
  105. package/dist/cli/diagnostic-relay.js +0 -6
  106. package/dist/cli/diagnostic-relay.js.map +0 -1
  107. package/dist/cli/main.d.ts +0 -21
  108. package/dist/cli/main.d.ts.map +0 -1
  109. package/dist/cli/main.js +0 -166
  110. package/dist/cli/main.js.map +0 -1
  111. package/dist/cli/protocol.d.ts +0 -57
  112. package/dist/cli/protocol.d.ts.map +0 -1
  113. package/dist/cli/protocol.js +0 -116
  114. package/dist/cli/protocol.js.map +0 -1
  115. package/dist/cli/reconcile-result.d.ts +0 -13
  116. package/dist/cli/reconcile-result.d.ts.map +0 -1
  117. package/dist/cli/reconcile-result.js +0 -300
  118. package/dist/cli/reconcile-result.js.map +0 -1
  119. package/dist/cli/run-parent.d.ts +0 -37
  120. package/dist/cli/run-parent.d.ts.map +0 -1
  121. package/dist/cli/run-parent.js +0 -491
  122. package/dist/cli/run-parent.js.map +0 -1
  123. package/dist/cli/signals.d.ts +0 -36
  124. package/dist/cli/signals.d.ts.map +0 -1
  125. package/dist/cli/signals.js +0 -85
  126. package/dist/cli/signals.js.map +0 -1
  127. package/dist/cli/static-commands.d.ts +0 -11
  128. package/dist/cli/static-commands.d.ts.map +0 -1
  129. package/dist/cli/static-commands.js +0 -83
  130. package/dist/cli/static-commands.js.map +0 -1
  131. package/dist/cli/worker-stream-guard.d.ts +0 -20
  132. package/dist/cli/worker-stream-guard.d.ts.map +0 -1
  133. package/dist/cli/worker-stream-guard.js +0 -41
  134. package/dist/cli/worker-stream-guard.js.map +0 -1
  135. package/dist/cli/worker.d.ts +0 -3
  136. package/dist/cli/worker.d.ts.map +0 -1
  137. package/dist/cli/worker.js +0 -111
  138. package/dist/cli/worker.js.map +0 -1
  139. package/dist/config/ingestion.d.ts +0 -58
  140. package/dist/config/ingestion.d.ts.map +0 -1
  141. package/dist/config/ingestion.js +0 -283
  142. package/dist/config/ingestion.js.map +0 -1
  143. package/dist/config/manifest.d.ts +0 -20
  144. package/dist/config/manifest.d.ts.map +0 -1
  145. package/dist/config/manifest.js +0 -91
  146. package/dist/config/manifest.js.map +0 -1
  147. package/dist/config/model-egress.d.ts +0 -10
  148. package/dist/config/model-egress.d.ts.map +0 -1
  149. package/dist/config/model-egress.js +0 -62
  150. package/dist/config/model-egress.js.map +0 -1
  151. package/dist/config/package-resolution.d.ts +0 -4
  152. package/dist/config/package-resolution.d.ts.map +0 -1
  153. package/dist/config/package-resolution.js +0 -24
  154. package/dist/config/package-resolution.js.map +0 -1
  155. package/dist/config/policy.d.ts +0 -44
  156. package/dist/config/policy.d.ts.map +0 -1
  157. package/dist/config/policy.js +0 -141
  158. package/dist/config/policy.js.map +0 -1
  159. package/dist/config/record.d.ts +0 -4
  160. package/dist/config/record.d.ts.map +0 -1
  161. package/dist/config/record.js +0 -10
  162. package/dist/config/record.js.map +0 -1
  163. package/dist/config/secrets.d.ts +0 -11
  164. package/dist/config/secrets.d.ts.map +0 -1
  165. package/dist/config/secrets.js +0 -42
  166. package/dist/config/secrets.js.map +0 -1
  167. package/dist/contracts/diagnostics.d.ts +0 -4
  168. package/dist/contracts/diagnostics.d.ts.map +0 -1
  169. package/dist/contracts/diagnostics.js +0 -31
  170. package/dist/contracts/diagnostics.js.map +0 -1
  171. package/dist/contracts/serialize.d.ts +0 -2
  172. package/dist/contracts/serialize.d.ts.map +0 -1
  173. package/dist/contracts/serialize.js +0 -90
  174. package/dist/contracts/serialize.js.map +0 -1
  175. package/dist/contracts/types.d.ts +0 -316
  176. package/dist/contracts/types.d.ts.map +0 -1
  177. package/dist/contracts/types.js +0 -74
  178. package/dist/contracts/types.js.map +0 -1
  179. package/dist/contracts/validator.d.ts +0 -8
  180. package/dist/contracts/validator.d.ts.map +0 -1
  181. package/dist/contracts/validator.js +0 -71
  182. package/dist/contracts/validator.js.map +0 -1
  183. package/dist/driver.d.ts +0 -97
  184. package/dist/driver.d.ts.map +0 -1
  185. package/dist/driver.js +0 -24
  186. package/dist/driver.js.map +0 -1
  187. package/dist/drivers/descriptor.d.ts +0 -15
  188. package/dist/drivers/descriptor.d.ts.map +0 -1
  189. package/dist/drivers/descriptor.js +0 -122
  190. package/dist/drivers/descriptor.js.map +0 -1
  191. package/dist/drivers/loader.d.ts +0 -18
  192. package/dist/drivers/loader.d.ts.map +0 -1
  193. package/dist/drivers/loader.js +0 -97
  194. package/dist/drivers/loader.js.map +0 -1
  195. package/dist/drivers/web/checkmate-driver.json +0 -47
  196. package/dist/drivers/web/index.d.ts +0 -6
  197. package/dist/drivers/web/index.d.ts.map +0 -1
  198. package/dist/drivers/web/index.js +0 -27
  199. package/dist/drivers/web/index.js.map +0 -1
  200. package/dist/drivers/web/session.d.ts +0 -25
  201. package/dist/drivers/web/session.d.ts.map +0 -1
  202. package/dist/drivers/web/session.js +0 -132
  203. package/dist/drivers/web/session.js.map +0 -1
  204. package/dist/drivers/web/tools/network-request-recorder.d.ts +0 -46
  205. package/dist/drivers/web/tools/network-request-recorder.d.ts.map +0 -1
  206. package/dist/drivers/web/tools/network-request-recorder.js +0 -164
  207. package/dist/drivers/web/tools/network-request-recorder.js.map +0 -1
  208. package/dist/drivers/web/tools/screenshot-service.d.ts +0 -10
  209. package/dist/drivers/web/tools/screenshot-service.d.ts.map +0 -1
  210. package/dist/drivers/web/tools/screenshot-service.js +0 -23
  211. package/dist/drivers/web/tools/screenshot-service.js.map +0 -1
  212. package/dist/drivers/web/tools/snapshot-filter/index.d.ts +0 -4
  213. package/dist/drivers/web/tools/snapshot-filter/index.d.ts.map +0 -1
  214. package/dist/drivers/web/tools/snapshot-filter/index.js +0 -4
  215. package/dist/drivers/web/tools/snapshot-filter/index.js.map +0 -1
  216. package/dist/drivers/web/tools/snapshot-filter/semantic-scorer.d.ts +0 -15
  217. package/dist/drivers/web/tools/snapshot-filter/semantic-scorer.d.ts.map +0 -1
  218. package/dist/drivers/web/tools/snapshot-filter/semantic-scorer.js +0 -99
  219. package/dist/drivers/web/tools/snapshot-filter/semantic-scorer.js.map +0 -1
  220. package/dist/drivers/web/tools/snapshot-filter/snapshot-filter.d.ts +0 -5
  221. package/dist/drivers/web/tools/snapshot-filter/snapshot-filter.d.ts.map +0 -1
  222. package/dist/drivers/web/tools/snapshot-filter/snapshot-filter.js +0 -56
  223. package/dist/drivers/web/tools/snapshot-filter/snapshot-filter.js.map +0 -1
  224. package/dist/drivers/web/tools/snapshot-filter/tree-reconstructor.d.ts +0 -3
  225. package/dist/drivers/web/tools/snapshot-filter/tree-reconstructor.d.ts.map +0 -1
  226. package/dist/drivers/web/tools/snapshot-filter/tree-reconstructor.js +0 -85
  227. package/dist/drivers/web/tools/snapshot-filter/tree-reconstructor.js.map +0 -1
  228. package/dist/drivers/web/tools/snapshot-service.d.ts +0 -31
  229. package/dist/drivers/web/tools/snapshot-service.d.ts.map +0 -1
  230. package/dist/drivers/web/tools/snapshot-service.js +0 -96
  231. package/dist/drivers/web/tools/snapshot-service.js.map +0 -1
  232. package/dist/drivers/web/tools/tool.d.ts +0 -76
  233. package/dist/drivers/web/tools/tool.d.ts.map +0 -1
  234. package/dist/drivers/web/tools/tool.js +0 -562
  235. package/dist/drivers/web/tools/tool.js.map +0 -1
  236. package/dist/drivers/web/tools/transient-state-tracker.d.ts +0 -26
  237. package/dist/drivers/web/tools/transient-state-tracker.d.ts.map +0 -1
  238. package/dist/drivers/web/tools/transient-state-tracker.js +0 -301
  239. package/dist/drivers/web/tools/transient-state-tracker.js.map +0 -1
  240. package/dist/drivers/web/tools/types.d.ts +0 -11
  241. package/dist/drivers/web/tools/types.d.ts.map +0 -1
  242. package/dist/drivers/web/tools/types.js +0 -2
  243. package/dist/drivers/web/tools/types.js.map +0 -1
  244. package/dist/evidence/atomic-file.d.ts +0 -23
  245. package/dist/evidence/atomic-file.d.ts.map +0 -1
  246. package/dist/evidence/atomic-file.js +0 -104
  247. package/dist/evidence/atomic-file.js.map +0 -1
  248. package/dist/evidence/layout.d.ts +0 -30
  249. package/dist/evidence/layout.d.ts.map +0 -1
  250. package/dist/evidence/layout.js +0 -152
  251. package/dist/evidence/layout.js.map +0 -1
  252. package/dist/evidence/retention.d.ts +0 -5
  253. package/dist/evidence/retention.d.ts.map +0 -1
  254. package/dist/evidence/retention.js +0 -10
  255. package/dist/evidence/retention.js.map +0 -1
  256. package/dist/evidence/store.d.ts +0 -101
  257. package/dist/evidence/store.d.ts.map +0 -1
  258. package/dist/evidence/store.js +0 -375
  259. package/dist/evidence/store.js.map +0 -1
  260. package/dist/evidence/terminal-finalizer.d.ts +0 -19
  261. package/dist/evidence/terminal-finalizer.d.ts.map +0 -1
  262. package/dist/evidence/terminal-finalizer.js +0 -56
  263. package/dist/evidence/terminal-finalizer.js.map +0 -1
  264. package/dist/index.d.ts +0 -9
  265. package/dist/index.d.ts.map +0 -1
  266. package/dist/index.js +0 -12
  267. package/dist/index.js.map +0 -1
  268. package/dist/logging/invocation-logger.d.ts +0 -4
  269. package/dist/logging/invocation-logger.d.ts.map +0 -1
  270. package/dist/logging/invocation-logger.js +0 -25
  271. package/dist/logging/invocation-logger.js.map +0 -1
  272. package/dist/logging/level-logger.d.ts +0 -11
  273. package/dist/logging/level-logger.d.ts.map +0 -1
  274. package/dist/logging/level-logger.js +0 -82
  275. package/dist/logging/level-logger.js.map +0 -1
  276. package/dist/logging/types.d.ts +0 -8
  277. package/dist/logging/types.d.ts.map +0 -1
  278. package/dist/logging/types.js +0 -7
  279. package/dist/logging/types.js.map +0 -1
  280. package/dist/redaction/diagnostic-sanitizer.d.ts +0 -7
  281. package/dist/redaction/diagnostic-sanitizer.d.ts.map +0 -1
  282. package/dist/redaction/diagnostic-sanitizer.js +0 -42
  283. package/dist/redaction/diagnostic-sanitizer.js.map +0 -1
  284. package/dist/redaction/redactor.d.ts +0 -40
  285. package/dist/redaction/redactor.d.ts.map +0 -1
  286. package/dist/redaction/redactor.js +0 -176
  287. package/dist/redaction/redactor.js.map +0 -1
  288. package/dist/redaction/scrub.d.ts +0 -29
  289. package/dist/redaction/scrub.d.ts.map +0 -1
  290. package/dist/redaction/scrub.js +0 -54
  291. package/dist/redaction/scrub.js.map +0 -1
  292. package/dist/runtime/config.d.ts +0 -16
  293. package/dist/runtime/config.d.ts.map +0 -1
  294. package/dist/runtime/config.js +0 -2
  295. package/dist/runtime/config.js.map +0 -1
  296. package/dist/runtime/driver-boundary.d.ts +0 -16
  297. package/dist/runtime/driver-boundary.d.ts.map +0 -1
  298. package/dist/runtime/driver-boundary.js +0 -71
  299. package/dist/runtime/driver-boundary.js.map +0 -1
  300. package/dist/runtime/driver-session.d.ts +0 -9
  301. package/dist/runtime/driver-session.d.ts.map +0 -1
  302. package/dist/runtime/driver-session.js +0 -64
  303. package/dist/runtime/driver-session.js.map +0 -1
  304. package/dist/runtime/internal-step-evidence.d.ts +0 -30
  305. package/dist/runtime/internal-step-evidence.d.ts.map +0 -1
  306. package/dist/runtime/internal-step-evidence.js +0 -82
  307. package/dist/runtime/internal-step-evidence.js.map +0 -1
  308. package/dist/runtime/result-adapter.d.ts +0 -9
  309. package/dist/runtime/result-adapter.d.ts.map +0 -1
  310. package/dist/runtime/result-adapter.js +0 -77
  311. package/dist/runtime/result-adapter.js.map +0 -1
  312. package/dist/runtime/runner.d.ts +0 -43
  313. package/dist/runtime/runner.d.ts.map +0 -1
  314. package/dist/runtime/runner.js +0 -72
  315. package/dist/runtime/runner.js.map +0 -1
  316. package/dist/runtime/scenario-control.d.ts +0 -40
  317. package/dist/runtime/scenario-control.d.ts.map +0 -1
  318. package/dist/runtime/scenario-control.js +0 -94
  319. package/dist/runtime/scenario-control.js.map +0 -1
  320. package/dist/runtime/scenario-runner.d.ts +0 -41
  321. package/dist/runtime/scenario-runner.d.ts.map +0 -1
  322. package/dist/runtime/scenario-runner.js +0 -274
  323. package/dist/runtime/scenario-runner.js.map +0 -1
  324. package/dist/runtime/scenario-state.d.ts +0 -55
  325. package/dist/runtime/scenario-state.d.ts.map +0 -1
  326. package/dist/runtime/scenario-state.js +0 -128
  327. package/dist/runtime/scenario-state.js.map +0 -1
  328. package/dist/runtime/step-execution.d.ts +0 -27
  329. package/dist/runtime/step-execution.d.ts.map +0 -1
  330. package/dist/runtime/step-execution.js +0 -143
  331. package/dist/runtime/step-execution.js.map +0 -1
  332. package/dist/runtime/text.d.ts +0 -10
  333. package/dist/runtime/text.d.ts.map +0 -1
  334. package/dist/runtime/text.js +0 -25
  335. package/dist/runtime/text.js.map +0 -1
  336. package/dist/runtime/types.d.ts +0 -53
  337. package/dist/runtime/types.d.ts.map +0 -1
  338. package/dist/runtime/types.js +0 -2
  339. package/dist/runtime/types.js.map +0 -1
  340. package/dist/runtime/usage-tracker.d.ts +0 -37
  341. package/dist/runtime/usage-tracker.d.ts.map +0 -1
  342. package/dist/runtime/usage-tracker.js +0 -110
  343. package/dist/runtime/usage-tracker.js.map +0 -1
  344. package/dist/tools/define-agent-tool.d.ts +0 -20
  345. package/dist/tools/define-agent-tool.d.ts.map +0 -1
  346. package/dist/tools/define-agent-tool.js +0 -30
  347. package/dist/tools/define-agent-tool.js.map +0 -1
  348. package/dist/tools/dispatcher.d.ts +0 -19
  349. package/dist/tools/dispatcher.d.ts.map +0 -1
  350. package/dist/tools/dispatcher.js +0 -91
  351. package/dist/tools/dispatcher.js.map +0 -1
  352. package/dist/tools/loop-detector.d.ts +0 -27
  353. package/dist/tools/loop-detector.d.ts.map +0 -1
  354. package/dist/tools/loop-detector.js +0 -76
  355. package/dist/tools/loop-detector.js.map +0 -1
  356. package/dist/tools/registry.d.ts +0 -19
  357. package/dist/tools/registry.d.ts.map +0 -1
  358. package/dist/tools/registry.js +0 -54
  359. package/dist/tools/registry.js.map +0 -1
  360. package/dist/tools/step/result-tool.d.ts +0 -7
  361. package/dist/tools/step/result-tool.d.ts.map +0 -1
  362. package/dist/tools/step/result-tool.js +0 -34
  363. package/dist/tools/step/result-tool.js.map +0 -1
  364. package/dist/tools/tool-contract.d.ts +0 -3
  365. package/dist/tools/tool-contract.d.ts.map +0 -1
  366. package/dist/tools/tool-contract.js +0 -2
  367. package/dist/tools/tool-contract.js.map +0 -1
  368. package/dist/tools/types.d.ts +0 -210
  369. package/dist/tools/types.d.ts.map +0 -1
  370. package/dist/tools/types.js +0 -4
  371. package/dist/tools/types.js.map +0 -1
  372. package/docs/CLI.md +0 -59
  373. package/docs/CONFIGURATION.md +0 -105
  374. package/docs/DRIVERS.md +0 -79
  375. package/docs/EVIDENCE.md +0 -64
  376. package/schemas/checkmate-config.v1.json +0 -176
  377. package/schemas/describe-result.v1.json +0 -414
  378. package/schemas/driver-descriptor.v1.json +0 -66
  379. package/schemas/run-request.v1.json +0 -67
  380. package/schemas/run-result.v1.json +0 -333
  381. 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
+ }