@legionworks/facet 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (327) hide show
  1. package/LICENSE-APACHE +202 -0
  2. package/LICENSE-MIT +23 -0
  3. package/README.md +158 -0
  4. package/dist/gallery/chunk-4g8rem85.css +656 -0
  5. package/dist/gallery/chunk-c6s9k9ty.js +5970 -0
  6. package/dist/gallery/frame/artifact.css +8424 -0
  7. package/dist/gallery/frame/chunks/abnfDiagram-N423BO3Z-jj74szbm.js +155 -0
  8. package/dist/gallery/frame/chunks/architecture-TIHT7OUA-cvdjn7dt.js +10 -0
  9. package/dist/gallery/frame/chunks/architectureDiagram-T3A2C74G-n4z77eg6.js +8675 -0
  10. package/dist/gallery/frame/chunks/blockDiagram-VBNYF7ZC-7xsqzrd1.js +3529 -0
  11. package/dist/gallery/frame/chunks/c4Diagram-5PPSVZJV-z2zjs7wx.js +2413 -0
  12. package/dist/gallery/frame/chunks/classDiagram-JCYQIIEL-ve8rngqp.js +47 -0
  13. package/dist/gallery/frame/chunks/classDiagram-v2-OCEON4UE-8cfaxb8z.js +47 -0
  14. package/dist/gallery/frame/chunks/cose-bilkent-JH36ORCC-sn92qexx.js +4822 -0
  15. package/dist/gallery/frame/chunks/cynefin-VYW2F7L2-tqjv913d.js +10 -0
  16. package/dist/gallery/frame/chunks/cynefinDiagram-MW4NZA55-c1c0dj12.js +500 -0
  17. package/dist/gallery/frame/chunks/dagre-VZM6K2ZE-7tyg4yt2.js +476 -0
  18. package/dist/gallery/frame/chunks/diagram-7IWD3JNH-y4eng9sc.js +526 -0
  19. package/dist/gallery/frame/chunks/diagram-B4RE2ZJO-rn3p7j5h.js +673 -0
  20. package/dist/gallery/frame/chunks/diagram-LBJQPF4R-w4a2cm9q.js +255 -0
  21. package/dist/gallery/frame/chunks/diagram-Q27KOJAE-aqc2v7wy.js +575 -0
  22. package/dist/gallery/frame/chunks/diagram-UB23O5K3-xncy8nyd.js +355 -0
  23. package/dist/gallery/frame/chunks/ebnfDiagram-BXEA7PRR-acwgxz16.js +175 -0
  24. package/dist/gallery/frame/chunks/erDiagram-JOGREHBK-mzp5nswt.js +1332 -0
  25. package/dist/gallery/frame/chunks/eventmodeling-45OFAUF4-sg07qr0c.js +10 -0
  26. package/dist/gallery/frame/chunks/flowDiagram-UKHOOZJN-2e5ygs44.js +29 -0
  27. package/dist/gallery/frame/chunks/ganttDiagram-PKOTCBZU-p2hhh7hr.js +2620 -0
  28. package/dist/gallery/frame/chunks/gitGraph-TEB2WS4Q-j6kw0naa.js +10 -0
  29. package/dist/gallery/frame/chunks/gitGraphDiagram-DS77QQ5N-4z9z8wjm.js +1347 -0
  30. package/dist/gallery/frame/chunks/info-DKCQHKI2-mpt80jd3.js +10 -0
  31. package/dist/gallery/frame/chunks/infoDiagram-6WML65LV-5rqm3mgg.js +65 -0
  32. package/dist/gallery/frame/chunks/ishikawaDiagram-WSZJBQD7-b51z96jy.js +975 -0
  33. package/dist/gallery/frame/chunks/journeyDiagram-NVQOT4AX-9jbfp0pk.js +1272 -0
  34. package/dist/gallery/frame/chunks/kanban-definition-27J2QSJJ-4x0vx4se.js +1116 -0
  35. package/dist/gallery/frame/chunks/katex-xfgk95tv.js +14034 -0
  36. package/dist/gallery/frame/chunks/markdown-1s5vwkf2.js +9 -0
  37. package/dist/gallery/frame/chunks/markdown-2h6bwsms.js +3239 -0
  38. package/dist/gallery/frame/chunks/markdown-2x113yh8.js +107 -0
  39. package/dist/gallery/frame/chunks/markdown-30b7rwmp.js +1943 -0
  40. package/dist/gallery/frame/chunks/markdown-356bfj4g.js +2031 -0
  41. package/dist/gallery/frame/chunks/markdown-44n9ea69.js +2406 -0
  42. package/dist/gallery/frame/chunks/markdown-56enrf2r.js +39 -0
  43. package/dist/gallery/frame/chunks/markdown-5avymm8t.js +725 -0
  44. package/dist/gallery/frame/chunks/markdown-6r4svars.js +33 -0
  45. package/dist/gallery/frame/chunks/markdown-6zz8efx2.js +24 -0
  46. package/dist/gallery/frame/chunks/markdown-7jp6prnb.js +19 -0
  47. package/dist/gallery/frame/chunks/markdown-809k7q41.js +55 -0
  48. package/dist/gallery/frame/chunks/markdown-82xmzhzd.js +1076 -0
  49. package/dist/gallery/frame/chunks/markdown-92n37gd4.js +7627 -0
  50. package/dist/gallery/frame/chunks/markdown-ag2sssf8.js +36 -0
  51. package/dist/gallery/frame/chunks/markdown-b4s7ntq2.js +19 -0
  52. package/dist/gallery/frame/chunks/markdown-c9k0fvp1.js +100 -0
  53. package/dist/gallery/frame/chunks/markdown-ceefymn8.js +125 -0
  54. package/dist/gallery/frame/chunks/markdown-d1wfbs70.js +243 -0
  55. package/dist/gallery/frame/chunks/markdown-dy5sgjhc.js +11022 -0
  56. package/dist/gallery/frame/chunks/markdown-dz3z5w30.js +30868 -0
  57. package/dist/gallery/frame/chunks/markdown-eg6ngz7y.js +30340 -0
  58. package/dist/gallery/frame/chunks/markdown-ff5wvb43.js +1149 -0
  59. package/dist/gallery/frame/chunks/markdown-ffg06nfz.js +86 -0
  60. package/dist/gallery/frame/chunks/markdown-fvfncadf.js +472 -0
  61. package/dist/gallery/frame/chunks/markdown-gvyszqpv.js +19 -0
  62. package/dist/gallery/frame/chunks/markdown-gx1p0ew0.js +88 -0
  63. package/dist/gallery/frame/chunks/markdown-gxye5frz.js +65 -0
  64. package/dist/gallery/frame/chunks/markdown-h7pzv34e.js +105 -0
  65. package/dist/gallery/frame/chunks/markdown-hab789t9.js +47 -0
  66. package/dist/gallery/frame/chunks/markdown-hjp6sj8t.js +125 -0
  67. package/dist/gallery/frame/chunks/markdown-jka64qgp.js +1993 -0
  68. package/dist/gallery/frame/chunks/markdown-k8xkn8mw.js +56 -0
  69. package/dist/gallery/frame/chunks/markdown-m7frq8x8.js +112 -0
  70. package/dist/gallery/frame/chunks/markdown-p740v4qm.js +36 -0
  71. package/dist/gallery/frame/chunks/markdown-pxfeh231.js +57 -0
  72. package/dist/gallery/frame/chunks/markdown-pzgd8z3s.js +87 -0
  73. package/dist/gallery/frame/chunks/markdown-qx3cngz2.js +1673 -0
  74. package/dist/gallery/frame/chunks/markdown-rawhfvvc.js +85 -0
  75. package/dist/gallery/frame/chunks/markdown-rd8w0ng9.js +49 -0
  76. package/dist/gallery/frame/chunks/markdown-rm2yxfjj.js +2773 -0
  77. package/dist/gallery/frame/chunks/markdown-s1jatrnk.js +993 -0
  78. package/dist/gallery/frame/chunks/markdown-sgfg5vvq.js +59 -0
  79. package/dist/gallery/frame/chunks/markdown-t4ethv9d.js +409 -0
  80. package/dist/gallery/frame/chunks/markdown-tnmabqvz.js +36 -0
  81. package/dist/gallery/frame/chunks/markdown-tvdpppw8.js +5292 -0
  82. package/dist/gallery/frame/chunks/markdown-v0x27twr.js +22 -0
  83. package/dist/gallery/frame/chunks/markdown-v14td68x.js +36 -0
  84. package/dist/gallery/frame/chunks/markdown-w6sgd54z.js +36 -0
  85. package/dist/gallery/frame/chunks/markdown-xw8dsk8b.js +2125 -0
  86. package/dist/gallery/frame/chunks/markdown-y18mve73.js +85 -0
  87. package/dist/gallery/frame/chunks/markdown-yengsms5.js +427 -0
  88. package/dist/gallery/frame/chunks/markdown-z1wc0v0d.js +1826 -0
  89. package/dist/gallery/frame/chunks/markdown-zazx9h4n.js +329 -0
  90. package/dist/gallery/frame/chunks/mermaid-de7p4v0x.js +30 -0
  91. package/dist/gallery/frame/chunks/mindmap-definition-FAOFIHXS-ra44cbem.js +1216 -0
  92. package/dist/gallery/frame/chunks/packet-7NZHBO7P-xppm5913.js +10 -0
  93. package/dist/gallery/frame/chunks/pegDiagram-VL7TDLO6-a2a4wg03.js +163 -0
  94. package/dist/gallery/frame/chunks/pie-RZYD4A2V-hnbxk152.js +10 -0
  95. package/dist/gallery/frame/chunks/pieDiagram-7S7Q4E2Y-f1vxjtmx.js +311 -0
  96. package/dist/gallery/frame/chunks/quadrantDiagram-CIZ2JOQS-ywafa6kh.js +1390 -0
  97. package/dist/gallery/frame/chunks/radar-I7S5WNFK-ewqd27se.js +10 -0
  98. package/dist/gallery/frame/chunks/railroad-3IZDKUUU-5hy4bdx3.js +10 -0
  99. package/dist/gallery/frame/chunks/railroad-abnf-AHOZXSZD-bh5sxz0d.js +10 -0
  100. package/dist/gallery/frame/chunks/railroad-ebnf-EBAXGLYW-6b9c3zeh.js +10 -0
  101. package/dist/gallery/frame/chunks/railroad-peg-LSFZ7HO6-avc8v39s.js +10 -0
  102. package/dist/gallery/frame/chunks/railroadDiagram-AXF67PYL-qc61wax4.js +131 -0
  103. package/dist/gallery/frame/chunks/requirementDiagram-LRYGKXZP-9bs4786c.js +1295 -0
  104. package/dist/gallery/frame/chunks/sankeyDiagram-W5VNT64P-awt1em8b.js +1339 -0
  105. package/dist/gallery/frame/chunks/sequenceDiagram-SI44F4Z6-qaf9mz3t.js +4324 -0
  106. package/dist/gallery/frame/chunks/sizeCapture-X5ZJPWSS-9fq685xd.js +64 -0
  107. package/dist/gallery/frame/chunks/stateDiagram-OKZ733FA-vx7a7hh0.js +456 -0
  108. package/dist/gallery/frame/chunks/stateDiagram-v2-UEYNNEHI-x4m6ws5t.js +46 -0
  109. package/dist/gallery/frame/chunks/swimlanes-SLNWSIFB-e4mxm1gh.js +8287 -0
  110. package/dist/gallery/frame/chunks/swimlanesDiagram-ULZ7WXOC-0h93e2qq.js +42 -0
  111. package/dist/gallery/frame/chunks/timeline-definition-Z64GVDOM-wp0bhbzg.js +1549 -0
  112. package/dist/gallery/frame/chunks/treeView-QDETBFTQ-hcx7n0vt.js +10 -0
  113. package/dist/gallery/frame/chunks/treemap-6X3UGDF4-av02skdw.js +10 -0
  114. package/dist/gallery/frame/chunks/vennDiagram-T6HMQDX7-pmvk1p7n.js +2566 -0
  115. package/dist/gallery/frame/chunks/wardley-OPB4EBWU-njsp6e40.js +10 -0
  116. package/dist/gallery/frame/chunks/wardleyDiagram-T6FBY63Y-eggmnv4z.js +979 -0
  117. package/dist/gallery/frame/chunks/xychartDiagram-ELKLHX3M-q40tnpe5.js +1954 -0
  118. package/dist/gallery/frame/frame.css +183 -0
  119. package/dist/gallery/frame/runtime/chart.js +42744 -0
  120. package/dist/gallery/frame/runtime/html.js +14 -0
  121. package/dist/gallery/frame/runtime/markdown.js +1403 -0
  122. package/dist/gallery/frame/runtime/mermaid.js +32 -0
  123. package/dist/gallery/frame/runtime/svg.js +14 -0
  124. package/dist/gallery/frame/runtime/tsx.js +52 -0
  125. package/dist/gallery/index.html +104 -0
  126. package/docs/reference/cli.md +163 -0
  127. package/docs/reference/export.md +106 -0
  128. package/docs/reference/html.md +215 -0
  129. package/docs/reference/http.md +85 -0
  130. package/docs/reference/mcp.md +78 -0
  131. package/docs/reference/security.md +119 -0
  132. package/docs/reference/storage.md +93 -0
  133. package/docs/reference/tsx.md +101 -0
  134. package/docs/reference/validation.md +212 -0
  135. package/package.json +89 -0
  136. package/scripts/build-gallery.ts +127 -0
  137. package/scripts/launch-netns.sh +67 -0
  138. package/skills/facet/SKILL.md +100 -0
  139. package/src/cli/client.ts +311 -0
  140. package/src/cli/commands/create.ts +49 -0
  141. package/src/cli/commands/doctor.ts +249 -0
  142. package/src/cli/commands/export.ts +179 -0
  143. package/src/cli/commands/instantiate.ts +35 -0
  144. package/src/cli/commands/list.ts +49 -0
  145. package/src/cli/commands/open.ts +75 -0
  146. package/src/cli/commands/pin.ts +39 -0
  147. package/src/cli/commands/promote.ts +49 -0
  148. package/src/cli/commands/publish.ts +122 -0
  149. package/src/cli/commands/read-back.ts +56 -0
  150. package/src/cli/commands/status.ts +114 -0
  151. package/src/cli/commands/watch.ts +146 -0
  152. package/src/cli/main.ts +522 -0
  153. package/src/cli/output.ts +106 -0
  154. package/src/cli/parser.ts +376 -0
  155. package/src/cli/presenter.ts +212 -0
  156. package/src/cli/service-metadata.ts +134 -0
  157. package/src/cli/spawn-service.ts +326 -0
  158. package/src/css.d.ts +4 -0
  159. package/src/gallery-web/app.ts +1581 -0
  160. package/src/gallery-web/export-menu.ts +121 -0
  161. package/src/gallery-web/export.ts +112 -0
  162. package/src/gallery-web/favicon.ts +54 -0
  163. package/src/gallery-web/frame/entries/chart.ts +6 -0
  164. package/src/gallery-web/frame/entries/html.ts +6 -0
  165. package/src/gallery-web/frame/entries/markdown.ts +6 -0
  166. package/src/gallery-web/frame/entries/mermaid.ts +6 -0
  167. package/src/gallery-web/frame/entries/svg.ts +6 -0
  168. package/src/gallery-web/frame/entries/tsx.ts +6 -0
  169. package/src/gallery-web/frame/frame-payload.ts +91 -0
  170. package/src/gallery-web/frame/renderer-validation.ts +13 -0
  171. package/src/gallery-web/frame/renderers/chart.ts +182 -0
  172. package/src/gallery-web/frame/renderers/dompurify-shim.ts +112 -0
  173. package/src/gallery-web/frame/renderers/html.ts +48 -0
  174. package/src/gallery-web/frame/renderers/markdown.ts +81 -0
  175. package/src/gallery-web/frame/renderers/mermaid.ts +120 -0
  176. package/src/gallery-web/frame/renderers/registry.ts +260 -0
  177. package/src/gallery-web/frame/renderers/svg.ts +393 -0
  178. package/src/gallery-web/frame/renderers/tsx.ts +61 -0
  179. package/src/gallery-web/frame/runtime.ts +501 -0
  180. package/src/gallery-web/frame/styles/artifact.css +8424 -0
  181. package/src/gallery-web/frame/styles/frame.css +183 -0
  182. package/src/gallery-web/frame/styles/html-source.css +23 -0
  183. package/src/gallery-web/frame/view-box.ts +33 -0
  184. package/src/gallery-web/frame-html.ts +72 -0
  185. package/src/gallery-web/index.html +104 -0
  186. package/src/gallery-web/session.ts +107 -0
  187. package/src/gallery-web/sse-client.ts +212 -0
  188. package/src/gallery-web/styles/app.css +420 -0
  189. package/src/gallery-web/styles/tokens.css +112 -0
  190. package/src/gallery-web/styles/verdict.css +186 -0
  191. package/src/gallery-web/swap.ts +64 -0
  192. package/src/gallery-web/theme.ts +40 -0
  193. package/src/gallery-web/view-state.ts +93 -0
  194. package/src/harness-adapters/claude-code/README.md +10 -0
  195. package/src/harness-adapters/claude-code/facet.sh +4 -0
  196. package/src/harness-adapters/codex/README.md +10 -0
  197. package/src/harness-adapters/codex/facet.sh +4 -0
  198. package/src/harness-adapters/mcp/README.md +13 -0
  199. package/src/harness-adapters/mcp/cli-bridge.ts +216 -0
  200. package/src/harness-adapters/mcp/main.ts +15 -0
  201. package/src/harness-adapters/mcp/server.ts +164 -0
  202. package/src/harness-adapters/mcp/tool-schemas.ts +53 -0
  203. package/src/harness-adapters/opencode/README.md +10 -0
  204. package/src/harness-adapters/opencode/facet.sh +4 -0
  205. package/src/runtime/compiled-entrypoints.ts +69 -0
  206. package/src/service/dispatcher.ts +717 -0
  207. package/src/service/export.ts +218 -0
  208. package/src/service/http-utils.ts +55 -0
  209. package/src/service/lexical/expectations.ts +210 -0
  210. package/src/service/lifecycle/idle-controller.ts +125 -0
  211. package/src/service/lifecycle/orphan-cleanup.ts +128 -0
  212. package/src/service/lifecycle/process-lock.ts +235 -0
  213. package/src/service/main.ts +136 -0
  214. package/src/service/process.ts +240 -0
  215. package/src/service/router-guards.ts +239 -0
  216. package/src/service/router.ts +627 -0
  217. package/src/service/security/auth.ts +168 -0
  218. package/src/service/security/host-origin.ts +138 -0
  219. package/src/service/security/http-guards.ts +13 -0
  220. package/src/service/security/leases.ts +182 -0
  221. package/src/service/security/token-store.ts +138 -0
  222. package/src/service/server.ts +356 -0
  223. package/src/service/store/database.ts +61 -0
  224. package/src/service/store/evidence-retention.ts +234 -0
  225. package/src/service/store/migrations.ts +167 -0
  226. package/src/service/store/repository-lifecycle.ts +121 -0
  227. package/src/service/store/repository.ts +732 -0
  228. package/src/service/store/schema.ts +218 -0
  229. package/src/service/stored-verdict.ts +97 -0
  230. package/src/service/stream.ts +301 -0
  231. package/src/service/verdict-enrichment.ts +45 -0
  232. package/src/shared/build/frame-bundle-plugins.ts +61 -0
  233. package/src/shared/build-mode.ts +28 -0
  234. package/src/shared/config/evidence-read.ts +98 -0
  235. package/src/shared/config/limits.ts +56 -0
  236. package/src/shared/config/paths.ts +96 -0
  237. package/src/shared/contracts/artifact-types.ts +2 -0
  238. package/src/shared/contracts/artifact.ts +98 -0
  239. package/src/shared/contracts/commands/_shared.ts +59 -0
  240. package/src/shared/contracts/commands/guards.ts +86 -0
  241. package/src/shared/contracts/commands/index.ts +204 -0
  242. package/src/shared/contracts/commands/names.ts +35 -0
  243. package/src/shared/contracts/commands/requests.ts +149 -0
  244. package/src/shared/contracts/commands/results.ts +200 -0
  245. package/src/shared/contracts/envelope.ts +140 -0
  246. package/src/shared/contracts/events.ts +41 -0
  247. package/src/shared/contracts/observed-counts.ts +19 -0
  248. package/src/shared/contracts/renderers.ts +6 -0
  249. package/src/shared/contracts/validation.ts +296 -0
  250. package/src/shared/errors/facet-error.ts +127 -0
  251. package/src/shared/errors/store-error.ts +68 -0
  252. package/src/shared/evidence-image.ts +37 -0
  253. package/src/shared/export.ts +31 -0
  254. package/src/shared/html/policy.ts +108 -0
  255. package/src/shared/html/style-vocabulary.ts +352 -0
  256. package/src/shared/logging/logger.ts +108 -0
  257. package/src/shared/logging/redact.ts +49 -0
  258. package/src/shared/security/frozen-csp.ts +13 -0
  259. package/src/shared/storage-version.ts +2 -0
  260. package/src/shared/tsx/execution.ts +29 -0
  261. package/src/shared/tsx/import-policy.ts +94 -0
  262. package/src/shared/util/dir-permissions.ts +94 -0
  263. package/src/shared/util/mermaid-nodes.ts +230 -0
  264. package/src/shared/util/process.ts +67 -0
  265. package/src/shared/util/time.ts +21 -0
  266. package/src/shared/version.ts +3 -0
  267. package/src/validation/sandbox/limits.ts +41 -0
  268. package/src/validation/sandbox/netns.ts +177 -0
  269. package/src/validation/tier0/chart.ts +177 -0
  270. package/src/validation/tier0/dom-shim.ts +54 -0
  271. package/src/validation/tier0/html.ts +408 -0
  272. package/src/validation/tier0/markdown.ts +285 -0
  273. package/src/validation/tier0/mermaid.ts +98 -0
  274. package/src/validation/tier0/runner.ts +504 -0
  275. package/src/validation/tier0/svg.ts +321 -0
  276. package/src/validation/tier0/tsx/allowlist-resolver.ts +77 -0
  277. package/src/validation/tier0/tsx/ast-policy-capabilities.ts +238 -0
  278. package/src/validation/tier0/tsx/ast-policy-shared.ts +71 -0
  279. package/src/validation/tier0/tsx/ast-policy.ts +181 -0
  280. package/src/validation/tier0/tsx/compiler.ts +156 -0
  281. package/src/validation/tier0/worker-dispatch.ts +211 -0
  282. package/src/validation/tier0/worker-entry.ts +96 -0
  283. package/src/validation/tier0/worker-input.ts +75 -0
  284. package/src/validation/tier1/browser-process.ts +147 -0
  285. package/src/validation/tier1/cdp-pipe.ts +281 -0
  286. package/src/validation/tier1/entries/chart.ts +5 -0
  287. package/src/validation/tier1/entries/html.ts +6 -0
  288. package/src/validation/tier1/entries/markdown.ts +5 -0
  289. package/src/validation/tier1/entries/mermaid.ts +5 -0
  290. package/src/validation/tier1/entries/svg.ts +5 -0
  291. package/src/validation/tier1/entries/tsx.ts +5 -0
  292. package/src/validation/tier1/frame-target.ts +162 -0
  293. package/src/validation/tier1/harness-entry.ts +261 -0
  294. package/src/validation/tier1/harness.ts +215 -0
  295. package/src/validation/tier1/isolated-probe.ts +94 -0
  296. package/src/validation/tier1/launcher.ts +177 -0
  297. package/src/validation/tier1/limits.ts +80 -0
  298. package/src/validation/tier1/nonce.ts +17 -0
  299. package/src/validation/tier1/protocol-probe.ts +519 -0
  300. package/src/validation/tier1/runner.ts +1122 -0
  301. package/src/validation/tier1/verdict.ts +246 -0
  302. package/src/validation/tier1/webp.ts +69 -0
  303. package/templates/README.md +88 -0
  304. package/templates/bar-compare.vl.json +63 -0
  305. package/templates/capacity-report.tsx +102 -0
  306. package/templates/decision-record.md +24 -0
  307. package/templates/deployment-state.mmd +40 -0
  308. package/templates/exemplar.md +91 -0
  309. package/templates/facet-story.tsx +799 -0
  310. package/templates/fleet-dashboard.html +141 -0
  311. package/templates/html-release-ledger.html +91 -0
  312. package/templates/html-status-report.html +114 -0
  313. package/templates/incident-console.tsx +147 -0
  314. package/templates/legion-boundaries.mmd +23 -0
  315. package/templates/legion-flow.mmd +15 -0
  316. package/templates/legion-sequence.mmd +14 -0
  317. package/templates/legion-state.mmd +7 -0
  318. package/templates/metric-card.svg +29 -0
  319. package/templates/observability-map.svg +101 -0
  320. package/templates/pipeline-audit.md +108 -0
  321. package/templates/release-metrics.vl.json +98 -0
  322. package/templates/service-topology.mmd +47 -0
  323. package/templates/status-report.md +32 -0
  324. package/templates/system-map.svg +53 -0
  325. package/templates/timeseries.vl.json +71 -0
  326. package/templates/tsx-interactive-counter.tsx +19 -0
  327. package/templates/tsx-status-report.tsx +31 -0
@@ -0,0 +1,732 @@
1
+ import type { Database } from "bun:sqlite";
2
+ import {
3
+ ArtifactSchema,
4
+ ArtifactTypeSchema,
5
+ ProjectSchema,
6
+ RenderRunSchema,
7
+ RevisionSchema,
8
+ TemplateSchema,
9
+ type Artifact,
10
+ type ArtifactType,
11
+ type Renderer,
12
+ type Project,
13
+ type RenderRun,
14
+ type Revision,
15
+ type Template,
16
+ } from "../../shared/contracts/artifact";
17
+ import type {
18
+ InsecureMarker,
19
+ ScreenshotError,
20
+ TsxExecutionMode,
21
+ } from "../../shared/contracts/validation";
22
+ import type { EvidenceImageFormat } from "../../shared/evidence-image";
23
+ import { DEFAULT_LIST_LIMIT } from "../../shared/config/limits";
24
+ import { now } from "../../shared/util/time";
25
+ import { asStoreError, FacetStoreError, hardenDatabaseFiles } from "./database";
26
+ import {
27
+ evictRevisions,
28
+ createTemplate as createLifecycleTemplate,
29
+ pinRevision as pinLifecycleRevision,
30
+ promoteRevision as promoteLifecycleRevision,
31
+ type PromoteRevisionInput,
32
+ type TemplateInput,
33
+ } from "./repository-lifecycle";
34
+ import { enforceEvidenceRetention, removeUnreferencedEvidence } from "./evidence-retention";
35
+
36
+ interface ProjectInput {
37
+ readonly projectRoot: string;
38
+ }
39
+ interface ArtifactInput {
40
+ readonly projectId: string;
41
+ readonly slug: string;
42
+ readonly title: string;
43
+ }
44
+ interface PublishInput {
45
+ readonly artifactId: string;
46
+ readonly artifactType: ArtifactType | "html";
47
+ readonly renderer?: Renderer;
48
+ readonly source: Uint8Array;
49
+ readonly note?: string | null;
50
+ readonly parentRevisionId?: string | null;
51
+ /**
52
+ * TSX execution mode (D2). Optional — defaults to `'static'` for
53
+ * every artifact type on disk so non-TSX rows carry the canonical
54
+ * value and the wire form simply omits it. Required to be `'static'`
55
+ * when `artifactType !== 'tsx'`; the dispatcher guard ensures this
56
+ * before the row is written. Derived from `TsxExecutionMode` so a
57
+ * new mode lands in one place.
58
+ */
59
+ readonly execution?: TsxExecutionMode;
60
+ }
61
+
62
+ interface RenderRunInput {
63
+ readonly revisionId: string;
64
+ readonly tier: 0 | 1;
65
+ readonly status: string;
66
+ readonly expected: unknown;
67
+ readonly observed: unknown;
68
+ readonly screenshotPath?: string | null;
69
+ readonly screenshotFormat?: EvidenceImageFormat | null;
70
+ readonly consolePath?: string | null;
71
+ readonly screenshotError?: ScreenshotError | null;
72
+ readonly insecure?: InsecureMarker | null;
73
+ /**
74
+ * TSX compiled-bundle evidence (D7). The bundle is derived output
75
+ * stored alongside the run, not a wire form — `null` (the default)
76
+ * for non-TSX runs and for TSX runs whose compilation did not
77
+ * produce a retained file. Retention cleanup deletes the file
78
+ * alongside its row.
79
+ */
80
+ readonly compiledPath?: string | null;
81
+ /**
82
+ * Retained-evidence carve-out: `true` exempts the row from the
83
+ * last-N cleanup policy. Pin/template call sites set this; the
84
+ * default false makes new runs eviction-eligible.
85
+ */
86
+ readonly retained?: boolean;
87
+ readonly startedAt?: string;
88
+ readonly finishedAt?: string;
89
+ }
90
+
91
+ interface WriteHookContext {
92
+ readonly phase: "after_insert" | "before_commit";
93
+ }
94
+ interface RepositoryOptions {
95
+ readonly onCommitted?: (revision: Revision) => void;
96
+ readonly writeHook?: (context: WriteHookContext) => void;
97
+ /**
98
+ * Evidence root used by the last-N retention cleanup. When omitted,
99
+ * retention is skipped (acceptable for in-process / unit tests that
100
+ * never write evidence files).
101
+ */
102
+ readonly evidenceRoot?: string;
103
+ /**
104
+ * Legacy evidence root (pre explicit-threading child-derived root). Read
105
+ * only — never written by retention. The export path consults it as a
106
+ * tolerant fallback for old evidence.
107
+ */
108
+ readonly legacyEvidenceRoot?: string;
109
+ }
110
+
111
+ interface ListArtifactsInput {
112
+ readonly projectId: string;
113
+ readonly slugPrefix?: string;
114
+ readonly limit?: number;
115
+ }
116
+
117
+ interface StatusForArtifactInput {
118
+ readonly artifactId: string;
119
+ }
120
+
121
+ type SqlRevision = Omit<
122
+ Revision,
123
+ | "artifactId"
124
+ | "revisionNumber"
125
+ | "parentRevisionId"
126
+ | "artifactType"
127
+ | "createdAt"
128
+ | "pinned"
129
+ | "source"
130
+ | "execution"
131
+ > & {
132
+ artifact_id: string;
133
+ revision_number: number;
134
+ parent_revision_id: string | null;
135
+ artifact_type: string;
136
+ renderer: string;
137
+ source: Uint8Array | ArrayBuffer;
138
+ note: string | null;
139
+ pinned: number;
140
+ created_at: string;
141
+ execution: TsxExecutionMode;
142
+ };
143
+
144
+ function sha256(source: Uint8Array): string {
145
+ const hasher = new Bun.CryptoHasher("sha256");
146
+ hasher.update(source);
147
+ return hasher.digest("hex");
148
+ }
149
+
150
+ function parseArtifactType(value: ArtifactType): ArtifactType {
151
+ try {
152
+ return ArtifactTypeSchema.parse(value);
153
+ } catch (error) {
154
+ throw new FacetStoreError("invalid_artifact_type", "Unsupported artifact type", {
155
+ cause: error,
156
+ });
157
+ }
158
+ }
159
+
160
+ function bytes(value: Uint8Array | ArrayBuffer): Uint8Array {
161
+ return value instanceof Uint8Array ? new Uint8Array(value) : new Uint8Array(value);
162
+ }
163
+
164
+ function revisionHasColumn(db: Database, name: string): boolean {
165
+ const rows = db.query(`PRAGMA table_info(revisions)`).all() as Array<{ name: string }>;
166
+ return rows.some((row) => row.name === name);
167
+ }
168
+
169
+ function renderRunHasColumn(db: Database, name: string): boolean {
170
+ const rows = db.query(`PRAGMA table_info(render_runs)`).all() as Array<{ name: string }>;
171
+ return rows.some((row) => row.name === name);
172
+ }
173
+
174
+ function mapRevision(row: SqlRevision): Revision {
175
+ // TSX carries `execution` on the wire; every other type carries it
176
+ // on disk but the wire form omits the field (the dispatch is
177
+ // explicit: `execution` must be absent, not null, for non-TSX).
178
+ return RevisionSchema.parse({
179
+ id: row.id,
180
+ artifactId: row.artifact_id,
181
+ revisionNumber: row.revision_number,
182
+ parentRevisionId: row.parent_revision_id,
183
+ artifactType: row.artifact_type,
184
+ renderer: row.renderer,
185
+ source: bytes(row.source),
186
+ sha256: row.sha256,
187
+ note: row.note,
188
+ pinned: row.pinned === 1,
189
+ createdAt: row.created_at,
190
+ ...(row.artifact_type === "tsx" ? { execution: row.execution } : {}),
191
+ });
192
+ }
193
+
194
+ export class ArtifactRepository {
195
+ constructor(
196
+ private readonly db: Database,
197
+ private readonly options: RepositoryOptions = {},
198
+ ) {}
199
+
200
+ getEvidenceRoot(): string | undefined {
201
+ return this.options.evidenceRoot;
202
+ }
203
+
204
+ getLegacyEvidenceRoot(): string | undefined {
205
+ return this.options.legacyEvidenceRoot;
206
+ }
207
+
208
+ createProject(input: ProjectInput): Project {
209
+ const value = { id: crypto.randomUUID(), projectRoot: input.projectRoot, createdAt: now() };
210
+ try {
211
+ this.db
212
+ .query("INSERT INTO projects(id, project_root, created_at) VALUES (?, ?, ?)")
213
+ .run(value.id, value.projectRoot, value.createdAt);
214
+ return ProjectSchema.parse(value);
215
+ } catch (error) {
216
+ throw asStoreError(error);
217
+ }
218
+ }
219
+
220
+ getProjectById(id: string): Project | null {
221
+ try {
222
+ const row = this.db
223
+ .query("SELECT id, project_root, created_at FROM projects WHERE id = ?")
224
+ .get(id) as { id: string; project_root: string; created_at: string } | null;
225
+ if (!row) return null;
226
+ return ProjectSchema.parse({
227
+ id: row.id,
228
+ projectRoot: row.project_root,
229
+ createdAt: row.created_at,
230
+ });
231
+ } catch (error) {
232
+ throw asStoreError(error);
233
+ }
234
+ }
235
+
236
+ /**
237
+ * Look up by id, creating the project on demand. The first lookup
238
+ * for a given id stamps it with the supplied root; subsequent lookups
239
+ * return the existing row. The synthesized root `${root}-${id}` keeps
240
+ * the `project_root` UNIQUE constraint satisfied while preserving
241
+ * the caller-chosen id for stable addressing.
242
+ */
243
+ getOrCreateProjectById(id: string, projectRoot: string): Project {
244
+ const existing = this.getProjectById(id);
245
+ if (existing) return existing;
246
+ const value = { id, projectRoot: `${projectRoot}-${id}`, createdAt: now() };
247
+ try {
248
+ this.db
249
+ .query("INSERT INTO projects(id, project_root, created_at) VALUES (?, ?, ?)")
250
+ .run(value.id, value.projectRoot, value.createdAt);
251
+ return ProjectSchema.parse(value);
252
+ } catch (error) {
253
+ throw asStoreError(error);
254
+ }
255
+ }
256
+
257
+ listArtifacts(input: ListArtifactsInput): Artifact[] {
258
+ const limit = input.limit ?? DEFAULT_LIST_LIMIT;
259
+ const prefix = input.slugPrefix ?? "";
260
+ try {
261
+ const rows = this.db
262
+ .query(
263
+ "SELECT id, project_id, slug, title, created_at, updated_at FROM artifacts WHERE project_id = ? AND slug LIKE ? ORDER BY created_at DESC LIMIT ?",
264
+ )
265
+ .all(input.projectId, `${prefix}%`, limit) as Array<{
266
+ id: string;
267
+ project_id: string;
268
+ slug: string;
269
+ title: string;
270
+ created_at: string;
271
+ updated_at: string;
272
+ }>;
273
+ return rows.map((row) =>
274
+ ArtifactSchema.parse({
275
+ id: row.id,
276
+ projectId: row.project_id,
277
+ slug: row.slug,
278
+ title: row.title,
279
+ createdAt: row.created_at,
280
+ updatedAt: row.updated_at,
281
+ }),
282
+ );
283
+ } catch (error) {
284
+ throw asStoreError(error);
285
+ }
286
+ }
287
+
288
+ getArtifactById(id: string): Artifact | null {
289
+ try {
290
+ const row = this.db
291
+ .query(
292
+ "SELECT id, project_id, slug, title, created_at, updated_at FROM artifacts WHERE id = ?",
293
+ )
294
+ .get(id) as {
295
+ id: string;
296
+ project_id: string;
297
+ slug: string;
298
+ title: string;
299
+ created_at: string;
300
+ updated_at: string;
301
+ } | null;
302
+ if (row === null) return null;
303
+ return ArtifactSchema.parse({
304
+ id: row.id,
305
+ projectId: row.project_id,
306
+ slug: row.slug,
307
+ title: row.title,
308
+ createdAt: row.created_at,
309
+ updatedAt: row.updated_at,
310
+ });
311
+ } catch (error) {
312
+ throw asStoreError(error);
313
+ }
314
+ }
315
+
316
+ listRenderRuns(input: { revisionId: string; tier: 0 | 1 }): RenderRun[] {
317
+ const hasCompiledPath = renderRunHasColumn(this.db, "compiled_path");
318
+ const hasScreenshotFormat = renderRunHasColumn(this.db, "screenshot_format");
319
+ try {
320
+ const columns = [
321
+ "id",
322
+ "revision_id",
323
+ "tier",
324
+ "status",
325
+ "expected_json",
326
+ "observed_json",
327
+ "screenshot_path",
328
+ ...(hasScreenshotFormat ? ["screenshot_format"] : []),
329
+ "console_path",
330
+ "screenshot_error_json",
331
+ "insecure_json",
332
+ "retained",
333
+ ...(hasCompiledPath ? ["compiled_path"] : []),
334
+ "started_at",
335
+ "finished_at",
336
+ ];
337
+ const selectSql = `SELECT ${columns.join(", ")} FROM render_runs WHERE revision_id = ? AND tier = ? ORDER BY finished_at DESC`;
338
+ const rows = this.db.query(selectSql).all(input.revisionId, input.tier) as Array<{
339
+ id: string;
340
+ revision_id: string;
341
+ tier: number;
342
+ status: string;
343
+ expected_json: string;
344
+ observed_json: string;
345
+ screenshot_path: string | null;
346
+ screenshot_format?: EvidenceImageFormat | null;
347
+ console_path: string | null;
348
+ screenshot_error_json: string | null;
349
+ insecure_json: string | null;
350
+ retained: number;
351
+ compiled_path?: string | null;
352
+ started_at: string;
353
+ finished_at: string;
354
+ }>;
355
+ return rows.map((row) =>
356
+ RenderRunSchema.parse({
357
+ id: row.id,
358
+ revisionId: row.revision_id,
359
+ tier: row.tier,
360
+ status: row.status,
361
+ expectedJson: row.expected_json,
362
+ observedJson: row.observed_json,
363
+ screenshotPath: row.screenshot_path,
364
+ screenshotFormat: hasScreenshotFormat ? (row.screenshot_format ?? null) : null,
365
+ consolePath: row.console_path,
366
+ screenshotErrorJson: row.screenshot_error_json,
367
+ insecureJson: row.insecure_json,
368
+ retained: row.retained === 1,
369
+ compiledPath: hasCompiledPath ? (row.compiled_path ?? null) : null,
370
+ startedAt: row.started_at,
371
+ finishedAt: row.finished_at,
372
+ }),
373
+ );
374
+ } catch (error) {
375
+ throw asStoreError(error);
376
+ }
377
+ }
378
+
379
+ statusForArtifact(input: StatusForArtifactInput): {
380
+ revisionCount: number;
381
+ pinnedCount: number;
382
+ templateCount: number;
383
+ } {
384
+ try {
385
+ const revisionRow = this.db
386
+ .query("SELECT COUNT(*) AS count FROM revisions WHERE artifact_id = ?")
387
+ .get(input.artifactId) as { count: number };
388
+ const pinnedRow = this.db
389
+ .query("SELECT COUNT(*) AS count FROM revisions WHERE artifact_id = ? AND pinned = 1")
390
+ .get(input.artifactId) as { count: number };
391
+ const templateRow = this.db
392
+ .query(
393
+ "SELECT COUNT(*) AS count FROM templates t JOIN revisions r ON r.id = t.revision_id WHERE r.artifact_id = ?",
394
+ )
395
+ .get(input.artifactId) as { count: number };
396
+ return {
397
+ revisionCount: revisionRow.count,
398
+ pinnedCount: pinnedRow.count,
399
+ templateCount: templateRow.count,
400
+ };
401
+ } catch (error) {
402
+ throw asStoreError(error);
403
+ }
404
+ }
405
+
406
+ findTemplateByName(name: string): Template | null {
407
+ try {
408
+ const row = this.db
409
+ .query(
410
+ "SELECT id, artifact_id, revision_id, name, description, promoted_by, promoted_at FROM templates WHERE name = ? ORDER BY promoted_at DESC LIMIT 1",
411
+ )
412
+ .get(name) as {
413
+ id: string;
414
+ artifact_id: string;
415
+ revision_id: string;
416
+ name: string;
417
+ description: string | null;
418
+ promoted_by: string;
419
+ promoted_at: string;
420
+ } | null;
421
+ if (!row) return null;
422
+ return TemplateSchema.parse({
423
+ id: row.id,
424
+ artifactId: row.artifact_id,
425
+ revisionId: row.revision_id,
426
+ name: row.name,
427
+ description: row.description,
428
+ promotedBy: row.promoted_by,
429
+ promotedAt: row.promoted_at,
430
+ });
431
+ } catch (error) {
432
+ throw asStoreError(error);
433
+ }
434
+ }
435
+
436
+ createArtifact(input: ArtifactInput): Artifact {
437
+ const createdAt = now();
438
+ const value = { id: crypto.randomUUID(), ...input, createdAt, updatedAt: createdAt };
439
+ try {
440
+ this.db
441
+ .query(
442
+ "INSERT INTO artifacts(id, project_id, slug, title, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)",
443
+ )
444
+ .run(value.id, value.projectId, value.slug, value.title, value.createdAt, value.updatedAt);
445
+ return ArtifactSchema.parse(value);
446
+ } catch (error) {
447
+ throw asStoreError(error);
448
+ }
449
+ }
450
+
451
+ publishRevision(input: PublishInput): Revision {
452
+ parseArtifactType(input.artifactType);
453
+ const source = new Uint8Array(input.source);
454
+ const revisionId = crypto.randomUUID();
455
+ const timestamp = now();
456
+ const sha = sha256(source);
457
+ // D2: every revision carries an execution mode on disk. Non-TSX
458
+ // rows store `'static'` (the canonical default) and the wire
459
+ // envelope omits the field; TSX rows carry the declared value
460
+ // end-to-end.
461
+ const execution = input.execution ?? "static";
462
+ // The `execution` column lands in the v8 migration. Detect it
463
+ // here so the v6-rollback test (which publishes against a v5
464
+ // schema) keeps its semantics: the rollback assertion is about
465
+ // the v6 transaction, not about whether the publish path itself
466
+ // needed v8.
467
+ const hasExecutionColumn = revisionHasColumn(this.db, "execution");
468
+ try {
469
+ const transact = this.db.transaction(() => {
470
+ const previous = this.db
471
+ .query(
472
+ "SELECT id, revision_number FROM revisions WHERE artifact_id = ? ORDER BY revision_number DESC LIMIT 1",
473
+ )
474
+ .get(input.artifactId) as { id: string; revision_number: number } | null;
475
+ const revisionNumber = (previous?.revision_number ?? 0) + 1;
476
+ if (hasExecutionColumn) {
477
+ this.db
478
+ .query(
479
+ "INSERT INTO revisions(id, artifact_id, revision_number, parent_revision_id, artifact_type, renderer, source, sha256, note, created_at, execution) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
480
+ )
481
+ .run(
482
+ revisionId,
483
+ input.artifactId,
484
+ revisionNumber,
485
+ input.parentRevisionId === undefined
486
+ ? (previous?.id ?? null)
487
+ : input.parentRevisionId,
488
+ input.artifactType,
489
+ input.renderer ?? "svg",
490
+ source,
491
+ sha,
492
+ input.note ?? null,
493
+ timestamp,
494
+ execution,
495
+ );
496
+ } else {
497
+ this.db
498
+ .query(
499
+ "INSERT INTO revisions(id, artifact_id, revision_number, parent_revision_id, artifact_type, renderer, source, sha256, note, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
500
+ )
501
+ .run(
502
+ revisionId,
503
+ input.artifactId,
504
+ revisionNumber,
505
+ input.parentRevisionId === undefined
506
+ ? (previous?.id ?? null)
507
+ : input.parentRevisionId,
508
+ input.artifactType,
509
+ input.renderer ?? "svg",
510
+ source,
511
+ sha,
512
+ input.note ?? null,
513
+ timestamp,
514
+ );
515
+ }
516
+ const revision = RevisionSchema.parse({
517
+ id: revisionId,
518
+ artifactId: input.artifactId,
519
+ revisionNumber,
520
+ parentRevisionId:
521
+ input.parentRevisionId === undefined ? (previous?.id ?? null) : input.parentRevisionId,
522
+ artifactType: input.artifactType,
523
+ renderer: input.renderer ?? "svg",
524
+ source,
525
+ sha256: sha,
526
+ note: input.note ?? null,
527
+ pinned: false,
528
+ createdAt: timestamp,
529
+ ...(input.artifactType === "tsx" && hasExecutionColumn ? { execution } : {}),
530
+ });
531
+ this.options.writeHook?.({ phase: "after_insert" });
532
+ evictRevisions(this.db, input.artifactId, revisionId);
533
+
534
+ this.options.writeHook?.({ phase: "before_commit" });
535
+ return revision;
536
+ });
537
+ const revision = transact.immediate();
538
+ hardenDatabaseFiles(this.db.filename);
539
+ this.options.onCommitted?.(revision);
540
+ return revision;
541
+ } catch (error) {
542
+ const mapped = asStoreError(error);
543
+ if (mapped.code === "duplicate_revision") {
544
+ throw new FacetStoreError("duplicate_revision", mapped.message, {
545
+ cause: error,
546
+ details: { revisionSha: sha },
547
+ });
548
+ }
549
+ throw mapped;
550
+ }
551
+ }
552
+
553
+ getRevisionBySha(artifactId: string, revisionSha: string): Revision | null {
554
+ try {
555
+ const row = this.db
556
+ .query("SELECT * FROM revisions WHERE artifact_id = ? AND sha256 = ?")
557
+ .get(artifactId, revisionSha) as SqlRevision | null;
558
+ return row ? mapRevision(row) : null;
559
+ } catch (error) {
560
+ throw asStoreError(error);
561
+ }
562
+ }
563
+
564
+ getLatestRevision(artifactId: string): Revision | null {
565
+ try {
566
+ const row = this.db
567
+ .query(
568
+ "SELECT * FROM revisions WHERE artifact_id = ? ORDER BY revision_number DESC LIMIT 1",
569
+ )
570
+ .get(artifactId) as SqlRevision | null;
571
+ return row === null ? null : mapRevision(row);
572
+ } catch (error) {
573
+ throw asStoreError(error);
574
+ }
575
+ }
576
+
577
+ getRevisionById(revisionId: string): Revision | null {
578
+ try {
579
+ const row = this.db
580
+ .query("SELECT * FROM revisions WHERE id = ?")
581
+ .get(revisionId) as SqlRevision | null;
582
+ return row ? mapRevision(row) : null;
583
+ } catch (error) {
584
+ throw asStoreError(error);
585
+ }
586
+ }
587
+
588
+ recordRenderRun(input: RenderRunInput): RenderRun {
589
+ const startedAt = input.startedAt ?? now();
590
+ const finishedAt = input.finishedAt ?? now();
591
+ const retained = input.retained ?? false;
592
+ const compiledPath = input.compiledPath ?? null;
593
+ const screenshotFormat = input.screenshotFormat ?? null;
594
+ // The `compiled_path` column lands in the v8 migration. Detect it
595
+ // so the v6-rollback test (which records runs against a v5
596
+ // schema) keeps its semantics: the rollback assertion is about
597
+ // the v6 transaction, not about whether the record path itself
598
+ // needed v8.
599
+ const hasCompiledPath = renderRunHasColumn(this.db, "compiled_path");
600
+ const hasScreenshotFormat = renderRunHasColumn(this.db, "screenshot_format");
601
+ const value: {
602
+ id: string;
603
+ revisionId: string;
604
+ tier: 0 | 1;
605
+ status: string;
606
+ expectedJson: string;
607
+ observedJson: string;
608
+ screenshotPath: string | null;
609
+ screenshotFormat: EvidenceImageFormat | null;
610
+ consolePath: string | null;
611
+ screenshotErrorJson: string | null;
612
+ insecureJson: string | null;
613
+ retained: boolean;
614
+ compiledPath?: string | null;
615
+ startedAt: string;
616
+ finishedAt: string;
617
+ } = {
618
+ id: crypto.randomUUID(),
619
+ revisionId: input.revisionId,
620
+ tier: input.tier,
621
+ status: input.status,
622
+ expectedJson: JSON.stringify(input.expected),
623
+ observedJson: JSON.stringify(input.observed),
624
+ screenshotPath: input.screenshotPath ?? null,
625
+ screenshotFormat,
626
+ consolePath: input.consolePath ?? null,
627
+ screenshotErrorJson:
628
+ input.screenshotError === null || input.screenshotError === undefined
629
+ ? null
630
+ : JSON.stringify(input.screenshotError),
631
+ insecureJson:
632
+ input.insecure === null || input.insecure === undefined
633
+ ? null
634
+ : JSON.stringify(input.insecure),
635
+ retained,
636
+ startedAt,
637
+ finishedAt,
638
+ };
639
+ if (hasCompiledPath) value.compiledPath = compiledPath;
640
+ let artifactIdForCleanup: string | null = null;
641
+ try {
642
+ // Validate BEFORE the INSERT, not after: `render_runs` has no unique
643
+ // schema-shape constraint SQLite would reject on its own (e.g. an
644
+ // empty `status`), so a post-insert `RenderRunSchema.parse` failure
645
+ // used to land in this same catch AFTER the row was already durable
646
+ // — unlinking evidence files a committed row still pointed at.
647
+ // Validating first means a schema-invalid input never reaches the
648
+ // INSERT, so the catch's cleanup-on-failure is only ever reached
649
+ // when no row was written.
650
+ const parsed = RenderRunSchema.parse(value);
651
+ const columns = [
652
+ "id",
653
+ "revision_id",
654
+ "tier",
655
+ "status",
656
+ "expected_json",
657
+ "observed_json",
658
+ "screenshot_path",
659
+ ...(hasScreenshotFormat ? ["screenshot_format"] : []),
660
+ "console_path",
661
+ "screenshot_error_json",
662
+ "insecure_json",
663
+ "retained",
664
+ ...(hasCompiledPath ? ["compiled_path"] : []),
665
+ "started_at",
666
+ "finished_at",
667
+ ];
668
+ const values = [
669
+ value.id,
670
+ value.revisionId,
671
+ value.tier,
672
+ value.status,
673
+ value.expectedJson,
674
+ value.observedJson,
675
+ value.screenshotPath,
676
+ ...(hasScreenshotFormat ? [value.screenshotFormat] : []),
677
+ value.consolePath,
678
+ value.screenshotErrorJson,
679
+ value.insecureJson,
680
+ value.retained ? 1 : 0,
681
+ ...(hasCompiledPath ? [value.compiledPath ?? null] : []),
682
+ value.startedAt,
683
+ value.finishedAt,
684
+ ];
685
+ this.db
686
+ .query(
687
+ `INSERT INTO render_runs(${columns.join(", ")}) VALUES (${columns.map(() => "?").join(", ")})`,
688
+ )
689
+ .run(...values);
690
+ const ownerRow = this.db
691
+ .query("SELECT artifact_id FROM revisions WHERE id = ?")
692
+ .get(input.revisionId) as { artifact_id: string } | null;
693
+ if (ownerRow !== null) artifactIdForCleanup = ownerRow.artifact_id;
694
+ // Last-N retention runs AFTER the row is durable; a failure
695
+ // here would lose the just-recorded run, so it sits outside the
696
+ // INSERT transaction. Cleanup is best-effort — the row is
697
+ // authoritative, a stale on-disk file is recoverable.
698
+ if (artifactIdForCleanup !== null && this.options.evidenceRoot !== undefined) {
699
+ try {
700
+ enforceEvidenceRetention({
701
+ db: this.db,
702
+ artifactId: artifactIdForCleanup,
703
+ evidenceRoot: this.options.evidenceRoot,
704
+ });
705
+ } catch {
706
+ // Retention cleanup is best-effort; do not fail the write.
707
+ }
708
+ }
709
+ hardenDatabaseFiles(this.db.filename);
710
+ return parsed;
711
+ } catch (error) {
712
+ removeUnreferencedEvidence(this.db, [input.screenshotPath, input.consolePath, compiledPath]);
713
+ throw asStoreError(error);
714
+ }
715
+ }
716
+
717
+ promoteRevision(input: PromoteRevisionInput): Template {
718
+ return promoteLifecycleRevision(this.db, input);
719
+ }
720
+
721
+ instantiateTemplate(input: TemplateInput): Template {
722
+ return createLifecycleTemplate(this.db, input);
723
+ }
724
+
725
+ pinRevision(revisionId: string, pinned = true): void {
726
+ pinLifecycleRevision(this.db, revisionId, pinned);
727
+ }
728
+
729
+ updateRevisionSource(_revisionId: string, _source: Uint8Array): never {
730
+ throw new FacetStoreError("immutable_revision", "Revision source and sha256 are immutable");
731
+ }
732
+ }