@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,408 @@
1
+ import { parse, Tokenizer, TokenizerMode, type TokenHandler } from "parse5";
2
+
3
+ import { MAX_HTML_NESTING_DEPTH } from "../../shared/config/limits";
4
+ import type { DiscriminativeError, HtmlStructureCounts } from "../../shared/contracts/validation";
5
+ import {
6
+ HTML_STRUCTURAL_GROUPS,
7
+ isAllowedHtmlUrl,
8
+ isHtmlDeniedElement,
9
+ isHtmlEventHandlerAttribute,
10
+ isHtmlInlineStyleAttribute,
11
+ isHtmlUrlAttributeName,
12
+ isHtmlUrlBearingAttribute,
13
+ } from "../../shared/html/policy";
14
+
15
+ export interface HtmlParseOk {
16
+ readonly status: "ok";
17
+ readonly html: HtmlStructureCounts;
18
+ }
19
+
20
+ export interface HtmlParseFail {
21
+ readonly status: "error";
22
+ readonly html: HtmlStructureCounts;
23
+ readonly errors: readonly DiscriminativeError[];
24
+ }
25
+
26
+ export type HtmlParseResult = HtmlParseOk | HtmlParseFail;
27
+
28
+ interface HtmlAttribute {
29
+ readonly name: string;
30
+ readonly value: string;
31
+ }
32
+
33
+ const HTML_VOID_ELEMENTS = new Set([
34
+ "area",
35
+ "base",
36
+ "br",
37
+ "col",
38
+ "embed",
39
+ "hr",
40
+ "img",
41
+ "input",
42
+ "link",
43
+ "meta",
44
+ "param",
45
+ "source",
46
+ "track",
47
+ "wbr",
48
+ ]);
49
+
50
+ function exceedsHtmlNestingDepth(text: string): boolean {
51
+ const openTags: string[] = [];
52
+ const positionsByTag = new Map<string, number[]>();
53
+ let exceeded = false;
54
+ let tokenizer: Tokenizer;
55
+ const handler: TokenHandler = {
56
+ onComment: () => {},
57
+ onDoctype: () => {},
58
+ onStartTag: (token) => {
59
+ if (token.selfClosing || HTML_VOID_ELEMENTS.has(token.tagName)) return;
60
+ const position = openTags.length;
61
+ openTags.push(token.tagName);
62
+ const positions = positionsByTag.get(token.tagName) ?? [];
63
+ positions.push(position);
64
+ positionsByTag.set(token.tagName, positions);
65
+ if (openTags.length > MAX_HTML_NESTING_DEPTH) {
66
+ exceeded = true;
67
+ tokenizer.pause();
68
+ }
69
+ },
70
+ onEndTag: (token) => {
71
+ const positions = positionsByTag.get(token.tagName);
72
+ const position = positions?.at(-1);
73
+ if (position === undefined) return;
74
+ while (openTags.length > position) {
75
+ const name = openTags.pop()!;
76
+ const entries = positionsByTag.get(name)!;
77
+ entries.pop();
78
+ if (entries.length === 0) positionsByTag.delete(name);
79
+ }
80
+ },
81
+ onEof: () => {},
82
+ onCharacter: () => {},
83
+ onNullCharacter: () => {},
84
+ onWhitespaceCharacter: () => {},
85
+ };
86
+ tokenizer = new Tokenizer({}, handler);
87
+ tokenizer.write(text, true);
88
+ return exceeded;
89
+ }
90
+
91
+ function initialCounts(): HtmlStructureCounts {
92
+ return {
93
+ rendererRootCount: 1,
94
+ headingCount: 0,
95
+ tableCount: 0,
96
+ listCount: 0,
97
+ imageCount: 0,
98
+ canvasCount: 0,
99
+ externalImageCount: 0,
100
+ };
101
+ }
102
+
103
+ function isRecord(value: unknown): value is Record<string, unknown> {
104
+ return typeof value === "object" && value !== null;
105
+ }
106
+
107
+ function elementDetails(
108
+ node: unknown,
109
+ ): { readonly tagName: string; readonly attrs: readonly HtmlAttribute[] } | null {
110
+ if (!isRecord(node) || typeof node.tagName !== "string" || !Array.isArray(node.attrs))
111
+ return null;
112
+ const attrs: HtmlAttribute[] = [];
113
+ for (const attribute of node.attrs) {
114
+ if (
115
+ !isRecord(attribute) ||
116
+ typeof attribute.name !== "string" ||
117
+ typeof attribute.value !== "string"
118
+ ) {
119
+ continue;
120
+ }
121
+ attrs.push({ name: attribute.name, value: attribute.value });
122
+ }
123
+ return { tagName: node.tagName.toLowerCase(), attrs };
124
+ }
125
+
126
+ function childNodes(node: unknown): readonly unknown[] {
127
+ return isRecord(node) && Array.isArray(node.childNodes) ? node.childNodes : [];
128
+ }
129
+
130
+ function templateContent(node: unknown): unknown | null {
131
+ return isRecord(node) && isRecord(node.content) ? node.content : null;
132
+ }
133
+
134
+ function isHttpsUrl(value: string): boolean {
135
+ try {
136
+ return new URL(value.trim()).protocol.toLowerCase() === "https:";
137
+ } catch {
138
+ return false;
139
+ }
140
+ }
141
+
142
+ function isWhitespace(character: string): boolean {
143
+ return (
144
+ character === " " ||
145
+ character === "\t" ||
146
+ character === "\n" ||
147
+ character === "\r" ||
148
+ character === "\f"
149
+ );
150
+ }
151
+
152
+ function srcsetCandidates(value: string): readonly string[] {
153
+ const candidates: string[] = [];
154
+ let index = 0;
155
+ while (index < value.length) {
156
+ while (index < value.length && (value[index] === "," || isWhitespace(value[index]!)))
157
+ index += 1;
158
+ if (index >= value.length) break;
159
+ const start = index;
160
+ while (index < value.length && !isWhitespace(value[index]!)) index += 1;
161
+ let candidate = value.slice(start, index);
162
+ while (candidate.endsWith(",")) candidate = candidate.slice(0, -1);
163
+ if (candidate.length > 0) candidates.push(candidate);
164
+ while (index < value.length && isWhitespace(value[index]!)) index += 1;
165
+ while (index < value.length && value[index] !== ",") index += 1;
166
+ if (value[index] === ",") index += 1;
167
+ }
168
+ return candidates;
169
+ }
170
+
171
+ function addError(errors: DiscriminativeError[], code: string, message: string): void {
172
+ errors.push({ code, message });
173
+ }
174
+
175
+ function validateUrl(
176
+ tagName: string,
177
+ attributeName: string,
178
+ value: string,
179
+ counts: HtmlStructureCounts,
180
+ errors: DiscriminativeError[],
181
+ countStructure: boolean,
182
+ ): void {
183
+ const candidates = attributeName === "srcset" ? srcsetCandidates(value) : [value];
184
+ for (const candidate of candidates) {
185
+ if (!isAllowedHtmlUrl(tagName, attributeName, candidate)) {
186
+ addError(
187
+ errors,
188
+ "html_denied_url_scheme",
189
+ `HTML <${tagName}> ${attributeName} contains a denied URL: ${candidate}`,
190
+ );
191
+ continue;
192
+ }
193
+ if (countStructure && (tagName === "img" || tagName === "source") && isHttpsUrl(candidate)) {
194
+ counts.externalImageCount += 1;
195
+ }
196
+ }
197
+ }
198
+
199
+ function walk(root: unknown, counts: HtmlStructureCounts, errors: DiscriminativeError[]): void {
200
+ const stack: Array<{ readonly node: unknown; readonly countStructure: boolean }> = [
201
+ { node: root, countStructure: true },
202
+ ];
203
+ while (stack.length > 0) {
204
+ const current = stack.pop()!;
205
+ const element = elementDetails(current.node);
206
+ if (element !== null) {
207
+ const { tagName, attrs } = element;
208
+ if (isHtmlDeniedElement(tagName)) {
209
+ addError(errors, "html_denied_element", `HTML contains denied <${tagName}> element`);
210
+ }
211
+ if (
212
+ current.countStructure &&
213
+ (HTML_STRUCTURAL_GROUPS.headings as readonly string[]).includes(tagName)
214
+ )
215
+ counts.headingCount += 1;
216
+ if (
217
+ current.countStructure &&
218
+ (HTML_STRUCTURAL_GROUPS.tables as readonly string[]).includes(tagName)
219
+ )
220
+ counts.tableCount += 1;
221
+ if (
222
+ current.countStructure &&
223
+ (HTML_STRUCTURAL_GROUPS.lists as readonly string[]).includes(tagName)
224
+ )
225
+ counts.listCount += 1;
226
+ if (
227
+ current.countStructure &&
228
+ (HTML_STRUCTURAL_GROUPS.images as readonly string[]).includes(tagName)
229
+ )
230
+ counts.imageCount += 1;
231
+ if (
232
+ current.countStructure &&
233
+ (HTML_STRUCTURAL_GROUPS.canvases as readonly string[]).includes(tagName)
234
+ )
235
+ counts.canvasCount += 1;
236
+ for (const attribute of attrs) {
237
+ const name = attribute.name.toLowerCase();
238
+ if (isHtmlEventHandlerAttribute(name) || isHtmlInlineStyleAttribute(name)) {
239
+ addError(
240
+ errors,
241
+ "html_denied_attribute",
242
+ `HTML <${tagName}> contains denied ${name} attribute`,
243
+ );
244
+ continue;
245
+ }
246
+ if (isHtmlUrlBearingAttribute(tagName, name)) {
247
+ validateUrl(tagName, name, attribute.value, counts, errors, current.countStructure);
248
+ continue;
249
+ }
250
+ if (isHtmlUrlAttributeName(name)) {
251
+ addError(
252
+ errors,
253
+ "html_denied_url_scheme",
254
+ `HTML <${tagName}> ${name} is not an allowed URL-bearing attribute`,
255
+ );
256
+ }
257
+ }
258
+ if (tagName === "template") {
259
+ const content = templateContent(current.node);
260
+ if (content !== null) stack.push({ node: content, countStructure: false });
261
+ }
262
+ }
263
+ const children = childNodes(current.node);
264
+ for (let index = children.length - 1; index >= 0; index -= 1) {
265
+ stack.push({ node: children[index], countStructure: current.countStructure });
266
+ }
267
+ }
268
+ }
269
+
270
+ export function parseHtml(bytes: Uint8Array): HtmlParseResult {
271
+ let text: string;
272
+ try {
273
+ text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
274
+ } catch {
275
+ const html = initialCounts();
276
+ return {
277
+ status: "error",
278
+ html,
279
+ errors: [
280
+ {
281
+ code: "html_encoding_unsupported",
282
+ message: "HTML bytes are not valid UTF-8",
283
+ },
284
+ ],
285
+ };
286
+ }
287
+ const html = initialCounts();
288
+ if (exceedsHtmlNestingDepth(text)) {
289
+ return {
290
+ status: "error",
291
+ html,
292
+ errors: [
293
+ {
294
+ code: "html_nesting_depth_exceeded",
295
+ message: `HTML nesting exceeds the ${MAX_HTML_NESTING_DEPTH} element limit`,
296
+ },
297
+ ],
298
+ };
299
+ }
300
+ const recovery = detectUnsupportedRecoveryFamilies(text);
301
+ if (recovery !== null) {
302
+ return {
303
+ status: "error",
304
+ html,
305
+ errors: [
306
+ {
307
+ code: "html_recovery_unsupported",
308
+ message: recovery,
309
+ },
310
+ ],
311
+ };
312
+ }
313
+ const errors: DiscriminativeError[] = [];
314
+ walk(parse(text, { scriptingEnabled: false }), html, errors);
315
+ return errors.length === 0 ? { status: "ok", html } : { status: "error", html, errors };
316
+ }
317
+
318
+ /**
319
+ * Tokenize the source and detect constructs whose recovery behaviour the
320
+ * parse5 prediction cannot guarantee against Chromium's DOMParser. Returns
321
+ * the rejection message for the FIRST unsupported construct found, or null
322
+ * if the bytes survive.
323
+ *
324
+ * Today the only sanctioned reject is the `<select>` family: parse5 drops
325
+ * table-scoped markup inside select (the WHATWG "in select in table" mode
326
+ * keeps it on Chromium). Shrinking the accepted input set here is cheaper
327
+ * than guaranteeing agreement on a niche construct static HTML reports do
328
+ * not actually need.
329
+ *
330
+ * Tokenizing (not scanning) is non-negotiable: a string scan cannot
331
+ * distinguish a `<select>` tag from the same characters appearing inside
332
+ * a comment, an attribute value, or a raw-text / RCDATA element's content.
333
+ * parse5's tree builder normally toggles tokenizer modes; a naive token
334
+ * detector therefore scores 7/9 and still false-positives on
335
+ * `<textarea>` / `<title>` content. We toggle the tokenizer mode
336
+ * ourselves on each RCDATA / RAWTEXT element so the score is 9/9.
337
+ */
338
+ const TABLE_SCOPED_TAGS = new Set([
339
+ "table",
340
+ "tr",
341
+ "td",
342
+ "th",
343
+ "tbody",
344
+ "thead",
345
+ "tfoot",
346
+ "caption",
347
+ "colgroup",
348
+ "col",
349
+ ]);
350
+
351
+ /** Escapable raw-text: character references ARE decoded, only the matching
352
+ * end tag is recognized. */
353
+ const RCDATA_ELEMENTS = new Set(["textarea", "title"]);
354
+
355
+ /** Raw text: no character references decoded, no tags at all (except the
356
+ * matching end tag).
357
+ *
358
+ * `noscript` is deliberately absent: this file parses with
359
+ * `scriptingEnabled: false` (matching the frame's DOMParser), and under
360
+ * scripting-disabled parsing `<noscript>` content is normal markup rather
361
+ * than raw text. Listing it here would blind the tokenizer walk to elements
362
+ * the tree walk below can see. These two must agree.
363
+ */
364
+ const RAWTEXT_ELEMENTS = new Set(["style", "xmp", "iframe", "noembed", "noframes"]);
365
+
366
+ function detectUnsupportedRecoveryFamilies(text: string): string | null {
367
+ let selectDepth = 0;
368
+ let divergent: string | null = null;
369
+ let tokenizer: Tokenizer;
370
+ const handler: TokenHandler = {
371
+ onComment: () => {},
372
+ onDoctype: () => {},
373
+ onStartTag: (token) => {
374
+ const tag = token.tagName.toLowerCase();
375
+ if (tag === "select") {
376
+ selectDepth += 1;
377
+ return;
378
+ }
379
+ if (divergent !== null) return;
380
+ if (selectDepth > 0 && TABLE_SCOPED_TAGS.has(tag)) {
381
+ divergent = `HTML <select> contains table-scoped <${tag}>; parse5 drops it, Chromium DOMParser keeps it`;
382
+ tokenizer.pause();
383
+ return;
384
+ }
385
+ // Toggle tokenizer mode so <select> / table markup inside raw text
386
+ // is not tokenized as a tag. This is the single detail that
387
+ // decides 9/9 vs 7/9.
388
+ if (RCDATA_ELEMENTS.has(tag)) {
389
+ tokenizer.state = TokenizerMode.RCDATA;
390
+ } else if (RAWTEXT_ELEMENTS.has(tag)) {
391
+ tokenizer.state = TokenizerMode.RAWTEXT;
392
+ }
393
+ },
394
+ onEndTag: (token) => {
395
+ const tag = token.tagName.toLowerCase();
396
+ if (tag === "select") {
397
+ if (selectDepth > 0) selectDepth -= 1;
398
+ }
399
+ },
400
+ onEof: () => {},
401
+ onCharacter: () => {},
402
+ onNullCharacter: () => {},
403
+ onWhitespaceCharacter: () => {},
404
+ };
405
+ tokenizer = new Tokenizer({}, handler);
406
+ tokenizer.write(text, true);
407
+ return divergent;
408
+ }
@@ -0,0 +1,285 @@
1
+ /**
2
+ * Tier 0 markdown parser.
3
+ *
4
+ * Marked's Lexer tokenizes the source into the CommonMark block tree
5
+ * without rendering. We use it to (a) confirm the source is structurally
6
+ * well-formed CommonMark, (b) count fenced blocks and per-language
7
+ * counts (cross-checking the service-side `computeLexicalExpectations`),
8
+ * and (c) verify raw HTML is treated as DATA, not executed.
9
+ *
10
+ * The Tier 0 renderer root expectation is one per ```mermaid fence.
11
+ * Marked produces inline `html` tokens for raw HTML blocks; those are
12
+ * counted but never attached to a DOM. The `marked` lexer therefore
13
+ * gives us a safe structural check without any rendering surface.
14
+ *
15
+ * External-image disclosure: the frozen CSP widens `img-src` to
16
+ * `https:` for every artifact type, so a markdown artifact carrying a
17
+ * native `![](https://…)` image loads that image at display time.
18
+ * `parseMarkdown` walks Marked's token tree (NOT a regex over the
19
+ * source) and tallies every image whose resolved `href` is `https:`.
20
+ * Marked resolves inline, reference-style, and autolink forms into the
21
+ * same `Tokens.Image` shape with the final `href` already populated,
22
+ * so a single walker pass is sufficient. The count surfaces as
23
+ * `observed.externalImageCount` so the verdict can downgrade the
24
+ * artifact to `partial:external_resources`. Raw-HTML smuggling of an
25
+ * `href`/`src` to `https:` remains a hard Tier 0 rejection — that
26
+ * pattern is hostile, not authored.
27
+ */
28
+
29
+ import { Lexer, type Token, type Tokens } from "marked";
30
+
31
+ import type { DiscriminativeError, VerdictObserved } from "../../shared/contracts/validation";
32
+ import { countMermaidNodeDeclarations } from "../../shared/util/mermaid-nodes";
33
+
34
+ export interface MarkdownParseOk {
35
+ readonly status: "ok";
36
+ readonly observed: VerdictObserved;
37
+ }
38
+
39
+ export interface MarkdownParseFail {
40
+ readonly status: "error";
41
+ readonly observed: VerdictObserved;
42
+ readonly errors: readonly DiscriminativeError[];
43
+ }
44
+
45
+ export type MarkdownParseResult = MarkdownParseOk | MarkdownParseFail;
46
+
47
+ interface MarkdownCounts {
48
+ totalFenced: number;
49
+ mermaidFenced: number;
50
+ mermaidNodeCount: number | null;
51
+ rendererRoots: number;
52
+ htmlTokens: number;
53
+ externalImageCount: number;
54
+ hasScript: boolean;
55
+ hasOnHandler: boolean;
56
+ hasExternalRef: boolean;
57
+ }
58
+
59
+ function isExternalHttpsUrl(value: string): boolean {
60
+ // The URL constructor canonicalizes the protocol; malformed URLs
61
+ // (e.g. `https://[`) return false here rather than throwing, so the
62
+ // token walk surfaces a typed zero instead of crashing Tier 0.
63
+ try {
64
+ return new URL(value.trim()).protocol.toLowerCase() === "https:";
65
+ } catch {
66
+ return false;
67
+ }
68
+ }
69
+
70
+ /**
71
+ * Walk the marked token tree and tally the surfaces that matter for
72
+ * the Tier 0 verdict: fenced blocks, mermaid blocks, raw HTML blocks,
73
+ * native-markdown external-image references, and three structural
74
+ * red flags the verifier must NOT execute (a `<script>` token, an
75
+ * `on*=…` handler, and an external URL in any raw-HTML attribute).
76
+ *
77
+ * Marked resolves every image form (inline `![](https://…)`,
78
+ * reference-style `![alt][ref]` whose `[ref]: https://…`, autolink
79
+ * `![<https://…>]()`) into the same `Tokens.Image` shape with the
80
+ * final `href` already populated — the walker counts one `https:`
81
+ * image per token regardless of which authoring form produced it.
82
+ * Raw-HTML `src="https://…"` is the smuggling pattern and is rejected
83
+ * below.
84
+ *
85
+ * Container recursion enumerates every shape Marked actually emits
86
+ * with children: `tokens[]` (blockquote, paragraph, heading, list_item,
87
+ * em, strong, link, image, del, tableCell), `items[]` (list carries
88
+ * ListItem tokens; each item has its own `tokens[]`), `header[]`
89
+ * (table header cells; each cell carries its own `tokens[]`), and
90
+ * `rows[][]` (table body rows of cells; each cell carries its own
91
+ * `tokens[]`). Missing any of these silently misses every image a real
92
+ * status report places in a list or table — see the per-container
93
+ * shape table in the regression report. The walker ALSO recurses the
94
+ * raw-HTML red flags into the same containers: a `<script>` or `on*=`
95
+ * smuggled into a list item or table cell must still reject the
96
+ * document, the same as a top-level smuggled reference.
97
+ */
98
+ function walkTokens(tokens: Token[], counts: MarkdownCounts): void {
99
+ for (const token of tokens) {
100
+ if (token.type === "code") {
101
+ const code = token as Tokens.Code;
102
+ counts.totalFenced += 1;
103
+ const lang = (code.lang ?? "").trim().toLowerCase();
104
+ if (lang === "mermaid") {
105
+ counts.mermaidFenced += 1;
106
+ counts.rendererRoots += 1;
107
+ const nodes = countMermaidNodeDeclarations(code.text);
108
+ counts.mermaidNodeCount =
109
+ counts.mermaidNodeCount === null || nodes === null
110
+ ? null
111
+ : counts.mermaidNodeCount + nodes;
112
+ }
113
+ } else if (token.type === "html") {
114
+ const html = token as Tokens.HTML;
115
+ counts.htmlTokens += 1;
116
+ const raw = html.raw ?? html.text ?? "";
117
+ if (/<script[\s>]/i.test(raw)) counts.hasScript = true;
118
+ if (/\son[a-z]+\s*=/i.test(raw)) counts.hasOnHandler = true;
119
+ if (/\b(?:href|src)\s*=\s*["']https?:/i.test(raw)) counts.hasExternalRef = true;
120
+ } else if (token.type === "image") {
121
+ const image = token as Tokens.Image;
122
+ if (isExternalHttpsUrl(image.href ?? "")) counts.externalImageCount += 1;
123
+ }
124
+ // Recurse every container Marked actually emits with children.
125
+ const recurseTokens = (sub: Token[] | undefined): void => {
126
+ if (Array.isArray(sub)) walkTokens(sub, counts);
127
+ };
128
+ const t = token as unknown as Record<string, unknown>;
129
+ // tokens[] — blockquote, paragraph, heading, list_item, em,
130
+ // strong, link, image (inline children), del, tableCell.
131
+ recurseTokens(t["tokens"] as Token[] | undefined);
132
+ // items[] — list carries ListItem tokens (each with its own
133
+ // tokens[] walked above when we visit them).
134
+ const items = t["items"] as Token[] | undefined;
135
+ if (Array.isArray(items)) walkTokens(items, counts);
136
+ // header[] — table header cells. Each cell carries its own
137
+ // tokens[] walked when we visit the cell token below.
138
+ const headerCells = t["header"] as Array<{ tokens?: Token[] }> | undefined;
139
+ if (Array.isArray(headerCells)) {
140
+ for (const cell of headerCells) {
141
+ if (cell !== null && typeof cell === "object") {
142
+ recurseTokens(cell.tokens as Token[] | undefined);
143
+ }
144
+ }
145
+ }
146
+ // rows[][] — table body rows, each row is an array of cells,
147
+ // each cell carries its own tokens[].
148
+ const rows = t["rows"] as Array<Array<{ tokens?: Token[] } | null>> | undefined;
149
+ if (Array.isArray(rows)) {
150
+ for (const row of rows) {
151
+ if (Array.isArray(row)) {
152
+ for (const cell of row) {
153
+ if (cell !== null && typeof cell === "object") {
154
+ recurseTokens(cell.tokens as Token[] | undefined);
155
+ }
156
+ }
157
+ }
158
+ }
159
+ }
160
+ }
161
+ }
162
+
163
+ /**
164
+ * Parse the source bytes as markdown. Pure tokenization — no DOM, no
165
+ * render, no script execution. Raw HTML is counted (it must NOT
166
+ * execute, and we surface structural red flags if it tries to smuggle
167
+ * one) but never interpreted.
168
+ */
169
+ export function parseMarkdown(bytes: Uint8Array): MarkdownParseResult {
170
+ const text = new TextDecoder("utf-8", { fatal: false }).decode(bytes);
171
+ const counts: MarkdownCounts = {
172
+ totalFenced: 0,
173
+ mermaidFenced: 0,
174
+ mermaidNodeCount: 0,
175
+ rendererRoots: 0,
176
+ htmlTokens: 0,
177
+ externalImageCount: 0,
178
+ hasScript: false,
179
+ hasOnHandler: false,
180
+ hasExternalRef: false,
181
+ };
182
+ let lexError: unknown = null;
183
+ let tokens: Token[] = [];
184
+ try {
185
+ const lexer = new Lexer({ gfm: true });
186
+ tokens = lexer.lex(text);
187
+ walkTokens(tokens, counts);
188
+ } catch (error) {
189
+ lexError = error;
190
+ }
191
+
192
+ if (lexError !== null) {
193
+ const message = lexError instanceof Error ? lexError.message : String(lexError);
194
+ return {
195
+ status: "error",
196
+ observed: {
197
+ rendererRootSvgCount: 0,
198
+ graphCount: 0,
199
+ mermaidNodeCount: counts.mermaidNodeCount ?? 0,
200
+ visibleSvgCount: 0,
201
+ externalImageCount: 0,
202
+ errorCount: 1,
203
+ opaqueRegionCount: 0,
204
+ },
205
+ errors: [{ code: "markdown_lex_error", message }],
206
+ };
207
+ }
208
+
209
+ // Raw HTML must NEVER execute. A token containing a script, an
210
+ // `on*` handler, or an external URL surfaces as a parser error so a
211
+ // hostile artifact cannot smuggle executable content past Tier 0.
212
+ if (counts.hasScript) {
213
+ return {
214
+ status: "error",
215
+ observed: {
216
+ rendererRootSvgCount: counts.rendererRoots,
217
+ graphCount: counts.mermaidFenced,
218
+ mermaidNodeCount: counts.mermaidNodeCount ?? 0,
219
+ visibleSvgCount: 0,
220
+ externalImageCount: counts.externalImageCount,
221
+ errorCount: 1,
222
+ opaqueRegionCount: 0,
223
+ },
224
+ errors: [
225
+ {
226
+ code: "html_script_in_markdown",
227
+ message: "Raw HTML in markdown contains a <script> element; Tier 0 rejects it",
228
+ },
229
+ ],
230
+ };
231
+ }
232
+ if (counts.hasOnHandler) {
233
+ return {
234
+ status: "error",
235
+ observed: {
236
+ rendererRootSvgCount: counts.rendererRoots,
237
+ graphCount: counts.mermaidFenced,
238
+ mermaidNodeCount: counts.mermaidNodeCount ?? 0,
239
+ visibleSvgCount: 0,
240
+ externalImageCount: counts.externalImageCount,
241
+ errorCount: 1,
242
+ opaqueRegionCount: 0,
243
+ },
244
+ errors: [
245
+ {
246
+ code: "html_event_handler_in_markdown",
247
+ message: "Raw HTML in markdown contains an on*= event handler; Tier 0 rejects it",
248
+ },
249
+ ],
250
+ };
251
+ }
252
+ if (counts.hasExternalRef) {
253
+ return {
254
+ status: "error",
255
+ observed: {
256
+ rendererRootSvgCount: counts.rendererRoots,
257
+ graphCount: counts.mermaidFenced,
258
+ mermaidNodeCount: counts.mermaidNodeCount ?? 0,
259
+ visibleSvgCount: 0,
260
+ externalImageCount: counts.externalImageCount,
261
+ errorCount: 1,
262
+ opaqueRegionCount: 0,
263
+ },
264
+ errors: [
265
+ {
266
+ code: "html_external_reference_in_markdown",
267
+ message: "Raw HTML in markdown contains an external href/src URL; Tier 0 rejects it",
268
+ },
269
+ ],
270
+ };
271
+ }
272
+
273
+ return {
274
+ status: "ok",
275
+ observed: {
276
+ rendererRootSvgCount: counts.rendererRoots,
277
+ graphCount: counts.mermaidFenced,
278
+ mermaidNodeCount: counts.mermaidNodeCount ?? 0,
279
+ visibleSvgCount: 0,
280
+ externalImageCount: counts.externalImageCount,
281
+ errorCount: 0,
282
+ opaqueRegionCount: 0,
283
+ },
284
+ };
285
+ }