@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,684 @@
1
+ import type { IncomingMessage, ServerResponse } from 'node:http'
2
+ import type { Store } from '../store.ts'
3
+ import type { Context, Page, Theme } from './layout.ts'
4
+ import type { FormState } from './forms.ts'
5
+ import type { EditState } from './edit.ts'
6
+ import { createServer } from 'node:http'
7
+ import { randomBytes, timingSafeEqual } from 'node:crypto'
8
+ import { existsSync, readFileSync } from 'node:fs'
9
+ import { fileURLToPath } from 'node:url'
10
+ import { closeStore, openReadOnlyStore, openStore } from '../store.ts'
11
+ import { adoptGaps, answerGap, approvePlan, editPlan, planCandidate, rejectPlan } from '../plans.ts'
12
+ import { exportPlan, exportRun } from '../render.ts'
13
+ import { acceptResult, rejectResult } from '../runs.ts'
14
+ import { changesAfter, evidenceBytes, inbox, operationLog, planDetail, runDetail } from '../views.ts'
15
+ import { defaultActor } from '../actor.ts'
16
+ import { html } from './html.ts'
17
+ import { heading, layout } from './layout.ts'
18
+ import { emptyState } from './forms.ts'
19
+ import { inboxCount, inboxItems, inboxPage, logPage, plansPage, runsPage } from './lists.ts'
20
+ import { gapPage, planHistoryList, planPage } from './plan.ts'
21
+ import { candidateFromForm, candidateJson, editPage } from './edit.ts'
22
+ import type { AdoptError } from './run.ts'
23
+ import { runPage } from './run.ts'
24
+
25
+ export type UiOptions = { workspace: string; port?: number; defaultActor?: string }
26
+ export type Ui = { url: string; close(): Promise<void> }
27
+
28
+ type Request = { req: IncomingMessage; res: ServerResponse; url: URL; cookies: Record<string, string> }
29
+ type Client = { res: ServerResponse; subjects: Set<string>; last: number }
30
+ type Change = ReturnType<typeof changesAfter>[number]
31
+
32
+ const id = '[A-Za-z0-9][A-Za-z0-9_-]{0,95}'
33
+ const routes = {
34
+ plan: new RegExp(`^/plans/(${id})@([1-9][0-9]{0,8})$`),
35
+ latestPlan: new RegExp(`^/plans/(${id})$`),
36
+ gap: new RegExp(`^/plans/(${id})@([1-9][0-9]{0,8})/gaps/(${id})$`),
37
+ edit: new RegExp(`^/plans/(${id})@([1-9][0-9]{0,8})/edit$`),
38
+ history: new RegExp(`^/plans/(${id})@([1-9][0-9]{0,8})/history$`),
39
+ planExport: new RegExp(`^/plans/(${id})@([1-9][0-9]{0,8})/export\\.(html|md|json)$`),
40
+ run: new RegExp(`^/runs/(${id})$`),
41
+ runGap: new RegExp(`^/runs/(${id})/gaps/(${id})$`),
42
+ report: new RegExp(`^/runs/(${id})/report\\.(html|md|json)$`),
43
+ evidence: /^\/evidence\/([0-9A-Z]{26})$/,
44
+ font: /^\/assets\/fonts\/(inter|space-grotesk|jetbrains-mono)\.woff2$/,
45
+ }
46
+ const contentSecurityPolicy =
47
+ "default-src 'self'; img-src 'self' data:; style-src 'self'; script-src 'self'; connect-src 'self'; font-src 'self'; frame-ancestors 'none'; form-action 'self'"
48
+ const inlineImages = ['image/png', 'image/jpeg', 'image/webp', 'image/gif']
49
+ const documentTypes: Record<string, string> = {
50
+ html: 'text/html; charset=utf-8',
51
+ md: 'text/markdown; charset=utf-8',
52
+ json: 'application/json; charset=utf-8',
53
+ }
54
+ const themes: Theme[] = ['system', 'light', 'dark']
55
+ const maxForm = 256 * 1024
56
+
57
+ function directory(candidates: string[], file: string) {
58
+ for (const candidate of candidates) {
59
+ const path = fileURLToPath(new URL(candidate, import.meta.url))
60
+ if (existsSync(path + file)) return path
61
+ }
62
+ throw new Error(`Checkmate UI files are missing: ${file} not found in ${candidates.join(', ')}`)
63
+ }
64
+
65
+ function assets() {
66
+ const files = directory(['./assets/', '../src/ui/assets/', '../plugin/src/ui/assets/'], 'style.css')
67
+ const fonts = `${files}fonts/`
68
+ const read = (name: string) => readFileSync(files + name)
69
+ return {
70
+ '/assets/style.css': { type: 'text/css; charset=utf-8', body: read('style.css') },
71
+ '/assets/client.js': { type: 'text/javascript; charset=utf-8', body: read('client.js') },
72
+ '/assets/favicon.svg': { type: 'image/svg+xml', body: read('favicon.svg') },
73
+ font: (name: string) => readFileSync(`${fonts}${name}.woff2`),
74
+ }
75
+ }
76
+
77
+ function securityHeaders(): Record<string, string> {
78
+ return {
79
+ 'Content-Security-Policy': contentSecurityPolicy,
80
+ 'X-Content-Type-Options': 'nosniff',
81
+ 'Referrer-Policy': 'same-origin',
82
+ 'X-Frame-Options': 'DENY',
83
+ }
84
+ }
85
+
86
+ function send(
87
+ res: ServerResponse,
88
+ status: number,
89
+ type: string,
90
+ body: string | Uint8Array,
91
+ headers: Record<string, string | string[]> = {}
92
+ ) {
93
+ res.writeHead(status, { ...securityHeaders(), 'Content-Type': type, 'Cache-Control': 'no-store', ...headers })
94
+ res.end(body)
95
+ }
96
+
97
+ function redirect(res: ServerResponse, location: string, cookies: string[] = []) {
98
+ res.writeHead(303, { ...securityHeaders(), Location: location, 'Set-Cookie': cookies })
99
+ res.end()
100
+ }
101
+
102
+ function parseCookies(header: string | undefined) {
103
+ const cookies: Record<string, string> = {}
104
+ for (const part of (header ?? '').split(';')) {
105
+ const index = part.indexOf('=')
106
+ if (index < 0) continue
107
+ try {
108
+ cookies[part.slice(0, index).trim()] = decodeURIComponent(part.slice(index + 1).trim())
109
+ } catch {}
110
+ }
111
+ return cookies
112
+ }
113
+
114
+ function cookie(name: string, value: string, maxAge = 60 * 60 * 24 * 365) {
115
+ return `${name}=${encodeURIComponent(value)}; Path=/; Max-Age=${maxAge}; HttpOnly; SameSite=Strict`
116
+ }
117
+
118
+ function readForm(req: IncomingMessage): Promise<URLSearchParams> {
119
+ return new Promise((resolve, reject) => {
120
+ const chunks: Buffer[] = []
121
+ let length = 0
122
+ req.on('data', (chunk: Buffer) => {
123
+ length += chunk.length
124
+ if (length > maxForm) {
125
+ reject(new Error('The form is too large.'))
126
+ req.destroy()
127
+ } else chunks.push(chunk)
128
+ })
129
+ req.on('end', () => resolve(new URLSearchParams(Buffer.concat(chunks).toString('utf8'))))
130
+ req.on('error', reject)
131
+ })
132
+ }
133
+
134
+ function same(a: string, b: string) {
135
+ const x = Buffer.from(a)
136
+ const y = Buffer.from(b)
137
+ return x.length === y.length && timingSafeEqual(x, y)
138
+ }
139
+
140
+ function safeBack(value: string | null) {
141
+ return value && value.startsWith('/') && !value.startsWith('//') ? value : '/'
142
+ }
143
+
144
+ function attachmentName(name: string) {
145
+ const plain = name.replace(/[^A-Za-z0-9._-]/g, '_')
146
+ return `filename="${plain}"; filename*=UTF-8''${encodeURIComponent(name)}`
147
+ }
148
+
149
+ function notFoundError(message: string) {
150
+ return /^Unknown|has no revision|not found/i.test(message)
151
+ }
152
+
153
+ function plainMessage(error: unknown) {
154
+ return error instanceof Error ? error.message : String(error)
155
+ }
156
+
157
+ export async function startUi(options: UiOptions): Promise<Ui> {
158
+ const files = assets()
159
+ const store = openStore(options.workspace)
160
+ const watch = openReadOnlyStore(options.workspace)
161
+ const csrf = randomBytes(24).toString('hex')
162
+ const fallbackActor = options.defaultActor ?? defaultActor(options.workspace, process.env) ?? ''
163
+ const clients = new Set<Client>()
164
+ let port = 0
165
+
166
+ const latestRowid = (source: Store) => operationLog(source, { limit: 1 })[0]?.rowid ?? 0
167
+ const dataVersion = () => Number(watch.db.prepare('PRAGMA data_version').get()?.data_version ?? 0)
168
+ let version = dataVersion()
169
+ let cursor = latestRowid(watch)
170
+
171
+ function deliver(client: Client, change: Change) {
172
+ if (change.rowid <= client.last) return
173
+ const matched = client.subjects.has('*')
174
+ ? change.subjects
175
+ : change.subjects.filter((subject) => client.subjects.has(subject))
176
+ if (!matched.length && !client.subjects.has('*')) return
177
+ client.last = change.rowid
178
+ client.res.write(`id: ${change.rowid}\ndata: ${matched.join(',') || '*'}\n\n`)
179
+ }
180
+
181
+ const poll = setInterval(() => {
182
+ const current = dataVersion()
183
+ if (current === version) return
184
+ version = current
185
+ const changes = changesAfter(watch, cursor)
186
+ if (!changes.length) return
187
+ cursor = changes[changes.length - 1].rowid
188
+ for (const client of clients) for (const change of changes) deliver(client, change)
189
+ }, 250)
190
+ const heartbeat = setInterval(() => {
191
+ for (const client of clients) client.res.write(': heartbeat\n\n')
192
+ }, 15_000)
193
+
194
+ function context(request: Request, path = request.url.pathname + request.url.search): Context {
195
+ const theme = themes.includes(request.cookies.checkmate_theme as Theme)
196
+ ? (request.cookies.checkmate_theme as Theme)
197
+ : 'system'
198
+ return {
199
+ path,
200
+ csrf,
201
+ actor: request.cookies.checkmate_actor?.trim() || fallbackActor || null,
202
+ theme,
203
+ toast:
204
+ (request.req.headers['sec-fetch-mode'] ?? 'navigate') === 'navigate'
205
+ ? request.cookies.checkmate_toast || null
206
+ : null,
207
+ inboxCount: inboxCount(inbox(store)),
208
+ cursor: latestRowid(store),
209
+ workspace: options.workspace,
210
+ }
211
+ }
212
+
213
+ function render(request: Request, page: Page, extraCookies: string[] = []) {
214
+ const shown = context(request)
215
+ const cookies = [...extraCookies]
216
+ if (shown.toast) cookies.push(cookie('checkmate_toast', '', 0))
217
+ send(request.res, page.status ?? 200, 'text/html; charset=utf-8', layout(shown, page).html, {
218
+ 'Set-Cookie': cookies,
219
+ Vary: 'Cookie',
220
+ })
221
+ }
222
+
223
+ function errorPage(request: Request, status: number, title: string, message: string) {
224
+ render(request, {
225
+ title,
226
+ section: null,
227
+ subjects: ['inbox'],
228
+ status,
229
+ body: html`${heading(title, message)}
230
+ <p class="actions">
231
+ <a class="btn primary" href="/">Back to inbox</a> <a class="btn" href="/plans">Plans</a>
232
+ <a class="btn" href="/runs">Runs</a>
233
+ </p>`,
234
+ })
235
+ }
236
+
237
+ function nextItem(flash: string, actorCookie: string[]) {
238
+ const next = inboxItems(inbox(store))[0] ?? '/'
239
+ return { location: next, cookies: [...actorCookie, cookie('checkmate_toast', flash, 60)] }
240
+ }
241
+
242
+ function serveEvidence(request: Request, evidenceId: string) {
243
+ let evidence
244
+ try {
245
+ evidence = evidenceBytes(store, evidenceId)
246
+ } catch {
247
+ return errorPage(request, 404, 'Evidence not found', `Checkmate has no evidence ${evidenceId}.`)
248
+ }
249
+ const inline = inlineImages.includes(evidence.mediaType) || evidence.mediaType === 'text/plain'
250
+ send(request.res, 200, evidence.mediaType, evidence.bytes, {
251
+ 'Content-Disposition': `${inline ? 'inline' : 'attachment'}; ${attachmentName(evidence.name)}`,
252
+ 'Cache-Control': 'private, max-age=31536000, immutable',
253
+ })
254
+ }
255
+
256
+ function download(request: Request, body: string, format: string, name: string) {
257
+ send(request.res, 200, documentTypes[format], body, {
258
+ 'Content-Disposition': `attachment; ${attachmentName(name)}`,
259
+ })
260
+ }
261
+
262
+ function events(request: Request) {
263
+ const subjects = new Set(
264
+ (request.url.searchParams.get('subjects') ?? '')
265
+ .split(',')
266
+ .map((subject) => subject.trim())
267
+ .filter(Boolean)
268
+ )
269
+ const header = request.req.headers['last-event-id']
270
+ const after = Number(header ?? request.url.searchParams.get('after') ?? Number.NaN)
271
+ request.res.writeHead(200, {
272
+ ...securityHeaders(),
273
+ 'Content-Type': 'text/event-stream; charset=utf-8',
274
+ 'Cache-Control': 'no-store',
275
+ Connection: 'keep-alive',
276
+ })
277
+ request.res.write('retry: 2000\n\n')
278
+ const client: Client = { res: request.res, subjects, last: -1 }
279
+ if (Number.isInteger(after) && after >= 0) {
280
+ client.last = after
281
+ for (const change of changesAfter(watch, after)) deliver(client, change)
282
+ } else client.last = cursor
283
+ clients.add(client)
284
+ request.req.on('close', () => clients.delete(client))
285
+ }
286
+
287
+ function get(request: Request) {
288
+ const { pathname, searchParams } = request.url
289
+ const asset = files[pathname as keyof typeof files]
290
+ if (asset && typeof asset === 'object')
291
+ return send(request.res, 200, asset.type, asset.body, { 'Cache-Control': 'no-cache' })
292
+ const font = routes.font.exec(pathname)
293
+ if (font)
294
+ return send(request.res, 200, 'font/woff2', files.font(font[1]), {
295
+ 'Cache-Control': 'public, max-age=31536000, immutable',
296
+ })
297
+ if (pathname === '/events') return events(request)
298
+ const evidence = routes.evidence.exec(pathname)
299
+ if (evidence) return serveEvidence(request, evidence[1])
300
+ const shown = context(request)
301
+ if (pathname === '/') return render(request, inboxPage(store))
302
+ if (pathname === '/plans') return render(request, plansPage(store, searchParams))
303
+ if (pathname === '/runs') return render(request, runsPage(store, searchParams))
304
+ if (pathname === '/log') return render(request, logPage(store, searchParams))
305
+ if (pathname === '/actor') return render(request, actorPage(shown, safeBack(searchParams.get('back'))))
306
+ const latest = routes.latestPlan.exec(pathname)
307
+ if (latest) {
308
+ const detail = planDetail(store, latest[1])
309
+ return redirect(request.res, `/plans/${latest[1]}@${detail.latestRevision}`)
310
+ }
311
+ const plan = routes.plan.exec(pathname)
312
+ if (plan)
313
+ return render(
314
+ request,
315
+ planPage(store, shown, plan[1], Number(plan[2]), null, searchParams.get('history') === 'all')
316
+ )
317
+ const edit = routes.edit.exec(pathname)
318
+ if (edit) return render(request, editPage(store, shown, edit[1], Number(edit[2])))
319
+ const history = routes.history.exec(pathname)
320
+ if (history) {
321
+ const detail = planDetail(store, history[1], Number(history[2]))
322
+ return send(request.res, 200, 'text/html; charset=utf-8', planHistoryList(store, detail, true).html)
323
+ }
324
+ const planExport = routes.planExport.exec(pathname)
325
+ if (planExport) {
326
+ const [, planId, revision, format] = planExport
327
+ const body = exportPlan(store, `${planId}@${revision}`, format)
328
+ return download(request, body, format, `${planId}-${revision}-plan.${format}`)
329
+ }
330
+ const report = routes.report.exec(pathname)
331
+ if (report) {
332
+ const [, runId, format] = report
333
+ if (!runDetail(store, runId).result)
334
+ return errorPage(
335
+ request,
336
+ 409,
337
+ 'Report not ready',
338
+ `${runId} is in progress. The report is ready when it finishes.`
339
+ )
340
+ return download(request, exportRun(store, runId, format), format, `${runId}-report.${format}`)
341
+ }
342
+ const gap = routes.gap.exec(pathname)
343
+ if (gap) return render(request, gapPage(store, shown, gap[1], Number(gap[2]), gap[3]))
344
+ const run = routes.run.exec(pathname)
345
+ if (run) return render(request, runPage(store, shown, run[1]))
346
+ return errorPage(request, 404, 'Page not found', `Checkmate has no page at ${pathname}. Start from the inbox.`)
347
+ }
348
+
349
+ function actorPage(shown: Context, back: string): Page {
350
+ return {
351
+ title: 'Your name',
352
+ section: null,
353
+ subjects: ['inbox'],
354
+ body: html`${heading('Your name', 'Checkmate records this name with each decision you make in this browser.')}
355
+ <form method="post" action="/actor" class="panel narrow">
356
+ <input type="hidden" name="csrf" value="${csrf}" />
357
+ <input type="hidden" name="back" value="${back}" />
358
+ <label class="lab" for="actor-name">Name</label>
359
+ <input id="actor-name" name="actor" required autocomplete="name" value="${shown.actor ?? ''}" />
360
+ <div class="btns">
361
+ <button class="btn primary">Save name</button> <a class="btn" href="${back}">Cancel</a>
362
+ </div>
363
+ </form>`,
364
+ }
365
+ }
366
+
367
+ function actorFrom(request: Request, form: URLSearchParams) {
368
+ const typed = form.get('actor')?.trim() ?? ''
369
+ const actor = typed || request.cookies.checkmate_actor?.trim() || fallbackActor
370
+ return { actor, cookies: typed ? [cookie('checkmate_actor', typed)] : [] }
371
+ }
372
+
373
+ function values(form: URLSearchParams, names: string[]) {
374
+ return Object.fromEntries(names.map((name) => [name, form.get(name) ?? '']))
375
+ }
376
+
377
+ function refuse(request: Request, page: () => Page, cookies: string[] = []) {
378
+ const shown = page()
379
+ render(request, { ...shown, status: shown.status ?? 422 }, cookies)
380
+ }
381
+
382
+ function answer(request: Request, form: URLSearchParams, planId: string, revision: number, gapId: string) {
383
+ const state: FormState = {
384
+ ...emptyState(),
385
+ values: values(form, ['kind', 'value', 'source', 'rationale', 'actor']),
386
+ }
387
+ const { actor, cookies } = actorFrom(request, form)
388
+ const kind = state.values.kind
389
+ const page = () => gapPage(store, context(request), planId, revision, gapId, state)
390
+ const check = (condition: boolean, message: string) => {
391
+ if (!condition) state.error ??= message
392
+ }
393
+ check(Boolean(actor), 'Enter your name. Checkmate records it with your answer.')
394
+ check(['clarify', 'accept_assumption', 'defer'].includes(kind), 'Choose Clarify, Accept assumption, or Defer.')
395
+ check(Boolean(state.values.value.trim()), 'Write the answer.')
396
+ check(
397
+ kind !== 'clarify' || Boolean(state.values.source.trim()),
398
+ 'A clarification needs a source. Name where the answer comes from.'
399
+ )
400
+ check(Boolean(state.values.rationale.trim()), 'Write a rationale. Checkmate records it with your answer.')
401
+ if (state.error) return refuse(request, page, cookies)
402
+ try {
403
+ answerGap(store, actor, {
404
+ planId,
405
+ gapId,
406
+ kind,
407
+ value: state.values.value.trim(),
408
+ source: kind === 'clarify' ? state.values.source.trim() : null,
409
+ rationale: state.values.rationale.trim(),
410
+ expectSha256: form.get('sha') ?? '',
411
+ })
412
+ } catch (error) {
413
+ const latest = planDetail(store, planId)
414
+ if (latest.sha256 !== form.get('sha'))
415
+ state.stale = html`<strong>${planId} has a newer revision (${latest.latestRevision}).</strong> Your
416
+ answer was not recorded. Your text is kept below.
417
+ <a href="/plans/${planId}@${latest.latestRevision}/gaps/${gapId}"
418
+ >Review revision ${latest.latestRevision}</a
419
+ >, then send again.`
420
+ else state.error = plainMessage(error)
421
+ return refuse(request, page, cookies)
422
+ }
423
+ const next = nextItem(`Answered ${planId} · ${gapId}`, cookies)
424
+ redirect(request.res, next.location, next.cookies)
425
+ }
426
+
427
+ function verdictState(form: URLSearchParams) {
428
+ return { ...emptyState(), values: values(form, ['verdict', 'rationale', 'actor']) }
429
+ }
430
+
431
+ function decide(
432
+ request: Request,
433
+ form: URLSearchParams,
434
+ choices: string[],
435
+ page: (state: FormState) => Page,
436
+ record: (actor: string, verdict: string, rationale: string, sha: string) => string,
437
+ staleCheck: (sha: string) => FormState['stale']
438
+ ) {
439
+ const state = verdictState(form)
440
+ const { actor, cookies } = actorFrom(request, form)
441
+ const view = () => page(state)
442
+ if (form.get('back')) return refuse(request, () => ({ ...view(), status: 200 }), cookies)
443
+ if (!choices.includes(state.values.verdict)) state.error = 'Choose a decision.'
444
+ else if (!actor) state.error = 'Enter your name. Checkmate records it with your decision.'
445
+ else if (!state.values.rationale.trim())
446
+ state.error = 'Write a rationale. Checkmate records it with your decision.'
447
+ if (state.error) return refuse(request, view, cookies)
448
+ if (form.get('confirmed') !== '1') {
449
+ state.confirm = true
450
+ return refuse(request, () => ({ ...view(), status: 200 }), cookies)
451
+ }
452
+ const sha = form.get('sha') ?? ''
453
+ let flash: string
454
+ try {
455
+ flash = record(actor, state.values.verdict, state.values.rationale.trim(), sha)
456
+ } catch (error) {
457
+ state.stale = staleCheck(sha)
458
+ if (!state.stale) state.error = plainMessage(error)
459
+ return refuse(request, view, cookies)
460
+ }
461
+ const next = nextItem(flash, cookies)
462
+ redirect(request.res, next.location, next.cookies)
463
+ }
464
+
465
+ function decidePlan(request: Request, form: URLSearchParams, planId: string, revision: number) {
466
+ const subject = `${planId}@${revision}`
467
+ decide(
468
+ request,
469
+ form,
470
+ ['approve', 'reject'],
471
+ (state) => planPage(store, context(request), planId, revision, state),
472
+ (actor, verdict, rationale, sha) => {
473
+ const input = { reference: subject, rationale, expectSha256: sha }
474
+ if (verdict === 'approve') approvePlan(store, actor, input)
475
+ else rejectPlan(store, actor, input)
476
+ return `${verdict === 'approve' ? 'Approved' : 'Rejected'} ${subject}`
477
+ },
478
+ (sha) => {
479
+ const detail = planDetail(store, planId, revision)
480
+ if (detail.sha256 === sha) return null
481
+ return html`<strong>${subject} has another digest now.</strong> Your decision was not recorded. Your
482
+ text is kept below. Review the plan, then send again.`
483
+ }
484
+ )
485
+ }
486
+
487
+ function decideRun(request: Request, form: URLSearchParams, runId: string) {
488
+ decide(
489
+ request,
490
+ form,
491
+ ['accept', 'reject'],
492
+ (state) => runPage(store, context(request), runId, state),
493
+ (actor, verdict, rationale, sha) => {
494
+ const input = { runId, rationale, expectSha256: sha }
495
+ if (verdict === 'accept') acceptResult(store, actor, input)
496
+ else rejectResult(store, actor, input)
497
+ return `${verdict === 'accept' ? 'Accepted' : 'Rejected'} ${runId}`
498
+ },
499
+ (sha) => {
500
+ const result = runDetail(store, runId).result
501
+ if (result?.sha256 === sha) return null
502
+ return html`<strong>${runId} has a new result.</strong> Your decision was not recorded. Your text is
503
+ kept below. Review the result, then send again.`
504
+ }
505
+ )
506
+ }
507
+
508
+ function adopt(request: Request, form: URLSearchParams, runId: string, gapId: string) {
509
+ const { actor, cookies } = actorFrom(request, form)
510
+ const planId = runDetail(store, runId).planId
511
+ const sha = form.get('sha') ?? ''
512
+ const page = (message: AdoptError['message']) => () =>
513
+ runPage(store, context(request), runId, null, { gapId, message })
514
+ if (!actor) return refuse(request, page('Enter your name. Checkmate records it with the change.'), cookies)
515
+ const latest = planDetail(store, planId)
516
+ let adopted
517
+ try {
518
+ adopted = adoptGaps(store, actor, {
519
+ reference: `${planId}@${latest.latestRevision}`,
520
+ runId,
521
+ gapIds: [gapId],
522
+ expectSha256: sha,
523
+ })
524
+ } catch (error) {
525
+ const message =
526
+ latest.sha256 === sha
527
+ ? plainMessage(error)
528
+ : html`<strong>${planId} changed after you opened this page.</strong> ${gapId} was not brought into
529
+ the plan. Review the plan, then send again.`
530
+ return refuse(request, page(message), cookies)
531
+ }
532
+ redirect(request.res, `/runs/${runId}#${gapId}`, [
533
+ ...cookies,
534
+ cookie('checkmate_toast', `Brought ${gapId} into ${adopted.subject}`, 60),
535
+ ])
536
+ }
537
+
538
+ function saveEdit(request: Request, form: URLSearchParams, planId: string, revision: number) {
539
+ const { actor, cookies } = actorFrom(request, form)
540
+ const subject = `${planId}@${revision}`
541
+ const detail = planDetail(store, planId, revision)
542
+ const base = planCandidate(detail.plan)
543
+ const mode = form.get('mode') === 'json' ? 'json' : 'form'
544
+ const sha = form.get('sha') ?? ''
545
+ const state: EditState = {
546
+ mode,
547
+ candidate: base,
548
+ json: form.get('json') ?? '',
549
+ actor: form.get('actor') ?? '',
550
+ error: null,
551
+ stale: null,
552
+ }
553
+ const page = () => editPage(store, context(request), planId, revision, state)
554
+ let candidate: unknown
555
+ if (mode === 'json') {
556
+ try {
557
+ candidate = JSON.parse(state.json)
558
+ } catch (error) {
559
+ state.error = `The JSON is not valid: ${plainMessage(error)}`
560
+ }
561
+ } else {
562
+ state.candidate = candidateFromForm(base, form)
563
+ state.json = candidateJson(state.candidate)
564
+ candidate = state.candidate
565
+ }
566
+ if (!actor) state.error ??= 'Enter your name. Checkmate records it with the edit.'
567
+ if (state.error) return refuse(request, page, cookies)
568
+ let saved
569
+ try {
570
+ saved = editPlan(store, actor, { reference: subject, candidate, expectSha256: sha }).data
571
+ } catch (error) {
572
+ const latest = planDetail(store, planId)
573
+ if (latest.latestRevision !== revision || latest.sha256 !== sha)
574
+ state.stale = html`<strong>${planId} changed after you opened the editor.</strong> Your edits were not
575
+ saved. Your text is kept below.
576
+ <a href="/plans/${planId}@${latest.latestRevision}/edit">Open the latest version</a>, then edit
577
+ again.`
578
+ else state.error = plainMessage(error)
579
+ return refuse(request, page, cookies)
580
+ }
581
+ const saving = `${planId}@${saved.revision}`
582
+ const flash =
583
+ saved.edit === 'in_place'
584
+ ? `Saved ${saving} in place`
585
+ : `Saved ${saving}${planDetail(store, planId).approval.policy === 'none' ? '' : '. It needs approval.'}`
586
+ redirect(request.res, `/plans/${saving}`, [...cookies, cookie('checkmate_toast', flash, 60)])
587
+ }
588
+
589
+ async function post(request: Request) {
590
+ const origin = request.req.headers.origin
591
+ if (origin !== undefined && origin !== `http://${request.req.headers.host}`)
592
+ return send(
593
+ request.res,
594
+ 403,
595
+ 'text/plain; charset=utf-8',
596
+ 'Refused: the request comes from another origin.'
597
+ )
598
+ const form = await readForm(request.req)
599
+ if (!same(form.get('csrf') ?? '', csrf))
600
+ return errorPage(
601
+ request,
602
+ 403,
603
+ 'Form expired',
604
+ 'This form came from an earlier start of the UI. Load the page again, then send the form again.'
605
+ )
606
+ const { pathname } = request.url
607
+ if (pathname === '/theme') {
608
+ const theme = form.get('theme') as Theme
609
+ return redirect(request.res, safeBack(form.get('back')), [
610
+ cookie('checkmate_theme', themes.includes(theme) ? theme : 'system'),
611
+ ])
612
+ }
613
+ if (pathname === '/actor') {
614
+ const name = form.get('actor')?.trim() ?? ''
615
+ if (!name)
616
+ return errorPage(request, 422, 'Your name', 'Enter a name. Checkmate records it with each decision.')
617
+ return redirect(request.res, safeBack(form.get('back')), [cookie('checkmate_actor', name)])
618
+ }
619
+ const edit = routes.edit.exec(pathname)
620
+ if (edit) return saveEdit(request, form, edit[1], Number(edit[2]))
621
+ const gap = routes.gap.exec(pathname)
622
+ if (gap) return answer(request, form, gap[1], Number(gap[2]), gap[3])
623
+ const plan = routes.plan.exec(pathname)
624
+ if (plan) return decidePlan(request, form, plan[1], Number(plan[2]))
625
+ const run = routes.run.exec(pathname)
626
+ if (run) return decideRun(request, form, run[1])
627
+ const runGap = routes.runGap.exec(pathname)
628
+ if (runGap) return adopt(request, form, runGap[1], runGap[2])
629
+ return errorPage(request, 404, 'Page not found', `Checkmate has no form at ${pathname}.`)
630
+ }
631
+
632
+ async function handle(req: IncomingMessage, res: ServerResponse) {
633
+ const host = req.headers.host
634
+ if (host !== `127.0.0.1:${port}` && host !== `localhost:${port}`)
635
+ return send(
636
+ res,
637
+ 421,
638
+ 'text/plain; charset=utf-8',
639
+ 'Refused: unknown host. Open the URL that checkmate ui printed.'
640
+ )
641
+ const request: Request = {
642
+ req,
643
+ res,
644
+ url: new URL(req.url ?? '/', `http://${host}`),
645
+ cookies: parseCookies(req.headers.cookie),
646
+ }
647
+ try {
648
+ if (req.method === 'GET' || req.method === 'HEAD') return get(request)
649
+ if (req.method === 'POST') return await post(request)
650
+ send(res, 405, 'text/plain; charset=utf-8', 'Method not allowed.', { Allow: 'GET, POST' })
651
+ } catch (error) {
652
+ const message = plainMessage(error)
653
+ if (res.headersSent) return res.end()
654
+ if (notFoundError(message)) return errorPage(request, 404, 'Not found', message)
655
+ errorPage(request, 500, 'Something went wrong', `Checkmate could not finish the request: ${message}`)
656
+ }
657
+ }
658
+
659
+ const server = createServer((req, res) => {
660
+ void handle(req, res)
661
+ })
662
+ await new Promise<void>((resolve, reject) => {
663
+ server.once('error', reject)
664
+ server.listen(options.port ?? 0, '127.0.0.1', () => resolve())
665
+ })
666
+ const address = server.address()
667
+ port = typeof address === 'object' && address ? address.port : 0
668
+
669
+ return {
670
+ url: `http://127.0.0.1:${port}/`,
671
+ close: async () => {
672
+ clearInterval(poll)
673
+ clearInterval(heartbeat)
674
+ for (const client of clients) client.res.end()
675
+ clients.clear()
676
+ await new Promise<void>((resolve) => {
677
+ server.close(() => resolve())
678
+ server.closeAllConnections()
679
+ })
680
+ closeStore(watch)
681
+ closeStore(store)
682
+ },
683
+ }
684
+ }
@@ -0,0 +1,13 @@
1
+ export type Validator = ((value: unknown) => boolean) & {
2
+ errors?: Array<{ instancePath: string; message?: string }> | null
3
+ }
4
+ export const finding: Validator
5
+ export const gap: Validator
6
+ export const plan: Validator
7
+ export const planCandidate: Validator
8
+ export const planDelta: Validator
9
+ export const planPatch: Validator
10
+ export const planRequest: Validator
11
+ export const profile: Validator
12
+ export const response: Validator
13
+ export const result: Validator