@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,101 @@
1
+ # TSX reference
2
+
3
+ ## Publish
4
+
5
+ ```sh
6
+ facet publish --artifact-id <id> --type tsx --file templates/tsx-status-report.tsx
7
+ facet publish --artifact-id <id> --type tsx --execution interactive --file templates/tsx-interactive-counter.tsx
8
+ ```
9
+
10
+ `static` is the default execution mode. `interactive` is declared with
11
+ `--execution`; Facet does not infer it from hooks or handlers.
12
+
13
+ ## Static mode
14
+
15
+ Static TSX compiles to HTML in Tier 0, then uses the existing HTML prediction
16
+ and Tier 1 comparison pipeline. It can earn the same predict-and-compare claim
17
+ as an HTML artifact.
18
+
19
+ ## Interactive mode
20
+
21
+ Interactive TSX is client-rendered observation only. There is no SSR or
22
+ hydration expectation. Tier 1 observes structure through CDP snapshot,
23
+ `getDocument`, and an isolated-world channel, then repeats the observation
24
+ after the bounded one-second stability window. Agreement is an observation that
25
+ the artifact ran and held its structure; it is not a server-rendered prediction.
26
+
27
+ Facet mounts the default export in both modes. Artifacts must export a component
28
+ and must not call `createRoot` or self-mount.
29
+
30
+ ## Imports and styling
31
+
32
+ The compiler accepts only vendored modules: `react`, `react-dom`,
33
+ `react-dom/client`, `react/jsx-runtime`, and `react/jsx-dev-runtime`.
34
+ Pinned versions are React and React DOM `19.2.8`, TypeScript `5.7.3`.
35
+
36
+ TSX reuses the [HTML style vocabulary](html.md#vendored-styling-vocabulary):
37
+ the shipped Tailwind/daisyUI classes, not JavaScript component kits. There is no
38
+ promise of arbitrary npm, Base UI, forms, FormBridge, or animations.
39
+
40
+ ## Publish-time policy
41
+
42
+ Tier 0 rejects direct `fetch`, `eval`, `new Function`, dynamic `import()`,
43
+ `Worker`, `SharedWorker`, `require`, obvious denied-global aliases, and
44
+ non-allowlisted imports. Capability errors use `tsx_capability_fetch`,
45
+ `tsx_capability_eval`, `tsx_capability_function_constructor`,
46
+ `tsx_capability_dynamic_import`, `tsx_capability_worker`,
47
+ `tsx_capability_shared_worker`, `tsx_capability_computed_global`,
48
+ `tsx_capability_require`, or `tsx_capability_global_alias`; imports report
49
+ `tsx_import_denied`. This AST policy is early feedback, not the enforcement
50
+ boundary: aliases and indirect JavaScript forms are deliberately not exhaustively
51
+ modeled. The frame document's CSP and netns runtime controls make denied
52
+ capabilities unreachable.
53
+
54
+ ## Execution boundary
55
+
56
+ Interactive bundles mount directly in the artifact's own gallery frame: the
57
+ compiled bundle imports as a `blob:` module in that frame document, with no
58
+ nested iframe. The frame is served under an ordinary restrictive CSP (`self`,
59
+ plus `blob:` for the compiled module import) — not the frozen Tier 1
60
+ verification CSP, which remains a validation-only concern. The artifact code
61
+ has no host port or service capability.
62
+
63
+ ## Storage and export
64
+
65
+ Revision source remains immutable. Compilation creates derived bytes recorded at
66
+ the run's `compiled_path`; TSX source export writes the original `.tsx` bytes.
67
+ Render export remains the retained Tier 1 screenshot. Evidence retains the last
68
+ 10 non-retained runs per artifact.
69
+
70
+ ## Verdicts
71
+
72
+ TSX verdicts carry `execution: "static" | "interactive"`. In interactive mode,
73
+ `partial:unstable` means the first observation changed during the stability
74
+ re-check. It loses to timeout, channel divergence, channel availability, and
75
+ durable errors, but outranks single-snapshot structural claims.
76
+
77
+ ## Measurements and limits
78
+
79
+ The supported Bun floor and CI pin is `1.4.0`; it includes the fd-reuse fix
80
+ needed for reliable CDP-pipe operation. Measured determinism, compiler bytes,
81
+ and nested-channel observations are in the separate [Bun 1.3.14](../verification/tsx-measurements.md#bun-1314)
82
+ and [Bun 1.4.0](../verification/tsx-measurements.md#bun-140) tables. The
83
+ evidence is runtime-scoped: do not treat hashes as invariant across Bun
84
+ versions.
85
+
86
+ Interactive TSX declares eligibility for animated WebP evidence without a
87
+ probe. Static TSX, HTML, and SVG can become eligible when live CSS or Web
88
+ Animations are detected. Eligibility does not assert visible motion.
89
+
90
+ New retained screenshots are WebP and may contain multiple frames. Source
91
+ export remains the original `.tsx` bytes, and the `execution` marker remains
92
+ conditional on TSX responses.
93
+
94
+ Interactive bundles explicitly select production React. They remain unminified
95
+ so operators can audit emitted bundle bytes; the current measured bundle size,
96
+ retention cost, and 2 MiB-cap headroom are derived in the evidence tables.
97
+
98
+ ## Starters
99
+
100
+ - [Static status report](../../templates/tsx-status-report.tsx)
101
+ - [Interactive counter](../../templates/tsx-interactive-counter.tsx)
@@ -0,0 +1,212 @@
1
+ # Validation reference
2
+
3
+ The tiered validation contract every read-back response is bound to.
4
+ Covers the `RenderStatus` taxonomy, what evidence each tier persists,
5
+ the retention policy, and the revision-binding guarantee.
6
+
7
+ ## Tier taxonomy
8
+
9
+ `RenderStatus` is the closed enum the verifier assigns to one run. The
10
+ verdict is decided in exactly one place (`src/validation/tier1/verdict.ts`).
11
+ Every other layer is bound to it through `VerdictSchema.status`.
12
+
13
+ | status | meaning |
14
+ | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
15
+ | `ok` | Counts agree across protocol + shim + isolated worlds, layout observable, no discriminative errors. |
16
+ | `error` | Counts disagree with the lexical expectation OR the protocol surfaced a discriminative error. |
17
+ | `partial:layout_unverified` | Counts agree but the layout pass is unverified (no SVG rendered with a non-degenerate viewBox). MUST carry a screenshot path on the wire. |
18
+ | `partial:opaque_content` | An opaque DOM region was observed, so structural contents were not verified. MUST carry a screenshot path, or a typed `screenshotError` marker when capture fails transiently. |
19
+ | `partial:external_resources` | The artifact references external HTTPS images the no-egress verifier could not observe. MUST carry a screenshot path, or a typed `screenshotError` marker when capture fails transiently. |
20
+ | `partial:unstable` | TSX interactive mode: the structure observed at the render barrier differed from the structure observed after a bounded stability window. Deliberately NOT `tampered` — a legitimately animated or async-loading component also changes structure between observations, and branding that a forgery would manufacture the false-verdict class this project has spent three arcs eliminating. `tampered` stays reserved for channel divergence. MUST carry a screenshot path, or a typed `screenshotError` marker when capture fails transiently. |
21
+ | `tampered` | Page-shim or isolated-world observation diverges from protocol authority. |
22
+ | `timeout` | The harness did not emit `render-complete` within `TIER1_RENDER_BARRIER_MS`. |
23
+ | `shim_only` | Isolated-world channel missing; only the untrusted page-shim produced usable counts. |
24
+ | `probe_only` | Both the page-shim and the isolated-world channel are missing; only the protocol channel is usable. |
25
+ | `insecure:unvalidated` | Level 3 intentionally skipped validation. The artifact is not represented as validated. |
26
+
27
+ A `partial:*` verdict is a verdict the verifier could not finalize, NOT
28
+ a degraded `ok`. The screenshot is mandatory FOR `partial:` so a human
29
+ or a re-verifier can see what the verifier saw — it is not a thing
30
+ that upgrades the verdict. When capture fails transiently, the typed
31
+ `screenshotError` marker records that honest degraded path. A `partial:`
32
+ without a screenshot or marker is rejected at the schema parse boundary.
33
+
34
+ ### `partial:unstable` precedence slot
35
+
36
+ `partial:unstable` splits the error paths in two, because `error` is not a
37
+ single tier. The discriminating question is whether a status rests on ONE
38
+ observation or on something a structure change cannot invalidate. The full
39
+ ordering, top to bottom:
40
+
41
+ 1. `timeout` — lifecycle failed at the render barrier (`renderComplete === false`).
42
+ 2. `tampered` — channel divergence; the page contradicts protocol authority.
43
+ 3. `probe_only` / `shim_only` — channel availability (meta-claim about the channels, not the page).
44
+ 4. `error` — discriminative errors are non-empty, or observed counts disagree with the lexical expectation. Both survive a structure change: a parse error is a parse error whenever it was observed, and a count that never matched the source's expectation never matches.
45
+ 5. **`partial:unstable`** — TSX interactive mode; structure changed between the render barrier and the stability window.
46
+ 6. `error` (declared-opaque, observed-zero) — the artifact declared opaque regions and none were seen. This sits BELOW `partial:unstable` deliberately: it compares the expectation against a SINGLE observation, so a structure change makes it unsafe to assert. A canvas that painted after the barrier would otherwise be reported as an artifact that never painted at all — a false accusation, which is the failure class the whole partial taxonomy exists to avoid.
47
+ 7. `partial:opaque_content` — single-snapshot claim: structure has opaque regions.
48
+ 8. `partial:external_resources` — single-snapshot claim: structure has external HTTP references.
49
+ 9. `partial:layout_unverified` — single-snapshot claim: visible SVG with zeroed viewBoxes.
50
+ 10. `ok`.
51
+
52
+ The rule generalizing rows 4 and 6: a claim that depends on ONE observation
53
+ loses to `partial:unstable`; a claim that holds regardless of when it was
54
+ observed outranks it.
55
+
56
+ The reasoning: when structure is changing between two observation snapshots, the verifier cannot honestly claim "this artifact has structure X" — every single-snapshot claim (opaque, external, layout) is moot. `partial:unstable` is a meta-claim about the page's runtime behavior that dominates the single-snapshot structural claims. `tampered` stays above it because channel divergence (the page contradicting protocol authority) is the more catastrophic reading of the page's behavior. The decision is pinned by `tests/unit/verdict.test.ts` (unstable outranks opaque, external, layout-unverified; unstable loses to tampered, timeout, and channel-availability).
57
+
58
+ ## Evidence captured per tier
59
+
60
+ | tier | evidence retained |
61
+ | ---- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
62
+ | 0 | DB row only (expected + observed JSON, status, timing). No on-disk evidence. |
63
+ | 1 | DB row + per-run directory under `<evidence>/tier1/<revisionSha>/<runId>/`: `screenshot.webp` for new captures, legacy `screenshot.png` when retained, `console.txt`, and `protocol-observation.json`. Captures cover the whole artifact within the 4096-pixel axis, 8,388,608-pixel total, and 8 MiB encoded limits. |
64
+
65
+ The evidence root is the XDG state path `paths.evidence` (or the
66
+ `FACET_HOME/evidence` override). Every directory the runner creates is
67
+ mode 0700 — the canonical secret-bearing layout matches the DB file
68
+ permissions.
69
+
70
+ Tier 1 capture happens AFTER the verdict is derived so the
71
+ `partial:layout_unverified`, `partial:opaque_content`, `partial:external_resources`, and `partial:unstable` screenshot mandates are honored. The runner
72
+ measures the whole artifact, bounds each axis at 4096 pixels and the total at
73
+ 8,388,608 pixels, and encodes evidence under the 8 MiB cap. New captures are
74
+ WebP; legacy PNG evidence remains readable and exportable. Before static
75
+ capture it emulates `prefers-reduced-motion: reduce` and awaits
76
+ `document.fonts.ready`; these pre-flights keep repeated captures byte-identical.
77
+
78
+ Interactive TSX declares animated-capture eligibility; it does not imply that
79
+ the component is always visibly changing. CSS/Web Animations are probed, and
80
+ eligible captures may contain multiple WebP frames. Static artifacts retain a
81
+ single frame. Whole-artifact capture is downscaled to fit the axis,
82
+ decoded-pixel, and encoded-size bounds; it is never clipped. If the complete
83
+ image cannot fit the encoded cap, the verdict remains honest with
84
+ `screenshotError.code = screenshot_unavailable` rather than claiming a partial
85
+ capture.
86
+
87
+ Levels 0–2 record Tier 0 on publish. A Tier 1 run is explicit: visual
88
+ read-back records it on demand, then reuses the revision-bound result. A
89
+ browser or network-namespace failure records a Tier 1 `error` verdict with its
90
+ typed `tier1_*` code; it does not erase the Tier 0 verdict or turn visual
91
+ read-back into `revision_not_found`.
92
+
93
+ ## Observed fields and renderer expectations
94
+
95
+ The canonical observed fields include `rendererRootSvgCount`, `graphCount`,
96
+ `mermaidNodeCount`, `visibleSvgCount`, `opaqueRegionCount`, `viewBoxes`,
97
+ `errorCount`, and `discriminativeErrors`. `opaqueRegionCount` counts DOM
98
+ regions whose contents are not structurally observable.
99
+
100
+ Renderer literals are `svg` and `canvas`. The `canvas` renderer is chart-only:
101
+ a canvas chart expects `rendererRootSvgCount = 0` and `opaqueRegionCount = 1`;
102
+ SVG renderers retain their structural root expectations.
103
+
104
+ HTML artifacts carry their own structural observable — `HtmlStructureCounts` —
105
+ anchored on the frame-owned `data-facet-renderer-root` wrapper. The fields
106
+ are `rendererRootCount`, `headingCount`, `tableCount`, `listCount`,
107
+ `imageCount`, `canvasCount`, and `externalImageCount`. The first is the
108
+ HTML analogue of `rendererRootSvgCount`; the rest are scoped beneath the
109
+ marker. See the [HTML reference](html.md) for the verdict claim these
110
+ counts support and the precedence over `partial:opaque_content` and
111
+ `partial:external_resources`.
112
+
113
+ ## Tier 0 vs Tier 1 channels for HTML
114
+
115
+ Tier 0 is a WHATWG parser (`parse5@8.0.1`, `scriptingEnabled: false`)
116
+ running in the existing netns worker with no egress. It produces the
117
+ structural prediction that is stored with the revision SHA and forwarded
118
+ to Tier 1. The parser handles every recovery family that the differential
119
+ corpus accepts; three families — UTF-8 encoding ambiguity, `<select>`
120
+ containing table-scoped markup, and nesting depth beyond the cap — are
121
+ rejected before Tier 1 ever runs.
122
+
123
+ Tier 1 is `chrome-headless-shell` `151.0.7922.77` inside its netns,
124
+ rendering the artifact through the gallery's vendored HTML renderer. It
125
+ observes the rendered DOM through CDP protocol authority, not the page
126
+ shim, and computes its own `HtmlStructureCounts` from the snapshot.
127
+ Agreement on every field is `ok`; disagreement is `tampered`.
128
+
129
+ The two channels are independent: one parses bytes with no egress, the
130
+ other renders in a browser. Their expected-vs-observed comparison is
131
+ what makes an HTML verdict a real prediction rather than a self-claim.
132
+
133
+ An HTML Tier 0 response is a normal publish/read-back verdict, for example:
134
+
135
+ ```json
136
+ {
137
+ "status": "ok",
138
+ "tier": 0,
139
+ "artifactId": "art-123",
140
+ "revisionSha": "<sha256>",
141
+ "observed": { "htmlElementCount": 4, "htmlTextNodeCount": 2 }
142
+ }
143
+ ```
144
+
145
+ The differential corpus at `tests/acceptance/html-differential.test.ts`
146
+ is the live gate that keeps the two parsers in agreement over a body
147
+ of real documents. Any divergence in the corpus is a design input
148
+ (shrink the accepted input set), never a verdict-comparison weakening.
149
+
150
+ ## TSX modes
151
+
152
+ Tier 0 compiles TSX after direct AST-policy checks. Static output enters the
153
+ HTML pipeline above. Interactive output runs in the nested artifact frame and
154
+ is observed through CDP snapshot, `getDocument`, and isolated-world channels at
155
+ the render barrier and after the one-second stability window. There is no SSR or
156
+ hydration expectation. `partial:unstable` records a changed structure; channel
157
+ divergence remains `tampered`.
158
+
159
+ Theme switching affects gallery Tier 2 display only. Tier 1 structural
160
+ verdicts and captures remain dark-theme parity behavior so counts and layout
161
+ comparisons are stable across display preferences.
162
+
163
+ ## Retention policy
164
+
165
+ Last-N retention runs INSIDE `recordRenderRun`'s write path. The
166
+ canonical knob is `EVIDENCE_LAST_N_PER_ARTIFACT` (default 10); the
167
+ policy keeps the N most recent non-retained runs per artifact and
168
+ unlinks the on-disk evidence of everything older.
169
+
170
+ Retained-evidence carve-out: a row marked `retained: true` is exempt
171
+ from the cutoff. Pin and template call sites set the flag (Task 14
172
+ wires them); the cleanup walker skips `retained = 1` rows.
173
+
174
+ Cleanup is best-effort: a row is the authoritative state, a stale
175
+ file is recoverable by the next orphan sweep. A failed `INSERT` runs
176
+ the converse cleanup — any caller-supplied `screenshotPath` /
177
+ `consolePath` is unlinked so no orphan pixels accumulate.
178
+
179
+ Retention controls evidence files, not read-back payload history. Read-back
180
+ reconstructs a revision-bound verdict from its stored render-run row; an
181
+ evicted screenshot or console path is intentionally not returned as bytes.
182
+
183
+ ## Revision-binding guarantee
184
+
185
+ Every `read-back` response is bound to the EXACT `(artifactId,
186
+ revisionSha)` the caller supplied. The lookup
187
+ (`repository.getRevisionBySha`) is keyed on both columns; a stale or
188
+ mismatched sha returns `revision_not_found` BEFORE any verdict row is
189
+ read, so two revisions to the same artifact can never cross-pollinate
190
+ their verdicts.
191
+
192
+ The dispatcher enriches every Tier 1 verdict with the real
193
+ `artifactId` + `revisionSha` it is committing (the worker runs out of
194
+ process and does not know the artifactId). Read-back returns the
195
+ enriched verdict — never the worker's placeholder identity.
196
+
197
+ ## Layering
198
+
199
+ Insecure execution conditions are metadata: every insecure-level verdict carries a `Verdict.insecure` marker with the effective level and reason, but verdict derivation never consumes that marker.
200
+ Level 3 additionally produces the `insecure:unvalidated` status.
201
+
202
+ ```
203
+ shared/contracts/validation.ts VerdictSchema, RenderStatus, and Tier 1 refinements
204
+ shared/contracts/artifact.ts RenderRunSchema and screenshot error metadata
205
+ service/store/schema.ts additive v2–v9 schema fragments
206
+ service/store/migrations.ts transactional migration application
207
+ service/store/evidence-retention.ts
208
+ last-N cleanup and 0700 directory enforcement
209
+ service/store/repository.ts render-run persistence and cleanup-on-failure
210
+ service/dispatcher.ts revision binding and screenshot-path handoff
211
+ validation/tier1/runner.ts post-verdict capture and evidence paths
212
+ ```
package/package.json ADDED
@@ -0,0 +1,89 @@
1
+ {
2
+ "name": "@legionworks/facet",
3
+ "version": "1.9.0",
4
+ "private": false,
5
+ "keywords": [
6
+ "artifacts",
7
+ "bun",
8
+ "facet",
9
+ "gallery",
10
+ "rendering",
11
+ "validation"
12
+ ],
13
+ "homepage": "https://github.com/legion-works/facet#readme",
14
+ "bugs": {
15
+ "url": "https://github.com/legion-works/facet/issues"
16
+ },
17
+ "license": "MIT OR Apache-2.0",
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/legion-works/facet.git"
21
+ },
22
+ "bin": {
23
+ "facet": "./src/cli/main.ts",
24
+ "facet-mcp": "./src/harness-adapters/mcp/main.ts"
25
+ },
26
+ "files": [
27
+ "src/**",
28
+ "scripts/launch-netns.sh",
29
+ "scripts/build-gallery.ts",
30
+ "templates/**",
31
+ "dist/gallery/**",
32
+ "skills/**",
33
+ "README.md",
34
+ "LICENSE-*",
35
+ "docs/reference/**"
36
+ ],
37
+ "type": "module",
38
+ "publishConfig": {
39
+ "access": "public"
40
+ },
41
+ "scripts": {
42
+ "build": "bun scripts/build-gallery.ts",
43
+ "prepack": "bun scripts/build-gallery.ts --if-stale",
44
+ "lint": "oxlint --deny-warnings",
45
+ "format": "oxfmt",
46
+ "format:check": "bun scripts/check-format.ts",
47
+ "typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.runtime-egress-fixture.json",
48
+ "test": "bun test",
49
+ "test:coverage": "bun test --coverage --coverage-reporter=lcov",
50
+ "coverage:check": "bun scripts/check-coverage.ts",
51
+ "check:boundaries": "bun scripts/check-boundaries.ts",
52
+ "verify:package-install": "bun scripts/verify-package-install.ts",
53
+ "perf-gate": "bun scripts/perf-gate.ts",
54
+ "perf-gate:quick": "bun scripts/perf-gate.ts --quick",
55
+ "verify-adapter-size": "bun scripts/verify-adapter-size.ts"
56
+ },
57
+ "dependencies": {
58
+ "@modelcontextprotocol/sdk": "1.30.0",
59
+ "fast-xml-parser": "5.10.1",
60
+ "linkedom": "0.18.13",
61
+ "marked": "18.0.9",
62
+ "mermaid": "11.16.1",
63
+ "parse5": "8.0.1",
64
+ "puppeteer-core": "25.5.0",
65
+ "react": "19.2.8",
66
+ "react-dom": "19.2.8",
67
+ "sharp": "^0.35.3",
68
+ "typescript": "5.7.3",
69
+ "vega": "6.3.1",
70
+ "vega-interpreter": "^2.3.1",
71
+ "vega-lite": "6.4.3",
72
+ "zod": "3.25.76"
73
+ },
74
+ "devDependencies": {
75
+ "@tailwindcss/cli": "4.3.3",
76
+ "@types/bun": "1.4.0",
77
+ "@types/react": "19.2.18",
78
+ "@types/react-dom": "19.2.4",
79
+ "daisyui": "5.7.16",
80
+ "lefthook": "1.11.13",
81
+ "oxfmt": "0.54.0",
82
+ "oxlint": "1.73.0",
83
+ "tailwindcss": "4.3.3"
84
+ },
85
+ "engines": {
86
+ "bun": ">=1.4.0"
87
+ },
88
+ "packageManager": "bun@1.4.0"
89
+ }
@@ -0,0 +1,127 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * Build the gallery shell + type-specific frame bundles into static assets
4
+ * the service can serve.
5
+ *
6
+ * 1. `dist/gallery/app.js` — the shell controller. Loaded as a
7
+ * `<script type="module">` in `index.html`.
8
+ * 2. `dist/gallery/frame/runtime/<type>.js` — one trusted frame bundle
9
+ * per artifact type. The frame URL selects the bundle before any
10
+ * artifact bytes reach the renderer.
11
+ *
12
+ * The build target is `browser` and minify is OFF — we want the
13
+ * emitted bundle to be human-readable so an audit can verify no
14
+ * artifact bytes leak into it. The `dist/` directory is gitignored.
15
+ */
16
+
17
+ import { copyFileSync, mkdirSync, readdirSync, rmSync, statSync } from "node:fs";
18
+ import { join } from "node:path";
19
+
20
+ import { frameBundlePlugins } from "../src/shared/build/frame-bundle-plugins";
21
+ import { ARTIFACT_TYPES } from "../src/shared/contracts/artifact-types";
22
+ import { resolveGalleryRoot } from "../src/shared/config/paths";
23
+
24
+ const REPO_ROOT = import.meta.dir.replace(/\/scripts$/, "");
25
+ const FRAME_ENTRY_DIR = join(REPO_ROOT, "src", "gallery-web", "frame", "entries");
26
+ const FRAME_STYLE_DIR = join(REPO_ROOT, "src", "gallery-web", "frame", "styles");
27
+ const GALLERY_INPUTS = [
28
+ join(REPO_ROOT, "scripts", "build-gallery.ts"),
29
+ join(REPO_ROOT, "src", "gallery-web"),
30
+ join(REPO_ROOT, "src", "shared", "build", "frame-bundle-plugins.ts"),
31
+ join(REPO_ROOT, "src", "shared", "contracts", "artifact-types.ts"),
32
+ ];
33
+
34
+ function latestMtime(path: string): number {
35
+ const stats = statSync(path);
36
+ if (!stats.isDirectory()) return stats.mtimeMs;
37
+ return Math.max(
38
+ stats.mtimeMs,
39
+ ...readdirSync(path).map((entry) => latestMtime(join(path, entry))),
40
+ );
41
+ }
42
+
43
+ function galleryNeedsBuild(outDir: string): boolean {
44
+ const outputPath = join(outDir, "index.html");
45
+ try {
46
+ return GALLERY_INPUTS.some((input) => latestMtime(input) > statSync(outputPath).mtimeMs);
47
+ } catch {
48
+ return true;
49
+ }
50
+ }
51
+
52
+ /**
53
+ * All frame entries build in ONE `Bun.build` call, not one call per
54
+ * artifact type. Splitting still applies (plain markdown must not pay
55
+ * Mermaid's multi-megabyte load + parse cost when it has no fence),
56
+ * but a shared dependency — Mermaid, reached from BOTH the `mermaid`
57
+ * and `markdown` entries — now gets ONE tree-shaking pass and ONE set
58
+ * of lazy diagram-type chunks instead of two independently rebuilt,
59
+ * NOT-byte-identical copies. Two separate `Bun.build` invocations per
60
+ * entry produced two divergent tree-shakes of Mermaid's lazy-loaded
61
+ * flowchart renderer; the copy reached through `markdown`'s dynamic
62
+ * `import("./mermaid")` silently dropped every flowchart label while
63
+ * the copy reached through `mermaid`'s own top-level entry did not
64
+ * (`nodes 6 · errors 0 · ok` either way — no gate saw it). One shared
65
+ * build graph means one shared chunk, so there is nothing left to
66
+ * diverge.
67
+ */
68
+ async function buildFrameEntries(entries: string[], outDir: string): Promise<void> {
69
+ const result = await Bun.build({
70
+ entrypoints: entries,
71
+ outdir: outDir,
72
+ target: "browser",
73
+ minify: false,
74
+ splitting: true,
75
+ naming: {
76
+ entry: "frame/runtime/[name].[ext]",
77
+ chunk: "frame/chunks/[name]-[hash].[ext]",
78
+ asset: "frame/assets/[name].[ext]",
79
+ },
80
+ plugins: frameBundlePlugins(),
81
+ });
82
+ if (!result.success) {
83
+ const messages = result.logs.map((log) => log.message).join("\n");
84
+ throw new Error(`Frame entry build failed:\n${messages}`);
85
+ }
86
+ }
87
+
88
+ async function main(): Promise<void> {
89
+ const outDir = resolveGalleryRoot(REPO_ROOT);
90
+ if (process.argv.includes("--if-stale") && !galleryNeedsBuild(outDir)) {
91
+ process.stderr.write(`${JSON.stringify({ event: "gallery.current", outdir: outDir })}\n`);
92
+ return;
93
+ }
94
+ mkdirSync(outDir, { recursive: true });
95
+ rmSync(join(outDir, "frame", "bootstrap.js"), { force: true });
96
+ rmSync(join(outDir, "frame", "bootstrap"), { recursive: true, force: true });
97
+ rmSync(join(outDir, "frame", "runtime"), { recursive: true, force: true });
98
+ rmSync(join(outDir, "frame", "chunks"), { recursive: true, force: true });
99
+ rmSync(join(outDir, "frame", "assets"), { recursive: true, force: true });
100
+ rmSync(join(outDir, "frame", "frame.css"), { force: true });
101
+ rmSync(join(outDir, "frame", "artifact.css"), { force: true });
102
+ await buildFrameEntries(
103
+ ARTIFACT_TYPES.map((artifactType) => join(FRAME_ENTRY_DIR, `${artifactType}.ts`)),
104
+ outDir,
105
+ );
106
+ mkdirSync(join(outDir, "frame"), { recursive: true });
107
+ copyFileSync(join(FRAME_STYLE_DIR, "frame.css"), join(outDir, "frame", "frame.css"));
108
+ copyFileSync(join(FRAME_STYLE_DIR, "artifact.css"), join(outDir, "frame", "artifact.css"));
109
+ // The shell controller is bundled from index.html so the CSS imports
110
+ // + app.ts are wired into a single bundle the service serves.
111
+ const shellResult = await Bun.build({
112
+ entrypoints: [join(REPO_ROOT, "src", "gallery-web", "index.html")],
113
+ outdir: outDir,
114
+ target: "browser",
115
+ minify: false,
116
+ naming: "[dir]/[name].[ext]",
117
+ });
118
+ if (!shellResult.success) {
119
+ const messages = shellResult.logs.map((log) => log.message).join("\n");
120
+ throw new Error(`Shell build failed:\n${messages}`);
121
+ }
122
+ process.stderr.write(`${JSON.stringify({ event: "gallery.built", outdir: outDir })}\n`);
123
+ }
124
+
125
+ if (import.meta.main) {
126
+ await main();
127
+ }
@@ -0,0 +1,67 @@
1
+ #!/bin/sh
2
+ #
3
+ # Tier 1 production netns launcher.
4
+ #
5
+ # Wraps the PINNED `chrome-headless-shell` (ADR 0001 D3) inside a
6
+ # rootless network namespace so every egress channel (HTTP, WS,
7
+ # EventSource, sendBeacon, anchor ping, DNS, WebRTC STUN, raw IP,
8
+ # future IP APIs) has no reachable interface or route.
9
+ #
10
+ # The boundary is the `unshare --map-current-user --net` namespace,
11
+ # not any flag passed to the shell. The egress penetration harness
12
+ # (scripts/egress-penetration.ts) is the regression test for this
13
+ # boundary; the spike at .facet-build/phase-0-spikes/egress/
14
+ # demonstrated that 12 of 12 attempted channels block under this
15
+ # wrapper on the same chromium-build family.
16
+ #
17
+ # Sandbox-flag trade (pinned chrome-headless-shell 151.0.7922.77):
18
+ #
19
+ # We DELIBERATELY keep Chromium's browser sandbox (`--no-sandbox` is
20
+ # NOT added). The fidelity spike ran with `--no-sandbox` because it
21
+ # was an isolated disposable experiment. The production tier keeps
22
+ # the sandbox because:
23
+ #
24
+ # 1. `unshare --map-current-user` keeps the user uid (no root
25
+ # transition), so Chromium does not require `--no-sandbox` to
26
+ # start under the new userns.
27
+ # 2. A compromised renderer reaching the netns (no interfaces)
28
+ # still benefits from the Chromium sandbox's syscall filter.
29
+ #
30
+ # If a future shell release regresses this, the wrapper surfaces a
31
+ # typed `tier1_launcher_missing` / `tier1_unavailable` and the
32
+ # runner falls back to erroring — NEVER to running un-sandboxed.
33
+ #
34
+ # Usage: this script is invoked by puppeteer-core with `pipe: true`;
35
+ # it must `exec` so the unshare PID is reused (otherwise signals
36
+ # targeting the shell PID bypass the namespace).
37
+
38
+ set -eu
39
+
40
+ if [ -z "${FACET_TIER1_BROWSER_CACHE:-}" ]; then
41
+ FACET_TIER1_BROWSER_CACHE="${HOME}/.cache/puppeteer/chrome-headless-shell"
42
+ fi
43
+
44
+ if [ -z "${FACET_TIER1_PINNED_VERSION:-}" ]; then
45
+ FACET_TIER1_PINNED_VERSION="151.0.7922.77"
46
+ fi
47
+
48
+ # Resolve the pinned shell binary inside the cache. The wrapper does
49
+ # not hard-code the layout because the cache directory can be relocated
50
+ # via env var (CI runners, vendored copies).
51
+ candidate_a="${FACET_TIER1_BROWSER_CACHE}/${FACET_TIER1_PINNED_VERSION}/chrome-headless-shell-linux64/chrome-headless-shell"
52
+ candidate_b="${FACET_TIER1_BROWSER_CACHE}/linux-${FACET_TIER1_PINNED_VERSION}/chrome-headless-shell-linux64/chrome-headless-shell"
53
+
54
+ if [ -x "${candidate_a}" ]; then
55
+ SHELL_BIN="${candidate_a}"
56
+ elif [ -x "${candidate_b}" ]; then
57
+ SHELL_BIN="${candidate_b}"
58
+ else
59
+ echo "tier1 launcher: pinned chrome-headless-shell ${FACET_TIER1_PINNED_VERSION} not found in ${FACET_TIER1_BROWSER_CACHE}" >&2
60
+ exit 127
61
+ fi
62
+
63
+ # Hand off to unshare. The `--` terminates unshare's option parsing so
64
+ # a flag-shaped argv from puppeteer-core cannot be mistaken for a
65
+ # wrapper flag. `exec` replaces the shell process so the unshare PID
66
+ # is the chrome PID (signals reach the namespace directly).
67
+ exec unshare --map-current-user --net -- "${SHELL_BIN}" "$@"