@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,112 @@
1
+ /**
2
+ * DOMPurify shim for bundled frame contexts.
3
+ *
4
+ * DOMPurify captures `document` methods at instance-construction time
5
+ * (`createNodeIterator`, `createTreeWalker`, `getElementsByTagName`,
6
+ * `importNode`, `createDocumentFragment`) and calls them with `this`
7
+ * detached from the document. Standalone this survives; inside the
8
+ * mermaid/d3/katex frame bundle the detached calls surface as
9
+ * `TypeError: Illegal invocation` on the first HTML-label sanitize
10
+ * (plain-text labels skip the NodeIterator path).
11
+ *
12
+ * Fix: DOMPurify's factory form builds a fresh instance against an
13
+ * explicit window, and the frame's document carries the captured
14
+ * methods as OWN properties pre-bound to the real document — the own
15
+ * property wins over the prototype at capture time, so every detached
16
+ * `.call(...)` lands on the right `this`. DOMPurify also reassigns its
17
+ * internal document to `template.content.ownerDocument` (a DIFFERENT
18
+ * Document object) when HTMLTemplateElement exists, so that document
19
+ * gets the bound methods too. The import-time default instance is
20
+ * never trusted in the verifier's inlined harness bundle.
21
+ */
22
+
23
+ import purifyFactory from "dompurify";
24
+
25
+ type PurifyInstance = typeof purifyFactory & { isSupported?: boolean };
26
+
27
+ /** Document methods DOMPurify captures and calls unbound. */
28
+ const DOCUMENT_METHODS = [
29
+ "createNodeIterator",
30
+ "createTreeWalker",
31
+ "getElementsByTagName",
32
+ "getElementsByTagNameNS",
33
+ "importNode",
34
+ "createDocumentFragment",
35
+ ] as const;
36
+
37
+ /**
38
+ * Shadow DOMPurify's captured methods with pre-bound OWN properties on
39
+ * the given document. Behavior is identical for all other callers — a
40
+ * bound copy of the same method.
41
+ */
42
+ function bindDocumentMethods(realDocument: Document): void {
43
+ for (const name of DOCUMENT_METHODS) {
44
+ const existing = Object.getOwnPropertyDescriptor(realDocument, name);
45
+ if (existing !== undefined) continue;
46
+ const method = (realDocument as unknown as Record<string, unknown>)[name];
47
+ if (typeof method === "function") {
48
+ const fn = method as (...args: unknown[]) => unknown;
49
+ Object.defineProperty(realDocument, name, {
50
+ // `this`-AWARE, never hard-bound. DOMPurify parses input into a
51
+ // FRESH document and invokes these against it; a copy bound to
52
+ // `realDocument` would query the wrong document, find nothing,
53
+ // and silently return empty output (every mermaid label came
54
+ // back blank). Honour the call-time receiver and fall back to
55
+ // the real document only for the detached calls that produced
56
+ // `TypeError: Illegal invocation` in the bundled frame.
57
+ value: function shadowed(this: unknown, ...args: unknown[]): unknown {
58
+ const receiver =
59
+ this === undefined || this === null || this === globalThis ? realDocument : this;
60
+ return fn.apply(receiver, args);
61
+ },
62
+ configurable: true,
63
+ writable: true,
64
+ });
65
+ }
66
+ }
67
+ }
68
+
69
+ function ensureBoundDocumentMethods(realWindow: Window & typeof globalThis): boolean {
70
+ const realDocument = realWindow.document;
71
+ if (realDocument === undefined) return false;
72
+ bindDocumentMethods(realDocument);
73
+ // DOMPurify REASSIGNS its internal document to
74
+ // `template.content.ownerDocument` when HTMLTemplateElement exists —
75
+ // a DIFFERENT Document object (the template's inert document). That
76
+ // one must carry the bound methods too, or the HTML-label path
77
+ // still hits the unbound capture.
78
+ try {
79
+ const probeTemplate = realDocument.createElement("template");
80
+ const contentDoc = probeTemplate.content?.ownerDocument;
81
+ if (contentDoc !== undefined && contentDoc !== null && contentDoc !== realDocument) {
82
+ bindDocumentMethods(contentDoc);
83
+ }
84
+ } catch {
85
+ // template probing is best-effort; the main document patch stands
86
+ }
87
+ return true;
88
+ }
89
+
90
+ let bound: PurifyInstance | null = null;
91
+
92
+ function resolveBound(): PurifyInstance {
93
+ if (bound !== null) return bound;
94
+ const w = globalThis.window as (Window & typeof globalThis) | undefined;
95
+ if (w !== undefined) ensureBoundDocumentMethods(w);
96
+ const candidate = purifyFactory(w) as PurifyInstance;
97
+ // A supported instance carries `sanitize`; an unsupported factory
98
+ // shell does not — fall back to the import-time default rather than
99
+ // break the render path.
100
+ bound = typeof candidate.sanitize === "function" ? candidate : purifyFactory;
101
+ return bound;
102
+ }
103
+
104
+ const shim = new Proxy({} as PurifyInstance, {
105
+ get(_target, prop, receiver) {
106
+ const instance = resolveBound();
107
+ const value = Reflect.get(instance, prop, receiver);
108
+ return typeof value === "function" ? value.bind(instance) : value;
109
+ },
110
+ });
111
+
112
+ export default shim;
@@ -0,0 +1,48 @@
1
+ import { decodeArtifactBytes, type RenderContext } from "./registry";
2
+ import {
3
+ isAllowedHtmlUrl,
4
+ isHtmlDeniedElement,
5
+ isHtmlEventHandlerAttribute,
6
+ isHtmlInlineStyleAttribute,
7
+ isHtmlUrlAttributeName,
8
+ isHtmlUrlBearingAttribute,
9
+ } from "../../../shared/html/policy";
10
+
11
+ function shouldKeepAttribute(elementName: string, name: string, value: string): boolean {
12
+ if (isHtmlEventHandlerAttribute(name) || isHtmlInlineStyleAttribute(name)) return false;
13
+ if (isHtmlUrlBearingAttribute(elementName, name))
14
+ return isAllowedHtmlUrl(elementName, name, value);
15
+ return !isHtmlUrlAttributeName(name);
16
+ }
17
+
18
+ function sanitizeParsedHtml(root: HTMLElement): void {
19
+ for (const element of Array.from(root.querySelectorAll("*"))) {
20
+ const tagName = element.localName.toLowerCase();
21
+ if (isHtmlDeniedElement(tagName)) {
22
+ element.remove();
23
+ continue;
24
+ }
25
+ for (const attribute of Array.from(element.attributes)) {
26
+ if (!shouldKeepAttribute(tagName, attribute.name.toLowerCase(), attribute.value))
27
+ element.removeAttribute(attribute.name);
28
+ }
29
+ }
30
+ }
31
+
32
+ export function createHtmlRendererRoot(ownerDocument: Document): HTMLElement {
33
+ const root = ownerDocument.createElement("div");
34
+ root.setAttribute("data-facet-renderer-root", "true");
35
+ root.className = "facet-html-root";
36
+ return root;
37
+ }
38
+
39
+ export async function renderHtml(ctx: RenderContext, bytes: Uint8Array): Promise<void> {
40
+ const ownerDocument = ctx.container.ownerDocument;
41
+ const root = createHtmlRendererRoot(ownerDocument);
42
+ const parsed = new DOMParser().parseFromString(decodeArtifactBytes(bytes), "text/html");
43
+ const parsedBody = parsed.body;
44
+ sanitizeParsedHtml(parsedBody);
45
+ for (const child of Array.from(parsedBody.childNodes))
46
+ root.appendChild(ownerDocument.adoptNode(child));
47
+ ctx.container.replaceChildren(root);
48
+ }
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Markdown renderer — Marked with raw HTML treated as DATA.
3
+ *
4
+ * Marked passes raw HTML tokens through to the output BY DEFAULT. The
5
+ * renderer override escapes them at TOKEN level — before any HTML is
6
+ * attached to the DOM — so raw `<script>`, `<img onerror=…>`, inline
7
+ * handlers, and iframes remain VISIBLE TEXT. Sanitize-after-insert is
8
+ * too late (the parser already ran); the frame CSP is the backstop.
9
+ *
10
+ * Fenced ```mermaid blocks are delegated to the real mermaid renderer
11
+ * and their `<pre>` is replaced by the sanitized SVG; the markdown
12
+ * render only resolves after every embedded diagram promise settles.
13
+ */
14
+
15
+ import { Marked } from "marked";
16
+
17
+ import { type RenderContext, decodeArtifactBytes } from "./registry";
18
+
19
+ const ESCAPES: ReadonlyMap<string, string> = new Map([
20
+ ["&", "&amp;"],
21
+ ["<", "&lt;"],
22
+ [">", "&gt;"],
23
+ ['"', "&quot;"],
24
+ ["'", "&#39;"],
25
+ ]);
26
+
27
+ export function escapeHtml(text: string): string {
28
+ return text.replace(/[&<>"']/g, (char) => ESCAPES.get(char) ?? char);
29
+ }
30
+
31
+ /**
32
+ * Instance-scoped Marked: the html-token override escapes raw HTML
33
+ * (block AND inline) so no raw-HTML token can ever create an element.
34
+ */
35
+ const marked = new Marked({
36
+ gfm: true,
37
+ renderer: {
38
+ html(token) {
39
+ return escapeHtml(token.text);
40
+ },
41
+ },
42
+ });
43
+
44
+ /**
45
+ * Pure stage: markdown text → HTML string with raw-HTML tokens
46
+ * escaped at TOKEN level. No DOM — testable without a browser.
47
+ */
48
+ export async function markdownToSanitizedHtml(text: string): Promise<string> {
49
+ return await Promise.resolve(marked.parse(text));
50
+ }
51
+
52
+ /**
53
+ * Render a markdown artifact. The HTML is assembled in a detached
54
+ * `<template>`, every mermaid fence is awaited and replaced by its
55
+ * sanitized SVG, and only then does the fragment cross into the live
56
+ * container — one attach, after all diagram promises settle.
57
+ */
58
+ export async function renderMarkdown(ctx: RenderContext, bytes: Uint8Array): Promise<void> {
59
+ const text = decodeArtifactBytes(bytes);
60
+ const html = await markdownToSanitizedHtml(text);
61
+ const template = document.createElement("template");
62
+ template.innerHTML = html;
63
+ const mermaidBlocks = Array.from(
64
+ template.content.querySelectorAll("pre > code.language-mermaid"),
65
+ );
66
+ // Plain markdown must not pay Mermaid's multi-megabyte load + parse cost.
67
+ const renderMermaidInto =
68
+ mermaidBlocks.length === 0 ? null : (await import("./mermaid")).renderMermaidInto;
69
+ for (const code of mermaidBlocks) {
70
+ const source = code.textContent ?? "";
71
+ const pre = code.parentElement;
72
+ if (pre === null) continue;
73
+ const region = document.createElement("div");
74
+ region.setAttribute("data-facet-diagram-region", "true");
75
+ region.tabIndex = 0;
76
+ await renderMermaidInto!(region, source, ctx.theme);
77
+ if (region.firstElementChild !== null) pre.replaceWith(region);
78
+ else pre.remove();
79
+ }
80
+ ctx.container.appendChild(template.content);
81
+ }
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Mermaid renderer — the REAL mermaid runtime, `securityLevel: "loose"`.
3
+ *
4
+ * Renders the diagram to an SVG string via `mermaid.render()`, awaits
5
+ * completion, then imports the result through the ONE sanitized-SVG
6
+ * import path (`svg.ts`). Mermaid's strict/sandbox levels add a
7
+ * whole-SVG DOMPurify pass on top — redundant here, and broken in the
8
+ * verifier's inlined harness bundle: the shim DOMPurify's SVG-input `_initDocument` path
9
+ * resolves `body` to null and returns `""` for every diagram. "loose"
10
+ * skips that pass (`!isLooseSecurityLevel` guard in mermaid's render),
11
+ * so `importSanitizedSvgText` is the SOLE outer-SVG sanitizer — script,
12
+ * foreignObject, on* handlers, and non-fragment URLs are still stripped
13
+ * there; the frame CSP is the backstop. Per-label `sanitizeText`
14
+ * (DOMPurify via the shim) is unaffected by the security level.
15
+ *
16
+ * Two render-time hazards are handled explicitly:
17
+ * 1. `mermaid.render()` leaves its sandbox container (`d<id>` div)
18
+ * in the document — it is removed after every render so it can
19
+ * never inflate the verifier's SVG census.
20
+ * 2. `suppressErrorRendering` keeps parse failures from injecting
21
+ * mermaid's own error SVG; a failed render surfaces as a
22
+ * facet-error marker instead.
23
+ *
24
+ * `flowchart.htmlLabels` is OFF: labels render as SVG text, so markup
25
+ * inside a label is DATA (visible text), never an HTML subtree — the
26
+ * nested-`<svg>`-in-label forgery probe has no surface to land on.
27
+ */
28
+
29
+ import mermaid from "mermaid";
30
+
31
+ import type { ResolvedGalleryTheme } from "../../theme";
32
+ import { FacetRenderError, type RenderContext, decodeArtifactBytes } from "./registry";
33
+ import { importSanitizedSvgText } from "./svg";
34
+
35
+ let initialized = false;
36
+
37
+ export function mermaidInitializeConfig(theme: ResolvedGalleryTheme) {
38
+ return {
39
+ startOnLoad: false,
40
+ securityLevel: "loose" as const,
41
+ suppressErrorRendering: true,
42
+ theme: theme === "dark" ? ("dark" as const) : ("default" as const),
43
+ darkMode: theme === "dark",
44
+ // htmlLabels OFF at EVERY level that can re-enable it. An HTML label
45
+ // is emitted inside a `<foreignObject>`, which the SVG import path
46
+ // strips as an XSS vector — so an HTML label renders as an EMPTY
47
+ // box. Text labels keep markup as DATA and survive the sanitizer.
48
+ htmlLabels: false,
49
+ flowchart: { htmlLabels: false },
50
+ class: { htmlLabels: false },
51
+ };
52
+ }
53
+
54
+ function ensureMermaidInitialized(theme: ResolvedGalleryTheme): void {
55
+ if (initialized) return;
56
+ initialized = true;
57
+ mermaid.initialize(mermaidInitializeConfig(theme));
58
+ }
59
+
60
+ let renderCounter = 0;
61
+
62
+ /**
63
+ * Render one mermaid diagram source into the container. Awaits
64
+ * `mermaid.render()` completion BEFORE the SVG crosses the sanitized
65
+ * import path — the direct render promise depends on this barrier.
66
+ */
67
+ export async function renderMermaidInto(
68
+ container: HTMLElement,
69
+ source: string,
70
+ theme: ResolvedGalleryTheme,
71
+ ): Promise<void> {
72
+ ensureMermaidInitialized(theme);
73
+ renderCounter += 1;
74
+ const id = `facet-mermaid-${renderCounter}-${crypto.randomUUID()}`;
75
+ let svg: string;
76
+ try {
77
+ const result = await mermaid.render(id, source);
78
+ svg = result.svg;
79
+ } catch (error) {
80
+ const message = error instanceof Error ? error.message : String(error);
81
+ throw new FacetRenderError(`mermaid render failed: ${message}`, "mermaid_render_error");
82
+ } finally {
83
+ // Mermaid parks its sandbox in `d<id>` and does not remove it.
84
+ document.getElementById(`d${id}`)?.remove();
85
+ }
86
+ await importSanitizedSvgText(container, svg, { marker: "graph" });
87
+ restoreNaturalSize(container.lastElementChild);
88
+ }
89
+
90
+ /**
91
+ * Mermaid stamps `width="100%"` + `style="max-width:<natural>px"` on its
92
+ * root, which SHRINKS a wide diagram to the container — a 10:1 flowchart
93
+ * fit-to-width renders as an unreadable strip (live-measured: 3791×377
94
+ * viewBox squeezed to 1248×124, 33% text scale). Readability wins: pin
95
+ * the root at its natural viewBox width and let the scrollable stage
96
+ * (both axes, top-left origin) reach the rest. viewBox-native zoom still
97
+ * applies on top — zoom mutates the viewBox, not these styles.
98
+ */
99
+ function restoreNaturalSize(el: Element | null): void {
100
+ if (el === null || el.tagName.toLowerCase() !== "svg") return;
101
+ const viewBox = el.getAttribute("viewBox");
102
+ if (viewBox === null) return;
103
+ const parts = viewBox
104
+ .trim()
105
+ .split(/[\s,]+/)
106
+ .map(Number);
107
+ const width = parts[2];
108
+ if (parts.length !== 4 || width === undefined || !Number.isFinite(width) || width <= 0) {
109
+ return;
110
+ }
111
+ const svg = el as SVGElement;
112
+ svg.style.width = `${Math.ceil(width)}px`;
113
+ svg.style.maxWidth = "none";
114
+ svg.style.height = "auto";
115
+ }
116
+
117
+ /** Render a standalone mermaid artifact (artifactType "mermaid"). */
118
+ export async function renderMermaidDocument(ctx: RenderContext, bytes: Uint8Array): Promise<void> {
119
+ await renderMermaidInto(ctx.container, decodeArtifactBytes(bytes), ctx.theme);
120
+ }
@@ -0,0 +1,260 @@
1
+ import type { ArtifactType } from "../../../shared/contracts/artifact-types";
2
+ import { isRenderer, type Renderer as RendererKind } from "../../../shared/contracts/renderers";
3
+ import { HTML_STRUCTURAL_GROUPS } from "../../../shared/html/policy";
4
+ import type { VerdictObserved } from "../../../shared/contracts/validation";
5
+ import type { ObservedCountKey } from "../../../shared/contracts/observed-counts";
6
+ import { isTsxExecutionMode, type TsxExecutionMode } from "../../../shared/tsx/execution";
7
+ import type { ResolvedGalleryTheme } from "../../theme";
8
+
9
+ export { ARTIFACT_TYPES, type ArtifactType } from "../../../shared/contracts/artifact-types";
10
+
11
+ /**
12
+ * Frame-side renderer registry — keyed by ArtifactType.
13
+ *
14
+ * Runs INSIDE the artifact frame. Gallery and Tier 1 have paired
15
+ * type-specific entries that instantiate this registry with the SAME
16
+ * renderer modules; the build-metafile parity gate turns red if those
17
+ * module sets diverge. Artifact bytes are DATA: dispatch routes them to
18
+ * the typed renderer and every renderer builds DOM through trusted APIs.
19
+ *
20
+ * No zod in the frame bundle: the wire shape is plain JS here.
21
+ */
22
+
23
+ export interface RenderContext {
24
+ readonly container: HTMLElement;
25
+ readonly theme: ResolvedGalleryTheme;
26
+ readonly nonce?: string;
27
+ }
28
+
29
+ export type Renderer = (
30
+ ctx: RenderContext,
31
+ bytes: Uint8Array,
32
+ renderer: RendererKind,
33
+ execution?: TsxExecutionMode,
34
+ ) => Promise<void>;
35
+
36
+ /**
37
+ * Typed render failure. `code` travels into the facet-error element's
38
+ * text so the verifier's discriminative surface can name the reason.
39
+ */
40
+ export class FacetRenderError extends Error {
41
+ constructor(
42
+ message: string,
43
+ readonly code: string,
44
+ ) {
45
+ super(message);
46
+ this.name = "FacetRenderError";
47
+ }
48
+ }
49
+
50
+ export function decodeArtifactBytes(bytes: Uint8Array): string {
51
+ return new TextDecoder("utf-8", { fatal: false }).decode(bytes);
52
+ }
53
+
54
+ export const RENDER_ERROR_ATTRIBUTE = "data-facet-error";
55
+ export const RENDER_ERROR_ELEMENT = "facet-error";
56
+
57
+ /**
58
+ * Append the renderer-owned error marker. The element carries
59
+ * `data-facet-error` so every observation channel (page shim,
60
+ * protocol probes) counts it the same way.
61
+ */
62
+ function renderErrorMessage(value: unknown): string {
63
+ if (value instanceof Error) return value.message;
64
+ if (typeof value === "string") return value;
65
+ if (typeof value === "object" && value !== null) {
66
+ const event = value as {
67
+ readonly error?: unknown;
68
+ readonly message?: unknown;
69
+ readonly reason?: unknown;
70
+ };
71
+ for (const candidate of [event.reason, event.error, event.message]) {
72
+ if (candidate instanceof Error) return candidate.message;
73
+ if (typeof candidate === "string") return candidate;
74
+ if (
75
+ typeof candidate === "object" &&
76
+ candidate !== null &&
77
+ "message" in candidate &&
78
+ typeof candidate.message === "string"
79
+ ) {
80
+ return candidate.message;
81
+ }
82
+ }
83
+ }
84
+ return "interactive TSX runtime error";
85
+ }
86
+
87
+ export function appendRenderError(container: HTMLElement, value: unknown): void {
88
+ const el = container.ownerDocument.createElement(RENDER_ERROR_ELEMENT);
89
+ el.setAttribute(RENDER_ERROR_ATTRIBUTE, "true");
90
+ el.textContent = renderErrorMessage(value);
91
+ container.appendChild(el);
92
+ }
93
+
94
+ export type PageShimCounts = Pick<VerdictObserved, ObservedCountKey | "html" | "errorCount">;
95
+
96
+ const RENDERER_ROOT_SELECTOR = 'svg[data-facet-renderer-root="true"]';
97
+ const MARKED_ROOT_SELECTOR = '[data-facet-renderer-root="true"]';
98
+ const RENDERER_GRAPH_ATTRIBUTE = "data-facet-renderer-graph";
99
+
100
+ function safeSelectorElementsWithin(scope: ParentNode, selector: string): Element[] {
101
+ // The shim lives in the PAGE world on purpose: a hostile page can
102
+ // monkey-patch querySelectorAll and lie here — the Tier 1 verdict
103
+ // detects that lie against the protocol authority. Reading through
104
+ // the patchable API is the design, not a bug.
105
+ try {
106
+ return Array.from(scope.querySelectorAll(selector));
107
+ } catch {
108
+ return [];
109
+ }
110
+ }
111
+
112
+ function safeSelectorElements(selector: string): Element[] {
113
+ return safeSelectorElementsWithin(document, selector);
114
+ }
115
+
116
+ function hasMarkedRootAncestor(root: Element, roots: ReadonlySet<Element>): boolean {
117
+ let parent = root.parentElement;
118
+ while (parent !== null) {
119
+ if (roots.has(parent)) return true;
120
+ parent = parent.parentElement;
121
+ }
122
+ return false;
123
+ }
124
+
125
+ function nonDegenerateViewBox(svg: Element): boolean {
126
+ const parts = (svg.getAttribute("viewBox") ?? "")
127
+ .trim()
128
+ .split(/[\s,]+/)
129
+ .map((part) => Number.parseFloat(part));
130
+ if (parts.length !== 4) return false;
131
+ const [, , width, height] = parts as [number, number, number, number];
132
+ return Number.isFinite(width) && Number.isFinite(height) && width > 0 && height > 0;
133
+ }
134
+
135
+ /** Page-world self-report the frame's direct render promise resolves with. UNTRUSTED by design. */
136
+ export function countPageShim(): PageShimCounts {
137
+ const markedCandidates = safeSelectorElements(RENDERER_ROOT_SELECTOR);
138
+ const candidateSet = new Set(markedCandidates);
139
+ const roots = markedCandidates.filter((root) => !hasMarkedRootAncestor(root, candidateSet));
140
+ const graphRoots = roots.filter((root) => root.getAttribute(RENDERER_GRAPH_ATTRIBUTE) === "true");
141
+ const allMarkedCandidates = safeSelectorElements(MARKED_ROOT_SELECTOR);
142
+ const markedSet = new Set(allMarkedCandidates);
143
+ const htmlRoots = allMarkedCandidates.filter(
144
+ (root) => root.nodeName.toLowerCase() !== "svg" && !hasMarkedRootAncestor(root, markedSet),
145
+ );
146
+ const html =
147
+ htmlRoots.length === 0
148
+ ? undefined
149
+ : {
150
+ rendererRootCount: htmlRoots.length,
151
+ headingCount: 0,
152
+ tableCount: 0,
153
+ listCount: 0,
154
+ imageCount: 0,
155
+ canvasCount: 0,
156
+ externalImageCount: 0,
157
+ };
158
+ for (const root of htmlRoots) {
159
+ html!.headingCount += safeSelectorElementsWithin(
160
+ root,
161
+ HTML_STRUCTURAL_GROUPS.headings.join(","),
162
+ ).length;
163
+ html!.tableCount += safeSelectorElementsWithin(
164
+ root,
165
+ HTML_STRUCTURAL_GROUPS.tables.join(","),
166
+ ).length;
167
+ html!.listCount += safeSelectorElementsWithin(
168
+ root,
169
+ HTML_STRUCTURAL_GROUPS.lists.join(","),
170
+ ).length;
171
+ const images = safeSelectorElementsWithin(root, HTML_STRUCTURAL_GROUPS.images.join(","));
172
+ html!.imageCount += images.length;
173
+ html!.externalImageCount += images.filter((image) => {
174
+ try {
175
+ return new URL(image.getAttribute("src") ?? "").protocol === "https:";
176
+ } catch {
177
+ return false;
178
+ }
179
+ }).length;
180
+ html!.canvasCount += safeSelectorElementsWithin(
181
+ root,
182
+ HTML_STRUCTURAL_GROUPS.canvases.join(","),
183
+ ).length;
184
+ }
185
+ const mermaidNodeCount = graphRoots.reduce(
186
+ (count, root) => count + safeSelectorElementsWithin(root, "g.node").length,
187
+ 0,
188
+ );
189
+ // Marker-scoped renderer counts deliberately differ from this document-wide
190
+ // canvas census: a smuggled canvas outside a renderer root remains observable.
191
+ // getContext() would create the surface being observed and turn the probe into a false positive.
192
+ const opaqueRegionCount = safeSelectorElements("*").filter(
193
+ (element) => element.nodeName.toLowerCase() === "canvas",
194
+ ).length;
195
+ return {
196
+ rendererRootSvgCount: roots.length,
197
+ graphCount: graphRoots.length,
198
+ mermaidNodeCount,
199
+ visibleSvgCount: roots.filter(nonDegenerateViewBox).length,
200
+ opaqueRegionCount,
201
+ externalImageCount: html?.externalImageCount ?? 0,
202
+ errorCount: safeSelectorElements("[data-facet-error]").length,
203
+ ...(html === undefined ? {} : { html }),
204
+ };
205
+ }
206
+
207
+ export interface RendererRegistry {
208
+ readonly get: (type: string) => Renderer | undefined;
209
+ }
210
+
211
+ export function createRendererRegistry(
212
+ entries: readonly (readonly [ArtifactType, Renderer])[],
213
+ ): RendererRegistry {
214
+ const registry = new Map<string, Renderer>(entries);
215
+ return {
216
+ get(type: string): Renderer | undefined {
217
+ return registry.get(type);
218
+ },
219
+ };
220
+ }
221
+
222
+ /**
223
+ * Dispatch one artifact through the registry. Unknown types throw — the caller
224
+ * turns the throw into a facet-error marker, never executable content.
225
+ */
226
+ export async function dispatchRender(
227
+ registry: RendererRegistry,
228
+ ctx: RenderContext,
229
+ payload: {
230
+ readonly artifactType: string;
231
+ readonly renderer: RendererKind;
232
+ readonly bytes: Uint8Array;
233
+ readonly theme: ResolvedGalleryTheme;
234
+ readonly execution?: TsxExecutionMode;
235
+ },
236
+ ): Promise<void> {
237
+ if (!isRenderer(payload.renderer)) {
238
+ throw new FacetRenderError(
239
+ `Artifact renderer '${payload.renderer}' is not supported in this frame`,
240
+ "invalid_request",
241
+ );
242
+ }
243
+ const renderer = registry.get(payload.artifactType);
244
+ if (renderer === undefined) {
245
+ throw new FacetRenderError(
246
+ `Artifact type '${payload.artifactType}' has no renderer in this frame`,
247
+ "unsupported_reserved_type",
248
+ );
249
+ }
250
+ if (payload.artifactType === "tsx" && !isTsxExecutionMode(payload.execution)) {
251
+ throw new FacetRenderError("TSX artifact payload is missing execution", "invalid_request");
252
+ }
253
+ if (payload.artifactType !== "tsx" && payload.execution === "interactive") {
254
+ throw new FacetRenderError(
255
+ "Interactive execution is only supported for TSX artifacts",
256
+ "invalid_request",
257
+ );
258
+ }
259
+ await renderer(ctx, payload.bytes, payload.renderer, payload.execution);
260
+ }