@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,717 @@
1
+ /**
2
+ * Command dispatcher.
3
+ *
4
+ * Pure handler: maps a parsed-and-validated `CommandRequest` to its
5
+ * repository call, returning the result body that the envelope wrapper
6
+ * will hand back to the caller. The dispatcher never touches HTTP
7
+ * concerns (headers, status codes) — those live in router.ts.
8
+ *
9
+ * Errors are surfaced as `FacetError` so the router's envelope helper
10
+ * can map them to the typed error response.
11
+ */
12
+
13
+ import {
14
+ checkArtifactTypeSupported,
15
+ checkExecutionSupported,
16
+ checkRendererSupported,
17
+ normalizeReadBackTier,
18
+ type ArtifactEnvelope,
19
+ type CommandRequest,
20
+ } from "../shared/contracts/commands";
21
+ import type { Artifact, ArtifactType, RenderRun, Revision } from "../shared/contracts/artifact";
22
+ import { ArtifactTypeSchema } from "../shared/contracts/artifact";
23
+ import { RevisionCommittedEventSchema } from "../shared/contracts/events";
24
+ import {
25
+ Tier0InputSchema,
26
+ Tier0ResultSchema,
27
+ Tier0WorkerResultSchema,
28
+ Tier1InputSchema,
29
+ Tier1ResultSchema,
30
+ VerdictObservedSchema,
31
+ VerdictSchema,
32
+ type Tier0Input,
33
+ type Tier0WorkerResult,
34
+ type Tier0Runner,
35
+ type Tier1Input,
36
+ type Tier1Result,
37
+ type Tier1Runner,
38
+ type InsecureLevel,
39
+ type InsecureMarker,
40
+ type TsxExecutionMode,
41
+ type Verdict,
42
+ } from "../shared/contracts/validation";
43
+ import { FacetError } from "../shared/errors/facet-error";
44
+ import { SOURCE_CAP_BYTES, TIER1_PINNED_VERSION } from "../shared/config/limits";
45
+ import { computeLexicalExpectations } from "./lexical/expectations";
46
+ import { enrichVerdict, insecureMarker } from "./verdict-enrichment";
47
+ import { exportStoredRender, exportStoredSource } from "./export";
48
+ import { verdictFromStoredRun } from "./stored-verdict";
49
+
50
+ import type { GalleryLeaseManager } from "./security/leases";
51
+ import type { IdleController } from "./lifecycle/idle-controller";
52
+ import type { ArtifactRepository } from "./store/repository";
53
+ import { ensureRunEvidenceDirectory } from "./store/evidence-retention";
54
+ import { readFileSync } from "node:fs";
55
+ import { join } from "node:path";
56
+
57
+ const TIER1_TRACE = process.env.FACET_TIER1_TRACE === "1";
58
+
59
+ function traceTier1Transport(stage: string): void {
60
+ if (!TIER1_TRACE) return;
61
+ process.stderr.write(`[tier1-transport] ${stage}\n`);
62
+ }
63
+
64
+ /**
65
+ * Tier 0 runner contract lives in `shared/contracts/validation.ts` so
66
+ * the service can depend on it as a TYPE without importing the
67
+ * implementation in `src/validation/`. The default runner is built by
68
+ * callers (`src/cli/`, integration tests) and injected; this keeps the
69
+ * service byte-dumb AND the boundary checker clean.
70
+ */
71
+
72
+ export interface DispatcherDeps {
73
+ readonly insecureLevel: InsecureLevel;
74
+ readonly insecureReason: string | null;
75
+ readonly repository: ArtifactRepository;
76
+ readonly leases: GalleryLeaseManager;
77
+ readonly idle: IdleController;
78
+ readonly tier0Runner: Tier0Runner;
79
+ /** Optional Tier 1 verifier, invoked only by explicit visual read-back. */
80
+ readonly tier1Runner: Tier1Runner | undefined;
81
+ /**
82
+ * Write-path seam: called AFTER the revision is committed AND its
83
+ * Tier 0 run is recorded, with the
84
+ * canonical `revision:committed` event. The server wires this to
85
+ * the SSE broadcaster so gallery leases see the revision land.
86
+ */
87
+ readonly onPublished?: (event: {
88
+ readonly type: "revision:committed";
89
+ readonly artifactId: string;
90
+ readonly revisionSha: string;
91
+ readonly revisionNumber: number;
92
+ readonly artifactType: ArtifactType;
93
+ readonly at: string;
94
+ }) => void;
95
+ }
96
+
97
+ function mapArtifact(a: Artifact): ArtifactEnvelope {
98
+ return {
99
+ id: a.id,
100
+ projectId: a.projectId,
101
+ slug: a.slug,
102
+ title: a.title,
103
+ createdAt: a.createdAt,
104
+ updatedAt: a.updatedAt,
105
+ };
106
+ }
107
+
108
+ function buildVerdict(input: {
109
+ artifactId: string;
110
+ revisionSha: string;
111
+ tier: 0 | 1;
112
+ status: string;
113
+ observed: unknown;
114
+ insecure?: InsecureMarker;
115
+ screenshotError?: unknown;
116
+ /**
117
+ * Passed only when the artifact is a TSX revision (static or interactive).
118
+ * Non-TSX verdicts carry NO
119
+ * execution field — the field is absent, not null, so the wire
120
+ * form for non-TSX stays byte-identical to the pre-arc shape.
121
+ */
122
+ execution?: TsxExecutionMode;
123
+ }): Verdict {
124
+ const observed = VerdictObservedSchema.parse(input.observed);
125
+ return VerdictSchema.parse({
126
+ status: input.status,
127
+ tier: input.tier,
128
+ artifactId: input.artifactId,
129
+ revisionSha: input.revisionSha,
130
+ observed,
131
+ ...(input.insecure !== undefined ? { insecure: input.insecure } : {}),
132
+ ...(input.screenshotError !== undefined ? { screenshotError: input.screenshotError } : {}),
133
+ ...(input.execution !== undefined ? { execution: input.execution } : {}),
134
+ });
135
+ }
136
+
137
+ /**
138
+ * Run the Tier 0 worker, mapping a thrown `FacetError` to a synthetic
139
+ * Tier0Result with `status: "error"`. The wire response must still be
140
+ * a publish envelope (the revision IS committed), so the run is
141
+ * recorded as an error-tier observation rather than as a publish
142
+ * failure. The original error code is surfaced via
143
+ * `discriminativeErrors[].code` so the read-back verdict carries the
144
+ * typed reason.
145
+ */
146
+ async function runTier0Safe(runner: Tier0Runner, input: Tier0Input): Promise<Tier0WorkerResult> {
147
+ try {
148
+ return await runner(input);
149
+ } catch (error) {
150
+ const facet = FacetError.from(error);
151
+ return Tier0WorkerResultSchema.parse({
152
+ tier: 0,
153
+ status: "error",
154
+ revisionSha: input.revisionSha,
155
+ expected: input.lexical,
156
+ observed: {
157
+ rendererRootSvgCount: 0,
158
+ graphCount: 0,
159
+ mermaidNodeCount: 0,
160
+ visibleSvgCount: 0,
161
+ opaqueRegionCount: 0,
162
+ externalImageCount: 0,
163
+ viewBoxes: [],
164
+ errorCount: 1,
165
+ discriminativeErrors: [{ code: facet.code, message: facet.message }],
166
+ },
167
+ ...(input.execution === undefined ? {} : { execution: input.execution }),
168
+ });
169
+ }
170
+ }
171
+
172
+ async function retainCompiledEvidence(
173
+ repository: ArtifactRepository,
174
+ artifactId: string,
175
+ revisionSha: string,
176
+ compiled: Tier0WorkerResult["compiled"],
177
+ ): Promise<string | null> {
178
+ if (compiled === undefined) return null;
179
+ const bytes = Uint8Array.from(Buffer.from(compiled.bytesBase64, "base64"));
180
+ const actualSha = new Bun.CryptoHasher("sha256");
181
+ actualSha.update(bytes);
182
+ const actualSha256 = actualSha.digest("hex");
183
+ if (actualSha256 !== compiled.sha256) {
184
+ throw new FacetError("tier0_protocol_error", "Compiled TSX bytes failed SHA-256 validation", {
185
+ retryable: false,
186
+ details: { expectedSha256: compiled.sha256, actualSha256 },
187
+ });
188
+ }
189
+ const evidenceRoot = repository.getEvidenceRoot();
190
+ if (evidenceRoot === undefined) return null;
191
+ const runId = crypto.randomUUID();
192
+ const evidence = ensureRunEvidenceDirectory({ evidenceRoot, artifactId, revisionSha, runId });
193
+ const path = join(
194
+ evidence.directory,
195
+ compiled.mediaType === "text/html" ? "compiled.html" : "compiled.js",
196
+ );
197
+ await Bun.write(path, bytes);
198
+ return path;
199
+ }
200
+
201
+ /**
202
+ * Map a Tier 1 runner failure into a synthetic Tier1Result with
203
+ * `status: "error"`. A failed launch is distinct from a rendered
204
+ * verdict failure, so callers can tell an actionable environment fault
205
+ * from an observed page defect.
206
+ */
207
+ async function runTier1Safe(
208
+ runner: Tier1Runner | undefined,
209
+ input: Tier1Input,
210
+ artifactId: string,
211
+ ): Promise<Tier1Result> {
212
+ try {
213
+ if (runner === undefined) {
214
+ throw new FacetError("tier1_unavailable", "Tier 1 runner is not configured", {
215
+ retryable: false,
216
+ });
217
+ }
218
+ return Tier1ResultSchema.parse(await runner(input));
219
+ } catch (error) {
220
+ return tier1Failure(input, artifactId, error);
221
+ }
222
+ }
223
+
224
+ function tier1Failure(input: Tier1Input, artifactId: string, error: unknown): Tier1Result {
225
+ const facet = FacetError.from(error);
226
+ return Tier1ResultSchema.parse({
227
+ tier: 1,
228
+ status: "error",
229
+ artifactId,
230
+ revisionSha: input.revisionSha,
231
+ expected: input.lexical,
232
+ observed: {
233
+ rendererRootSvgCount: 0,
234
+ graphCount: 0,
235
+ mermaidNodeCount: 0,
236
+ visibleSvgCount: 0,
237
+ opaqueRegionCount: 0,
238
+ externalImageCount: 0,
239
+ viewBoxes: [],
240
+ errorCount: 1,
241
+ discriminativeErrors: [{ code: facet.code, message: facet.message }],
242
+ },
243
+ screenshotPath: null,
244
+ consolePath: null,
245
+ });
246
+ }
247
+
248
+ function tier1InputForRevision(
249
+ revision: Revision,
250
+ tier0Run: RenderRun,
251
+ evidenceDir: string | undefined,
252
+ ): Tier1Input {
253
+ const compiledPath = tier0Run.compiledPath ?? null;
254
+ const source =
255
+ revision.artifactType === "tsx" && compiledPath !== null
256
+ ? new Uint8Array(readFileSync(compiledPath))
257
+ : new Uint8Array(revision.source);
258
+ return Tier1InputSchema.parse({
259
+ revisionSha: revision.sha256,
260
+ artifactType: revision.artifactType,
261
+ renderer: revision.renderer,
262
+ source,
263
+ lexical: JSON.parse(tier0Run.expectedJson),
264
+ ...(revision.artifactType === "tsx" ? { execution: revision.execution ?? "static" } : {}),
265
+ launcherVersion: TIER1_PINNED_VERSION,
266
+ networkNamespace: "facet-tier1-egress-isolated",
267
+ // Thread the canonical evidence root through to the Tier 1 runner so
268
+ // its screenshot/console evidence lands in the SAME root the service
269
+ // (and the parent CLI) agreed on — otherwise the runner falls back to
270
+ // `computeFacetPaths().evidence` and re-introduces the XDG divergence.
271
+ ...(evidenceDir !== undefined ? { evidenceDir } : {}),
272
+ });
273
+ }
274
+
275
+ function recordTier1Run(
276
+ deps: DispatcherDeps,
277
+ revision: Revision,
278
+ result: Tier1Result,
279
+ ): Tier1Result {
280
+ const execution = revision.artifactType === "tsx" ? (revision.execution ?? "static") : undefined;
281
+ const enriched = Tier1ResultSchema.parse(
282
+ enrichVerdict(
283
+ result,
284
+ revision.artifactId,
285
+ revision.sha256,
286
+ insecureMarker(deps.insecureLevel, deps.insecureReason),
287
+ execution,
288
+ ),
289
+ );
290
+ deps.repository.recordRenderRun({
291
+ revisionId: revision.id,
292
+ tier: 1,
293
+ status: enriched.status,
294
+ expected: enriched.expected,
295
+ observed: enriched.observed,
296
+ ...(enriched.screenshotPath !== null ? { screenshotPath: enriched.screenshotPath } : {}),
297
+ ...(enriched.screenshotFormat !== undefined
298
+ ? { screenshotFormat: enriched.screenshotFormat }
299
+ : {}),
300
+ ...(enriched.consolePath !== null ? { consolePath: enriched.consolePath } : {}),
301
+ ...(enriched.screenshotError !== undefined
302
+ ? { screenshotError: enriched.screenshotError }
303
+ : {}),
304
+ insecure: enriched.insecure ?? null,
305
+ });
306
+ return enriched;
307
+ }
308
+
309
+ export async function dispatch(
310
+ deps: DispatcherDeps,
311
+ command: CommandRequest,
312
+ requestId: string,
313
+ ): Promise<unknown> {
314
+ switch (command.command) {
315
+ case "create": {
316
+ const project = deps.repository.getOrCreateProjectById(command.projectId, "/facet");
317
+ const artifact = deps.repository.createArtifact({
318
+ projectId: project.id,
319
+ slug: command.slug,
320
+ title: command.title,
321
+ });
322
+ return { command: "create", requestId, artifact: mapArtifact(artifact) };
323
+ }
324
+ case "publish": {
325
+ const unsupported = checkArtifactTypeSupported(command.artifactType);
326
+ if (unsupported !== null) throw unsupported;
327
+ const rendererError = checkRendererSupported(command.artifactType, command.renderer);
328
+ if (rendererError !== null) throw rendererError;
329
+ const executionError = checkExecutionSupported(command.artifactType, command.execution);
330
+ if (executionError !== null) throw executionError;
331
+ // The reserved-type gate above rejected `html`; re-parse so the
332
+ // rest of the publish path holds the narrowed supported type.
333
+ const artifactType: ArtifactType = ArtifactTypeSchema.parse(command.artifactType);
334
+ // Decode the base64 string into a Uint8Array. The schema validates
335
+ // base64 syntax; here we enforce the SOURCE_CAP_BYTES on the
336
+ // decoded length BEFORE any further work, throwing a typed
337
+ // payload_too_large so the wire response is 413 (not 400).
338
+ const bytes = Uint8Array.from(Buffer.from(command.bytes, "base64"));
339
+ if (bytes.byteLength > SOURCE_CAP_BYTES) {
340
+ throw new FacetError("payload_too_large", "Publish bytes exceed SOURCE_CAP_BYTES", {
341
+ retryable: false,
342
+ details: { sizeBytes: bytes.byteLength, capBytes: SOURCE_CAP_BYTES },
343
+ });
344
+ }
345
+ // D2: TSX execution mode is declared on the request and stored
346
+ // on the revision row. Non-TSX artifacts carry 'static' on disk
347
+ // (the canonical default) and the wire form simply omits the
348
+ // field; TSX artifacts carry the declared value end-to-end.
349
+ const executionMode = artifactType === "tsx" ? (command.execution ?? "static") : "static";
350
+ if (deps.repository.getArtifactById(command.artifactId) === null) {
351
+ throw new FacetError("artifact_not_found", "Artifact not found", {
352
+ retryable: false,
353
+ details: { artifactId: command.artifactId },
354
+ });
355
+ }
356
+ const revision = deps.repository.publishRevision({
357
+ artifactId: command.artifactId,
358
+ artifactType,
359
+ renderer: command.renderer,
360
+ source: bytes,
361
+ ...(command.note !== undefined ? { note: command.note } : {}),
362
+ ...(command.parentRevisionId !== undefined
363
+ ? { parentRevisionId: command.parentRevisionId }
364
+ : {}),
365
+ execution: executionMode,
366
+ });
367
+ // 2. Run Tier 0 over the SAME immutable bytes — never re-decode
368
+ // from the wire or substitute. The runner lives in
369
+ // src/validation (netns'd subprocess); only its contract type
370
+ // is visible to the service. A thrown FacetError here (unshare
371
+ // unavailable, worker died, protocol error, …) aborts the
372
+ // publish AFTER the revision is committed; we still record the
373
+ // partial run so the wire response can carry a verdict-shaped
374
+ // body. A non-ok Tier0Result is normal: the parser rejected the
375
+ // artifact and we persist that decision as a render_run.
376
+ const lexical = computeLexicalExpectations(bytes, artifactType, command.renderer);
377
+ const legacyCounters =
378
+ artifactType === "html"
379
+ ? {
380
+ rendererRootSvgCount: 0,
381
+ mermaidNodeCount: 0,
382
+ visibleSvgCount: 0,
383
+ opaqueRegionCount: 0,
384
+ externalImageCount: 0,
385
+ }
386
+ : {
387
+ rendererRootSvgCount: lexical.expectedRendererRoots,
388
+ mermaidNodeCount: lexical.mermaidNodeCount,
389
+ visibleSvgCount: 0,
390
+ opaqueRegionCount: lexical.expectedOpaqueRegions,
391
+ externalImageCount: 0,
392
+ };
393
+ const tier0Input = Tier0InputSchema.parse({
394
+ revisionSha: revision.sha256,
395
+ artifactType,
396
+ renderer: command.renderer,
397
+ source: bytes,
398
+ lexical: legacyCounters,
399
+ ...(artifactType === "tsx" ? { execution: executionMode } : {}),
400
+ });
401
+ const insecure = insecureMarker(deps.insecureLevel, deps.insecureReason);
402
+ // Execution is revision metadata for TSX only. Other types omit the
403
+ // field rather than serializing null; TSX uses its declared mode.
404
+ const verdictExecution: TsxExecutionMode | undefined =
405
+ artifactType === "tsx" ? executionMode : undefined;
406
+ if (deps.insecureLevel === 3) {
407
+ const verdict = enrichVerdict(
408
+ buildVerdict({
409
+ artifactId: command.artifactId,
410
+ revisionSha: revision.sha256,
411
+ tier: 0,
412
+ status: "insecure:unvalidated",
413
+ observed: {
414
+ rendererRootSvgCount: 0,
415
+ graphCount: 0,
416
+ mermaidNodeCount: 0,
417
+ visibleSvgCount: 0,
418
+ opaqueRegionCount: 0,
419
+ externalImageCount: 0,
420
+ errorCount: 0,
421
+ },
422
+ ...(verdictExecution !== undefined ? { execution: verdictExecution } : {}),
423
+ }),
424
+ command.artifactId,
425
+ revision.sha256,
426
+ insecure,
427
+ verdictExecution,
428
+ );
429
+ deps.repository.recordRenderRun({
430
+ revisionId: revision.id,
431
+ tier: 0,
432
+ status: verdict.status,
433
+ expected: tier0Input.lexical,
434
+ observed: verdict.observed,
435
+ insecure: verdict.insecure ?? null,
436
+ });
437
+ deps.onPublished?.(
438
+ RevisionCommittedEventSchema.parse({
439
+ type: "revision:committed",
440
+ artifactId: command.artifactId,
441
+ revisionSha: revision.sha256,
442
+ revisionNumber: revision.revisionNumber,
443
+ artifactType,
444
+ at: new Date().toISOString(),
445
+ }),
446
+ );
447
+ const { source: _source, ...envelope } = revision;
448
+ void _source;
449
+ return {
450
+ command: "publish",
451
+ requestId,
452
+ revision: envelope,
453
+ verdict,
454
+ };
455
+ }
456
+ const tier0Result = await runTier0Safe(deps.tier0Runner, tier0Input);
457
+ const compiledPath =
458
+ artifactType === "tsx"
459
+ ? await retainCompiledEvidence(
460
+ deps.repository,
461
+ command.artifactId,
462
+ revision.sha256,
463
+ tier0Result.compiled,
464
+ )
465
+ : null;
466
+ // 3. Bind the verdict to (artifactId, revisionSha) via the
467
+ // canonical render_run row so read-back returns it later.
468
+ const enriched = Tier0ResultSchema.parse(
469
+ enrichVerdict(tier0Result, command.artifactId, revision.sha256, insecure, verdictExecution),
470
+ );
471
+ deps.repository.recordRenderRun({
472
+ revisionId: revision.id,
473
+ tier: 0,
474
+ status: enriched.status,
475
+ expected: enriched.expected,
476
+ observed: enriched.observed,
477
+ insecure: enriched.insecure ?? null,
478
+ ...(compiledPath === null ? {} : { compiledPath }),
479
+ });
480
+ // 4. Write-path SSE seam: the revision is committed and its Tier 0
481
+ // verdict is recorded, so gallery streams bound to this
482
+ // artifact may now learn the exact revision to fetch + swap to.
483
+ deps.onPublished?.(
484
+ RevisionCommittedEventSchema.parse({
485
+ type: "revision:committed",
486
+ artifactId: command.artifactId,
487
+ revisionSha: revision.sha256,
488
+ revisionNumber: revision.revisionNumber,
489
+ artifactType,
490
+ at: new Date().toISOString(),
491
+ }),
492
+ );
493
+ const { source: _source, ...envelope } = revision;
494
+ void _source;
495
+ return {
496
+ command: "publish",
497
+ requestId,
498
+ revision: envelope,
499
+ verdict: enriched,
500
+ };
501
+ }
502
+ case "list": {
503
+ const project = deps.repository.getOrCreateProjectById(command.projectId, "/facet");
504
+ const artifacts = deps.repository.listArtifacts({
505
+ projectId: project.id,
506
+ ...(command.slugPrefix !== undefined ? { slugPrefix: command.slugPrefix } : {}),
507
+ ...(command.limit !== undefined ? { limit: command.limit } : {}),
508
+ });
509
+ return {
510
+ command: "list",
511
+ requestId,
512
+ artifacts: artifacts.map(mapArtifact),
513
+ nextCursor: null,
514
+ };
515
+ }
516
+ case "readBack": {
517
+ const revision =
518
+ command.revisionSha === undefined
519
+ ? deps.repository.getLatestRevision(command.artifactId)
520
+ : deps.repository.getRevisionBySha(command.artifactId, command.revisionSha);
521
+ if (revision === null) {
522
+ throw new FacetError("revision_not_found", "Revision not found", {
523
+ retryable: false,
524
+ details: {
525
+ artifactId: command.artifactId,
526
+ ...(command.revisionSha === undefined ? {} : { revisionSha: command.revisionSha }),
527
+ },
528
+ });
529
+ }
530
+ const tier = normalizeReadBackTier(command.tier);
531
+ let runs = deps.repository.listRenderRuns({ revisionId: revision.id, tier });
532
+ if (tier === 1 && runs.length === 0) {
533
+ const tier0Run = deps.repository.listRenderRuns({ revisionId: revision.id, tier: 0 })[0];
534
+ if (tier0Run === undefined) {
535
+ throw new FacetError("tier1_unavailable", "Tier 1 requires a Tier 0 render run", {
536
+ retryable: false,
537
+ details: { revisionId: revision.id },
538
+ });
539
+ }
540
+ const tier1Input = tier1InputForRevision(
541
+ revision,
542
+ tier0Run,
543
+ deps.repository.getEvidenceRoot(),
544
+ );
545
+ const tier1Result =
546
+ revision.artifactType === "tsx" && tier0Run.compiledPath === null
547
+ ? tier1Failure(
548
+ tier1Input,
549
+ revision.artifactId,
550
+ new FacetError(
551
+ "tier1_unavailable",
552
+ "Tier 1 requires compiled TSX evidence from a successful Tier 0 run",
553
+ { retryable: false },
554
+ ),
555
+ )
556
+ : await runTier1Safe(deps.tier1Runner, tier1Input, revision.artifactId);
557
+ recordTier1Run(deps, revision, tier1Result);
558
+ runs = deps.repository.listRenderRuns({ revisionId: revision.id, tier });
559
+ }
560
+ if (runs.length === 0) {
561
+ throw new FacetError("revision_not_found", "No render runs recorded for revision", {
562
+ retryable: false,
563
+ details: { revisionId: revision.id, tier },
564
+ });
565
+ }
566
+ // Single source of truth for cross-boundary verdict reconstruction:
567
+ // `verdictFromStoredRun` handles the tolerant observed read,
568
+ // the execution marker for TSX revisions, and the
569
+ // insecure/screenshotError conditional-spreads. Re-deriving
570
+ // any of these inline drifts; the export sidecar and the
571
+ // gallery router use this helper, and the read-back route must
572
+ // use it too.
573
+ const verdict = verdictFromStoredRun(revision, runs[0]!);
574
+ traceTier1Transport(`readback:build-complete status=${verdict.status}`);
575
+ return { command: "readBack", requestId, renderer: revision.renderer, verdict };
576
+ }
577
+ case "status": {
578
+ const counts =
579
+ command.artifactId === undefined
580
+ ? undefined
581
+ : deps.repository.statusForArtifact({ artifactId: command.artifactId });
582
+ const latestRevision =
583
+ command.artifactId === undefined
584
+ ? undefined
585
+ : deps.repository.getLatestRevision(command.artifactId);
586
+ return {
587
+ command: "status",
588
+ requestId,
589
+ state: "active",
590
+ ...(command.artifactId === undefined ? {} : { artifactId: command.artifactId }),
591
+ ...(counts === undefined
592
+ ? {}
593
+ : {
594
+ revisionCount: counts.revisionCount,
595
+ pinnedCount: counts.pinnedCount,
596
+ templateCount: counts.templateCount,
597
+ ...(latestRevision === undefined || latestRevision === null
598
+ ? {}
599
+ : { latestRevisionSha: latestRevision.sha256 }),
600
+ }),
601
+ activeLeases: deps.leases.list().length,
602
+ };
603
+ }
604
+ case "open": {
605
+ const revision =
606
+ command.revisionSha === undefined
607
+ ? deps.repository.getLatestRevision(command.artifactId)
608
+ : deps.repository.getRevisionBySha(command.artifactId, command.revisionSha);
609
+ if (revision === null) {
610
+ throw new FacetError("revision_not_found", "Revision not found", {
611
+ retryable: false,
612
+ details: {
613
+ artifactId: command.artifactId,
614
+ ...(command.revisionSha === undefined ? {} : { revisionSha: command.revisionSha }),
615
+ },
616
+ });
617
+ }
618
+ const lease = deps.leases.issue({ artifactId: command.artifactId, pid: process.pid });
619
+ deps.idle.acquire(`lease:${lease.leaseId}`);
620
+ // The lease id is NOT embedded in the frameUrl — clients carry
621
+ // it out-of-band (header / cache) so a URL log line, referrer,
622
+ // or browser history cannot leak the lease. The frameUrl only
623
+ // identifies the artifact + revision; the SSE route takes the
624
+ // lease id via `X-Gallery-Lease` header.
625
+ const frameUrl = `facet://frame/${command.artifactId}/${revision.sha256}`;
626
+ return {
627
+ command: "open",
628
+ requestId,
629
+ artifactId: command.artifactId,
630
+ revisionSha: revision.sha256,
631
+ frameUrl,
632
+ launched: false,
633
+ lease: {
634
+ leaseId: lease.leaseId,
635
+ expiresAt: lease.expiresAt,
636
+ },
637
+ };
638
+ }
639
+ case "promote": {
640
+ const template = deps.repository.promoteRevision({
641
+ ...(command.artifactId !== undefined ? { artifactId: command.artifactId } : {}),
642
+ revisionId: command.revisionId,
643
+ name: command.name,
644
+ promotedBy: command.promotedBy,
645
+ ...(command.description !== undefined ? { description: command.description } : {}),
646
+ });
647
+ return { command: "promote", requestId, template };
648
+ }
649
+ case "instantiate": {
650
+ const template = deps.repository.findTemplateByName(command.name);
651
+ if (template === null) {
652
+ throw new FacetError("template_not_found", "Template not found", {
653
+ retryable: false,
654
+ details: { name: command.name },
655
+ });
656
+ }
657
+ const project = deps.repository.getOrCreateProjectById(
658
+ command.projectId ?? template.artifactId,
659
+ "/facet",
660
+ );
661
+ const artifact = deps.repository.createArtifact({
662
+ projectId: project.id,
663
+ slug: command.newSlug,
664
+ title: template.name,
665
+ });
666
+ const source = deps.repository.getRevisionById(template.revisionId);
667
+ if (source === null) {
668
+ throw new FacetError("revision_not_found", "Template revision not found", {
669
+ retryable: false,
670
+ details: { revisionId: template.revisionId },
671
+ });
672
+ }
673
+ deps.repository.publishRevision({
674
+ artifactId: artifact.id,
675
+ artifactType: source.artifactType,
676
+ renderer: source.renderer,
677
+ source: new Uint8Array(source.source),
678
+ note: `Instantiated from template ${template.name}`,
679
+ // Template instantiation preserves declared TSX mode. Non-TSX source
680
+ // omits it and the publish path applies its existing default.
681
+ ...(source.execution !== undefined ? { execution: source.execution } : {}),
682
+ });
683
+ return {
684
+ command: "instantiate",
685
+ requestId,
686
+ artifact: mapArtifact(artifact),
687
+ template,
688
+ };
689
+ }
690
+ case "pin": {
691
+ const revision = deps.repository.getRevisionById(command.revisionId);
692
+ if (revision === null) {
693
+ throw new FacetError("revision_not_found", "Revision not found", {
694
+ retryable: false,
695
+ details: { revisionId: command.revisionId },
696
+ });
697
+ }
698
+ deps.repository.pinRevision(command.revisionId, command.pinned);
699
+ return {
700
+ command: "pin",
701
+ requestId,
702
+ revisionId: command.revisionId,
703
+ pinned: command.pinned,
704
+ };
705
+ }
706
+ case "export": {
707
+ return command.format === "render"
708
+ ? exportStoredRender({ repository: deps.repository, command, requestId })
709
+ : exportStoredSource({ repository: deps.repository, command, requestId });
710
+ }
711
+ default: {
712
+ const exhaustive: never = command;
713
+ void exhaustive;
714
+ throw new FacetError("invalid_request", "Unhandled command", { retryable: false });
715
+ }
716
+ }
717
+ }