@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,218 @@
1
+ import { TSX_EXECUTION_MODES } from "../../shared/tsx/execution";
2
+
3
+ /**
4
+ * The canonical DDL applied to a fresh database. Every column the
5
+ * schema holds today lives here; the v1 migration runs this block
6
+ * verbatim. Subsequent migrations extend this table set via additive
7
+ * ALTER TABLE statements (never a destructive rewrite — the schema
8
+ * migrations ledger records each version).
9
+ *
10
+ * Fresh v8 databases apply this block first (v1), then the v2-v8
11
+ * fragments in order. The v8 fragment widens the CHECK to include
12
+ * `tsx`, adds `revisions.execution`, and adds `render_runs.compiled_path`.
13
+ * Migrations are tracked, so a re-run on an already-migrated DB skips
14
+ * already-applied versions.
15
+ */
16
+ export const INITIAL_SCHEMA = `
17
+ CREATE TABLE projects(
18
+ id TEXT PRIMARY KEY,
19
+ project_root TEXT NOT NULL UNIQUE,
20
+ created_at TEXT NOT NULL
21
+ );
22
+ CREATE TABLE artifacts(
23
+ id TEXT PRIMARY KEY,
24
+ project_id TEXT NOT NULL REFERENCES projects(id),
25
+ slug TEXT NOT NULL,
26
+ title TEXT NOT NULL,
27
+ created_at TEXT NOT NULL,
28
+ updated_at TEXT NOT NULL,
29
+ UNIQUE(project_id, slug)
30
+ );
31
+ CREATE TABLE revisions(
32
+ id TEXT PRIMARY KEY,
33
+ artifact_id TEXT NOT NULL REFERENCES artifacts(id),
34
+ revision_number INTEGER NOT NULL,
35
+ parent_revision_id TEXT REFERENCES revisions(id),
36
+ artifact_type TEXT NOT NULL CHECK(artifact_type IN ('markdown','mermaid','svg','chart','html')),
37
+ source BLOB NOT NULL,
38
+ sha256 TEXT NOT NULL,
39
+ note TEXT,
40
+ pinned INTEGER NOT NULL DEFAULT 0,
41
+ created_at TEXT NOT NULL,
42
+ UNIQUE(artifact_id, revision_number),
43
+ UNIQUE(artifact_id, sha256)
44
+ );
45
+ CREATE TABLE render_runs(
46
+ id TEXT PRIMARY KEY,
47
+ revision_id TEXT NOT NULL REFERENCES revisions(id),
48
+ tier INTEGER NOT NULL CHECK(tier IN (0,1)),
49
+ status TEXT NOT NULL,
50
+ expected_json TEXT NOT NULL,
51
+ observed_json TEXT NOT NULL,
52
+ screenshot_path TEXT,
53
+ console_path TEXT,
54
+ started_at TEXT NOT NULL,
55
+ finished_at TEXT NOT NULL
56
+ );
57
+ CREATE TABLE templates(
58
+ id TEXT PRIMARY KEY,
59
+ artifact_id TEXT NOT NULL REFERENCES artifacts(id),
60
+ revision_id TEXT NOT NULL REFERENCES revisions(id),
61
+ name TEXT NOT NULL UNIQUE,
62
+ description TEXT,
63
+ promoted_by TEXT NOT NULL,
64
+ promoted_at TEXT NOT NULL
65
+ );
66
+ `;
67
+
68
+ /**
69
+ * The v2 DDL fragment — appended after `INITIAL_SCHEMA` for fresh
70
+ * databases that land on v2 directly. Existing v1 databases pick up
71
+ * the v2 column via the migration step in `migrations.ts`. Keeping
72
+ * the fragment here (and not duplicated in the migration) means the
73
+ * canonical DDL and the migration body cannot drift apart.
74
+ */
75
+ export const V2_SCHEMA_FRAGMENT = `
76
+ ALTER TABLE render_runs ADD COLUMN retained INTEGER NOT NULL DEFAULT 0;
77
+ `;
78
+
79
+ /** The v3 DDL fragment adds renderer persistence; its literal CHECK mirrors RENDERERS deliberately and is parity-tested. */
80
+ export const V3_SCHEMA_FRAGMENT = `
81
+ ALTER TABLE revisions ADD COLUMN renderer TEXT NOT NULL DEFAULT 'svg' CHECK(renderer IN ('svg','canvas'));
82
+ `;
83
+
84
+ /** The v4 fragment preserves typed screenshot-capture failures across read-back. */
85
+ export const V4_SCHEMA_FRAGMENT = `
86
+ ALTER TABLE render_runs ADD COLUMN screenshot_error_json TEXT;
87
+ `;
88
+
89
+ /** The v5 fragment persists the insecure execution marker with each run. */
90
+ export const V5_SCHEMA_FRAGMENT = `
91
+ ALTER TABLE render_runs ADD COLUMN insecure_json TEXT;
92
+ `;
93
+
94
+ /**
95
+ * The v6 rebuild widens revisions.artifact_type. SQLite cannot alter a CHECK
96
+ * in place, so the official create-copy-drop-rename procedure preserves child
97
+ * foreign-key clauses by never renaming the original table.
98
+ */
99
+ export const V6_SCHEMA_FRAGMENT = `
100
+ CREATE TABLE revisions_v6(
101
+ id TEXT PRIMARY KEY,
102
+ artifact_id TEXT NOT NULL REFERENCES artifacts(id),
103
+ revision_number INTEGER NOT NULL,
104
+ parent_revision_id TEXT REFERENCES revisions(id),
105
+ artifact_type TEXT NOT NULL CHECK(artifact_type IN ('markdown','mermaid','svg','chart','html')),
106
+ source BLOB NOT NULL,
107
+ sha256 TEXT NOT NULL,
108
+ note TEXT,
109
+ pinned INTEGER NOT NULL DEFAULT 0,
110
+ created_at TEXT NOT NULL,
111
+ renderer TEXT NOT NULL DEFAULT 'svg' CHECK(renderer IN ('svg','canvas')),
112
+ UNIQUE(artifact_id, revision_number),
113
+ UNIQUE(artifact_id, sha256)
114
+ );
115
+ INSERT INTO revisions_v6(
116
+ id, artifact_id, revision_number, parent_revision_id, artifact_type, source, sha256,
117
+ note, pinned, created_at, renderer
118
+ )
119
+ SELECT
120
+ id, artifact_id, revision_number, parent_revision_id, artifact_type, source, sha256,
121
+ note, pinned, created_at, renderer
122
+ FROM revisions;
123
+ DROP TABLE revisions;
124
+ ALTER TABLE revisions_v6 RENAME TO revisions;
125
+ `;
126
+
127
+ /**
128
+ * The v7 backfill repairs render_runs.observed_json rows written by
129
+ * prior releases that did NOT include the `opaqueRegionCount` and
130
+ * `externalImageCount` counters. A strict `VerdictObservedSchema.parse`
131
+ * would 400 read-back / export / gallery-source on every pre-arc
132
+ * artifact; the schema-level repair writes a 0 for every missing
133
+ * counter so future reads don't have to.
134
+ *
135
+ * The migration is idempotent: `json_extract(observed_json, '$.x')` is
136
+ * null when the field is absent, and `COALESCE` replaces that null with
137
+ * 0. Re-running the migration against an already-migrated row is a
138
+ * no-op because `COALESCE(null, 0)` and `COALESCE(0, 0)` both yield 0
139
+ * and the rebuilt JSON is identical to the existing one.
140
+ *
141
+ * This is the schema-side companion to the runtime tolerant read in
142
+ * `src/service/stored-verdict.ts` `withTolerantObserved`. The tolerant
143
+ * read defends the boundary against a row that was written before
144
+ * this migration ran; the migration moves the on-disk JSON to the
145
+ * current shape so future readers can be strict again if they want.
146
+ */
147
+ export const V7_SCHEMA_FRAGMENT = `
148
+ UPDATE render_runs
149
+ SET observed_json = json_set(
150
+ json_set(
151
+ observed_json,
152
+ '$.opaqueRegionCount',
153
+ COALESCE(json_extract(observed_json, '$.opaqueRegionCount'), 0)
154
+ ),
155
+ '$.externalImageCount',
156
+ COALESCE(json_extract(observed_json, '$.externalImageCount'), 0)
157
+ )
158
+ WHERE
159
+ json_type(observed_json, '$.opaqueRegionCount') IS NULL
160
+ OR json_type(observed_json, '$.externalImageCount') IS NULL
161
+ `;
162
+
163
+ /**
164
+ * The v8 rebuild widens `revisions.artifact_type` to include `tsx`,
165
+ * adds `revisions.execution` (D2 of the TSX design — declared mode,
166
+ * default `static`, backfilled for every prior row), and adds
167
+ * `render_runs.compiled_path` (D7 — derived bundle stored alongside
168
+ * the run, NULLABLE so non-TSX runs and pre-arc rows stay NULL).
169
+ *
170
+ * SQLite cannot alter a CHECK in place, so the create-copy-drop-rename
171
+ * pattern from v6 is reused here. Every column is copied explicitly,
172
+ * and `execution` is set to `'static'` for every v7 row so the
173
+ * NOT NULL DEFAULT on the new column is satisfied without a separate
174
+ * UPDATE.
175
+ *
176
+ * Trap that shipped twice in this project (opaque-content v6 and
177
+ * HTML v7): adding a REQUIRED column to a stored shape breaks
178
+ * read-back of every row written by earlier releases. The fix is
179
+ * BOTH halves: a tolerant read in `src/service/stored-verdict.ts`
180
+ * for the runtime boundary AND a backfill migration on disk. The
181
+ * v8 backfill below is the schema-side half; the existing
182
+ * `withTolerantObserved` defends the runtime half for `observed_json`
183
+ * while this fragment backfills `execution` and widens the CHECK.
184
+ */
185
+ export const V8_SCHEMA_FRAGMENT = `
186
+ CREATE TABLE revisions_v8(
187
+ id TEXT PRIMARY KEY,
188
+ artifact_id TEXT NOT NULL REFERENCES artifacts(id),
189
+ revision_number INTEGER NOT NULL,
190
+ parent_revision_id TEXT REFERENCES revisions(id),
191
+ artifact_type TEXT NOT NULL CHECK(artifact_type IN ('markdown','mermaid','svg','chart','html','tsx')),
192
+ source BLOB NOT NULL,
193
+ sha256 TEXT NOT NULL,
194
+ note TEXT,
195
+ pinned INTEGER NOT NULL DEFAULT 0,
196
+ created_at TEXT NOT NULL,
197
+ renderer TEXT NOT NULL DEFAULT 'svg' CHECK(renderer IN ('svg','canvas')),
198
+ execution TEXT NOT NULL DEFAULT 'static' CHECK(execution IN (${TSX_EXECUTION_MODES.map((mode) => `'${mode}'`).join(",")})),
199
+ UNIQUE(artifact_id, revision_number),
200
+ UNIQUE(artifact_id, sha256)
201
+ );
202
+ INSERT INTO revisions_v8(
203
+ id, artifact_id, revision_number, parent_revision_id, artifact_type, source, sha256,
204
+ note, pinned, created_at, renderer, execution
205
+ )
206
+ SELECT
207
+ id, artifact_id, revision_number, parent_revision_id, artifact_type, source, sha256,
208
+ note, pinned, created_at, renderer, 'static'
209
+ FROM revisions;
210
+ DROP TABLE revisions;
211
+ ALTER TABLE revisions_v8 RENAME TO revisions;
212
+ ALTER TABLE render_runs ADD COLUMN compiled_path TEXT;
213
+ `;
214
+
215
+ export const V9_SCHEMA_FRAGMENT = `
216
+ ALTER TABLE render_runs ADD COLUMN screenshot_format TEXT
217
+ CHECK(screenshot_format IN ('png','webp'));
218
+ `;
@@ -0,0 +1,97 @@
1
+ import type { Revision, RenderRun } from "../shared/contracts/artifact";
2
+ import { VerdictSchema, type Verdict, type VerdictObserved } from "../shared/contracts/validation";
3
+ import type { ArtifactRepository } from "./store/repository";
4
+
5
+ /**
6
+ * Tolerate counters the pre-arc stored-observation schema did not
7
+ * include. Every prior release pre-dates both `externalImageCount`
8
+ * (added by this arc) and `opaqueRegionCount` (added by the prior
9
+ * opaque-content arc); a strict `VerdictObservedSchema.parse` would
10
+ * 400 the read-back / export / gallery-source routes on every
11
+ * pre-arc artifact. Default missing counters to 0 — the verdict's
12
+ * downstream behavior for a zero counter is what the verifier
13
+ * would have computed in the absence of the field (no partial,
14
+ * no opaque, no external_resources), and that is the conservative
15
+ * answer for a verifier that never observed the field at all.
16
+ *
17
+ * The companion migration (`src/service/store/migrations.ts`)
18
+ * backfills the missing counters to 0 in the on-disk JSON so the
19
+ * tolerant read is a defensive belt-and-braces against a row that
20
+ * was written before the migration ran. Treating the instance
21
+ * (`.optional()` on the schema) would fix this specific break but
22
+ * leave the same defect class to recur on the next counter
23
+ * addition; treating the boundary is what makes it durable.
24
+ */
25
+ export function withTolerantObserved(parsed: unknown): VerdictObserved {
26
+ const observed = (parsed ?? {}) as Record<string, unknown>;
27
+ // Per-counter fallback: each field defaults to 0 when the stored
28
+ // JSON omits it (the pre-arc shape) and preserves the on-disk value
29
+ // when present. This is the durable treatment — every counter ever
30
+ // added can be extended the same way without re-breaking old rows.
31
+ const num = (key: string, fallback: number): number => {
32
+ const value = observed[key];
33
+ return typeof value === "number" ? value : fallback;
34
+ };
35
+ return {
36
+ rendererRootSvgCount: num("rendererRootSvgCount", 0),
37
+ graphCount: num("graphCount", 0),
38
+ mermaidNodeCount: num("mermaidNodeCount", 0),
39
+ visibleSvgCount: num("visibleSvgCount", 0),
40
+ opaqueRegionCount: num("opaqueRegionCount", 0),
41
+ externalImageCount: num("externalImageCount", 0),
42
+ // errorCount is required on the schema; pre-arc rows that never
43
+ // recorded it default to 0.
44
+ errorCount: num("errorCount", 0),
45
+ // Preserve optional fields the on-disk JSON may carry.
46
+ ...(observed.html !== undefined ? { html: observed.html as VerdictObserved["html"] } : {}),
47
+ ...(Array.isArray(observed.viewBoxes) ? { viewBoxes: observed.viewBoxes as string[] } : {}),
48
+ ...(Array.isArray(observed.discriminativeErrors)
49
+ ? {
50
+ discriminativeErrors:
51
+ observed.discriminativeErrors as VerdictObserved["discriminativeErrors"],
52
+ }
53
+ : {}),
54
+ };
55
+ }
56
+
57
+ export function verdictFromStoredRun(revision: Revision, run: RenderRun): Verdict {
58
+ return VerdictSchema.parse({
59
+ status: run.status,
60
+ tier: run.tier,
61
+ artifactId: revision.artifactId,
62
+ revisionSha: revision.sha256,
63
+ observed: withTolerantObserved(JSON.parse(run.observedJson)),
64
+ ...(run.screenshotErrorJson !== null
65
+ ? { screenshotError: JSON.parse(run.screenshotErrorJson) }
66
+ : {}),
67
+ ...(run.insecureJson !== null ? { insecure: JSON.parse(run.insecureJson) } : {}),
68
+ // Reconstruct execution from the revision row rather than the stored
69
+ // verdict. The render_run row carries no
70
+ // execution column (the marker is a verdict-level property, not a
71
+ // run-level one), and the marker is only emitted for TSX
72
+ // revisions — every other artifact type reads back with the
73
+ // field absent, so the wire form for non-TSX stays byte-identical.
74
+ //
75
+ // Execution-null policy (deliberate): when a TSX row's column
76
+ // is undefined on disk (purely defensive — the schema CHECK
77
+ // prevents NULL inserts and the v8 migration backfills pre-arc
78
+ // rows to 'static'), default to 'static' on the wire. 'static'
79
+ // is the canonical D2 default; emitting nothing was the prior
80
+ // silent behavior and an unpinned accident. Operators reading
81
+ // the wire form always see a marker on TSX rows.
82
+ ...(revision.artifactType === "tsx" ? { execution: revision.execution ?? "static" } : {}),
83
+ });
84
+ }
85
+
86
+ export function latestStoredVerdict(
87
+ repository: ArtifactRepository,
88
+ revision: Revision,
89
+ ): Verdict | null {
90
+ const runs = ([0, 1] as const)
91
+ .flatMap((tier) => repository.listRenderRuns({ revisionId: revision.id, tier }))
92
+ .toSorted(
93
+ (left, right) => right.finishedAt.localeCompare(left.finishedAt) || right.tier - left.tier,
94
+ );
95
+ const run = runs[0];
96
+ return run === undefined ? null : verdictFromStoredRun(revision, run);
97
+ }
@@ -0,0 +1,301 @@
1
+ /**
2
+ * SSE stream handler.
3
+ *
4
+ * The /api/v1/stream route emits revision-committed events to clients
5
+ * that hold a valid gallery lease. Auth happens BEFORE the lease check
6
+ * (a request without Bearer never reaches the lease logic), and the
7
+ * lease capability is carried via the authed fetch header — the
8
+ * `leaseId` is never embedded in a URL query parameter.
9
+ *
10
+ * Stream lifetime is bound to the lease: when the lease expires, the
11
+ * per-lease timer fires the manager's expiry-listener, which closes
12
+ * the controller and releases the stream's idle reason. Without this
13
+ * binding, the SSE connection would outlast its lease and the
14
+ * stream:<id> idle reason would pin the service alive forever.
15
+ */
16
+
17
+ import { randomUUID } from "node:crypto";
18
+
19
+ import type { z } from "zod";
20
+
21
+ import { errEnvelope } from "../shared/contracts/envelope";
22
+ import type { RevisionCommittedEventSchema } from "../shared/contracts/events";
23
+
24
+ import { requireBearer } from "./security/auth";
25
+ import { checkHostOrigin, resolveHost } from "./security/host-origin";
26
+ import type { GalleryLease, GalleryLeaseManager } from "./security/leases";
27
+ import type { FacetLogger } from "../shared/logging/logger";
28
+ import type { IdleController } from "./lifecycle/idle-controller";
29
+ import { envelopeResponse, generateRequestId } from "./http-utils";
30
+
31
+ const NO_STORE = "no-store";
32
+ export const STREAM_HEARTBEAT_INTERVAL_MS = 15_000;
33
+
34
+ export function assertHeartbeatBeforeLeaseTtl(
35
+ heartbeatIntervalMs: number,
36
+ leaseTtlMs: number,
37
+ ): void {
38
+ if (heartbeatIntervalMs >= leaseTtlMs) {
39
+ throw new RangeError("SSE heartbeat interval must be shorter than the lease TTL");
40
+ }
41
+ }
42
+
43
+ export type RevisionCommittedEvent = z.infer<typeof RevisionCommittedEventSchema>;
44
+
45
+ /**
46
+ * Per-artifact fan-out for `revision:committed`. The write path
47
+ * (dispatcher, after commit + verdict runs) calls `emit`; every live
48
+ * SSE stream registered for that artifact receives the event. Sinks
49
+ * are isolated — one dead stream cannot wedge the others.
50
+ */
51
+ export interface RevisionBroadcaster {
52
+ register(artifactId: string, send: (event: RevisionCommittedEvent) => void): () => void;
53
+ emit(event: RevisionCommittedEvent): void;
54
+ readonly size: number;
55
+ }
56
+
57
+ export function createRevisionBroadcaster(): RevisionBroadcaster {
58
+ const sinks = new Map<string, Set<(event: RevisionCommittedEvent) => void>>();
59
+ let size = 0;
60
+ return {
61
+ register(artifactId, send) {
62
+ let set = sinks.get(artifactId);
63
+ if (set === undefined) {
64
+ set = new Set();
65
+ sinks.set(artifactId, set);
66
+ }
67
+ set.add(send);
68
+ size += 1;
69
+ return () => {
70
+ const current = sinks.get(artifactId);
71
+ if (current === undefined) return;
72
+ if (current.delete(send)) size -= 1;
73
+ if (current.size === 0) sinks.delete(artifactId);
74
+ };
75
+ },
76
+ emit(event) {
77
+ const set = sinks.get(event.artifactId);
78
+ if (set === undefined) return;
79
+ // Spread snapshots the sinks so a callback that unregisters
80
+ // itself mid-fan-out does not skip the remaining peers.
81
+ // oxlint-disable-next-line unicorn/no-useless-spread
82
+ for (const send of [...set]) {
83
+ try {
84
+ send(event);
85
+ } catch {
86
+ // one dead stream must not wedge the fan-out
87
+ }
88
+ }
89
+ },
90
+ get size() {
91
+ return size;
92
+ },
93
+ };
94
+ }
95
+
96
+ export interface StreamHandlerDeps {
97
+ readonly installToken: string;
98
+ readonly leases: GalleryLeaseManager;
99
+ readonly idle: IdleController;
100
+ readonly logger: FacetLogger;
101
+ readonly expectedHost: string | (() => string);
102
+ readonly ownOrigin: string | (() => string);
103
+ readonly broadcaster: RevisionBroadcaster;
104
+ readonly heartbeatIntervalMs?: number;
105
+ }
106
+
107
+ interface StreamParsedRequest {
108
+ readonly url: string;
109
+ readonly method: string;
110
+ readonly host: string | null;
111
+ readonly origin: string | null;
112
+ readonly secFetchSite: string | null;
113
+ readonly authorization: string | null;
114
+ readonly headers: { get(name: string): string | null };
115
+ }
116
+
117
+ export function handleStream(deps: StreamHandlerDeps, req: StreamParsedRequest): Response {
118
+ const requestId = generateRequestId();
119
+
120
+ const hostCheck = checkHostOrigin({
121
+ method: req.method,
122
+ host: req.host,
123
+ origin: req.origin,
124
+ secFetchSite: req.secFetchSite,
125
+ expectedHost: resolveHost(deps.expectedHost),
126
+ ownOrigin: resolveHost(deps.ownOrigin),
127
+ });
128
+ if (!hostCheck.ok) {
129
+ let status = 400;
130
+ if (hostCheck.error.code === "invalid_request") {
131
+ const reason = (hostCheck.error.details as { reason?: string } | undefined)?.reason;
132
+ if (reason === "cross_site_mutation") status = 403;
133
+ else if (reason === "missing_host") status = 421;
134
+ }
135
+ return envelopeResponse(errEnvelope(requestId, hostCheck.error.toBody()), status);
136
+ }
137
+
138
+ const authResult = requireBearer(req.authorization, deps.installToken);
139
+ if (!authResult.ok) {
140
+ return envelopeResponse(errEnvelope(requestId, authResult.error.toBody()), 401);
141
+ }
142
+
143
+ // Lease capability travels in the `X-Gallery-Lease` header, never in
144
+ // a URL query parameter. URL tokens leak via server logs, the
145
+ // `Referer` header on outbound navigations, and the browser history
146
+ // — any of those would expose the lease id to processes the bearer
147
+ // holder did not authorise. A query-string `?lease=` is REJECTED (not
148
+ // silently accepted) so a misconfigured client sees a typed 401
149
+ // instead of an unguarded stream.
150
+ const leaseId = req.headers.get("x-gallery-lease");
151
+ const artifactId = req.headers.get("x-gallery-artifact");
152
+ if (leaseId === null || artifactId === null) {
153
+ return envelopeResponse(
154
+ errEnvelope(requestId, {
155
+ code: "invalid_envelope",
156
+ message:
157
+ "SSE requires X-Gallery-Lease + X-Gallery-Artifact headers (no query-string fallback)",
158
+ retryable: false,
159
+ details: { reason: "lease_header_missing" },
160
+ }),
161
+ 401,
162
+ );
163
+ }
164
+
165
+ const lease: GalleryLease = {
166
+ leaseId,
167
+ artifactId,
168
+ pid: process.pid,
169
+ expiresAt: Date.now() + 1_000,
170
+ };
171
+ if (!deps.leases.validate(lease)) {
172
+ return envelopeResponse(
173
+ errEnvelope(requestId, {
174
+ code: "invalid_envelope",
175
+ message: "Lease is invalid or expired",
176
+ retryable: false,
177
+ details: { reason: "lease_invalid" },
178
+ }),
179
+ 401,
180
+ );
181
+ }
182
+
183
+ const streamId = randomUUID();
184
+ deps.idle.acquire(`stream:${streamId}`);
185
+ const encoder = new TextEncoder();
186
+
187
+ // Per-stream teardown. Every teardown path (cancel, lease-gone,
188
+ // renew-failure) funnels through `releaseStreamOnly`; calling any of
189
+ // them twice is a no-op (the inner `closed` guard short-circuits).
190
+ let closed = false;
191
+ let unregister: (() => void) | null = null;
192
+ let heartbeat: ReturnType<typeof setInterval> | null = null;
193
+
194
+ // Shared stream-only teardown: clear the heartbeat interval, drop the
195
+ // broadcaster registration, and release the stream's idle reason. The
196
+ // lease is deliberately NOT touched here — the cancel path keeps the
197
+ // lease alive for the F5 refresh (the lease has its own TTL on the
198
+ // manager and is the credential the shell persists).
199
+ const releaseStreamOnly = (): void => {
200
+ if (closed) return;
201
+ closed = true;
202
+ if (heartbeat !== null) clearInterval(heartbeat);
203
+ unregister?.();
204
+ deps.idle.release(`stream:${streamId}`);
205
+ };
206
+
207
+ // Full teardown: shared stream cleanup plus lease release. Runs when
208
+ // the lease is gone (expired or explicitly released).
209
+ const releaseLease = (): void => {
210
+ if (closed) return;
211
+ releaseStreamOnly();
212
+ deps.leases.release(leaseId);
213
+ };
214
+
215
+ const readable = new ReadableStream({
216
+ start(controller) {
217
+ const send = (event: unknown): void => {
218
+ try {
219
+ controller.enqueue(encoder.encode(`data: ${JSON.stringify(event)}\n\n`));
220
+ } catch {
221
+ // controller already closed (lease expired mid-send)
222
+ }
223
+ };
224
+ send({ type: "stream:open", streamId, artifactId, at: new Date().toISOString() });
225
+
226
+ // Write-path fan-out: committed revisions for THIS artifact land
227
+ // on this stream. Unregistered on lease expiry / disconnect.
228
+ unregister = deps.broadcaster.register(artifactId, (event) => send(event));
229
+
230
+ // Bind stream lifetime to lease lifetime. When the per-lease
231
+ // timer fires, we close the stream + release the idle reason.
232
+ deps.leases.onExpireNotify(leaseId, () => {
233
+ try {
234
+ controller.enqueue(
235
+ encoder.encode(
236
+ `data: ${JSON.stringify({
237
+ type: "stream:close",
238
+ streamId,
239
+ at: new Date().toISOString(),
240
+ reason: "lease_expired",
241
+ })}\n\n`,
242
+ ),
243
+ );
244
+ } catch {
245
+ // already closed
246
+ }
247
+ try {
248
+ controller.close();
249
+ } catch {
250
+ // already closed
251
+ }
252
+ releaseLease();
253
+ });
254
+
255
+ heartbeat = setInterval(() => {
256
+ // Re-check liveness — if the lease was removed by the expiry
257
+ // hook or an explicit release in the meantime, the controller
258
+ // is closed and the teardown already ran, so `closed` is set
259
+ // and we return without re-enqueueing.
260
+ if (closed) return;
261
+ try {
262
+ controller.enqueue(encoder.encode(`: heartbeat\n\n`));
263
+ if (!deps.leases.renew(leaseId)) {
264
+ // Lease is gone (released or expired). The notification path
265
+ // normally fires first, but converge on the shared cleanup so
266
+ // the interval and idle reason are always released.
267
+ releaseStreamOnly();
268
+ return;
269
+ }
270
+ send({ type: "stream:heartbeat", streamId, at: new Date().toISOString() });
271
+ } catch {
272
+ // stream torn down between check and send
273
+ releaseStreamOnly();
274
+ }
275
+ }, deps.heartbeatIntervalMs ?? STREAM_HEARTBEAT_INTERVAL_MS);
276
+ if (typeof heartbeat.unref === "function") heartbeat.unref();
277
+ },
278
+ cancel() {
279
+ // Client-initiated disconnect: tear down the stream and the
280
+ // idle reason, but leave the lease alone. The lease has its own
281
+ // per-lease TTL on the lease manager and is the authoritative
282
+ // credential the shell persists to sessionStorage. Releasing it
283
+ // here would defeat the F5 refresh path: the cancel fires when
284
+ // the navigation tears down the SSE stream, the new shell would
285
+ // retry the lease it just released, and the typed "session
286
+ // expired" state would mask a perfectly valid credential.
287
+ // `stream:${streamId}` is the per-stream idle reason; the lease
288
+ // remains governed by GalleryLeaseManager.schedule.
289
+ releaseStreamOnly();
290
+ },
291
+ });
292
+
293
+ return new Response(readable, {
294
+ status: 200,
295
+ headers: {
296
+ "content-type": "text/event-stream",
297
+ "cache-control": NO_STORE,
298
+ "x-accel-buffering": "no",
299
+ },
300
+ });
301
+ }
@@ -0,0 +1,45 @@
1
+ import type {
2
+ InsecureLevel,
3
+ InsecureMarker,
4
+ TsxExecutionMode,
5
+ } from "../shared/contracts/validation";
6
+
7
+ export const defaultInsecureReason = (level: InsecureLevel): string =>
8
+ `manual insecure level ${level}`;
9
+
10
+ export function insecureMarker(
11
+ level: InsecureLevel,
12
+ reason: string | null | undefined,
13
+ ): InsecureMarker | undefined {
14
+ if (level === 0) return undefined;
15
+ return { level, reason: reason ?? defaultInsecureReason(level) };
16
+ }
17
+
18
+ /**
19
+ * Bind the verdict to its (artifactId, revisionSha) pair and attach
20
+ * the insecure marker and the TSX execution marker when set.
21
+ * The execution marker is conditional spread so non-TSX verdicts
22
+ * stay BYTE-IDENTICAL to the pre-arc wire shape — the field is
23
+ * absent, not null. The Tier 0/1 result schema extends VerdictSchema,
24
+ * so the marker flows through TSX publish and read-back uniformly.
25
+ */
26
+ export function enrichVerdict<T extends object>(
27
+ result: T,
28
+ artifactId: string,
29
+ revisionSha: string,
30
+ insecure?: InsecureMarker,
31
+ execution?: TsxExecutionMode,
32
+ ): T & {
33
+ artifactId: string;
34
+ revisionSha: string;
35
+ insecure?: InsecureMarker;
36
+ execution?: TsxExecutionMode;
37
+ } {
38
+ return {
39
+ ...result,
40
+ ...(insecure !== undefined ? { insecure } : {}),
41
+ ...(execution !== undefined ? { execution } : {}),
42
+ artifactId,
43
+ revisionSha,
44
+ };
45
+ }