@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,577 @@
1
+ import type { Store } from '../store.ts'
2
+ import type { Plan } from '../types.ts'
3
+ import type { Html } from './html.ts'
4
+ import type { Context, Page } from './layout.ts'
5
+ import type { FormState } from './forms.ts'
6
+ import { planDetail, planHistory, planStatus } from '../views.ts'
7
+ import { planRunGaps } from '../plans.ts'
8
+ import { gapsAffecting } from '../contracts.ts'
9
+ import { html, join, pill, plural, short, time, words } from './html.ts'
10
+ import { banner, downloads, heading } from './layout.ts'
11
+ import { emptyState, gapPanel, recordPanel, verdictPanel } from './forms.ts'
12
+ import { counts, loopState, runStatus } from './lists.ts'
13
+
14
+ type Detail = ReturnType<typeof planDetail>
15
+ type Test = Plan['tests'][number]
16
+ type Gap = Plan['gaps'][number]
17
+
18
+ function testLink(planId: string, revision: number, reference: string) {
19
+ const [owner, id] = reference.includes(':') ? reference.split(':') : [null, reference]
20
+ return owner
21
+ ? html`<a class="mono" href="/plans/${owner}#${id}">${reference}</a>`
22
+ : html`<a class="mono" href="/plans/${planId}@${revision}#${id}">${reference}</a>`
23
+ }
24
+
25
+ function ids(values: string[], link: (id: string) => Html) {
26
+ return values.length ? join(values.map(link), ', ') : html`<span class="muted">none</span>`
27
+ }
28
+
29
+ function facts(rows: Array<[string, Html | string | false]>) {
30
+ const shown = rows.filter(([, value]) => value !== false)
31
+ return html`<dl class="facts">
32
+ ${shown.map(
33
+ ([label, value]) =>
34
+ html`<div>
35
+ <dt>${label}</dt>
36
+ <dd>${value}</dd>
37
+ </div>`
38
+ )}
39
+ </dl>`
40
+ }
41
+
42
+ export function testCard(detail: Detail, test: Test) {
43
+ const { planId, revision } = detail
44
+ return html`<article class="test" id="${test.id}" data-key="test:${test.id}">
45
+ <header class="test-h">
46
+ ${pill(test.readiness)}
47
+ <a class="id" href="#${test.id}">${test.id}</a>
48
+ <h3>${test.title}</h3>
49
+ <span class="count">${test.capability} · ${plural(test.assertions.length, 'assertion')}</span>
50
+ </header>
51
+ <div class="test-b">
52
+ ${facts([
53
+ ['Requirements', ids(test.requirementIds, (id) => html`<a class="mono" href="#${id}">${id}</a>`)],
54
+ [
55
+ 'Depends on',
56
+ test.dependsOn.length > 0 && ids(test.dependsOn, (id) => testLink(planId, revision, id)),
57
+ ],
58
+ [
59
+ 'Gaps',
60
+ test.gapIds.length > 0 && ids(test.gapIds, (id) => html`<a class="mono" href="#${id}">${id}</a>`),
61
+ ],
62
+ [
63
+ 'Operations',
64
+ test.operations.length > 0 && html`<span class="mono">${test.operations.join(', ')}</span>`,
65
+ ],
66
+ [
67
+ 'Preconditions',
68
+ test.preconditions.length > 0 &&
69
+ html`<ul class="plain">
70
+ ${test.preconditions.map((item) => html`<li>${item}</li>`)}
71
+ </ul>`,
72
+ ],
73
+ ['Action', test.action],
74
+ ])}
75
+ </div>
76
+ ${
77
+ test.assertions.length
78
+ ? html`<ul class="checklist" aria-label="Assertions of ${test.id}">
79
+ ${test.assertions.map(
80
+ (assertion) =>
81
+ html`<li class="asr" id="${test.id}.${assertion.id}">
82
+ <span class="box" aria-hidden="true"></span>
83
+ <span class="aid">${assertion.id}</span>
84
+ <span class="check">${assertion.check}</span>
85
+ <span class="tag">${words(assertion.checkKind)}</span>
86
+ </li>`
87
+ )}
88
+ </ul>`
89
+ : html`<p class="test-note">No assertions yet. A blocking gap must be answered first.</p>`
90
+ }
91
+ </article>`
92
+ }
93
+
94
+ function decisionFor(plan: Plan, gap: Gap) {
95
+ return plan.decisions.find((decision) => decision.id === gap.decisionId)
96
+ }
97
+
98
+ function gapRow(detail: Detail, gap: Gap) {
99
+ const decision = decisionFor(detail.plan, gap)
100
+ const pending = detail.pendingDecisions.find((item) => item.gapId === gap.id)
101
+ const answerable = gap.status === 'open' && !pending && detail.revision === detail.latestRevision
102
+ const affects = [...gap.requirementIds, ...gap.testIds]
103
+ const head = html`<span class="id mono">${gap.id}</span>
104
+ ${pill(gap.severity)} ${pill(gap.status)}
105
+ <span class="gap-refs muted"
106
+ >${affects.length ? `affects ${affects.join(', ')}` : 'affects the whole plan'}</span
107
+ >`
108
+ const content = html`<p class="muted">${gap.consequence}</p>
109
+ ${
110
+ decision
111
+ ? html`<p class="gap-d">
112
+ ${pill(decision.kind)} ${decision.value} <span class="muted">· ${decision.actor}</span>
113
+ </p>`
114
+ : ''
115
+ }
116
+ ${
117
+ pending
118
+ ? html`<p class="gap-d">
119
+ ${pill('pending', 'decision pending')} ${words(pending.kind)}: ${pending.value}
120
+ <span class="muted">· ${pending.actor} · applies in the next revision</span>
121
+ </p>`
122
+ : ''
123
+ }
124
+ ${
125
+ answerable
126
+ ? html`<p>
127
+ <a class="btn small" href="/plans/${detail.planId}@${detail.revision}/gaps/${gap.id}"
128
+ >Answer ${gap.id}</a
129
+ >
130
+ </p>`
131
+ : ''
132
+ }`
133
+ if (gap.status === 'open')
134
+ return html`<li class="gap" id="${gap.id}" data-key="gap:${gap.id}">
135
+ <div class="gap-h">${head}</div>
136
+ <p class="gap-q">${gap.question}</p>
137
+ ${content}
138
+ </li>`
139
+ return html`<li class="gap closed" id="${gap.id}" data-key="gap:${gap.id}">
140
+ <details data-open-key="gap:${gap.id}">
141
+ <summary>
142
+ <span class="gap-h">${head}</span>
143
+ <span class="gap-q">${gap.question}</span>
144
+ </summary>
145
+ <div class="gap-b">${content}</div>
146
+ </details>
147
+ </li>`
148
+ }
149
+
150
+ function gapsList(detail: Detail) {
151
+ const gaps = detail.plan.gaps
152
+ if (!gaps.length) return html`<p class="empty-line">No gaps.</p>`
153
+ const open = gaps.filter((gap) => gap.status === 'open')
154
+ const closed = gaps.filter((gap) => gap.status !== 'open')
155
+ return html`<ul class="gaps">
156
+ ${[...open, ...closed].map((gap) => gapRow(detail, gap))}
157
+ </ul>`
158
+ }
159
+
160
+ function runsTable(detail: Detail) {
161
+ if (!detail.runs.length) return html`<p class="empty-line">No run uses this plan yet.</p>`
162
+ return html`<div class="scroll">
163
+ <table>
164
+ <thead>
165
+ <tr>
166
+ <th scope="col">Run</th>
167
+ <th scope="col">Revision</th>
168
+ <th scope="col">Build</th>
169
+ <th scope="col">Tests</th>
170
+ <th scope="col">Completion</th>
171
+ <th scope="col">Acceptance</th>
172
+ <th scope="col">Started</th>
173
+ </tr>
174
+ </thead>
175
+ <tbody>
176
+ ${[...detail.runs].reverse().map(
177
+ (run) =>
178
+ html`<tr data-key="run:${run.runId}">
179
+ <td><a class="mono" href="/runs/${run.runId}">${run.runId}</a></td>
180
+ <td>
181
+ <a class="mono" href="/plans/${detail.planId}@${run.revision}">@${run.revision}</a>
182
+ </td>
183
+ <td class="mono">${run.build}</td>
184
+ <td>${counts(run.counts)}</td>
185
+ <td>${run.completion ? pill(run.completion) : html`<span class="muted">running</span>`}</td>
186
+ <td>${runStatus(run)}</td>
187
+ <td class="nowrap">${time(run.startedAt)}</td>
188
+ </tr>`
189
+ )}
190
+ </tbody>
191
+ </table>
192
+ </div>`
193
+ }
194
+
195
+ function requirementsTable(plan: Plan) {
196
+ return html`<div class="scroll">
197
+ <table>
198
+ <thead>
199
+ <tr>
200
+ <th scope="col">ID</th>
201
+ <th scope="col">Requirement</th>
202
+ <th scope="col">Tests</th>
203
+ <th scope="col">Coverage</th>
204
+ </tr>
205
+ </thead>
206
+ <tbody>
207
+ ${plan.requirements.map((requirement) => {
208
+ const coverage = plan.coverage.find((row) => row.requirementId === requirement.id)
209
+ return html`<tr id="${requirement.id}">
210
+ <td class="mono">${requirement.id}</td>
211
+ <td>
212
+ ${requirement.text}
213
+ <div class="muted small">Source <code>${requirement.source}</code></div>
214
+ </td>
215
+ <td>${ids(coverage?.testIds ?? [], (id) => html`<a class="mono" href="#${id}">${id}</a>`)}</td>
216
+ <td>${coverage ? pill(coverage.status) : ''}</td>
217
+ </tr>`
218
+ })}
219
+ </tbody>
220
+ </table>
221
+ </div>`
222
+ }
223
+
224
+ function decisionsList(plan: Plan) {
225
+ if (!plan.decisions.length && !plan.assumptions.length)
226
+ return html`<p class="empty-line">No decision applied yet.</p>`
227
+ return html`<ul class="records">
228
+ ${plan.decisions.map(
229
+ (decision) =>
230
+ html`<li>
231
+ <div class="record-h">
232
+ ${pill(decision.kind)} <a class="mono" href="#${decision.gapId}">${decision.gapId}</a>
233
+ <span class="muted">by ${decision.actor}</span>
234
+ <span class="mono muted small">${decision.id}</span>
235
+ </div>
236
+ <p>${decision.value}</p>
237
+ <p class="muted small">
238
+ Rationale:
239
+ ${decision.rationale}${decision.source ? html` · Source <code>${decision.source}</code>` : ''}
240
+ </p>
241
+ </li>`
242
+ )}
243
+ </ul>
244
+ ${
245
+ plan.assumptions.length
246
+ ? html`<h3 class="minor">Assumptions</h3>
247
+ <ul class="plain">
248
+ ${plan.assumptions.map((item) => html`<li>${item.text}</li>`)}
249
+ </ul>`
250
+ : ''
251
+ }`
252
+ }
253
+
254
+ function changeLine(line: string) {
255
+ const arrow = line.indexOf(' → ')
256
+ const colon = line.indexOf(': ')
257
+ if (arrow < 0 || colon < 0 || colon > arrow) return html`<li><code>${line}</code></li>`
258
+ return html`<li>
259
+ <code>${line.slice(0, colon)}</code>
260
+ <span class="was">${line.slice(colon + 2, arrow)}</span>
261
+ <span aria-hidden="true" class="arrow">→</span><span class="sr-only">changed to</span>
262
+ <span class="now">${line.slice(arrow + 3)}</span>
263
+ </li>`
264
+ }
265
+
266
+ const historyShown = 6
267
+
268
+ export function planHistoryList(store: Store, detail: Detail, all: boolean) {
269
+ const revisions = planHistory(store, detail.planId).reverse()
270
+ const shown = all ? revisions : revisions.slice(0, historyShown)
271
+ const hidden = revisions.length - shown.length
272
+ const subject = `${detail.planId}@${detail.revision}`
273
+ return html`<div class="history-wrap" data-history>
274
+ <ol class="history${hidden ? ' clipped' : ''}">
275
+ ${shown.map(
276
+ (item, index) =>
277
+ html`<li class="${item.revision === detail.revision ? 'current' : ''}">
278
+ <details ${index === 0 ? html`open` : ''} data-open-key="revision:${item.revision}">
279
+ <summary class="record-h">
280
+ <span class="mono rev">${detail.planId}@${item.revision}</span>
281
+ <span class="muted">by ${item.actor} · ${time(item.at)}</span>
282
+ <code class="muted small" title="${item.sha256}">${short(item.sha256)}</code>
283
+ <span class="muted small"
284
+ >${item.changes.length ? plural(item.changes.length, 'change') : 'first revision'}</span
285
+ >
286
+ </summary>
287
+ <div class="history-b">
288
+ ${
289
+ item.changes.length
290
+ ? html`<ul class="changes">
291
+ ${item.changes.map(changeLine)}
292
+ </ul>`
293
+ : html`<p class="muted small">First revision.</p>`
294
+ }
295
+ <p class="small">
296
+ <a href="/plans/${detail.planId}@${item.revision}"
297
+ >Open ${detail.planId}@${item.revision}</a
298
+ >
299
+ </p>
300
+ </div>
301
+ </details>
302
+ </li>`
303
+ )}
304
+ </ol>
305
+ ${
306
+ hidden
307
+ ? html`<a
308
+ class="history-more"
309
+ href="/plans/${subject}?history=all#history"
310
+ data-history-more="/plans/${subject}/history"
311
+ ><span class="btn small">Show full history · ${plural(hidden, 'earlier revision')}</span></a
312
+ >`
313
+ : ''
314
+ }
315
+ </div>`
316
+ }
317
+
318
+ function approvalPanel(context: Context, detail: Detail, state: FormState | null) {
319
+ const subject = `${detail.planId}@${detail.revision}`
320
+ const latest = detail.approval.latest
321
+ if (detail.approval.policy === 'none')
322
+ return recordPanel('Approval', html`<p class="help">The target policy does not require plan approval.</p>`)
323
+ if (latest && latest.sha256 === detail.sha256 && !state)
324
+ return recordPanel(
325
+ 'Approval',
326
+ html`<p class="subj">${pill(latest.verdict)} ${subject}</p>
327
+ <p class="help">
328
+ By ${latest.actor} · ${time(latest.at)} · sha256 <code>${short(latest.sha256)}</code>
329
+ </p>
330
+ <blockquote>${latest.rationale}</blockquote>
331
+ ${latest.sha256 !== detail.sha256 ? banner('warn', 'This decision was for another digest.') : ''}`
332
+ )
333
+ return verdictPanel(
334
+ context,
335
+ {
336
+ action: `/plans/${subject}`,
337
+ subject,
338
+ question: `Approve ${subject}?`,
339
+ sha256: detail.sha256,
340
+ digestLabel: 'Plan',
341
+ approve: {
342
+ value: 'approve',
343
+ label: `Approve ${subject}`,
344
+ confirm: 'Confirm approval',
345
+ summary: `Approve ${subject}. Runs can then use this revision.`,
346
+ },
347
+ reject: {
348
+ value: 'reject',
349
+ label: 'Reject',
350
+ confirm: 'Confirm rejection',
351
+ summary: `Reject ${subject}. The agent must revise the plan.`,
352
+ },
353
+ staleMessage: `${detail.planId} changed. Review it before you decide.`,
354
+ intro: html`<p class="help">${planSummaryLine(detail.plan)}</p>`,
355
+ },
356
+ state ?? emptyState()
357
+ )
358
+ }
359
+
360
+ function planSummaryLine(plan: Plan) {
361
+ const count = (readiness: string) => plan.tests.filter((test) => test.readiness === readiness).length
362
+ const open = plan.gaps.filter((gap) => gap.status === 'open').length
363
+ return `${plural(plan.tests.length, 'test')}: ${count('ready')} ready, ${count('blocked')} blocked, ${count('deferred')} deferred. ${plural(open, 'open gap')}.`
364
+ }
365
+
366
+ function newerBanner(detail: Detail) {
367
+ if (detail.revision === detail.latestRevision) return ''
368
+ return banner(
369
+ 'info',
370
+ html`Revision ${detail.latestRevision} is available.
371
+ <a href="/plans/${detail.planId}@${detail.latestRevision}"
372
+ >Review ${detail.planId}@${detail.latestRevision}</a
373
+ >`
374
+ )
375
+ }
376
+
377
+ function side(content: Html) {
378
+ return html`<aside class="side" aria-label="Decision">
379
+ ${content}
380
+ <p class="back"><a href="/">← Back to inbox</a></p>
381
+ </aside>`
382
+ }
383
+
384
+ export function planPage(
385
+ store: Store,
386
+ context: Context,
387
+ planId: string,
388
+ revision: number,
389
+ state: FormState | null = null,
390
+ allHistory = false
391
+ ): Page {
392
+ const detail = planDetail(store, planId, revision)
393
+ const { plan } = detail
394
+ const subject = `${planId}@${revision}`
395
+ const latest = detail.revision === detail.latestRevision
396
+ const meta = html`<div class="meta">
397
+ ${loopState(planStatus(store, planId))} ${pill(detail.approval.state)}
398
+ <span class="pill">target <span class="mono">${plan.target.targetId}@${plan.target.revision}</span></span>
399
+ <span class="pill" title="${detail.sha256}">sha256 <span class="mono">${short(detail.sha256)}</span></span>
400
+ <span class="head-actions">
401
+ ${latest ? html`<a class="btn small" href="/plans/${subject}/edit">Edit plan</a>` : ''}
402
+ ${downloads('Export', `/plans/${subject}/export`, 'The HTML export is print-ready. Open it and print to PDF.')}
403
+ </span>
404
+ </div>`
405
+ const pending = detail.pendingDecisions.length
406
+ ? banner(
407
+ 'info',
408
+ html`${plural(detail.pendingDecisions.length, 'decision')}
409
+ ${detail.pendingDecisions.length === 1 ? 'waits' : 'wait'} for the next revision:
410
+ ${detail.pendingDecisions.map((item) => `${item.gapId} ${words(item.kind)}`).join(', ')}.`
411
+ )
412
+ : ''
413
+ const body = html`${heading(subject, plan.objective, meta)}
414
+ <div data-region="banners">${newerBanner(detail)} ${pending}</div>
415
+ <div class="split">
416
+ <div class="main-col">
417
+ <nav class="jump" aria-label="On this page">
418
+ <a href="#tests">Tests</a><a href="#gaps">Gaps</a><a href="#requirements">Requirements</a
419
+ ><a href="#runs">Runs</a><a href="#decisions">Decisions</a><a href="#history">History</a>
420
+ </nav>
421
+ <section aria-labelledby="tests" data-region="tests">
422
+ <h2 id="tests">
423
+ Tests <span class="n">${plan.tests.length}</span>
424
+ ${latest ? html`<a class="h-link" href="/plans/${subject}/edit#edit-tests">Edit</a>` : ''}
425
+ </h2>
426
+ <div class="stack">${plan.tests.map((test) => testCard(detail, test))}</div>
427
+ </section>
428
+ <section aria-labelledby="gaps" data-region="gaps">
429
+ <h2 id="gaps">Gaps <span class="n">${plan.gaps.length}</span></h2>
430
+ ${gapsList(detail)}
431
+ </section>
432
+ <section aria-labelledby="requirements">
433
+ <h2 id="requirements">
434
+ Requirements and coverage
435
+ ${latest ? html`<a class="h-link" href="/plans/${subject}/edit#edit-requirements">Edit</a>` : ''}
436
+ </h2>
437
+ ${requirementsTable(plan)}
438
+ </section>
439
+ <section aria-labelledby="runs" data-region="plan-runs">
440
+ <h2 id="runs">Runs <span class="n">${detail.runs.length}</span></h2>
441
+ ${runsTable(detail)}
442
+ </section>
443
+ <section aria-labelledby="decisions">
444
+ <h2 id="decisions">Decisions</h2>
445
+ ${decisionsList(plan)}
446
+ </section>
447
+ <section aria-labelledby="history" data-region="history">
448
+ <h2 id="history">History</h2>
449
+ ${planHistoryList(store, detail, allHistory)}
450
+ </section>
451
+ </div>
452
+ ${side(approvalPanel(context, detail, state))}
453
+ </div>`
454
+ return {
455
+ title: subject,
456
+ section: 'plans',
457
+ subjects: [`plan:${planId}`, 'inbox', ...detail.runs.map((run) => `run:${run.runId}`)],
458
+ body,
459
+ }
460
+ }
461
+
462
+ export function gapPage(
463
+ store: Store,
464
+ context: Context,
465
+ planId: string,
466
+ revision: number,
467
+ gapId: string,
468
+ state: FormState | null = null
469
+ ): Page {
470
+ const detail = planDetail(store, planId, revision)
471
+ const { plan } = detail
472
+ const latest = detail.revision === detail.latestRevision
473
+ const fromRun =
474
+ plan.gaps.some((item) => item.id === gapId) || !latest ? null : unadoptedRunGap(store, planId, gapId)
475
+ const gap = plan.gaps.find((item) => item.id === gapId) ?? fromRun?.gap
476
+ if (!gap) throw new Error(`Unknown gap ${gapId} in ${planId}@${revision}`)
477
+ const subject = `${planId}@${revision}`
478
+ const pending = detail.pendingDecisions.find((item) => item.gapId === gap.id)
479
+ const decision = decisionFor(plan, gap)
480
+ const tests = plan.tests.filter((test) => gapsAffecting([gap], test).length > 0)
481
+ const requirements = plan.requirements.filter((requirement) => gap.requirementIds.includes(requirement.id))
482
+ const runBanner = fromRun
483
+ ? banner(
484
+ 'info',
485
+ html`This gap comes from run <a class="mono" href="/runs/${fromRun.runId}">${fromRun.runId}</a>. Your
486
+ answer adds it to ${planId}.`
487
+ )
488
+ : ''
489
+ let panel: Html
490
+ if (state || (latest && gap.status === 'open' && !pending))
491
+ panel = gapPanel(
492
+ context,
493
+ {
494
+ action: `/plans/${subject}/gaps/${gap.id}`,
495
+ cancel: `/plans/${subject}#${gap.id}`,
496
+ sha256: detail.sha256,
497
+ title: html`${planId} · ${gap.id} · ${words(gap.severity)}`,
498
+ question: gap.question,
499
+ staleMessage: `${planId} changed. Review it before you answer.`,
500
+ },
501
+ state ?? emptyState()
502
+ )
503
+ else if (pending)
504
+ panel = recordPanel(
505
+ 'Answer recorded',
506
+ html`<p class="subj">${pill(pending.kind)} ${gap.id}</p>
507
+ <blockquote>${pending.value}</blockquote>
508
+ <p class="help">By ${pending.actor}. The agent applies it in the next revision of ${planId}.</p>`
509
+ )
510
+ else if (decision)
511
+ panel = recordPanel(
512
+ 'Decision',
513
+ html`<p class="subj">${pill(decision.kind)} ${gap.id}</p>
514
+ <blockquote>${decision.value}</blockquote>
515
+ <p class="help">By ${decision.actor}. Rationale: ${decision.rationale}</p>`
516
+ )
517
+ else
518
+ panel = recordPanel(
519
+ 'Answer gap',
520
+ html`<p class="help">Answer this gap on the latest revision.</p>
521
+ <p>
522
+ <a class="btn" href="/plans/${planId}@${detail.latestRevision}/gaps/${gap.id}"
523
+ >Open ${planId}@${detail.latestRevision}</a
524
+ >
525
+ </p>`
526
+ )
527
+ const meta = html`<div class="meta">
528
+ ${pill(gap.severity)} ${pill(gap.status)} <a class="pill" href="/plans/${subject}">${subject}</a>
529
+ </div>`
530
+ const body = html`${heading(html`${planId} · ${gap.id}`, gap.question, meta)}
531
+ <div data-region="banners">${newerBanner(detail)} ${runBanner}</div>
532
+ <div class="split">
533
+ <div class="main-col">
534
+ <section aria-labelledby="consequence">
535
+ <h2 id="consequence">Consequence</h2>
536
+ <p class="lead">${gap.consequence}</p>
537
+ </section>
538
+ ${
539
+ requirements.length
540
+ ? html`<section aria-labelledby="gap-requirements">
541
+ <h2 id="gap-requirements">Requirements</h2>
542
+ <ul class="records">
543
+ ${requirements.map(
544
+ (requirement) =>
545
+ html`<li>
546
+ <div class="record-h"><span class="mono">${requirement.id}</span></div>
547
+ <p>${requirement.text}</p>
548
+ <p class="muted small">Source <code>${requirement.source}</code></p>
549
+ </li>`
550
+ )}
551
+ </ul>
552
+ </section>`
553
+ : ''
554
+ }
555
+ <section aria-labelledby="gap-tests">
556
+ <h2 id="gap-tests">Affected tests <span class="n">${tests.length}</span></h2>
557
+ ${
558
+ tests.length
559
+ ? html`<div class="stack">${tests.map((test) => testCard(detail, test))}</div>`
560
+ : html`<p class="empty-line">No test names this gap.</p>`
561
+ }
562
+ </section>
563
+ </div>
564
+ ${side(panel)}
565
+ </div>`
566
+ return {
567
+ title: `${planId} ${gap.id}`,
568
+ section: 'inbox',
569
+ subjects: [`plan:${planId}`, 'inbox', ...(fromRun ? [`run:${fromRun.runId}`] : [])],
570
+ body,
571
+ }
572
+ }
573
+
574
+ function unadoptedRunGap(store: Store, planId: string, gapId: string) {
575
+ const found = planRunGaps(store, planId).filter((item) => item.gap.id === gapId && !item.adopted)
576
+ return found.length === 1 ? found[0] : null
577
+ }