@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,645 @@
1
+ import type { Store } from '../store.ts'
2
+ import type { Html } from './html.ts'
3
+ import type { Page } from './layout.ts'
4
+ import type { ListPage, ListQuery } from '../views.ts'
5
+ import { inbox, operationLog, pagedOperations, pagedPlans, pagedRuns, planList, runList } from '../views.ts'
6
+ import { html, join, pill, plural, time, words } from './html.ts'
7
+ import { heading } from './layout.ts'
8
+
9
+ type Waiting = ReturnType<typeof inbox>
10
+
11
+ export function inboxItems(waiting: Waiting) {
12
+ return [
13
+ ...waiting.gaps.map((gap) => `/plans/${gap.planId}@${gap.revision}/gaps/${gap.gapId}#answer`),
14
+ ...waiting.plans.map((plan) => `/plans/${plan.planId}@${plan.revision}#decide`),
15
+ ...waiting.results.map((result) => `/runs/${result.runId}#decide`),
16
+ ]
17
+ }
18
+
19
+ export function loopState(loop: { state: string; waitsOn: string }) {
20
+ const role = loop.waitsOn === 'qa' ? 'QA' : loop.waitsOn
21
+ return html`<span class="loop"
22
+ >${pill(loop.state)}${loop.waitsOn === 'none' ? '' : html` <span class="muted small">waits on ${role}</span>`}</span
23
+ >`
24
+ }
25
+
26
+ export function inboxCount(waiting: Waiting) {
27
+ return waiting.gaps.length + waiting.plans.length + waiting.results.length + waiting.requests.length
28
+ }
29
+
30
+ const countOrder = ['passed', 'failed', 'inconclusive', 'error', 'blocked', 'not_run'] as const
31
+ const countLook: Record<string, [string, string]> = {
32
+ passed: ['✓', 'pass'],
33
+ failed: ['✗', 'fail'],
34
+ inconclusive: ['?', 'unsure'],
35
+ error: ['!', 'warn'],
36
+ blocked: ['–', 'block'],
37
+ not_run: ['–', 'block'],
38
+ }
39
+
40
+ export function counts(values: Record<string, number>) {
41
+ const shown = countOrder.filter((key) => values[key])
42
+ if (!shown.length) return html`<span class="muted">no outcomes yet</span>`
43
+ return html`<span class="counts"
44
+ >${shown.map(
45
+ (key) =>
46
+ html`<span class="st ${countLook[key][1]}" title="${values[key]} ${words(key)}"
47
+ ><span aria-hidden="true">${countLook[key][0]}</span> ${values[key]}<span class="sr-only">
48
+ ${words(key)}</span
49
+ ></span
50
+ >`
51
+ )}</span
52
+ >`
53
+ }
54
+
55
+ function group(title: string, items: Html[], key: string) {
56
+ return html`<section class="group" aria-labelledby="group-${key}">
57
+ <h2 class="group-h" id="group-${key}">${title} · ${items.length}</h2>
58
+ ${
59
+ items.length
60
+ ? html`<ul class="items">
61
+ ${items.map((item) => html`<li>${item}</li>`)}
62
+ </ul>`
63
+ : html`<p class="none">None.</p>`
64
+ }
65
+ </section>`
66
+ }
67
+
68
+ export function inboxPage(store: Store): Page {
69
+ const waiting = inbox(store)
70
+ const total = inboxCount(waiting)
71
+ const gaps = waiting.gaps.map(
72
+ (gap) =>
73
+ html`<a
74
+ class="item"
75
+ href="/plans/${gap.planId}@${gap.revision}/gaps/${gap.gapId}#answer"
76
+ data-nav-item
77
+ data-key="gap:${gap.planId}:${gap.gapId}"
78
+ data-announce="New gap ${gap.planId} ${gap.gapId}"
79
+ >
80
+ <span class="id">${gap.planId} · ${gap.gapId}</span>
81
+ <span class="what"
82
+ >${gap.question}${gap.runId ? html` <span class="muted">· from run ${gap.runId}</span>` : ''}</span
83
+ >
84
+ <span class="end">${pill(gap.severity)} <span class="age">${time(gap.since)}</span></span>
85
+ </a>`
86
+ )
87
+ const plans = waiting.plans.map(
88
+ (plan) =>
89
+ html`<a
90
+ class="item"
91
+ href="/plans/${plan.planId}@${plan.revision}#decide"
92
+ data-nav-item
93
+ data-key="plan:${plan.planId}@${plan.revision}"
94
+ data-announce="${plan.planId}@${plan.revision} waits for approval"
95
+ >
96
+ <span class="id">${plan.planId}@${plan.revision}</span>
97
+ <span class="what">${plan.summary}</span>
98
+ <span class="end"><span class="age">${time(plan.since)}</span></span>
99
+ </a>`
100
+ )
101
+ const results = waiting.results.map(
102
+ (result) =>
103
+ html`<a
104
+ class="item"
105
+ href="/runs/${result.runId}#decide"
106
+ data-nav-item
107
+ data-key="run:${result.runId}"
108
+ data-announce="${result.runId} waits for acceptance"
109
+ >
110
+ <span class="id">${result.runId}</span>
111
+ <span class="what"
112
+ >${result.planId}@${result.revision} · build <code>${result.build}</code> · ${counts(result.counts)}
113
+ · coverage ${result.coverage}</span
114
+ >
115
+ <span class="end"><span class="age">${time(result.since)}</span></span>
116
+ </a>`
117
+ )
118
+ const requests = waiting.requests.map(
119
+ (request) =>
120
+ html`<div class="item" data-key="request:${request.planId}">
121
+ <span class="id">${request.planId}</span>
122
+ <span class="what"
123
+ >${request.objective} · target <code>${request.target}</code> · run
124
+ <code>/checkmate:plan ${request.planId}</code></span
125
+ >
126
+ <span class="end">${pill('requested')} <span class="age">${time(request.since)}</span></span>
127
+ </div>`
128
+ )
129
+ const sub = total ? `${plural(total, 'item')} ${total === 1 ? 'waits' : 'wait'} for you, oldest first.` : null
130
+ const body = total
131
+ ? html`${group('Gaps to answer', gaps, 'gaps')} ${group('Plans to approve', plans, 'plans')}
132
+ ${group('Results to accept', results, 'results')} ${group('Plans to draft', requests, 'requests')}`
133
+ : emptyInbox(store)
134
+ return {
135
+ title: 'Inbox',
136
+ section: 'inbox',
137
+ subjects: ['inbox'],
138
+ body: html`${heading('Inbox', sub)}
139
+ <div class="stack" data-region="inbox" data-list>${body}</div>`,
140
+ }
141
+ }
142
+
143
+ function emptyInbox(store: Store) {
144
+ const latest = runList(store).slice(-3).reverse()
145
+ return html`<div class="empty">
146
+ <p class="empty-title">Nothing waits for you.</p>
147
+ ${
148
+ latest.length
149
+ ? html`<p>Latest runs:</p>
150
+ <ul class="items">
151
+ ${latest.map(
152
+ (run) =>
153
+ html`<li>
154
+ <a class="item" href="/runs/${run.runId}" data-nav-item
155
+ ><span class="id">${run.runId}</span
156
+ ><span class="what"
157
+ >${run.planId}@${run.revision} · build <code>${run.build}</code></span
158
+ ><span class="end">${pill(run.acceptance ?? run.state)}</span></a
159
+ >
160
+ </li>`
161
+ )}
162
+ </ul>`
163
+ : ''
164
+ }
165
+ <p><a href="/runs">All runs</a></p>
166
+ </div>`
167
+ }
168
+
169
+ type Direction = 'asc' | 'desc'
170
+ type Column = { label: string; sort?: string; first?: Direction }
171
+ type Filter = { name: string; label: string; choices: string[][]; strict: boolean }
172
+
173
+ export type ListSpec = {
174
+ path: string
175
+ region: string
176
+ noun: string
177
+ search: string
178
+ columns: Column[]
179
+ sort: string
180
+ direction: Direction
181
+ filters: Filter[]
182
+ }
183
+
184
+ export type ListState = {
185
+ q: string
186
+ size: number
187
+ page: number
188
+ sort: string
189
+ direction: Direction
190
+ filters: Record<string, string>
191
+ }
192
+
193
+ const pageSizes = [25, 50, 100]
194
+
195
+ export function listState(params: URLSearchParams, spec: ListSpec): ListState {
196
+ const size = Number(params.get('size'))
197
+ const page = Math.floor(Number(params.get('page')))
198
+ const sort = params.get('sort') ?? ''
199
+ const direction = params.get('dir')
200
+ const filters: Record<string, string> = {}
201
+ for (const filter of spec.filters) {
202
+ const value = params.get(filter.name)?.trim() ?? ''
203
+ if (!value) continue
204
+ if (filter.strict && !filter.choices.some(([choice]) => choice === value)) continue
205
+ filters[filter.name] = value
206
+ }
207
+ return {
208
+ q: params.get('q')?.trim() ?? '',
209
+ size: pageSizes.includes(size) ? size : 25,
210
+ page: page > 1 ? page : 1,
211
+ sort: spec.columns.some((column) => column.sort === sort) ? sort : spec.sort,
212
+ direction: direction === 'asc' || direction === 'desc' ? direction : spec.direction,
213
+ filters,
214
+ }
215
+ }
216
+
217
+ export function listHref(spec: ListSpec, state: ListState, change: Partial<ListState> = {}) {
218
+ const next = { ...state, ...change }
219
+ const params = new URLSearchParams()
220
+ if (next.q) params.set('q', next.q)
221
+ for (const filter of spec.filters) if (next.filters[filter.name]) params.set(filter.name, next.filters[filter.name])
222
+ if (next.size !== 25) params.set('size', String(next.size))
223
+ if (next.sort !== spec.sort) params.set('sort', next.sort)
224
+ if (next.direction !== spec.direction) params.set('dir', next.direction)
225
+ if (next.page > 1) params.set('page', String(next.page))
226
+ const query = params.toString()
227
+ return query ? `${spec.path}?${query}` : spec.path
228
+ }
229
+
230
+ function listPage<T>(state: ListState, load: (query: ListQuery) => ListPage<T>) {
231
+ const query = (page: number) => ({
232
+ limit: state.size,
233
+ offset: (page - 1) * state.size,
234
+ sort: state.sort,
235
+ direction: state.direction,
236
+ q: state.q,
237
+ filters: state.filters,
238
+ })
239
+ const page = load(query(state.page))
240
+ const last = Math.max(1, Math.ceil(page.total / state.size))
241
+ if (state.page <= last) return { page, state }
242
+ return { page: load(query(last)), state: { ...state, page: last } }
243
+ }
244
+
245
+ function options(choices: string[][], selected: string) {
246
+ return choices.map(
247
+ ([value, label]) => html`<option value="${value}" ${value === selected ? html`selected` : ''}>${label}</option>`
248
+ )
249
+ }
250
+
251
+ function filterChoices(filter: Filter, selected: string) {
252
+ if (!selected || filter.choices.some(([value]) => value === selected)) return filter.choices
253
+ return [...filter.choices, [selected, selected]]
254
+ }
255
+
256
+ function filterBar(spec: ListSpec, state: ListState) {
257
+ const hiddenField = (name: string, value: string, fallback: string) =>
258
+ html`<input type="hidden" name="${name}" value="${value}" data-default="${fallback}" />`
259
+ return html`<form class="filters" method="get" action="${spec.path}" role="search" data-list-form>
260
+ <label class="sr-only" for="q">Search ${spec.noun}s</label>
261
+ <input
262
+ id="q"
263
+ type="search"
264
+ name="q"
265
+ value="${state.q}"
266
+ placeholder="${spec.search}"
267
+ autocomplete="off"
268
+ data-search
269
+ />
270
+ ${spec.filters.map(
271
+ (filter) =>
272
+ html`<label class="sr-only" for="filter-${filter.name}">${filter.label}</label>
273
+ <select id="filter-${filter.name}" name="${filter.name}" data-default="">
274
+ ${options(filterChoices(filter, state.filters[filter.name] ?? ''), state.filters[filter.name] ?? '')}
275
+ </select>`
276
+ )}
277
+ <label class="sr-only" for="size">Rows per page</label>
278
+ <select id="size" name="size" data-default="25">
279
+ ${options(
280
+ pageSizes.map((size) => [String(size), `${size} rows`]),
281
+ String(state.size)
282
+ )}
283
+ </select>
284
+ ${hiddenField('sort', state.sort, spec.sort)} ${hiddenField('dir', state.direction, spec.direction)}
285
+ <button class="btn" data-list-submit>Filter</button>
286
+ <a class="btn ghost" href="${spec.path}" data-list-clear>Clear</a>
287
+ </form>`
288
+ }
289
+
290
+ function headerCell(spec: ListSpec, state: ListState, column: Column) {
291
+ if (!column.sort) return html`<th scope="col">${column.label}</th>`
292
+ const active = state.sort === column.sort
293
+ const direction: Direction = active ? (state.direction === 'asc' ? 'desc' : 'asc') : (column.first ?? 'asc')
294
+ const href = listHref(spec, state, { sort: column.sort, direction, page: 1 })
295
+ const sorted = active ? html` aria-sort="${state.direction === 'asc' ? 'ascending' : 'descending'}"` : ''
296
+ const arrow = active ? (state.direction === 'asc' ? '↑' : '↓') : '↕'
297
+ return html`<th scope="col" ${sorted}>
298
+ <a class="sort${active ? ' on' : ''}" href="${href}" data-list-link
299
+ >${column.label}<span class="arrow" aria-hidden="true">${arrow}</span></a
300
+ >
301
+ </th>`
302
+ }
303
+
304
+ function pager(spec: ListSpec, state: ListState, total: number) {
305
+ const last = Math.max(1, Math.ceil(total / state.size))
306
+ if (last === 1) return ''
307
+ const link = (page: number, label: string, rel: string) =>
308
+ page === state.page || page < 1 || page > last
309
+ ? html`<span class="btn small" aria-disabled="true">${label}</span>`
310
+ : html`<a class="btn small" href="${listHref(spec, state, { page })}" rel="${rel}" data-list-link
311
+ >${label}</a
312
+ >`
313
+ return html`<nav class="pager" aria-label="Pages">
314
+ ${link(1, '« First', 'first')} ${link(state.page - 1, '‹ Previous', 'prev')}
315
+ <span class="pager-at">Page ${state.page} of ${last}</span>
316
+ ${link(state.page + 1, 'Next ›', 'next')} ${link(last, 'Last »', 'last')}
317
+ </nav>`
318
+ }
319
+
320
+ function listRegion(spec: ListSpec, state: ListState, total: number, rows: Html[], tableClass = '') {
321
+ const first = total ? (state.page - 1) * state.size + 1 : 0
322
+ const last = Math.min(total, state.page * state.size)
323
+ const count = total
324
+ ? `Showing ${first}–${last} of ${plural(total, spec.noun)}`
325
+ : `No ${spec.noun} matches these filters.`
326
+ return html`<div data-region="${spec.region}" data-list-region>
327
+ <p class="list-count" data-list-count role="status">${count}</p>
328
+ ${
329
+ total
330
+ ? html`<div class="scroll">
331
+ <table class="${tableClass}">
332
+ <thead>
333
+ <tr>
334
+ ${spec.columns.map((column) => headerCell(spec, state, column))}
335
+ </tr>
336
+ </thead>
337
+ <tbody>
338
+ ${rows}
339
+ </tbody>
340
+ </table>
341
+ </div>
342
+ ${pager(spec, state, total)}`
343
+ : ''
344
+ }
345
+ </div>`
346
+ }
347
+
348
+ const planShows = [
349
+ ['', 'All plans'],
350
+ ['needs_decision', 'Open gaps'],
351
+ ['awaiting_approval', 'Awaiting approval'],
352
+ ['blocked', 'Blocked'],
353
+ ]
354
+
355
+ const approvals = [
356
+ ['', 'Any approval'],
357
+ ['pending', 'Pending'],
358
+ ['approved', 'Approved'],
359
+ ['rejected', 'Rejected'],
360
+ ['not_required', 'Not required'],
361
+ ]
362
+
363
+ const waits = [
364
+ ['', 'Anyone'],
365
+ ['oracle', 'Oracle'],
366
+ ['planner', 'Planner'],
367
+ ['qa', 'QA'],
368
+ ['factory', 'Factory'],
369
+ ['none', 'Nobody'],
370
+ ]
371
+
372
+ export function plansSpec(): ListSpec {
373
+ return {
374
+ path: '/plans',
375
+ region: 'plans',
376
+ noun: 'plan',
377
+ search: 'Search plans',
378
+ columns: [
379
+ { label: 'Plan', sort: 'plan' },
380
+ { label: 'Loop', sort: 'loop' },
381
+ { label: 'Tests', sort: 'tests', first: 'desc' },
382
+ { label: 'Open gaps', sort: 'gaps', first: 'desc' },
383
+ { label: 'Approval', sort: 'approval' },
384
+ { label: 'Blocks', sort: 'blocked', first: 'desc' },
385
+ { label: 'Updated', sort: 'updated', first: 'desc' },
386
+ ],
387
+ sort: 'updated',
388
+ direction: 'desc',
389
+ filters: [
390
+ { name: 'show', label: 'Show', choices: planShows, strict: true },
391
+ { name: 'approval', label: 'Approval', choices: approvals, strict: true },
392
+ { name: 'waits', label: 'Waits on', choices: waits, strict: true },
393
+ ],
394
+ }
395
+ }
396
+
397
+ type PlanRow = ReturnType<typeof pagedPlans>['rows'][number]
398
+
399
+ function testSummary(tests: PlanRow['tests']) {
400
+ return html`${plural(tests.total, 'test')}
401
+ <span class="muted"
402
+ >· ${tests.ready}
403
+ ready${tests.blocked ? ` · ${tests.blocked} blocked` : ''}${tests.deferred ? ` · ${tests.deferred} deferred` : ''}</span
404
+ >`
405
+ }
406
+
407
+ function requestedPlanRow(row: PlanRow) {
408
+ return html`<tr data-key="plan:${row.planId}" data-nav-row>
409
+ <td><span class="mono">${row.planId}</span></td>
410
+ <td>${loopState(row.loop)}</td>
411
+ <td><span class="muted">no revision yet</span></td>
412
+ <td><span class="muted">none</span></td>
413
+ <td>${pill('requested')}</td>
414
+ <td><span class="muted">none</span></td>
415
+ <td class="nowrap">${time(row.since)}</td>
416
+ </tr>`
417
+ }
418
+
419
+ function planRow(row: PlanRow) {
420
+ if (row.status === 'requested') return requestedPlanRow(row)
421
+ return html`<tr data-key="plan:${row.planId}" data-nav-row>
422
+ <td>
423
+ <a class="mono" href="/plans/${row.planId}@${row.revision}" data-nav-item>${row.planId}@${row.revision}</a>
424
+ </td>
425
+ <td>${loopState(row.loop)}</td>
426
+ <td>${testSummary(row.tests)}</td>
427
+ <td>
428
+ ${
429
+ row.openGaps.length
430
+ ? join(
431
+ row.openGaps.map(
432
+ (id) =>
433
+ html`<a class="mono" href="/plans/${row.planId}@${row.revision}/gaps/${id}"
434
+ >${id}${row.openBlockingGaps.includes(id) ? html`<span class="sr-only"> blocking</span>` : ''}</a
435
+ >`
436
+ ),
437
+ ', '
438
+ )
439
+ : html`<span class="muted">none</span>`
440
+ }
441
+ </td>
442
+ <td>${pill(row.approval ?? row.status)}</td>
443
+ <td>
444
+ ${
445
+ row.blocked.length
446
+ ? html`<ul class="plain">
447
+ ${row.blocked.map(
448
+ (block) =>
449
+ html`<li>
450
+ <span class="mono">${block.testId}</span> by
451
+ <span class="mono">${block.dependency}</span>: ${block.cause}
452
+ </li>`
453
+ )}
454
+ </ul>`
455
+ : html`<span class="muted">none</span>`
456
+ }
457
+ </td>
458
+ <td class="nowrap">${time(row.since)}</td>
459
+ </tr>`
460
+ }
461
+
462
+ export function plansPage(store: Store, params: URLSearchParams): Page {
463
+ const spec = plansSpec()
464
+ const { page, state } = listPage(listState(params, spec), (query) => pagedPlans(store, query))
465
+ return {
466
+ title: 'Plans',
467
+ section: 'plans',
468
+ subjects: ['*'],
469
+ body: html`${heading('Plans', 'Each plan at its latest revision.')} ${filterBar(spec, state)}
470
+ ${listRegion(spec, state, page.total, page.rows.map(planRow))}`,
471
+ }
472
+ }
473
+
474
+ const runStatuses = [
475
+ ['', 'All runs'],
476
+ ['started', 'In progress'],
477
+ ['pending', 'Awaiting acceptance'],
478
+ ['approved', 'Accepted'],
479
+ ['rejected', 'Rejected'],
480
+ ['not_required', 'No acceptance needed'],
481
+ ]
482
+
483
+ export function runStatus(row: { state: string; acceptance: string | null }) {
484
+ if (row.state === 'started') return pill('started', 'in progress')
485
+ if (row.acceptance === 'approved') return pill('approved', 'accepted')
486
+ if (row.acceptance === 'pending') return pill('pending', 'awaiting acceptance')
487
+ return pill(row.acceptance ?? 'finalized')
488
+ }
489
+
490
+ function distinct(values: string[]) {
491
+ return [...new Set(values)].sort().map((value) => [value, value])
492
+ }
493
+
494
+ export function runsSpec(store: Store): ListSpec {
495
+ const runs = runList(store)
496
+ return {
497
+ path: '/runs',
498
+ region: 'runs',
499
+ noun: 'run',
500
+ search: 'Search runs',
501
+ columns: [
502
+ { label: 'Run', sort: 'run' },
503
+ { label: 'Plan', sort: 'plan' },
504
+ { label: 'Build', sort: 'build' },
505
+ { label: 'Tests' },
506
+ { label: 'Completion', sort: 'completion' },
507
+ { label: 'Coverage', sort: 'coverage' },
508
+ { label: 'Status', sort: 'status' },
509
+ { label: 'Started', sort: 'started', first: 'desc' },
510
+ ],
511
+ sort: 'started',
512
+ direction: 'desc',
513
+ filters: [
514
+ {
515
+ name: 'plan',
516
+ label: 'Plan',
517
+ choices: [['', 'All plans'], ...distinct(planList(store).map((row) => row.planId))],
518
+ strict: false,
519
+ },
520
+ {
521
+ name: 'build',
522
+ label: 'Build',
523
+ choices: [['', 'All builds'], ...distinct(runs.map((row) => row.build))],
524
+ strict: false,
525
+ },
526
+ { name: 'status', label: 'Status', choices: runStatuses, strict: true },
527
+ ],
528
+ }
529
+ }
530
+
531
+ type RunRow = ReturnType<typeof pagedRuns>['rows'][number]
532
+
533
+ function runRow(row: RunRow) {
534
+ return html`<tr data-key="run:${row.runId}" data-nav-row>
535
+ <td><a class="mono" href="/runs/${row.runId}" data-nav-item>${row.runId}</a></td>
536
+ <td><a class="mono" href="/plans/${row.planId}@${row.revision}">${row.planId}@${row.revision}</a></td>
537
+ <td class="mono">${row.build}</td>
538
+ <td>${counts(row.counts)}</td>
539
+ <td>${row.completion ? pill(row.completion) : html`<span class="muted">running</span>`}</td>
540
+ <td>${row.coverage ? pill(row.coverage) : html`<span class="muted">–</span>`}</td>
541
+ <td>${runStatus(row)}</td>
542
+ <td class="nowrap">${time(row.startedAt)}</td>
543
+ </tr>`
544
+ }
545
+
546
+ export function runsPage(store: Store, params: URLSearchParams): Page {
547
+ const spec = runsSpec(store)
548
+ const { page, state } = listPage(listState(params, spec), (query) => pagedRuns(store, query))
549
+ return {
550
+ title: 'Runs',
551
+ section: 'runs',
552
+ subjects: ['*'],
553
+ body: html`${heading('Runs', 'Every run of every plan.')} ${filterBar(spec, state)}
554
+ ${listRegion(spec, state, page.total, page.rows.map(runRow))}`,
555
+ }
556
+ }
557
+
558
+ export function subjectLink(subject: string) {
559
+ const [kind, id] = subject.split(/:(.*)/)
560
+ if (kind === 'plan') return html`<a class="mono" href="/plans/${id}">${subject}</a>`
561
+ if (kind === 'run') return html`<a class="mono" href="/runs/${id}">${subject}</a>`
562
+ if (kind === 'inbox') return html`<a class="mono" href="/">${subject}</a>`
563
+ return html`<span class="mono">${subject}</span>`
564
+ }
565
+
566
+ function reasons(response: unknown) {
567
+ const list = (response as { reasons?: unknown })?.reasons
568
+ return Array.isArray(list) ? list.map(String) : []
569
+ }
570
+
571
+ export function logSpec(store: Store): ListSpec {
572
+ const recent = operationLog(store, { limit: 1000 })
573
+ return {
574
+ path: '/log',
575
+ region: 'log',
576
+ noun: 'operation',
577
+ search: 'Search the log',
578
+ columns: [
579
+ { label: 'Time', sort: 'time', first: 'desc' },
580
+ { label: 'Actor', sort: 'actor' },
581
+ { label: 'Command', sort: 'command' },
582
+ { label: 'Status', sort: 'status' },
583
+ { label: 'Subjects' },
584
+ { label: 'Operation' },
585
+ ],
586
+ sort: 'time',
587
+ direction: 'desc',
588
+ filters: [
589
+ {
590
+ name: 'subject',
591
+ label: 'Subject',
592
+ choices: [['', 'All subjects'], ...distinct(recent.flatMap((row) => row.subjects))],
593
+ strict: false,
594
+ },
595
+ {
596
+ name: 'actor',
597
+ label: 'Actor',
598
+ choices: [['', 'All actors'], ...distinct(recent.map((row) => row.actor))],
599
+ strict: false,
600
+ },
601
+ {
602
+ name: 'command',
603
+ label: 'Command',
604
+ choices: [['', 'All commands'], ...distinct(recent.map((row) => row.command))],
605
+ strict: false,
606
+ },
607
+ {
608
+ name: 'status',
609
+ label: 'Status',
610
+ choices: [['', 'All statuses'], ...distinct(recent.map((row) => row.status))],
611
+ strict: false,
612
+ },
613
+ ],
614
+ }
615
+ }
616
+
617
+ type LogRow = ReturnType<typeof pagedOperations>['rows'][number]
618
+
619
+ function logRow(row: LogRow) {
620
+ return html`<tr data-key="op:${row.rowid}">
621
+ <td class="nowrap">${time(row.at)}</td>
622
+ <td>${row.actor}</td>
623
+ <td>
624
+ <span class="mono">${row.command}</span>
625
+ ${row.status === 'rejected' ? html`<div class="reason">${reasons(row.response).join(' ')}</div>` : ''}
626
+ </td>
627
+ <td>${pill(row.status === 'rejected' ? 'rejected_operation' : row.status, words(row.status))}</td>
628
+ <td>
629
+ ${row.subjects.length ? join(row.subjects.map(subjectLink), ' ') : html`<span class="muted">none</span>`}
630
+ </td>
631
+ <td class="mono muted">${row.id}</td>
632
+ </tr>`
633
+ }
634
+
635
+ export function logPage(store: Store, params: URLSearchParams): Page {
636
+ const spec = logSpec(store)
637
+ const { page, state } = listPage(listState(params, spec), (query) => pagedOperations(store, query))
638
+ return {
639
+ title: 'Log',
640
+ section: 'log',
641
+ subjects: ['*'],
642
+ body: html`${heading('Log', 'Every write to the workspace.')} ${filterBar(spec, state)}
643
+ ${listRegion(spec, state, page.total, page.rows.map(logRow), 'log')}`,
644
+ }
645
+ }