@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,218 @@
1
+ import { readFileSync, realpathSync } from "node:fs";
2
+ import { isAbsolute, relative } from "node:path";
3
+
4
+ import {
5
+ ExportResultSchema,
6
+ type ExportRequest,
7
+ type ExportResult,
8
+ } from "../shared/contracts/commands";
9
+ import type { Artifact, Revision } from "../shared/contracts/artifact";
10
+ import {
11
+ mediaTypeForEvidenceImage,
12
+ sniffEvidenceImageFormat,
13
+ type EvidenceImageFormat,
14
+ } from "../shared/evidence-image";
15
+ import { buildExportSidecar } from "../shared/export";
16
+ import { FacetError } from "../shared/errors/facet-error";
17
+ import { now } from "../shared/util/time";
18
+ import { latestStoredVerdict, verdictFromStoredRun } from "./stored-verdict";
19
+ import type { ArtifactRepository } from "./store/repository";
20
+
21
+ export { buildExportSidecar } from "../shared/export";
22
+
23
+ function resolveExportTarget(
24
+ repository: ArtifactRepository,
25
+ command: ExportRequest,
26
+ ): { artifact: Artifact; revision: Revision } {
27
+ const artifact = repository.getArtifactById(command.artifactId);
28
+ if (artifact === null) {
29
+ throw new FacetError("artifact_not_found", "Artifact not found", {
30
+ retryable: false,
31
+ details: { artifactId: command.artifactId },
32
+ });
33
+ }
34
+
35
+ const revision =
36
+ command.revisionSha === undefined
37
+ ? repository.getLatestRevision(command.artifactId)
38
+ : repository.getRevisionBySha(command.artifactId, command.revisionSha);
39
+ if (revision === null) {
40
+ throw new FacetError("revision_not_found", "Revision not found", {
41
+ retryable: false,
42
+ details: {
43
+ artifactId: command.artifactId,
44
+ ...(command.revisionSha === undefined ? {} : { revisionSha: command.revisionSha }),
45
+ },
46
+ });
47
+ }
48
+ return { artifact, revision };
49
+ }
50
+
51
+ function evidenceUnavailable(artifact: Artifact, revision: Revision, cause?: unknown): FacetError {
52
+ return new FacetError("evidence_unavailable", "Screenshot evidence unavailable", {
53
+ retryable: false,
54
+ ...(cause === undefined ? {} : { cause }),
55
+ details: { artifactId: artifact.id, revisionSha: revision.sha256 },
56
+ });
57
+ }
58
+
59
+ // Legacy evidence roots whose tolerant read has already been reported.
60
+ // The service is long-lived, so the migration warning fires at most once
61
+ // per process per legacy root.
62
+ const loggedLegacyEvidenceRoots = new Set<string>();
63
+
64
+ function resolveEvidencePath(
65
+ repository: ArtifactRepository,
66
+ screenshotPath: string,
67
+ artifact: Artifact,
68
+ revision: Revision,
69
+ ): string {
70
+ const evidenceRoot = repository.getEvidenceRoot();
71
+ if (evidenceRoot === undefined) throw evidenceUnavailable(artifact, revision);
72
+ // The stored screenshot path is absolute, so the read root is decided per
73
+ // path, not by "is the canonical root empty": try the canonical root first,
74
+ // then the legacy child-derived root (pre explicit-threading installs wrote
75
+ // evidence there). The legacy root is read-only — never written.
76
+ const roots: Array<{ root: string; legacy: boolean }> = [{ root: evidenceRoot, legacy: false }];
77
+ const legacyRoot = repository.getLegacyEvidenceRoot();
78
+ if (legacyRoot !== undefined && legacyRoot !== evidenceRoot) {
79
+ roots.push({ root: legacyRoot, legacy: true });
80
+ }
81
+ for (const { root, legacy } of roots) {
82
+ try {
83
+ const resolvedRoot = realpathSync(root);
84
+ const resolvedCandidate = realpathSync(screenshotPath);
85
+ const relativeCandidate = relative(resolvedRoot, resolvedCandidate);
86
+ if (
87
+ relativeCandidate.length === 0 ||
88
+ isAbsolute(relativeCandidate) ||
89
+ relativeCandidate === ".." ||
90
+ relativeCandidate.startsWith("../")
91
+ ) {
92
+ // Not under this root — try the next (legacy) root.
93
+ continue;
94
+ }
95
+ if (legacy && !loggedLegacyEvidenceRoots.has(root)) {
96
+ loggedLegacyEvidenceRoots.add(root);
97
+ console.warn(
98
+ `facet: reading legacy evidence root ${root} (tolerant read; new evidence is written to ${evidenceRoot})`,
99
+ );
100
+ }
101
+ return resolvedCandidate;
102
+ } catch {
103
+ // root missing or candidate unresolvable — try the next root
104
+ }
105
+ }
106
+ throw evidenceUnavailable(artifact, revision);
107
+ }
108
+
109
+ export function readStoredRenderEvidence(input: {
110
+ readonly repository: ArtifactRepository;
111
+ readonly artifact: Artifact;
112
+ readonly revision: Revision;
113
+ }): {
114
+ bytes: Uint8Array;
115
+ format: EvidenceImageFormat;
116
+ mediaType: ReturnType<typeof mediaTypeForEvidenceImage>;
117
+ verdict: ReturnType<typeof verdictFromStoredRun>;
118
+ } {
119
+ const run = input.repository.listRenderRuns({ revisionId: input.revision.id, tier: 1 })[0];
120
+ if (run === undefined || run.screenshotPath === null) {
121
+ throw evidenceUnavailable(input.artifact, input.revision);
122
+ }
123
+
124
+ try {
125
+ const screenshotPath = resolveEvidencePath(
126
+ input.repository,
127
+ run.screenshotPath,
128
+ input.artifact,
129
+ input.revision,
130
+ );
131
+ const bytes = new Uint8Array(readFileSync(screenshotPath));
132
+ // File bytes are authoritative; an unknown signature is unavailable evidence, not PNG.
133
+ const format = sniffEvidenceImageFormat(bytes);
134
+ if (format === null) throw evidenceUnavailable(input.artifact, input.revision);
135
+ return {
136
+ bytes,
137
+ format,
138
+ mediaType: mediaTypeForEvidenceImage(format),
139
+ verdict: verdictFromStoredRun(input.revision, run),
140
+ };
141
+ } catch (cause) {
142
+ if (cause instanceof FacetError) throw cause;
143
+ throw evidenceUnavailable(input.artifact, input.revision, cause);
144
+ }
145
+ }
146
+
147
+ export function exportStoredSource(input: {
148
+ readonly repository: ArtifactRepository;
149
+ readonly command: ExportRequest;
150
+ readonly requestId: string;
151
+ readonly exportedAt?: string;
152
+ }): ExportResult {
153
+ if (input.command.format === "render") {
154
+ throw new FacetError("evidence_unavailable", "Render export is not implemented", {
155
+ retryable: false,
156
+ details: { format: input.command.format },
157
+ });
158
+ }
159
+
160
+ const { artifact, revision } = resolveExportTarget(input.repository, input.command);
161
+
162
+ const verdict = latestStoredVerdict(input.repository, revision);
163
+ if (verdict === null) {
164
+ throw new FacetError("revision_not_found", "No stored verdict for revision", {
165
+ retryable: false,
166
+ details: { revisionId: revision.id },
167
+ });
168
+ }
169
+
170
+ const format = input.command.format;
171
+ return ExportResultSchema.parse({
172
+ command: "export",
173
+ requestId: input.requestId,
174
+ format,
175
+ bytes: Buffer.from(revision.source).toString("base64"),
176
+ sidecar: buildExportSidecar({
177
+ artifactId: artifact.id,
178
+ slug: artifact.slug,
179
+ revisionSha: revision.sha256,
180
+ artifactType: revision.artifactType,
181
+ renderer: revision.renderer,
182
+ verdict,
183
+ format,
184
+ exportedAt: input.exportedAt ?? now(),
185
+ }),
186
+ });
187
+ }
188
+
189
+ export function exportStoredRender(input: {
190
+ readonly repository: ArtifactRepository;
191
+ readonly command: ExportRequest;
192
+ readonly requestId: string;
193
+ readonly exportedAt?: string;
194
+ }): ExportResult {
195
+ const { artifact, revision } = resolveExportTarget(input.repository, input.command);
196
+ const evidence = readStoredRenderEvidence({
197
+ repository: input.repository,
198
+ artifact,
199
+ revision,
200
+ });
201
+ return ExportResultSchema.parse({
202
+ command: "export",
203
+ requestId: input.requestId,
204
+ format: "render",
205
+ bytes: Buffer.from(evidence.bytes).toString("base64"),
206
+ sidecar: buildExportSidecar({
207
+ artifactId: artifact.id,
208
+ slug: artifact.slug,
209
+ revisionSha: revision.sha256,
210
+ artifactType: revision.artifactType,
211
+ renderer: revision.renderer,
212
+ verdict: evidence.verdict,
213
+ format: "render",
214
+ renderFormat: evidence.format,
215
+ exportedAt: input.exportedAt ?? now(),
216
+ }),
217
+ });
218
+ }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * HTTP utilities shared by the router and the SSE stream.
3
+ *
4
+ * `envelopeResponse` is the single Response builder for every Facet
5
+ * route. The `cache-control: no-store` header is universal — no Facet
6
+ * response is cacheable, and a divergent cache header between routes
7
+ * would let a proxy pin a stale error envelope for a future caller.
8
+ *
9
+ * `generateRequestId` lives in `src/shared/util/time.ts` and is
10
+ * re-exported here so the existing import sites (router.ts, stream.ts)
11
+ * keep working without churn.
12
+ */
13
+
14
+ import { type FacetEnvelope } from "../shared/contracts/envelope";
15
+ import { generateRequestId } from "../shared/util/time";
16
+
17
+ export { generateRequestId };
18
+
19
+ const NO_STORE = "no-store";
20
+ const TIER1_TRACE = process.env.FACET_TIER1_TRACE === "1";
21
+
22
+ function traceTier1Transport(stage: string): void {
23
+ if (!TIER1_TRACE) return;
24
+ process.stderr.write(`[tier1-transport] ${stage}\n`);
25
+ }
26
+
27
+ export function envelopeResponse(envelope: FacetEnvelope<unknown>, status: number): Response {
28
+ traceTier1Transport(
29
+ `envelope-response:serialize:start status=${status} requestId=${envelope.requestId}`,
30
+ );
31
+ const body = JSON.stringify(envelope);
32
+ traceTier1Transport(
33
+ `envelope-response:serialize:complete bytes=${body.length} status=${status} requestId=${envelope.requestId}`,
34
+ );
35
+ const response = new Response(body, {
36
+ status,
37
+ headers: {
38
+ "content-type": "application/json",
39
+ "cache-control": NO_STORE,
40
+ },
41
+ });
42
+ traceTier1Transport(`envelope-response:return status=${status} requestId=${envelope.requestId}`);
43
+ return response;
44
+ }
45
+
46
+ /**
47
+ * Prefer a caller-supplied request id (when present and non-empty),
48
+ * otherwise mint one. The router uses the `null` form to always mint;
49
+ * a future caller can forward an upstream correlation id unchanged.
50
+ */
51
+ export function pickRequestId(header: string | null | undefined): string {
52
+ if (header === null || header === undefined) return generateRequestId();
53
+ const trimmed = header.trim();
54
+ return trimmed.length > 0 ? trimmed : generateRequestId();
55
+ }
@@ -0,0 +1,210 @@
1
+ /**
2
+ * Lexical expectations — the ONLY type-adjacent service module. It
3
+ * scans source bytes as TEXT to count fenced blocks (total + per-language
4
+ * for `mermaid`) and to estimate the renderer root count, WITHOUT
5
+ * importing any markdown, mermaid, svg, or dom-shim library.
6
+ *
7
+ * The boundary checker (scripts/check-boundaries.ts) tolerates this
8
+ * single file under src/service/ because the whole point of the
9
+ * service is to remain byte-dumb while still being able to compute
10
+ * lexical expectations the verifier compares against its own observed
11
+ * counts.
12
+ */
13
+
14
+ import { MAX_MERMAID_BLOCKS, MAX_MERMAID_NODES } from "../../shared/config/limits";
15
+ import type { ArtifactType } from "../../shared/contracts/artifact";
16
+ import type { Renderer } from "../../shared/contracts/renderers";
17
+ import { countMermaidNodeDeclarations } from "../../shared/util/mermaid-nodes";
18
+
19
+ export interface FencedBlockCounts {
20
+ /** Total number of fenced blocks (any language) in the source. */
21
+ readonly total: number;
22
+ /** Number of fenced blocks whose info string equals "mermaid" (trimmed, case-sensitive). */
23
+ readonly mermaid: number;
24
+ /**
25
+ * Per-language counts. Key is the trimmed info string of the fence
26
+ * (e.g. "mermaid", "typescript"); fenced blocks with no info string
27
+ * collapse into the empty-string key.
28
+ */
29
+ readonly byLanguage: ReadonlyMap<string, number>;
30
+ }
31
+
32
+ export interface LexicalExpectations {
33
+ readonly totalFencedBlocks: number;
34
+ readonly mermaidBlocks: number;
35
+ readonly fencedBlocksByLanguage: ReadonlyMap<string, number>;
36
+ /**
37
+ * Expected number of renderer-root SVGs. For markdown sources this
38
+ * is the mermaid block count (each fenced mermaid block produces one
39
+ * renderer root); for mermaid/svg and SVG-rendered chart sources it is 1
40
+ * (the whole document renders as one renderer-owned root). Canvas charts
41
+ * have no SVG root. The verifier counts
42
+ * the actual SVGs and the verdict fails closed on any divergence.
43
+ */
44
+ readonly expectedRendererRoots: number;
45
+ /** Expected opaque regions produced by renderer modes without SVG roots. */
46
+ readonly expectedOpaqueRegions: number;
47
+ /**
48
+ * Lexical node expectation across all Mermaid blocks. `null` is a typed
49
+ * no-expectation marker for a diagram type without a maintained `g.node`
50
+ * grammar, so Tier 1 skips this one comparison rather than treating zero
51
+ * as a vacuous match.
52
+ */
53
+ readonly mermaidNodeCount: number | null;
54
+ /** True iff the source exceeds MAX_MERMAID_BLOCKS or MAX_MERMAID_NODES. */
55
+ readonly exceedsComplexityBudget: boolean;
56
+ }
57
+
58
+ /**
59
+ * Fence opener: a line that begins with three-or-more backticks OR
60
+ * three-or-more tildes, optionally followed by an info string. Closing
61
+ * fence: a line of the same character whose run length is at least the
62
+ * opener's run length. Info strings are trimmed; an empty info string
63
+ * is allowed and maps to the "" key.
64
+ */
65
+ const FENCE_OPENER_RE = /^[ ]{0,3}(`{3,}|~{3,})([^\n]*)$/gm;
66
+
67
+ interface FenceSpan {
68
+ readonly start: number;
69
+ readonly end: number;
70
+ readonly info: string;
71
+ readonly char: "`" | "~";
72
+ }
73
+
74
+ interface FenceCandidate {
75
+ readonly index: number;
76
+ readonly length: number;
77
+ readonly char: "`" | "~";
78
+ /** Trimmed info string after the fence chars; "" when only whitespace. */
79
+ readonly info: string;
80
+ }
81
+
82
+ /**
83
+ * A closing code fence must use the same character as the opening
84
+ * fence, be at least as long, and carry NO info string (only whitespace
85
+ * after the fence chars). Without the info-string check, an inner fence
86
+ * with a language tag (e.g. ```mermaid inside a ```mermaid body)
87
+ * would silently close its enclosing fence and split one block into two.
88
+ */
89
+ function isValidCloser(candidate: FenceCandidate, open: FenceCandidate): boolean {
90
+ return candidate.char === open.char && candidate.length >= open.length && candidate.info === "";
91
+ }
92
+
93
+ function findFenceSpans(text: string): FenceSpan[] {
94
+ const spans: FenceSpan[] = [];
95
+ const candidates: FenceCandidate[] = [];
96
+ for (const match of text.matchAll(FENCE_OPENER_RE)) {
97
+ const index = match.index ?? 0;
98
+ const fence = match[1] ?? "";
99
+ const info = (match[2] ?? "").trim();
100
+ const char: "`" | "~" = fence.startsWith("`") ? "`" : "~";
101
+ candidates.push({ index, length: fence.length, char, info });
102
+ }
103
+ let open: FenceCandidate | null = null;
104
+ for (const candidate of candidates) {
105
+ if (open === null) {
106
+ open = candidate;
107
+ continue;
108
+ }
109
+ if (isValidCloser(candidate, open)) {
110
+ spans.push({
111
+ start: open.index,
112
+ end: candidate.index + candidate.length,
113
+ info: open.info,
114
+ char: open.char,
115
+ });
116
+ open = null;
117
+ continue;
118
+ }
119
+ // Not a valid closer (different char, shorter run, or carries an
120
+ // info string). The candidate is body content of the open fence;
121
+ // keep looking for the real closer.
122
+ }
123
+ return spans;
124
+ }
125
+
126
+ function countMermaidNodes(text: string, spans: readonly FenceSpan[]): number | null {
127
+ let count = 0;
128
+ for (const span of spans) {
129
+ if (span.info !== "mermaid") continue;
130
+ const body = text.slice(span.start, span.end);
131
+ const nodes = countMermaidNodeDeclarations(body);
132
+ if (nodes === null) return null;
133
+ count += nodes;
134
+ }
135
+ return count;
136
+ }
137
+
138
+ /**
139
+ * Count fenced code blocks. Returns total + per-language + the
140
+ * special-cased mermaid count. The byte input is decoded as UTF-8;
141
+ * the caller is expected to pass artifact source bytes, not arbitrary
142
+ * binary blobs.
143
+ */
144
+ export function countFencedBlocks(bytes: Uint8Array): {
145
+ total: number;
146
+ mermaid: number;
147
+ byLanguage: Map<string, number>;
148
+ } {
149
+ const text = new TextDecoder("utf-8", { fatal: false }).decode(bytes);
150
+ const spans = findFenceSpans(text);
151
+ const byLanguage = new Map<string, number>();
152
+ let mermaid = 0;
153
+ for (const span of spans) {
154
+ const key = span.info;
155
+ byLanguage.set(key, (byLanguage.get(key) ?? 0) + 1);
156
+ if (key === "mermaid") mermaid += 1;
157
+ }
158
+ return { total: spans.length, mermaid, byLanguage };
159
+ }
160
+
161
+ /** Convenience wrapper that returns only the mermaid block count. */
162
+ export function countMermaidBlocks(bytes: Uint8Array): number {
163
+ return countFencedBlocks(bytes).mermaid;
164
+ }
165
+
166
+ /**
167
+ * Compute the full lexical expectation surface for a source. The
168
+ * shape is the one consumed later by Tier 0 / Tier 1 verifiers to
169
+ * compare lexical expectations against observed counters.
170
+ *
171
+ * The expectations are artifact-type aware because the renderers are:
172
+ * markdown produces one renderer root per mermaid fence; mermaid,
173
+ * svg, and chart each render the whole document as ONE renderer-owned
174
+ * root. Node declarations are predicted only for the diagram-bearing
175
+ * types (markdown fences, mermaid documents).
176
+ */
177
+ export function computeLexicalExpectations(
178
+ bytes: Uint8Array,
179
+ artifactType: ArtifactType,
180
+ renderer: Renderer = "svg",
181
+ ): LexicalExpectations {
182
+ const text = new TextDecoder("utf-8", { fatal: false }).decode(bytes);
183
+ const spans = findFenceSpans(text);
184
+ const byLanguage = new Map<string, number>();
185
+ let mermaid = 0;
186
+ for (const span of spans) {
187
+ byLanguage.set(span.info, (byLanguage.get(span.info) ?? 0) + 1);
188
+ if (span.info === "mermaid") mermaid += 1;
189
+ }
190
+ const mermaidNodeCount =
191
+ artifactType === "markdown"
192
+ ? countMermaidNodes(text, spans)
193
+ : artifactType === "mermaid"
194
+ ? countMermaidNodeDeclarations(text)
195
+ : 0;
196
+ const exceedsComplexityBudget =
197
+ mermaid > MAX_MERMAID_BLOCKS ||
198
+ (mermaidNodeCount !== null && mermaidNodeCount > MAX_MERMAID_NODES);
199
+ const canvasChart = artifactType === "chart" && renderer === "canvas";
200
+ const expectedRendererRoots = canvasChart ? 0 : artifactType === "markdown" ? mermaid : 1;
201
+ return {
202
+ totalFencedBlocks: spans.length,
203
+ mermaidBlocks: mermaid,
204
+ fencedBlocksByLanguage: byLanguage,
205
+ expectedRendererRoots,
206
+ expectedOpaqueRegions: canvasChart ? 1 : 0,
207
+ mermaidNodeCount,
208
+ exceedsComplexityBudget,
209
+ };
210
+ }
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Idle controller — the service's single reason-counted state machine.
3
+ *
4
+ * Three reason kinds exist today (in-flight HTTP request, active SSE
5
+ * gallery lease, running validation job) and the rule is the same for
6
+ * all three: count > 0 keeps the service alive, count == 0 starts a
7
+ * short timer, count still 0 at expiry triggers graceful shutdown.
8
+ *
9
+ * No DB polling, no filesystem watcher, no periodic sweep — the timer
10
+ * is scheduled only when the count transitions to 0. This keeps the
11
+ * dormant cost literally zero: a service that never receives a request
12
+ * never wakes up to check.
13
+ *
14
+ * Lease expiry is handled in the lease manager itself (per-lease
15
+ * unrefed timer) and notifies the controller via its own acquire/release
16
+ * API — there is no separate lease-expiry polling loop here.
17
+ */
18
+
19
+ export interface IdleControllerOptions {
20
+ readonly idleTimeoutMs: number;
21
+ readonly onIdle?: () => void | Promise<void>;
22
+ readonly now?: () => number;
23
+ readonly setTimer?: (handler: () => void, ms: number) => () => void;
24
+ }
25
+
26
+ export interface IdleController {
27
+ acquire(tag: string): void;
28
+ release(tag: string): void;
29
+ count(): number;
30
+ waitUntilIdle(): Promise<void>;
31
+ stop(): void;
32
+ }
33
+
34
+ function defaultClear(handle: ReturnType<typeof setTimeout>): void {
35
+ clearTimeout(handle);
36
+ }
37
+
38
+ export function createIdleController(options: IdleControllerOptions): IdleController {
39
+ const counts = new Map<string, number>();
40
+ const setTimer =
41
+ options.setTimer ??
42
+ ((handler: () => void, ms: number) => {
43
+ const handle = setTimeout(handler, ms);
44
+ if (typeof handle.unref === "function") handle.unref();
45
+ return () => defaultClear(handle);
46
+ });
47
+ let timer: (() => void) | null = null;
48
+ let fired = false;
49
+ let idlePromise: Promise<void> | null = null;
50
+
51
+ function ensureIdlePromise(): Promise<void> {
52
+ if (idlePromise === null) {
53
+ idlePromise = new Promise<void>((resolve) => {
54
+ if (fired) {
55
+ resolve();
56
+ } else {
57
+ // Stash the resolver on a private field via closure.
58
+ idleResolvers.push(resolve);
59
+ }
60
+ });
61
+ }
62
+ return idlePromise;
63
+ }
64
+
65
+ const idleResolvers: Array<() => void> = [];
66
+
67
+ function fire(): void {
68
+ if (fired) return;
69
+ fired = true;
70
+ try {
71
+ options.onIdle?.();
72
+ } catch {
73
+ // The handler is allowed to throw; we deliberately swallow so
74
+ // the idle path always reaches its terminal state.
75
+ }
76
+ for (const resolve of idleResolvers.splice(0)) resolve();
77
+ }
78
+
79
+ function cancelTimer(): void {
80
+ if (timer !== null) {
81
+ timer();
82
+ timer = null;
83
+ }
84
+ }
85
+
86
+ function scheduleIdle(): void {
87
+ cancelTimer();
88
+ timer = setTimer(() => {
89
+ timer = null;
90
+ // Only fire if we are STILL idle when the timer expires.
91
+ if (counts.size === 0) fire();
92
+ }, options.idleTimeoutMs);
93
+ }
94
+
95
+ return {
96
+ acquire(tag) {
97
+ counts.set(tag, (counts.get(tag) ?? 0) + 1);
98
+ if (counts.size === 1) cancelTimer();
99
+ },
100
+ release(tag) {
101
+ const current = counts.get(tag);
102
+ if (current === undefined) return;
103
+ if (current <= 1) counts.delete(tag);
104
+ else counts.set(tag, current - 1);
105
+ if (counts.size === 0 && !fired) scheduleIdle();
106
+ },
107
+ count() {
108
+ let total = 0;
109
+ for (const v of counts.values()) total += v;
110
+ return total;
111
+ },
112
+ waitUntilIdle() {
113
+ if (fired) return Promise.resolve();
114
+ // If count is currently 0 and no timer is scheduled, schedule one
115
+ // now so waitUntilIdle() actually fires — the count-starting-at-zero
116
+ // case has no release() to schedule the timer on.
117
+ if (counts.size === 0 && timer === null) scheduleIdle();
118
+ return ensureIdlePromise();
119
+ },
120
+ stop() {
121
+ cancelTimer();
122
+ fire();
123
+ },
124
+ };
125
+ }