@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,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
+ }