@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,121 @@
1
+ import {
2
+ buildGallerySidecar,
3
+ commitGalleryExportState,
4
+ downloadBlob,
5
+ renderFilename,
6
+ sidecarFilename,
7
+ sourceFilename,
8
+ type GalleryExportState,
9
+ } from "./export";
10
+ import { mediaTypeForEvidenceImage } from "../shared/evidence-image";
11
+
12
+ export interface GalleryExportMenuOptions {
13
+ readonly document: Document;
14
+ readonly isExpired: () => boolean;
15
+ }
16
+
17
+ export interface GalleryExportMenuController {
18
+ readonly getState: () => GalleryExportState | null;
19
+ readonly setState: (next: GalleryExportState | null) => void;
20
+ readonly clear: () => void;
21
+ readonly close: () => void;
22
+ readonly sync: () => void;
23
+ }
24
+
25
+ export function disableGalleryExportMenu(document: Document): void {
26
+ for (const id of ["facet-export-source", "facet-export-render", "facet-export-sidecar"]) {
27
+ const item = document.getElementById(id) as HTMLButtonElement | null;
28
+ if (item !== null) item.disabled = true;
29
+ }
30
+ }
31
+
32
+ export function installGalleryExportMenu(
33
+ options: GalleryExportMenuOptions,
34
+ ): GalleryExportMenuController {
35
+ const { document } = options;
36
+ let state: GalleryExportState | null = null;
37
+ const toggle = document.getElementById("facet-export-toggle");
38
+ const menu = document.getElementById("facet-export-menu");
39
+ const source = document.getElementById("facet-export-source") as HTMLButtonElement | null;
40
+ const render = document.getElementById("facet-export-render") as HTMLButtonElement | null;
41
+ const sidecar = document.getElementById("facet-export-sidecar") as HTMLButtonElement | null;
42
+ const close = (): void => {
43
+ if (menu !== null) menu.hidden = true;
44
+ toggle?.setAttribute("aria-expanded", "false");
45
+ };
46
+ const sync = (): void => {
47
+ const terminal = options.isExpired();
48
+ if (source !== null) source.disabled = terminal || state === null;
49
+ if (render !== null) {
50
+ const available = state?.renderBytes != null;
51
+ render.disabled = terminal || !available;
52
+ render.title = available ? "" : "no stored render";
53
+ }
54
+ if (sidecar !== null) {
55
+ const available = state?.verdict != null;
56
+ sidecar.disabled = terminal || !available;
57
+ sidecar.title = available ? "" : "no stored verdict";
58
+ }
59
+ };
60
+ toggle?.addEventListener("click", () => {
61
+ if (options.isExpired() || menu === null) return;
62
+ menu.hidden = !menu.hidden;
63
+ toggle.setAttribute("aria-expanded", menu.hidden ? "false" : "true");
64
+ });
65
+ document.addEventListener("keydown", (event) => {
66
+ if (event.key !== "Escape" || menu === null || menu.hidden) return;
67
+ menu.hidden = true;
68
+ toggle?.setAttribute("aria-expanded", "false");
69
+ });
70
+ document.addEventListener("click", (event) => {
71
+ if (menu === null || menu.hidden) return;
72
+ const target = event.target as Node | null;
73
+ const wrapper = document.getElementById("facet-export");
74
+ if (wrapper !== null && target !== null && wrapper.contains(target)) return;
75
+ menu.hidden = true;
76
+ toggle?.setAttribute("aria-expanded", "false");
77
+ });
78
+ source?.addEventListener("click", () => {
79
+ if (state === null) return;
80
+ downloadBlob(document, sourceFilename(state), state.sourceBytes, "application/octet-stream");
81
+ });
82
+ render?.addEventListener("click", () => {
83
+ if (state?.renderBytes == null) return;
84
+ downloadBlob(
85
+ document,
86
+ renderFilename(state),
87
+ state.renderBytes,
88
+ mediaTypeForEvidenceImage(state.renderFormat ?? "png"),
89
+ );
90
+ });
91
+ sidecar?.addEventListener("click", () => {
92
+ if (state === null) return;
93
+ const value = buildGallerySidecar(state, new Date().toISOString());
94
+ if (value === null) return;
95
+ downloadBlob(
96
+ document,
97
+ sidecarFilename(state),
98
+ `${JSON.stringify(value, null, 2)}\n`,
99
+ "application/json",
100
+ );
101
+ });
102
+ sync();
103
+ return {
104
+ getState: () => state,
105
+ setState: (next) => {
106
+ if (next === null) {
107
+ state = null;
108
+ sync();
109
+ return;
110
+ }
111
+ state = commitGalleryExportState(state, next, { expired: options.isExpired() });
112
+ sync();
113
+ },
114
+ clear: () => {
115
+ state = null;
116
+ sync();
117
+ },
118
+ close,
119
+ sync,
120
+ };
121
+ }
@@ -0,0 +1,112 @@
1
+ import {
2
+ buildExportSidecar,
3
+ extensionForExport,
4
+ sidecarFilenameForArtifact,
5
+ } from "../shared/export";
6
+ import type { EvidenceImageFormat } from "../shared/evidence-image";
7
+ import type { ArtifactType } from "../shared/contracts/artifact-types";
8
+ import type { Renderer } from "../shared/contracts/artifact";
9
+ import type { ExportSidecar } from "../shared/contracts/commands/results";
10
+ import type { Verdict } from "../shared/contracts/validation";
11
+
12
+ export interface GalleryExportState {
13
+ readonly artifactId: string;
14
+ readonly revisionSha: string;
15
+ readonly slug: string;
16
+ readonly title: string;
17
+ readonly artifactType: ArtifactType;
18
+ readonly renderer: Renderer;
19
+ readonly sourceBytes: Uint8Array;
20
+ readonly verdict: Verdict | null;
21
+ readonly renderBytes: Uint8Array | null;
22
+ readonly renderFormat: EvidenceImageFormat | null;
23
+ }
24
+
25
+ export interface DownloadBlobUrlRuntime {
26
+ readonly createObjectURL: (blob: Blob) => string;
27
+ readonly revokeObjectURL: (url: string) => void;
28
+ }
29
+
30
+ const browserDownloadBlobUrlRuntime: DownloadBlobUrlRuntime = {
31
+ createObjectURL: (blob) => URL.createObjectURL(blob),
32
+ revokeObjectURL: (url) => URL.revokeObjectURL(url),
33
+ };
34
+
35
+ let downloadBlobUrlRuntime = browserDownloadBlobUrlRuntime;
36
+
37
+ /** Test-only injection keeps blob URL assertions independent of global URL state. */
38
+ export function setDownloadBlobUrlForTests(runtime: DownloadBlobUrlRuntime | undefined): void {
39
+ downloadBlobUrlRuntime = runtime ?? browserDownloadBlobUrlRuntime;
40
+ }
41
+
42
+ export function commitGalleryExportState(
43
+ current: GalleryExportState | null,
44
+ next: GalleryExportState,
45
+ options: { readonly expired?: boolean; readonly swapSucceeded?: boolean } = {},
46
+ ): GalleryExportState | null {
47
+ if (options.expired === true || options.swapSucceeded === false) return current;
48
+ return next;
49
+ }
50
+
51
+ export function exportMenuStateFrom(
52
+ revision: Pick<
53
+ GalleryExportState,
54
+ | "artifactId"
55
+ | "revisionSha"
56
+ | "slug"
57
+ | "title"
58
+ | "artifactType"
59
+ | "renderer"
60
+ | "sourceBytes"
61
+ | "verdict"
62
+ >,
63
+ renderBytes: Uint8Array | null,
64
+ renderFormat: EvidenceImageFormat | null,
65
+ ): GalleryExportState {
66
+ return { ...revision, renderBytes, renderFormat };
67
+ }
68
+
69
+ export function sourceFilename(state: Pick<GalleryExportState, "slug" | "artifactType">): string {
70
+ return `${state.slug}${extensionForExport("source", state.artifactType)}`;
71
+ }
72
+
73
+ export function renderFilename(
74
+ state: Pick<GalleryExportState, "slug" | "artifactType" | "renderFormat">,
75
+ ): string {
76
+ return `${state.slug}${extensionForExport("render", state.artifactType, state.renderFormat ?? "png")}`;
77
+ }
78
+
79
+ export function sidecarFilename(state: Pick<GalleryExportState, "slug" | "artifactType">): string {
80
+ return sidecarFilenameForArtifact(sourceFilename(state));
81
+ }
82
+
83
+ export function buildGallerySidecar(
84
+ state: GalleryExportState,
85
+ exportedAt: string,
86
+ ): ExportSidecar | null {
87
+ if (state.verdict === null) return null;
88
+ return buildExportSidecar({
89
+ artifactId: state.artifactId,
90
+ slug: state.slug,
91
+ revisionSha: state.revisionSha,
92
+ artifactType: state.artifactType,
93
+ renderer: state.renderer,
94
+ verdict: state.verdict,
95
+ format: "source",
96
+ exportedAt,
97
+ });
98
+ }
99
+
100
+ export function downloadBlob(
101
+ document: Document,
102
+ filename: string,
103
+ bytes: BlobPart,
104
+ type: string,
105
+ ): void {
106
+ const url = downloadBlobUrlRuntime.createObjectURL(new Blob([bytes], { type }));
107
+ const anchor = document.createElement("a");
108
+ anchor.href = url;
109
+ anchor.download = filename;
110
+ anchor.click();
111
+ downloadBlobUrlRuntime.revokeObjectURL(url);
112
+ }
@@ -0,0 +1,54 @@
1
+ import type { RenderStatus } from "../shared/contracts/validation";
2
+ import { RenderStatusSchema } from "../shared/contracts/validation";
3
+
4
+ export type FaviconTint = "teal" | "amber" | "red" | "amber-red" | "grey";
5
+
6
+ export const FAVICON_TINT_BY_STATUS = {
7
+ ok: "teal",
8
+ error: "red",
9
+ "partial:layout_unverified": "amber",
10
+ "partial:opaque_content": "amber",
11
+ "partial:external_resources": "amber",
12
+ "partial:unstable": "amber",
13
+ tampered: "red",
14
+ timeout: "red",
15
+ shim_only: "grey",
16
+ probe_only: "grey",
17
+ "insecure:unvalidated": "amber-red",
18
+ } as const satisfies Record<RenderStatus, FaviconTint>;
19
+
20
+ const FAVICON_COLOR_BY_TINT: Record<FaviconTint, string> = {
21
+ teal: "#86e1fc",
22
+ amber: "#ffc777",
23
+ red: "#ff6e6e",
24
+ "amber-red": "#ff9b6e",
25
+ grey: "#77809a",
26
+ };
27
+
28
+ export function faviconTint(state: RenderStatus | "idle" | "expired" | "unverified"): FaviconTint {
29
+ if (!RenderStatusSchema.options.includes(state as RenderStatus)) return "grey";
30
+ return FAVICON_TINT_BY_STATUS[state as RenderStatus];
31
+ }
32
+
33
+ export function renderFavicon(
34
+ tint: FaviconTint,
35
+ documentOverride?: Pick<Document, "createElement">,
36
+ ): string | null {
37
+ try {
38
+ const canvasDocument = documentOverride ?? (typeof document === "undefined" ? null : document);
39
+ if (canvasDocument === null) return null;
40
+ const canvas = canvasDocument.createElement("canvas");
41
+ canvas.width = 32;
42
+ canvas.height = 32;
43
+ const context = canvas.getContext("2d");
44
+ if (context === null) return null;
45
+ context.fillStyle = FAVICON_COLOR_BY_TINT[tint];
46
+ context.font = "24px sans-serif";
47
+ context.textAlign = "center";
48
+ context.textBaseline = "middle";
49
+ context.fillText("◆", 16, 16);
50
+ return canvas.toDataURL("image/png");
51
+ } catch {
52
+ return null;
53
+ }
54
+ }
@@ -0,0 +1,6 @@
1
+ import { installGalleryFrameApi } from "../runtime";
2
+ import { renderChart } from "../renderers/chart";
3
+ import { createRendererRegistry } from "../renderers/registry";
4
+
5
+ const registry = createRendererRegistry([["chart", renderChart]]);
6
+ installGalleryFrameApi(registry);
@@ -0,0 +1,6 @@
1
+ import { installGalleryFrameApi } from "../runtime";
2
+ import { renderHtml } from "../renderers/html";
3
+ import { createRendererRegistry } from "../renderers/registry";
4
+
5
+ const registry = createRendererRegistry([["html", renderHtml]]);
6
+ installGalleryFrameApi(registry);
@@ -0,0 +1,6 @@
1
+ import { installGalleryFrameApi } from "../runtime";
2
+ import { renderMarkdown } from "../renderers/markdown";
3
+ import { createRendererRegistry } from "../renderers/registry";
4
+
5
+ const registry = createRendererRegistry([["markdown", renderMarkdown]]);
6
+ installGalleryFrameApi(registry);
@@ -0,0 +1,6 @@
1
+ import { installGalleryFrameApi } from "../runtime";
2
+ import { renderMermaidDocument } from "../renderers/mermaid";
3
+ import { createRendererRegistry } from "../renderers/registry";
4
+
5
+ const registry = createRendererRegistry([["mermaid", renderMermaidDocument]]);
6
+ installGalleryFrameApi(registry);
@@ -0,0 +1,6 @@
1
+ import { installGalleryFrameApi } from "../runtime";
2
+ import { createRendererRegistry } from "../renderers/registry";
3
+ import { renderSvgDocument } from "../renderers/svg";
4
+
5
+ const registry = createRendererRegistry([["svg", renderSvgDocument]]);
6
+ installGalleryFrameApi(registry);
@@ -0,0 +1,6 @@
1
+ import { installGalleryFrameApi } from "../runtime";
2
+ import { renderTsx } from "../renderers/tsx";
3
+ import { createRendererRegistry } from "../renderers/registry";
4
+
5
+ const registry = createRendererRegistry([["tsx", renderTsx]]);
6
+ installGalleryFrameApi(registry);
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Payload + view-state helpers shared by the frame's two coexisting
3
+ * entry paths (bootstrap channel + direct API). One copy instead of
4
+ * two — divergence while both paths are installed is the drift this
5
+ * module exists to prevent.
6
+ */
7
+
8
+ import type { TsxExecutionMode } from "../../shared/tsx/execution";
9
+ import type { ResolvedGalleryTheme } from "../theme";
10
+ import type { SvgViewBox } from "./view-box";
11
+
12
+ /**
13
+ * Payload the shell sends to the frame's direct render API. Bytes are
14
+ * always Uint8Array by the time this crosses that boundary — the
15
+ * service delivers bytes, so a string payload here is a bug to
16
+ * surface, not a form to accept. One definition; both the frame
17
+ * runtime and the shell import it instead of re-declaring.
18
+ */
19
+ export interface FrameRenderPayload {
20
+ readonly artifactType: string;
21
+ readonly renderer: string;
22
+ readonly bytes: Uint8Array;
23
+ readonly theme: ResolvedGalleryTheme;
24
+ readonly execution?: TsxExecutionMode;
25
+ }
26
+
27
+ export interface FrameViewState {
28
+ readonly zoom: number;
29
+ readonly panX: number;
30
+ readonly panY: number;
31
+ }
32
+
33
+ /**
34
+ * Whether the frame's wheel/drag gesture listeners are live. `native`
35
+ * leaves wheel/pointer events untouched (document scroll, text
36
+ * selection); `panzoom` installs wheel-zoom-at-cursor and drag-pan and
37
+ * suppresses the rendered root's own pointer events so a drag can't
38
+ * also select text or click through. Standalone diagram artifacts
39
+ * (mermaid/svg/chart) default to `panzoom`; documents default to
40
+ * `native` with a shell toolbar toggle — see `installGalleryFrameApi`.
41
+ */
42
+ export const GESTURE_MODES = ["native", "panzoom"] as const;
43
+ export type GestureMode = (typeof GESTURE_MODES)[number];
44
+
45
+ export function isGestureMode(value: unknown): value is GestureMode {
46
+ return typeof value === "string" && (GESTURE_MODES as readonly string[]).includes(value);
47
+ }
48
+
49
+ /** Cross-realm-safe — frame payloads arrive via postMessage from other realms. */
50
+ export function isUint8Array(value: unknown): value is Uint8Array {
51
+ return Object.prototype.toString.call(value) === "[object Uint8Array]";
52
+ }
53
+
54
+ export function decodePayloadBytes(bytes: Uint8Array | string): Uint8Array {
55
+ if (typeof bytes !== "string") return new Uint8Array(bytes);
56
+ // Base64 form (used by hosts that must embed bytes as text).
57
+ const binary = atob(bytes);
58
+ const out = new Uint8Array(binary.length);
59
+ for (let i = 0; i < binary.length; i += 1) out[i] = binary.charCodeAt(i);
60
+ return out;
61
+ }
62
+
63
+ export function parseViewBox(value: string | null): SvgViewBox | null {
64
+ if (value === null) return null;
65
+ const values = value
66
+ .trim()
67
+ .split(/[\s,]+/)
68
+ .map(Number);
69
+ if (
70
+ values.length !== 4 ||
71
+ values.some((entry) => !Number.isFinite(entry)) ||
72
+ values[2]! <= 0 ||
73
+ values[3]! <= 0
74
+ )
75
+ return null;
76
+ return { minX: values[0]!, minY: values[1]!, width: values[2]!, height: values[3]! };
77
+ }
78
+
79
+ export function isFrameViewState(value: unknown): value is FrameViewState {
80
+ if (value === null || typeof value !== "object") return false;
81
+ const state = value as Record<string, unknown>;
82
+ return (
83
+ typeof state.zoom === "number" &&
84
+ Number.isFinite(state.zoom) &&
85
+ state.zoom > 0 &&
86
+ typeof state.panX === "number" &&
87
+ Number.isFinite(state.panX) &&
88
+ typeof state.panY === "number" &&
89
+ Number.isFinite(state.panY)
90
+ );
91
+ }
@@ -0,0 +1,13 @@
1
+ import { isRenderer, type Renderer } from "../../shared/contracts/renderers";
2
+
3
+ import { FacetRenderError } from "./renderers/registry";
4
+
5
+ export function validateRenderer(value: unknown): Renderer {
6
+ if (!isRenderer(value)) {
7
+ throw new FacetRenderError(
8
+ "artifact payload is missing a supported renderer",
9
+ "invalid_request",
10
+ );
11
+ }
12
+ return value;
13
+ }
@@ -0,0 +1,182 @@
1
+ /**
2
+ * Chart renderer — validated Vega-Lite spec → SVG via the vendored
3
+ * Vega runtime in SVG mode.
4
+ *
5
+ * The data loader is DISABLED: inline/named data only. Every loader
6
+ * entry point (`load`, `http`, `file`) rejects, so a spec carrying
7
+ * `data.url` fails closed with a facet-error instead of fetching —
8
+ * the frame CSP (`connect-src 'none'`) is the backstop.
9
+ *
10
+ * A spec that renders ZERO marks is an error, not an ok render: an
11
+ * empty SVG root would tell the verifier "one renderer root" while
12
+ * showing the operator nothing.
13
+ */
14
+
15
+ import { compile } from "vega-lite";
16
+ import { View, loader, parse, type Loader } from "vega";
17
+ import { expressionInterpreter } from "vega-interpreter";
18
+
19
+ import type { Renderer } from "../../../shared/contracts/renderers";
20
+ import type { ResolvedGalleryTheme } from "../../theme";
21
+ import { FacetRenderError, type RenderContext, decodeArtifactBytes } from "./registry";
22
+ import { importSanitizedSvgText } from "./svg";
23
+
24
+ /** Every fetch path on the blocked loader rejects with the same typed error. */
25
+ const denyBlockedFetch = (): Promise<never> =>
26
+ Promise.reject(
27
+ new FacetRenderError("external chart data is not permitted", "chart_external_data"),
28
+ );
29
+
30
+ /** A loader whose every fetch path rejects — inline data only. */
31
+ export function createBlockedLoader(): Loader {
32
+ const blocked = loader();
33
+ blocked.load = denyBlockedFetch;
34
+ blocked.http = denyBlockedFetch;
35
+ blocked.file = denyBlockedFetch;
36
+ return blocked;
37
+ }
38
+
39
+ /**
40
+ * Count vega data-mark groups in rendered SVG text. Vega wraps each
41
+ * DATA mark set in a group carrying `role-mark` (axes/legends are
42
+ * `role-axis`/`role-legend` and do NOT count). Vega emits the group
43
+ * even when it holds zero items, so the zero-marks gate counts the
44
+ * tuples in the view's data sets instead (`countDataTuples`).
45
+ */
46
+ export function countVegaMarks(svgText: string): number {
47
+ return (svgText.match(/class="[^"]*\bmark-[a-z]+[^"]*role-mark/g) ?? []).length;
48
+ }
49
+
50
+ /**
51
+ * Total tuples across the compiled spec's named data sets — the mark
52
+ * items the renderer will actually draw. A spec whose pipeline yields
53
+ * zero tuples renders axes-only: an empty chart the gate must reject.
54
+ * Reads through the public `view.data(name)` surface; unknown sets
55
+ * count as zero.
56
+ */
57
+ export function countDataTuples(view: View, compiledSpec: unknown): number {
58
+ const data =
59
+ typeof compiledSpec === "object" && compiledSpec !== null
60
+ ? (compiledSpec as { data?: unknown }).data
61
+ : undefined;
62
+ if (!Array.isArray(data)) return 0;
63
+ let total = 0;
64
+ for (const entry of data) {
65
+ const name =
66
+ typeof entry === "object" && entry !== null ? (entry as { name?: unknown }).name : undefined;
67
+ if (typeof name !== "string") continue;
68
+ try {
69
+ total += view.data(name).length;
70
+ } catch {
71
+ // unknown data set — counts as zero tuples
72
+ }
73
+ }
74
+ return total;
75
+ }
76
+
77
+ /**
78
+ * Vega's loader swallows fetch failures and silently substitutes its
79
+ * built-in sample data — a spec with `data.url` would render a WRONG
80
+ * chart that looks like success. Reject url-bearing compiled specs
81
+ * up front: inline/named/generated data only.
82
+ */
83
+ export function compiledSpecHasExternalUrl(spec: unknown): boolean {
84
+ if (typeof spec !== "object" || spec === null) return false;
85
+ const data = (spec as { data?: unknown }).data;
86
+ if (!Array.isArray(data)) return false;
87
+ return data.some(
88
+ (entry) =>
89
+ typeof entry === "object" &&
90
+ entry !== null &&
91
+ typeof (entry as { url?: unknown }).url === "string",
92
+ );
93
+ }
94
+
95
+ const FACET_CHART_CONFIG: Readonly<
96
+ Record<ResolvedGalleryTheme, Readonly<Record<string, unknown>>>
97
+ > = {
98
+ dark: {
99
+ background: "#151823",
100
+ style: {
101
+ "guide-label": { fill: "#c8d3f5" },
102
+ "guide-title": { fill: "#c8d3f5" },
103
+ },
104
+ },
105
+ light: {
106
+ background: "#ffffff",
107
+ style: {
108
+ "guide-label": { fill: "#172033" },
109
+ "guide-title": { fill: "#172033" },
110
+ },
111
+ },
112
+ };
113
+
114
+ export function withFacetChartTheme(spec: unknown, theme: ResolvedGalleryTheme): unknown {
115
+ if (spec === null || typeof spec !== "object" || Array.isArray(spec)) return spec;
116
+ const source = spec as Readonly<Record<string, unknown>>;
117
+ const authoredConfig = source.config;
118
+ return {
119
+ ...source,
120
+ config: {
121
+ ...FACET_CHART_CONFIG[theme],
122
+ ...(authoredConfig !== null &&
123
+ typeof authoredConfig === "object" &&
124
+ !Array.isArray(authoredConfig)
125
+ ? authoredConfig
126
+ : {}),
127
+ },
128
+ };
129
+ }
130
+
131
+ /** Render a chart artifact (artifactType "chart"): Vega-Lite JSON bytes. */
132
+ export async function renderChart(
133
+ ctx: RenderContext,
134
+ bytes: Uint8Array,
135
+ renderer: Renderer = "svg",
136
+ ): Promise<void> {
137
+ const text = decodeArtifactBytes(bytes);
138
+ let spec: unknown;
139
+ try {
140
+ spec = JSON.parse(text);
141
+ } catch (error) {
142
+ const message = error instanceof Error ? error.message : String(error);
143
+ throw new FacetRenderError(`chart spec is not valid JSON: ${message}`, "chart_invalid_json");
144
+ }
145
+ let compiled: { spec: unknown };
146
+ try {
147
+ compiled = compile(withFacetChartTheme(spec, ctx.theme) as never);
148
+ } catch (error) {
149
+ const message = error instanceof Error ? error.message : String(error);
150
+ throw new FacetRenderError(`vega-lite compile failed: ${message}`, "chart_compile_error");
151
+ }
152
+ if (compiledSpecHasExternalUrl(compiled.spec)) {
153
+ throw new FacetRenderError("external chart data is not permitted", "chart_external_data");
154
+ }
155
+ // `ast: true` + the expression INTERPRETER is Vega's CSP-safe path.
156
+ // The default compiles expressions with `Function`/eval, which the
157
+ // frame's nonce-only CSP blocks outright — every chart rendered as an
158
+ // "unsafe-eval" error instead of a plot.
159
+ const runtime = parse(compiled.spec as never, {}, { ast: true });
160
+ const view = new View(runtime, {
161
+ loader: createBlockedLoader(),
162
+ renderer: "none",
163
+ expr: expressionInterpreter,
164
+ });
165
+ try {
166
+ if (renderer === "canvas") view.renderer("canvas").initialize(ctx.container);
167
+ await view.runAsync();
168
+ if (countDataTuples(view, compiled.spec) === 0) {
169
+ throw new FacetRenderError("chart spec renders zero marks", "chart_zero_marks");
170
+ }
171
+ if (renderer === "svg") {
172
+ const svgText = await view.toSVG();
173
+ await importSanitizedSvgText(ctx.container, svgText);
174
+ }
175
+ } catch (error) {
176
+ if (error instanceof FacetRenderError) throw error;
177
+ const message = error instanceof Error ? error.message : String(error);
178
+ throw new FacetRenderError(`vega render failed: ${message}`, "chart_render_error");
179
+ } finally {
180
+ view.finalize();
181
+ }
182
+ }