@vfrey/vfrey 0.0.1 → 1.0.0-beta.1

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 (610) hide show
  1. package/LICENSE +33 -0
  2. package/README.md +78 -3
  3. package/accounts.example.json +61 -0
  4. package/accounts.first-run.json +41 -0
  5. package/bin/vfrey.mjs +36 -0
  6. package/integration-tools/README.md +43 -0
  7. package/integration-tools/image/pillow.md +32 -0
  8. package/integration-tools/image/playwright.md +33 -0
  9. package/integration-tools/image/resvg.md +36 -0
  10. package/integration-tools/image/satori.md +36 -0
  11. package/integration-tools/image/sharp.md +38 -0
  12. package/integration-tools/video/blender.md +34 -0
  13. package/integration-tools/video/ffmpeg.md +41 -0
  14. package/integration-tools/video/manim.md +37 -0
  15. package/integration-tools/video/motion-canvas.md +37 -0
  16. package/integration-tools/video/remotion.md +50 -0
  17. package/integration-tools/video/revideo.md +32 -0
  18. package/package.json +74 -6
  19. package/skills/vfrey-orchestrator/SKILL.md +15 -0
  20. package/skills/vfrey-orchestrator/channel-video/SKILL.md +41 -0
  21. package/skills/vfrey-product/SKILL.md +63 -0
  22. package/skills/vfrey-product/examples/concurrency-vs-parallelism/framey/cook-one.svg +14 -0
  23. package/skills/vfrey-product/examples/concurrency-vs-parallelism/framey/cook-two.svg +22 -0
  24. package/skills/vfrey-product/examples/concurrency-vs-parallelism/framey/index.html +70 -0
  25. package/skills/vfrey-product/examples/concurrency-vs-parallelism/remotion/cook-one.svg +14 -0
  26. package/skills/vfrey-product/examples/concurrency-vs-parallelism/remotion/cook-two.svg +22 -0
  27. package/skills/vfrey-product/examples/concurrency-vs-parallelism/remotion/index.tsx +49 -0
  28. package/skills/vfrey-product/examples/concurrency-vs-parallelism/script.json +15 -0
  29. package/skills/vfrey-product/examples/octopus-facts/index.html +60 -0
  30. package/skills/vfrey-product/examples/octopus-facts/octopus.svg +8 -0
  31. package/skills/vfrey-product/examples/octopus-facts/script.json +14 -0
  32. package/skills/vfrey-product/push/SKILL.md +15 -0
  33. package/skills/vfrey-product/scenes/NOTICE +5 -0
  34. package/skills/vfrey-product/scenes/blocks.md +57 -0
  35. package/skills/vfrey-product/scenes/craft.md +118 -0
  36. package/skills/vfrey-product/scenes/motion.md +88 -0
  37. package/src/analytics/dashboard.ts +109 -0
  38. package/src/analytics/index.ts +260 -0
  39. package/src/api/client.ts +361 -0
  40. package/src/api/cloud-files.ts +197 -0
  41. package/src/api/cloud-runs.ts +75 -0
  42. package/src/api/cloud.ts +413 -0
  43. package/src/api/commands.ts +330 -0
  44. package/src/api/config.ts +34 -0
  45. package/src/api/creators.ts +463 -0
  46. package/src/api/credentials.ts +152 -0
  47. package/src/api/mail.ts +333 -0
  48. package/src/api/topics.ts +207 -0
  49. package/src/api/types.ts +273 -0
  50. package/src/cli.ts +468 -0
  51. package/src/cloud/bundle.ts +70 -0
  52. package/src/cloud/cloudrun-executor.ts +237 -0
  53. package/src/cloud/executor.ts +281 -0
  54. package/src/cloud/gcs-sign.ts +76 -0
  55. package/src/cloud/job.ts +106 -0
  56. package/src/cloud/remote-job.ts +67 -0
  57. package/src/cloud/render-job.ts +191 -0
  58. package/src/config.ts +309 -0
  59. package/src/dashboard-link.ts +180 -0
  60. package/src/dashboard-start.ts +103 -0
  61. package/src/desk/channel.ts +64 -0
  62. package/src/desk/cli.ts +216 -0
  63. package/src/desk/creators-route.ts +129 -0
  64. package/src/desk/fonts/JetBrainsMono-OFL.txt +93 -0
  65. package/src/desk/fonts/google-sans-flex-latin.woff2 +0 -0
  66. package/src/desk/fonts/jetbrains-mono-medium.woff2 +0 -0
  67. package/src/desk/http.ts +460 -0
  68. package/src/desk/mail-route.ts +103 -0
  69. package/src/desk/overlay.ts +112 -0
  70. package/src/desk/preview.ts +422 -0
  71. package/src/desk/screens.ts +149 -0
  72. package/src/desk/secrets.ts +143 -0
  73. package/src/desk/series.ts +127 -0
  74. package/src/desk/state.ts +77 -0
  75. package/src/desk/studio-route.ts +119 -0
  76. package/src/desk/tick-runner.ts +66 -0
  77. package/src/desk/tick.ts +56 -0
  78. package/src/desk/topics-route.ts +41 -0
  79. package/src/desk/ui/app.css +505 -0
  80. package/src/desk/ui/app.js +185 -0
  81. package/src/desk/ui/apple-icon.png +0 -0
  82. package/src/desk/ui/components.js +79 -0
  83. package/src/desk/ui/core.js +156 -0
  84. package/src/desk/ui/favicon.ico +0 -0
  85. package/src/desk/ui/icon.svg +1 -0
  86. package/src/desk/ui/icons.js +49 -0
  87. package/src/desk/ui/index.html +41 -0
  88. package/src/desk/ui/metrics-math.d.ts +22 -0
  89. package/src/desk/ui/metrics-math.js +135 -0
  90. package/src/desk/ui/theme.js +5 -0
  91. package/src/desk/ui/views-creators.js +209 -0
  92. package/src/desk/ui/views-keys.js +114 -0
  93. package/src/desk/ui/views-mail.js +188 -0
  94. package/src/desk/ui/views-misc.js +123 -0
  95. package/src/desk/ui/views-review.js +119 -0
  96. package/src/desk/ui/views-runs.js +350 -0
  97. package/src/desk/ui/views-screens.js +163 -0
  98. package/src/desk/ui/views-series.js +318 -0
  99. package/src/desk/ui/views-topics.js +87 -0
  100. package/src/desk/verbs.ts +361 -0
  101. package/src/doctor.ts +212 -0
  102. package/src/drain.ts +14 -0
  103. package/src/engage/index.ts +217 -0
  104. package/src/generate/index.ts +110 -0
  105. package/src/generate/script.ts +152 -0
  106. package/src/generate/title.ts +78 -0
  107. package/src/help.ts +252 -0
  108. package/src/home.ts +45 -0
  109. package/src/llm/client.ts +177 -0
  110. package/src/log.ts +7 -0
  111. package/src/oauth/tokens.ts +189 -0
  112. package/src/oauth/vault.ts +72 -0
  113. package/src/pipeline/review.ts +642 -0
  114. package/src/pipeline/run.ts +443 -0
  115. package/src/pipeline/series-guard.ts +26 -0
  116. package/src/pipeline/source-fetch.ts +69 -0
  117. package/src/pipeline/stages.ts +728 -0
  118. package/src/pipeline/topics-source.ts +90 -0
  119. package/src/product/commands.ts +296 -0
  120. package/src/product/config-file.ts +38 -0
  121. package/src/product/engine-choice.ts +89 -0
  122. package/src/product/engines.ts +103 -0
  123. package/src/product/insights.ts +50 -0
  124. package/src/product/keys.ts +67 -0
  125. package/src/product/paths.ts +18 -0
  126. package/src/product/probes.ts +88 -0
  127. package/src/product/push-flow.ts +90 -0
  128. package/src/product/render-video.ts +21 -0
  129. package/src/product/short.ts +62 -0
  130. package/src/product/update.ts +32 -0
  131. package/src/project/canonical.ts +14 -0
  132. package/src/project/commands.ts +177 -0
  133. package/src/project/dashboard-status.ts +28 -0
  134. package/src/project/discover.ts +19 -0
  135. package/src/project/index-file.ts +73 -0
  136. package/src/project/marker.ts +92 -0
  137. package/src/project/migrate.ts +201 -0
  138. package/src/project/names.ts +27 -0
  139. package/src/project/notice.ts +16 -0
  140. package/src/project/open.ts +88 -0
  141. package/src/project/scaffold.ts +112 -0
  142. package/src/project/templates.ts +42 -0
  143. package/src/project/videos.ts +64 -0
  144. package/src/publish/facebook.ts +91 -0
  145. package/src/publish/index.ts +12 -0
  146. package/src/publish/instagram.ts +97 -0
  147. package/src/publish/rate.ts +62 -0
  148. package/src/publish/session.ts +242 -0
  149. package/src/publish/tiktok.ts +202 -0
  150. package/src/publish/youtube.ts +177 -0
  151. package/src/queue/desk-ops.ts +1170 -0
  152. package/src/queue/seal.ts +3 -0
  153. package/src/queue/store.ts +1006 -0
  154. package/src/remotion/AiLabel.tsx +26 -0
  155. package/src/remotion/Root.tsx +73 -0
  156. package/src/remotion/defaults.ts +26 -0
  157. package/src/remotion/films/claude/ClaudeFilm.tsx +1014 -0
  158. package/src/remotion/films/engine/EngineMark.tsx +17 -0
  159. package/src/remotion/films/rea/ReaShort.tsx +552 -0
  160. package/src/remotion/films/rea/drawings.tsx +907 -0
  161. package/src/remotion/films/rea/timing.ts +144 -0
  162. package/src/remotion/index.ts +4 -0
  163. package/src/remotion/templates/EditorialSplit.tsx +39 -0
  164. package/src/remotion/templates/KineticType.tsx +39 -0
  165. package/src/remotion/templates/SignalBoard.tsx +50 -0
  166. package/src/remotion/timing.ts +22 -0
  167. package/src/render/chrome.ts +78 -0
  168. package/src/render/engine-mark.ts +29 -0
  169. package/src/render/framey-template.ts +130 -0
  170. package/src/render/html-engine.ts +149 -0
  171. package/src/render/node-crypto-stub.ts +11 -0
  172. package/src/render/probe.ts +29 -0
  173. package/src/render/props.ts +17 -0
  174. package/src/render/vertical-short.ts +35 -0
  175. package/src/render/webpack.ts +33 -0
  176. package/src/render/worker.ts +65 -0
  177. package/src/scene/assets/fonts/TitanOne-OFL.txt +94 -0
  178. package/src/scene/assets/fonts/titan-one.woff2 +0 -0
  179. package/src/scene/assets/gsap/LICENSE.txt +8 -0
  180. package/src/scene/assets/gsap/gsap.min.js +11 -0
  181. package/src/scene/audio.ts +54 -0
  182. package/src/scene/blocks/LICENSE +190 -0
  183. package/src/scene/blocks/NOTICE +5 -0
  184. package/src/scene/blocks/bar-chart-race/bar-chart-race.html +585 -0
  185. package/src/scene/blocks/beat-freeze-cut/beat-freeze-cut.html +874 -0
  186. package/src/scene/blocks/camcorder-hud/camcorder-hud.html +164 -0
  187. package/src/scene/blocks/camera-dolly-zoom/camera-dolly-zoom.html +355 -0
  188. package/src/scene/blocks/code-diff/code-diff.html +817 -0
  189. package/src/scene/blocks/code-snippet-apple-terminal-pro/code-snippet-apple-terminal-pro.html +248 -0
  190. package/src/scene/blocks/code-typing/code-typing.html +764 -0
  191. package/src/scene/blocks/data-chart/data-chart.html +452 -0
  192. package/src/scene/blocks/flowchart-vertical/flowchart-vertical.html +454 -0
  193. package/src/scene/blocks/hw-scribble-transition/hw-scribble-transition.html +153 -0
  194. package/src/scene/blocks/index.json +824 -0
  195. package/src/scene/blocks/lt-accent-underline/lt-accent-underline.html +107 -0
  196. package/src/scene/blocks/lt-bold-block/lt-bold-block.html +109 -0
  197. package/src/scene/blocks/lt-clean-bar/lt-clean-bar.html +112 -0
  198. package/src/scene/blocks/lt-dark-card/lt-dark-card.html +111 -0
  199. package/src/scene/blocks/lt-kicker-name/lt-kicker-name.html +114 -0
  200. package/src/scene/blocks/lt-mask-reveal/lt-mask-reveal.html +120 -0
  201. package/src/scene/blocks/lt-neon-border/lt-neon-border.html +391 -0
  202. package/src/scene/blocks/lt-stack-bars/lt-stack-bars.html +109 -0
  203. package/src/scene/blocks/macos-notification/macos-notification.html +194 -0
  204. package/src/scene/blocks/mk-background/mk-background.html +236 -0
  205. package/src/scene/blocks/mk-callout-highlight/mk-callout-highlight.html +183 -0
  206. package/src/scene/blocks/mk-clone-wall-transition/mk-clone-wall-transition.html +226 -0
  207. package/src/scene/blocks/mk-line-graph/mk-line-graph.html +293 -0
  208. package/src/scene/blocks/mk-progress-stat/mk-progress-stat.html +186 -0
  209. package/src/scene/blocks/mk-specs-list/mk-specs-list.html +223 -0
  210. package/src/scene/blocks/news-ticker/news-ticker.html +409 -0
  211. package/src/scene/blocks/organic-light-leak-overlay/organic-light-leak-overlay.html +126 -0
  212. package/src/scene/blocks/oscilloscope-trace/oscilloscope-trace.html +519 -0
  213. package/src/scene/blocks/snow/snow.html +205 -0
  214. package/src/scene/blocks/split-flap-board/split-flap-board.html +347 -0
  215. package/src/scene/blocks/spotify-card/spotify-card.html +294 -0
  216. package/src/scene/blocks/starfield/starfield.html +188 -0
  217. package/src/scene/blocks/thread-message-stack/thread-message-stack.html +239 -0
  218. package/src/scene/blocks/week-in-merges/week-in-merges.html +470 -0
  219. package/src/scene/blocks/x-post/x-post.html +437 -0
  220. package/src/scene/blocks/yt-comment-card/yt-comment-card.html +319 -0
  221. package/src/scene/blocks/yt-lcd-background/yt-lcd-background.html +307 -0
  222. package/src/scene/blocks/yt-prism-title/yt-prism-title.html +205 -0
  223. package/src/scene/blocks.ts +229 -0
  224. package/src/scene/command.ts +272 -0
  225. package/src/scene/figure.ts +181 -0
  226. package/src/scene/glyphs.ts +107 -0
  227. package/src/scene/init.ts +86 -0
  228. package/src/scene/inspect/css-tree.d.ts +6 -0
  229. package/src/scene/inspect/page-scripts.ts +272 -0
  230. package/src/scene/inspect/report.ts +405 -0
  231. package/src/scene/inspect/run.ts +200 -0
  232. package/src/scene/inspect/source.ts +236 -0
  233. package/src/scene/label.ts +131 -0
  234. package/src/scene/net-guard.ts +74 -0
  235. package/src/scene/render.ts +292 -0
  236. package/src/scene/runtime/browser.ts +199 -0
  237. package/src/scene/runtime/core.ts +181 -0
  238. package/src/scene/runtime/react.tsx +115 -0
  239. package/src/scene/runtime/transitions.ts +133 -0
  240. package/src/scene/sanitize.ts +181 -0
  241. package/src/scene/scene.ts +152 -0
  242. package/src/scene/studio-command.ts +48 -0
  243. package/src/scene/studio-skin.ts +59 -0
  244. package/src/scene/studio.ts +337 -0
  245. package/src/scene/supply.ts +111 -0
  246. package/src/scene/timing.ts +288 -0
  247. package/src/scene/transitions/LICENSE +190 -0
  248. package/src/scene/transitions/NOTICE +5 -0
  249. package/src/scene/transitions/SOURCE.json +24 -0
  250. package/src/scene/transitions/transitions.global.js +38 -0
  251. package/src/scene/transitions.ts +26 -0
  252. package/src/service/all-projects.ts +153 -0
  253. package/src/service/commands.ts +256 -0
  254. package/src/service/due.ts +82 -0
  255. package/src/service/lock.ts +101 -0
  256. package/src/service/log-file.ts +56 -0
  257. package/src/service/state.ts +41 -0
  258. package/src/service/units.ts +90 -0
  259. package/src/service/worker.ts +51 -0
  260. package/src/skill-agents.ts +74 -0
  261. package/src/skill-install.ts +334 -0
  262. package/src/sync-write.ts +21 -0
  263. package/src/tick-report.ts +205 -0
  264. package/src/tts.ts +124 -0
  265. package/vendor/@vfrey/contracts/dist/account.d.ts +97 -0
  266. package/vendor/@vfrey/contracts/dist/account.js +27 -0
  267. package/vendor/@vfrey/contracts/dist/checks.d.ts +8 -0
  268. package/vendor/@vfrey/contracts/dist/checks.js +1 -0
  269. package/vendor/@vfrey/contracts/dist/credentials/crypto.d.ts +4 -0
  270. package/vendor/@vfrey/contracts/dist/credentials/crypto.js +38 -0
  271. package/vendor/@vfrey/contracts/dist/domain/advice-persona.d.ts +10 -0
  272. package/vendor/@vfrey/contracts/dist/domain/advice-persona.js +24 -0
  273. package/vendor/@vfrey/contracts/dist/domain/backoff.d.ts +13 -0
  274. package/vendor/@vfrey/contracts/dist/domain/backoff.js +27 -0
  275. package/vendor/@vfrey/contracts/dist/domain/cadence.d.ts +29 -0
  276. package/vendor/@vfrey/contracts/dist/domain/cadence.js +95 -0
  277. package/vendor/@vfrey/contracts/dist/domain/creators.d.ts +146 -0
  278. package/vendor/@vfrey/contracts/dist/domain/creators.js +2 -0
  279. package/vendor/@vfrey/contracts/dist/domain/disclosure.d.ts +127 -0
  280. package/vendor/@vfrey/contracts/dist/domain/disclosure.js +250 -0
  281. package/vendor/@vfrey/contracts/dist/domain/errors.d.ts +34 -0
  282. package/vendor/@vfrey/contracts/dist/domain/errors.js +68 -0
  283. package/vendor/@vfrey/contracts/dist/domain/redact.d.ts +1 -0
  284. package/vendor/@vfrey/contracts/dist/domain/redact.js +21 -0
  285. package/vendor/@vfrey/contracts/dist/domain/revenue.d.ts +13 -0
  286. package/vendor/@vfrey/contracts/dist/domain/revenue.js +16 -0
  287. package/vendor/@vfrey/contracts/dist/domain/safety.d.ts +20 -0
  288. package/vendor/@vfrey/contracts/dist/domain/safety.js +61 -0
  289. package/vendor/@vfrey/contracts/dist/domain/similarity.d.ts +13 -0
  290. package/vendor/@vfrey/contracts/dist/domain/similarity.js +100 -0
  291. package/vendor/@vfrey/contracts/dist/domain/templates.d.ts +18 -0
  292. package/vendor/@vfrey/contracts/dist/domain/templates.js +67 -0
  293. package/vendor/@vfrey/contracts/dist/domain/tiktok-chunks.d.ts +19 -0
  294. package/vendor/@vfrey/contracts/dist/domain/tiktok-chunks.js +52 -0
  295. package/vendor/@vfrey/contracts/dist/domain/topics.d.ts +78 -0
  296. package/vendor/@vfrey/contracts/dist/domain/topics.js +4 -0
  297. package/vendor/@vfrey/contracts/dist/domain/types.d.ts +216 -0
  298. package/vendor/@vfrey/contracts/dist/domain/types.js +1 -0
  299. package/vendor/@vfrey/contracts/dist/domain/variation.d.ts +57 -0
  300. package/vendor/@vfrey/contracts/dist/domain/variation.js +278 -0
  301. package/vendor/@vfrey/contracts/dist/events.d.ts +20 -0
  302. package/vendor/@vfrey/contracts/dist/events.js +1 -0
  303. package/vendor/@vfrey/contracts/dist/http.d.ts +11 -0
  304. package/vendor/@vfrey/contracts/dist/http.js +37 -0
  305. package/vendor/@vfrey/contracts/dist/index.d.ts +22 -0
  306. package/vendor/@vfrey/contracts/dist/index.js +22 -0
  307. package/vendor/@vfrey/contracts/dist/log.d.ts +2 -0
  308. package/vendor/@vfrey/contracts/dist/log.js +10 -0
  309. package/vendor/@vfrey/contracts/dist/mail.d.ts +55 -0
  310. package/vendor/@vfrey/contracts/dist/mail.js +1 -0
  311. package/vendor/@vfrey/contracts/package.json +18 -0
  312. package/vendor/@vfrey/ingest/dist/index.d.ts +41 -0
  313. package/vendor/@vfrey/ingest/dist/index.js +221 -0
  314. package/vendor/@vfrey/ingest/dist/parse.d.ts +5 -0
  315. package/vendor/@vfrey/ingest/dist/parse.js +165 -0
  316. package/vendor/@vfrey/ingest/package.json +21 -0
  317. package/vendor/framey/LICENSE +190 -0
  318. package/vendor/framey/NOTICE +15 -0
  319. package/vendor/framey/SOURCE.json +24 -0
  320. package/vendor/framey/STUDIO-SOURCE.json +9 -0
  321. package/vendor/framey/bin/framey-localize-fonts.mjs +12 -0
  322. package/vendor/framey/bin/framey.mjs +16 -0
  323. package/vendor/framey/dist/add-VMJD5KBS.js +75 -0
  324. package/vendor/framey/dist/animatedGifPrep-QPKGJ6PM.js +43 -0
  325. package/vendor/framey/dist/api.d.ts +211 -0
  326. package/vendor/framey/dist/api.js +76 -0
  327. package/vendor/framey/dist/appHistory-WW56FZTT.js +31 -0
  328. package/vendor/framey/dist/asset-WQKGOELE.js +26 -0
  329. package/vendor/framey/dist/audio-RSJWVWRA.js +1936 -0
  330. package/vendor/framey/dist/batchRender-3HPICDCT.js +366 -0
  331. package/vendor/framey/dist/beats-HL2BDUZI.js +273 -0
  332. package/vendor/framey/dist/benchmark-RWCWHI5Z.js +230 -0
  333. package/vendor/framey/dist/browser-ECYET3Q2.js +206 -0
  334. package/vendor/framey/dist/bundleWithLocalizedFonts-ALYBQEQT.js +52 -0
  335. package/vendor/framey/dist/captureCompositionFrame-4DA6SEH2.js +94 -0
  336. package/vendor/framey/dist/catalog-5JVDEUWK.js +979 -0
  337. package/vendor/framey/dist/catch-up-NEBCL5NO.js +106 -0
  338. package/vendor/framey/dist/check-F5N544ML.js +111 -0
  339. package/vendor/framey/dist/checkBrowser-LZ66CMJG.js +110 -0
  340. package/vendor/framey/dist/chunk-2K7Z2HVP.js +372 -0
  341. package/vendor/framey/dist/chunk-2UZQHDM7.js +323 -0
  342. package/vendor/framey/dist/chunk-2X4PQEYM.js +34 -0
  343. package/vendor/framey/dist/chunk-3422KMTD.js +168 -0
  344. package/vendor/framey/dist/chunk-3FYD4YEK.js +72 -0
  345. package/vendor/framey/dist/chunk-3GXQPCJC.js +682 -0
  346. package/vendor/framey/dist/chunk-3LM6YPBR.js +179 -0
  347. package/vendor/framey/dist/chunk-442JNXRM.js +20 -0
  348. package/vendor/framey/dist/chunk-4BNN5FGX.js +182 -0
  349. package/vendor/framey/dist/chunk-4P7NX5BM.js +11532 -0
  350. package/vendor/framey/dist/chunk-4UMPIPCQ.js +88 -0
  351. package/vendor/framey/dist/chunk-4YYJOFKS.js +28 -0
  352. package/vendor/framey/dist/chunk-54HJ63Q2.js +29 -0
  353. package/vendor/framey/dist/chunk-5ETJX6WM.js +89 -0
  354. package/vendor/framey/dist/chunk-5SG54JRS.js +68 -0
  355. package/vendor/framey/dist/chunk-634HSJ3J.js +58 -0
  356. package/vendor/framey/dist/chunk-66LREXGS.js +48 -0
  357. package/vendor/framey/dist/chunk-6BEFEWFB.js +25 -0
  358. package/vendor/framey/dist/chunk-6BNTGNGD.js +286 -0
  359. package/vendor/framey/dist/chunk-6DDPOHMK.js +140 -0
  360. package/vendor/framey/dist/chunk-6GV5XSOR.js +214 -0
  361. package/vendor/framey/dist/chunk-6JR2XUEE.js +127 -0
  362. package/vendor/framey/dist/chunk-73QVATIM.js +362 -0
  363. package/vendor/framey/dist/chunk-7FMUTGOH.js +279 -0
  364. package/vendor/framey/dist/chunk-7JQFJ6JJ.js +291 -0
  365. package/vendor/framey/dist/chunk-AHPGAESX.js +113 -0
  366. package/vendor/framey/dist/chunk-AHX34EFP.js +15 -0
  367. package/vendor/framey/dist/chunk-AJ5CSAEZ.js +487 -0
  368. package/vendor/framey/dist/chunk-B7RJV74O.js +5047 -0
  369. package/vendor/framey/dist/chunk-BL4U6U52.js +254 -0
  370. package/vendor/framey/dist/chunk-BSQ7XANM.js +210 -0
  371. package/vendor/framey/dist/chunk-BZ56MTDX.js +6 -0
  372. package/vendor/framey/dist/chunk-C5BO7PMT.js +282 -0
  373. package/vendor/framey/dist/chunk-CFUCHO4T.js +358 -0
  374. package/vendor/framey/dist/chunk-CZGHFATT.js +31 -0
  375. package/vendor/framey/dist/chunk-DLA44CMS.js +5649 -0
  376. package/vendor/framey/dist/chunk-DPVXBVLL.js +31292 -0
  377. package/vendor/framey/dist/chunk-DQBY7FRR.js +397 -0
  378. package/vendor/framey/dist/chunk-EAMLBAHA.js +154 -0
  379. package/vendor/framey/dist/chunk-EOVYQ3IG.js +143 -0
  380. package/vendor/framey/dist/chunk-EVMY7IPL.js +612 -0
  381. package/vendor/framey/dist/chunk-FG4BBFZI.js +122 -0
  382. package/vendor/framey/dist/chunk-FI4I7CME.js +56 -0
  383. package/vendor/framey/dist/chunk-FLREUEHW.js +2081 -0
  384. package/vendor/framey/dist/chunk-FUFP7A7S.js +1126 -0
  385. package/vendor/framey/dist/chunk-FV3PIKGH.js +56 -0
  386. package/vendor/framey/dist/chunk-FXMKHHPO.js +123 -0
  387. package/vendor/framey/dist/chunk-GGZAM5VH.js +1187 -0
  388. package/vendor/framey/dist/chunk-GKZEVJIB.js +39 -0
  389. package/vendor/framey/dist/chunk-GMECXFAN.js +57 -0
  390. package/vendor/framey/dist/chunk-GNEFDLFB.js +621 -0
  391. package/vendor/framey/dist/chunk-GPMLSOEG.js +19274 -0
  392. package/vendor/framey/dist/chunk-GPYVT5EE.js +59 -0
  393. package/vendor/framey/dist/chunk-H6IEMZEV.js +40 -0
  394. package/vendor/framey/dist/chunk-HEFQCIXR.js +170 -0
  395. package/vendor/framey/dist/chunk-HFQ4CWHA.js +118 -0
  396. package/vendor/framey/dist/chunk-HPDTCOKE.js +304 -0
  397. package/vendor/framey/dist/chunk-HTUHQKDB.js +548 -0
  398. package/vendor/framey/dist/chunk-HU2SPFNG.js +103 -0
  399. package/vendor/framey/dist/chunk-HVW7ABXD.js +19 -0
  400. package/vendor/framey/dist/chunk-IG7NDKXO.js +26 -0
  401. package/vendor/framey/dist/chunk-II3J3WXK.js +246 -0
  402. package/vendor/framey/dist/chunk-IR35BCLK.js +13 -0
  403. package/vendor/framey/dist/chunk-ISN5GRYI.js +20 -0
  404. package/vendor/framey/dist/chunk-IUONB55J.js +669 -0
  405. package/vendor/framey/dist/chunk-IVSUQR2B.js +161 -0
  406. package/vendor/framey/dist/chunk-IXU6PQSW.js +259 -0
  407. package/vendor/framey/dist/chunk-IZCW2NKT.js +29 -0
  408. package/vendor/framey/dist/chunk-JGXJU5PF.js +68 -0
  409. package/vendor/framey/dist/chunk-JHOCK3DG.js +27 -0
  410. package/vendor/framey/dist/chunk-JVBRJMOP.js +121 -0
  411. package/vendor/framey/dist/chunk-K4MJX7SR.js +17 -0
  412. package/vendor/framey/dist/chunk-K4WKGXRC.js +18 -0
  413. package/vendor/framey/dist/chunk-KGPLIH7B.js +127 -0
  414. package/vendor/framey/dist/chunk-KLIKCX6Q.js +63 -0
  415. package/vendor/framey/dist/chunk-KXTKKBOC.js +61 -0
  416. package/vendor/framey/dist/chunk-KZI44OPA.js +185 -0
  417. package/vendor/framey/dist/chunk-LEKZNRKI.js +45 -0
  418. package/vendor/framey/dist/chunk-LFGJWOVA.js +61 -0
  419. package/vendor/framey/dist/chunk-LLMSX6V6.js +17 -0
  420. package/vendor/framey/dist/chunk-LTGTN2TH.js +195 -0
  421. package/vendor/framey/dist/chunk-M2WBOQ2Q.js +54 -0
  422. package/vendor/framey/dist/chunk-MI5FTLYK.js +399 -0
  423. package/vendor/framey/dist/chunk-MLQP2QUF.js +1809 -0
  424. package/vendor/framey/dist/chunk-MMQ52NIZ.js +956 -0
  425. package/vendor/framey/dist/chunk-ND46XKK3.js +742 -0
  426. package/vendor/framey/dist/chunk-NYEXNSHM.js +504 -0
  427. package/vendor/framey/dist/chunk-O4YRB27L.js +57 -0
  428. package/vendor/framey/dist/chunk-O7BGSQ5F.js +92 -0
  429. package/vendor/framey/dist/chunk-OFQWTKPW.js +117 -0
  430. package/vendor/framey/dist/chunk-P5ZBLXJ6.js +91 -0
  431. package/vendor/framey/dist/chunk-P6LJ4Y32.js +4662 -0
  432. package/vendor/framey/dist/chunk-PKZ7RK5N.js +281 -0
  433. package/vendor/framey/dist/chunk-PURHAXFL.js +15 -0
  434. package/vendor/framey/dist/chunk-Q5X2B5WT.js +29 -0
  435. package/vendor/framey/dist/chunk-QAMKOG67.js +248 -0
  436. package/vendor/framey/dist/chunk-R6YBK4XX.js +514 -0
  437. package/vendor/framey/dist/chunk-RGLQURIX.js +2997 -0
  438. package/vendor/framey/dist/chunk-RNV3DM4S.js +3573 -0
  439. package/vendor/framey/dist/chunk-ROEW726S.js +4638 -0
  440. package/vendor/framey/dist/chunk-RQVDTPGD.js +6 -0
  441. package/vendor/framey/dist/chunk-RVZHGOGR.js +26 -0
  442. package/vendor/framey/dist/chunk-RXATE6FZ.js +385 -0
  443. package/vendor/framey/dist/chunk-S3S4TGEH.js +424 -0
  444. package/vendor/framey/dist/chunk-SDYDNHEE.js +11202 -0
  445. package/vendor/framey/dist/chunk-SSWBU67I.js +173 -0
  446. package/vendor/framey/dist/chunk-ST35C4K6.js +28 -0
  447. package/vendor/framey/dist/chunk-SZNTYTIV.js +990 -0
  448. package/vendor/framey/dist/chunk-T2EUWCJE.js +29 -0
  449. package/vendor/framey/dist/chunk-T6AGUTZJ.js +444 -0
  450. package/vendor/framey/dist/chunk-THJALHFJ.js +11131 -0
  451. package/vendor/framey/dist/chunk-TIQB63MX.js +18128 -0
  452. package/vendor/framey/dist/chunk-U242RTCT.js +134 -0
  453. package/vendor/framey/dist/chunk-UJZREAV6.js +3901 -0
  454. package/vendor/framey/dist/chunk-UMD3V7CU.js +264 -0
  455. package/vendor/framey/dist/chunk-UOFAE5RF.js +42 -0
  456. package/vendor/framey/dist/chunk-UTEIUGAZ.js +29 -0
  457. package/vendor/framey/dist/chunk-VAS6HQX3.js +138 -0
  458. package/vendor/framey/dist/chunk-VC76R6W2.js +27 -0
  459. package/vendor/framey/dist/chunk-VFZ7W5W7.js +226 -0
  460. package/vendor/framey/dist/chunk-VHNLZAE2.js +348 -0
  461. package/vendor/framey/dist/chunk-VMC4WSH5.js +278 -0
  462. package/vendor/framey/dist/chunk-VORQIH64.js +108 -0
  463. package/vendor/framey/dist/chunk-VTXFRIHO.js +186 -0
  464. package/vendor/framey/dist/chunk-W2LGJSEN.js +182 -0
  465. package/vendor/framey/dist/chunk-WKBM7NDG.js +32 -0
  466. package/vendor/framey/dist/chunk-WMQNIQDJ.js +2057 -0
  467. package/vendor/framey/dist/chunk-WN4XZC5F.js +504 -0
  468. package/vendor/framey/dist/chunk-WQ6ZXHFQ.js +133 -0
  469. package/vendor/framey/dist/chunk-X7HBSTCZ.js +53 -0
  470. package/vendor/framey/dist/chunk-X7MTZ4T2.js +6 -0
  471. package/vendor/framey/dist/chunk-XPTOJZJF.js +308 -0
  472. package/vendor/framey/dist/chunk-XT6MXC7A.js +6 -0
  473. package/vendor/framey/dist/chunk-XT7X6QCP.js +6 -0
  474. package/vendor/framey/dist/chunk-XTMDWNEC.js +2820 -0
  475. package/vendor/framey/dist/chunk-XUHOIDKG.js +6 -0
  476. package/vendor/framey/dist/chunk-XX7LTW5Y.js +138 -0
  477. package/vendor/framey/dist/chunk-Y3YGTZEG.js +382 -0
  478. package/vendor/framey/dist/chunk-Y5SBSGN7.js +346 -0
  479. package/vendor/framey/dist/chunk-YCMVI4MZ.js +625 -0
  480. package/vendor/framey/dist/chunk-YOR4CTCE.js +261 -0
  481. package/vendor/framey/dist/chunk-YPRBGITP.js +2748 -0
  482. package/vendor/framey/dist/chunk-ZKDMAOFU.js +32 -0
  483. package/vendor/framey/dist/chunk-ZW6DURPF.js +1318 -0
  484. package/vendor/framey/dist/clean-ZJOGDKDM.js +336 -0
  485. package/vendor/framey/dist/cli.js +439 -0
  486. package/vendor/framey/dist/cloudrun-HGJ3B6ZV.js +767 -0
  487. package/vendor/framey/dist/commands/contrast-audit.browser.js +643 -0
  488. package/vendor/framey/dist/commands/layout-audit.browser.js +2914 -0
  489. package/vendor/framey/dist/commands/motion-sample.browser.js +74 -0
  490. package/vendor/framey/dist/commands/motion-signature.browser.js +475 -0
  491. package/vendor/framey/dist/compare-W7UY3BAZ.js +463 -0
  492. package/vendor/framey/dist/compiler-TM3W6GYF.js +136 -0
  493. package/vendor/framey/dist/component-QF5VGYMU.js +176 -0
  494. package/vendor/framey/dist/compositions-XT3PMNDF.js +238 -0
  495. package/vendor/framey/dist/contactSheet-7W6GYHBQ.js +24 -0
  496. package/vendor/framey/dist/deploy-6FJPWKF2.js +299 -0
  497. package/vendor/framey/dist/destroy-HE2RIPKZ.js +57 -0
  498. package/vendor/framey/dist/deterministicFonts-RADD3BSX.js +50 -0
  499. package/vendor/framey/dist/dist-4S2FGGVY.js +422 -0
  500. package/vendor/framey/dist/dist-BE4B2EZE.js +32 -0
  501. package/vendor/framey/dist/dist-EWXJGN63.js +129 -0
  502. package/vendor/framey/dist/docs-VKIRFEEX.js +131 -0
  503. package/vendor/framey/dist/doctor-2UNJ2G7U.js +74 -0
  504. package/vendor/framey/dist/dom-37K6YOH4.js +14 -0
  505. package/vendor/framey/dist/dotEnv-7PZQTVIM.js +54 -0
  506. package/vendor/framey/dist/events-ACY3P2PA.js +76 -0
  507. package/vendor/framey/dist/events-IHB3B2MK.js +69 -0
  508. package/vendor/framey/dist/ffmpeg-DR5LGGSK.js +63 -0
  509. package/vendor/framey/dist/figma-BODVUPHB.js +63 -0
  510. package/vendor/framey/dist/fontData.generated-NXAOI4GH.js +13 -0
  511. package/vendor/framey/dist/fontLocalizeCli.js +142 -0
  512. package/vendor/framey/dist/gpuPolicy-XLEXORZY.js +49 -0
  513. package/vendor/framey/dist/grade-compare-GAXFQ74C.js +661 -0
  514. package/vendor/framey/dist/gsapParser-IEUSLYDJ.js +2885 -0
  515. package/vendor/framey/dist/gsapParserAcorn-LZJKCFQI.js +30 -0
  516. package/vendor/framey/dist/help-VCB5X2HS.js +184 -0
  517. package/vendor/framey/dist/history-ENGZEVFT.js +471 -0
  518. package/vendor/framey/dist/hyperframe.manifest.json +22 -0
  519. package/vendor/framey/dist/hyperframe.runtime.iife.js +1132 -0
  520. package/vendor/framey/dist/hyperframes-player.global.js +478 -0
  521. package/vendor/framey/dist/hyperframes-slideshow.global.js +148 -0
  522. package/vendor/framey/dist/info-WPCJHBMU.js +182 -0
  523. package/vendor/framey/dist/init-G4WGLYGD.js +1176 -0
  524. package/vendor/framey/dist/inspect-5IQ6CNEQ.js +106 -0
  525. package/vendor/framey/dist/installer-ML4HWUV7.js +40 -0
  526. package/vendor/framey/dist/keyframes-Q6XJO7VW.js +2755 -0
  527. package/vendor/framey/dist/lambda-A7JC74JF.js +468 -0
  528. package/vendor/framey/dist/layout-YET6XNXA.js +95 -0
  529. package/vendor/framey/dist/lint-I7FZN2VJ.js +162 -0
  530. package/vendor/framey/dist/lintProject-PDB3UN72.js +30 -0
  531. package/vendor/framey/dist/manager-CD6LJ7ZN.js +45 -0
  532. package/vendor/framey/dist/manager-WA4F2GST.js +67 -0
  533. package/vendor/framey/dist/media-treatment-BXYS4WVA.js +1030 -0
  534. package/vendor/framey/dist/media-use-LWH3MANK.js +113 -0
  535. package/vendor/framey/dist/models-K363FYM4.js +195 -0
  536. package/vendor/framey/dist/motionShot-GPAVWXLH.js +640 -0
  537. package/vendor/framey/dist/normalize-FASIPBUM.js +24 -0
  538. package/vendor/framey/dist/normalize-audio-VDWYRHN6.js +268 -0
  539. package/vendor/framey/dist/open-E3TB6P6J.js +128 -0
  540. package/vendor/framey/dist/parakeetRunner-UP34UOI2.js +58 -0
  541. package/vendor/framey/dist/pipeline-VXIUYEBU.js +64 -0
  542. package/vendor/framey/dist/play-BUTDQ4DV.js +353 -0
  543. package/vendor/framey/dist/policies-OLDYGFK5.js +309 -0
  544. package/vendor/framey/dist/preflight-2KZ2GWAZ.js +55 -0
  545. package/vendor/framey/dist/present-GXYSM7NX.js +353 -0
  546. package/vendor/framey/dist/preview-EKZ33JOJ.js +1910 -0
  547. package/vendor/framey/dist/producer-SKU5EV3J.js +13 -0
  548. package/vendor/framey/dist/progress-ZWB4Z4ET.js +76 -0
  549. package/vendor/framey/dist/projectConfig-3MZAYVTS.js +60 -0
  550. package/vendor/framey/dist/publication-DTKWNF74.js +33 -0
  551. package/vendor/framey/dist/remove-background-OY5L7XN2.js +265 -0
  552. package/vendor/framey/dist/render-CDV4GDFX.js +187 -0
  553. package/vendor/framey/dist/render-YE5YAMOD.js +2620 -0
  554. package/vendor/framey/dist/render-batch-ZAMFPCLL.js +274 -0
  555. package/vendor/framey/dist/renderSetupWorker.js +120 -0
  556. package/vendor/framey/dist/resolver-D5BIPSEW.js +42 -0
  557. package/vendor/framey/dist/runtimeVersion.js +18 -0
  558. package/vendor/framey/dist/sam-WFZXI4F7.js +23 -0
  559. package/vendor/framey/dist/shaderTransitionWorker.js +44 -0
  560. package/vendor/framey/dist/sherpa-RQWLV7I3.js +79 -0
  561. package/vendor/framey/dist/sherpaWorker.js +84 -0
  562. package/vendor/framey/dist/sites-AMOVABTK.js +54 -0
  563. package/vendor/framey/dist/skill-ZHVLH7KX.js +15 -0
  564. package/vendor/framey/dist/slideshow-SJGK4RQS.js +24 -0
  565. package/vendor/framey/dist/snapshot-TUXL6LBQ.js +1515 -0
  566. package/vendor/framey/dist/src-JHQGB2S6.js +215 -0
  567. package/vendor/framey/dist/src-Z5PGEMXB.js +532 -0
  568. package/vendor/framey/dist/state-B6H3WHCF.js +28 -0
  569. package/vendor/framey/dist/staticProjectServer-DFVZOUGI.js +63 -0
  570. package/vendor/framey/dist/studio/assets/hyperframes-player-DLaKjw0L.js +478 -0
  571. package/vendor/framey/dist/studio/assets/index-BX3KHhGX.js +71 -0
  572. package/vendor/framey/dist/studio/assets/index-CYbvOVp-.css +1 -0
  573. package/vendor/framey/dist/studio/assets/index-Dp3QEUGL.js +402 -0
  574. package/vendor/framey/dist/studio/assets/index-qrTAXbqc.js +531 -0
  575. package/vendor/framey/dist/studio/favicon.svg +14 -0
  576. package/vendor/framey/dist/studio/icons/timeline/audio.svg +7 -0
  577. package/vendor/framey/dist/studio/icons/timeline/captions.svg +5 -0
  578. package/vendor/framey/dist/studio/icons/timeline/composition.svg +12 -0
  579. package/vendor/framey/dist/studio/icons/timeline/image.svg +18 -0
  580. package/vendor/framey/dist/studio/icons/timeline/music.svg +10 -0
  581. package/vendor/framey/dist/studio/icons/timeline/text.svg +3 -0
  582. package/vendor/framey/dist/studio/index.html +23 -0
  583. package/vendor/framey/dist/studio-api-2NECGRMB.js +179 -0
  584. package/vendor/framey/dist/studioSelectionClient-IUUPKUHZ.js +25 -0
  585. package/vendor/framey/dist/studioServer-N2OPUMOQ.js +1569 -0
  586. package/vendor/framey/dist/synthesize-PKTB5ATG.js +183 -0
  587. package/vendor/framey/dist/system-7F3UEHQT.js +27 -0
  588. package/vendor/framey/dist/telemetry-DQGPEKM6.js +145 -0
  589. package/vendor/framey/dist/telemetry-UN55N6U6.js +77 -0
  590. package/vendor/framey/dist/timeline-LHUUMK63.js +1295 -0
  591. package/vendor/framey/dist/tokens-OLECWDFK.js +92 -0
  592. package/vendor/framey/dist/transcribe-O7HQKMIV.js +331 -0
  593. package/vendor/framey/dist/transcribe-UJESPBT5.js +76 -0
  594. package/vendor/framey/dist/tts-YCCQHZD4.js +234 -0
  595. package/vendor/framey/dist/urlDownloader-H2LXLG4U.js +48 -0
  596. package/vendor/framey/dist/usage-HG4WMM7A.js +2167 -0
  597. package/vendor/framey/dist/validate-F2XMN6EV.js +87 -0
  598. package/vendor/framey/dist/vendor/npm/gsap@3.15.0/GSAP-LICENSE.txt +8 -0
  599. package/vendor/framey/dist/vendor/npm/gsap@3.15.0/dist/CustomEase.min.js +11 -0
  600. package/vendor/framey/dist/vendor/npm/gsap@3.15.0/dist/MotionPathPlugin.min.js +11 -0
  601. package/vendor/framey/dist/vendor/npm/gsap@3.15.0/dist/gsap.min.js +11 -0
  602. package/vendor/framey/dist/warmCheck.js +220 -0
  603. package/vendor/framey/dist/warmCheckClient.js +139 -0
  604. package/vendor/framey/dist/workspaceBuildCheck-XYD2YTFW.js +133 -0
  605. package/vendor/framey-player/LICENSE +190 -0
  606. package/vendor/framey-player/NOTICE +16 -0
  607. package/vendor/framey-player/SOURCE.json +6 -0
  608. package/vendor/framey-player/hyperframes-player.global.js +478 -0
  609. package/vendor/framey.pin +1 -0
  610. package/bin/index.js +0 -3
@@ -0,0 +1,2820 @@
1
+ import { createRequire as __hf_createRequire } from "node:module";
2
+ import { fileURLToPath as __hf_fileURLToPath } from "node:url";
3
+ import { dirname as __hf_dirname } from "node:path";
4
+ var require = __hf_createRequire(import.meta.url);
5
+ var __filename = __hf_fileURLToPath(import.meta.url);
6
+ var __dirname = __hf_dirname(__filename);
7
+ import {
8
+ pngDimensions,
9
+ reportArtifacts,
10
+ withProjectLock
11
+ } from "./chunk-3LM6YPBR.js";
12
+ import {
13
+ captureArtifactRevision
14
+ } from "./chunk-UTEIUGAZ.js";
15
+ import {
16
+ loadProducer
17
+ } from "./chunk-PURHAXFL.js";
18
+ import {
19
+ trackLintRun
20
+ } from "./chunk-IVSUQR2B.js";
21
+ import {
22
+ beginCheckSnapshots,
23
+ checkSnapshotDirectory,
24
+ finishCheckSnapshots,
25
+ preResolveHostileMediaProxies,
26
+ rectToBbox
27
+ } from "./chunk-MLQP2QUF.js";
28
+ import {
29
+ CONTENT_OVERLAP_HELD_ERROR_MS,
30
+ DEFAULT_MAX_STATIC_SEC,
31
+ END_CARD_HOLD_SEC,
32
+ assertionTargets,
33
+ buildLayoutSampleTimes,
34
+ buildTransitionSampleTimes,
35
+ collapseStaticLayoutIssues,
36
+ collectSamplingTargets,
37
+ dedupeLayoutIssues,
38
+ evaluateMotion,
39
+ findMotionSpec,
40
+ findingElementKey,
41
+ formatFindingTimes,
42
+ formatLayoutIssue,
43
+ frozenSpans,
44
+ groupSampledFindings,
45
+ limitLayoutIssues,
46
+ longestContiguousRunMs,
47
+ mergeSampleTimes,
48
+ parseAt,
49
+ readMotionSpec
50
+ } from "./chunk-FUFP7A7S.js";
51
+ import {
52
+ withMeta
53
+ } from "./chunk-O4YRB27L.js";
54
+ import {
55
+ resolveProject
56
+ } from "./chunk-O7BGSQ5F.js";
57
+ import {
58
+ recordArtifacts
59
+ } from "./chunk-YPRBGITP.js";
60
+ import {
61
+ BrowserCleanupError,
62
+ closeManagedBrowser,
63
+ withRemainingBudget
64
+ } from "./chunk-SZNTYTIV.js";
65
+ import {
66
+ resolveLocalBrowserGpuMode
67
+ } from "./chunk-WKBM7NDG.js";
68
+ import {
69
+ trackCheckReport,
70
+ trackCommandFailure
71
+ } from "./chunk-GNEFDLFB.js";
72
+ import {
73
+ swallowedFlagUsageError
74
+ } from "./chunk-U242RTCT.js";
75
+ import {
76
+ defineCommand,
77
+ parseArgs
78
+ } from "./chunk-S3S4TGEH.js";
79
+ import {
80
+ getRunId
81
+ } from "./chunk-54HJ63Q2.js";
82
+ import {
83
+ c
84
+ } from "./chunk-VORQIH64.js";
85
+ import {
86
+ setCommandExitCode
87
+ } from "./chunk-JVBRJMOP.js";
88
+ import {
89
+ extractMediaMetadata,
90
+ findHdrAutoPromotion
91
+ } from "./chunk-TIQB63MX.js";
92
+ import {
93
+ normalizeErrorMessage
94
+ } from "./chunk-T2EUWCJE.js";
95
+ import {
96
+ lintProject,
97
+ shouldBlockRender
98
+ } from "./chunk-4P7NX5BM.js";
99
+ import {
100
+ resolveProjectRelativeSrc
101
+ } from "./chunk-6BNTGNGD.js";
102
+
103
+ // src/commands/check.ts
104
+ import { randomUUID } from "crypto";
105
+ import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
106
+ import { join as join2, resolve } from "path";
107
+
108
+ // src/utils/checkDeadline.ts
109
+ var CHECK_IDLE_MS = 12e4;
110
+ var SCRIPT_START = "[hyperframes:script:start] ";
111
+ var SCRIPT_FINISH = "[hyperframes:script:finish] ";
112
+ var PHASE_LABELS = {
113
+ copies: "making video copies",
114
+ launch: "starting Chrome",
115
+ load: "loading the page",
116
+ settle: "waiting for the page to be ready",
117
+ samples: "checking samples",
118
+ overlap: "running the overlap pass",
119
+ crops: "capturing finding crops"
120
+ };
121
+ var BEFORE_DEADLINE = /* @__PURE__ */ new Set(["copies", "launch"]);
122
+ var CheckDeadlineError = class extends Error {
123
+ constructor(sourceFile, phases = "") {
124
+ super(
125
+ (sourceFile === void 0 ? "Check did not finish within its deadline" : `${sourceFile}: its script did not finish within the check deadline`) + (phases || ".")
126
+ );
127
+ this.sourceFile = sourceFile;
128
+ }
129
+ sourceFile;
130
+ };
131
+ var CheckDeadline = class {
132
+ abort = new AbortController();
133
+ operations = /* @__PURE__ */ new Set();
134
+ signal = this.abort.signal;
135
+ browsers = /* @__PURE__ */ new Set();
136
+ scripts = [];
137
+ expired;
138
+ timer;
139
+ expiry;
140
+ rejectExpiry;
141
+ phase = "waiting";
142
+ work;
143
+ spent = /* @__PURE__ */ new Map();
144
+ partials = [];
145
+ constructor() {
146
+ this.expiry = new Promise((_, reject) => {
147
+ this.rejectExpiry = reject;
148
+ });
149
+ }
150
+ keepPartial = (read) => {
151
+ this.partials.push(read);
152
+ };
153
+ partialResult() {
154
+ return Object.assign({}, ...this.partials.map((read) => read()));
155
+ }
156
+ enter = (phase) => {
157
+ if (this.expired) return;
158
+ const now = Date.now();
159
+ if (this.work) this.spend(this.work.phase, now - this.work.since);
160
+ this.work = { phase, since: now };
161
+ this.spend(phase, 0);
162
+ this.rearm();
163
+ };
164
+ spend(phase, ms) {
165
+ this.spent.set(phase, (this.spent.get(phase) ?? 0) + ms);
166
+ }
167
+ describe() {
168
+ if (!this.work) return "";
169
+ const open = Date.now() - this.work.since;
170
+ const parts = [...this.spent].map(([phase, ms]) => {
171
+ const seconds2 = ((phase === this.work?.phase ? ms + open : ms) / 1e3).toFixed(1);
172
+ return `${PHASE_LABELS[phase]} ${seconds2}${BEFORE_DEADLINE.has(phase) ? " (before the deadline)" : ""}`;
173
+ });
174
+ return ` (no progress for ${CHECK_IDLE_MS / 1e3} s after Chrome was ready), while ${PHASE_LABELS[this.work.phase]}. Seconds per phase: ${parts.join(", ")}.`;
175
+ }
176
+ start = () => {
177
+ if (this.phase !== "waiting") return;
178
+ this.phase = "started";
179
+ this.enter("load");
180
+ };
181
+ rearm() {
182
+ if (this.phase !== "started" || this.expired) return;
183
+ clearTimeout(this.timer);
184
+ this.timer = setTimeout(() => {
185
+ this.expired = new CheckDeadlineError(this.scripts.at(-1), this.describe());
186
+ this.rejectExpiry(this.expired);
187
+ this.abort.abort(this.expired);
188
+ }, CHECK_IDLE_MS);
189
+ }
190
+ ownBrowser = (browser) => {
191
+ if (this.expired) throw this.expired;
192
+ if (this.phase === "closed") throw new Error("Check has already finished.");
193
+ this.browsers.add(browser);
194
+ };
195
+ async preparePage(page) {
196
+ page.on("console", (message) => {
197
+ const text = message.text();
198
+ if (text.startsWith(SCRIPT_START)) this.scripts.push(text.slice(SCRIPT_START.length));
199
+ if (text.startsWith(SCRIPT_FINISH)) {
200
+ const index = this.scripts.lastIndexOf(text.slice(SCRIPT_FINISH.length));
201
+ if (index !== -1) this.scripts.splice(index, 1);
202
+ }
203
+ });
204
+ await page.evaluateOnNewDocument(
205
+ (start, finish) => {
206
+ Reflect.set(window, "__hfScriptState", (phase, source) => {
207
+ console.debug((phase === "start" ? start : finish) + source);
208
+ });
209
+ },
210
+ SCRIPT_START,
211
+ SCRIPT_FINISH
212
+ );
213
+ }
214
+ run(operation) {
215
+ if (this.expired) return Promise.reject(this.expired);
216
+ const pending = operation();
217
+ this.operations.add(pending);
218
+ const settled = () => this.rearm();
219
+ pending.then(settled, settled);
220
+ return Promise.race([pending, this.expiry]);
221
+ }
222
+ async close() {
223
+ this.phase = "closed";
224
+ clearTimeout(this.timer);
225
+ const closes = await Promise.allSettled([...this.browsers].map(closeManagedBrowser));
226
+ const errors = closes.filter((entry) => entry.status === "rejected").map((entry) => entry.reason);
227
+ try {
228
+ const drained = await withRemainingBudget(
229
+ Promise.allSettled(this.operations),
230
+ 1e3,
231
+ "draining check cleanup"
232
+ );
233
+ for (const entry of drained) {
234
+ if (entry.status === "rejected" && entry.reason instanceof BrowserCleanupError)
235
+ errors.push(entry.reason);
236
+ }
237
+ } catch (error) {
238
+ errors.push(error);
239
+ }
240
+ if (errors.length > 0)
241
+ throw new BrowserCleanupError(errors, errors.map(normalizeErrorMessage).join("; "));
242
+ }
243
+ };
244
+
245
+ // src/utils/checkPipeline.ts
246
+ import { mkdirSync, writeFileSync } from "fs";
247
+ import { readFile } from "fs/promises";
248
+ import { join, relative } from "path";
249
+
250
+ // src/utils/craftAudit.ts
251
+ var CRAFT_THRESHOLDS = {
252
+ readHeightAt1080: 30,
253
+ readOpacity: 0.5,
254
+ shortLineWords: 2,
255
+ shortLineFloorSec: 0.4,
256
+ holdBaseSec: 0.5,
257
+ holdPerWordSec: 0.3,
258
+ holdFloorFraction: 0.75,
259
+ finalLineFloorSec: 2,
260
+ deadStretchSec: 3,
261
+ endCardSec: END_CARD_HOLD_SEC,
262
+ typeFloorFraction: 0.026,
263
+ typeFloorHoldSec: 0.5,
264
+ silentFilmSec: 15,
265
+ /** One video frame: a line held exactly at its floor measures a hair under it. */
266
+ measureToleranceSec: 1 / 30,
267
+ /** Halvings of a grid step when re-measuring a line's edges (0.125 s → ~8 ms). */
268
+ refineSteps: 4,
269
+ maxRefinedLines: 24
270
+ };
271
+ var T = CRAFT_THRESHOLDS;
272
+ var ZERO_BBOX = { x: 0, y: 0, width: 0, height: 0 };
273
+ var WORD_RUNS = /[\p{L}\p{M}\p{N}%$]+/gu;
274
+ var WORD_SEGMENTER = new Intl.Segmenter("ja", { granularity: "word" });
275
+ function readingWords(text) {
276
+ const runs = text.toLowerCase().normalize("NFC").match(WORD_RUNS) ?? [];
277
+ return runs.reduce(
278
+ (count, run) => count + Math.max(1, [...WORD_SEGMENTER.segment(run)].filter((s) => s.isWordLike).length),
279
+ 0
280
+ );
281
+ }
282
+ function lineTargetSec(words) {
283
+ return T.holdBaseSec + T.holdPerWordSec * words;
284
+ }
285
+ function lineFloorSec(words, finalLine) {
286
+ const floor = words <= T.shortLineWords ? T.shortLineFloorSec : T.holdFloorFraction * lineTargetSec(words);
287
+ return finalLine ? Math.max(floor, T.finalLineFloorSec) : floor;
288
+ }
289
+ async function auditCraft(input, probe) {
290
+ const frames = [...input.frames].sort((a, b) => a.time - b.time);
291
+ const step = sampleStep(frames);
292
+ const findings = [
293
+ ...detectFirstFrameEmpty(frames),
294
+ ...await detectLineUnread(frames, input.duration, input.canvas, probe),
295
+ ...input.stillnessJudgedElsewhere ? [] : detectDeadStretches(frames, input.duration),
296
+ ...detectTextBelowFloor(frames, input.duration, input.canvas),
297
+ ...detectFilmSilent(input.duration, input.soundSources)
298
+ ].sort((a, b) => a.time - b.time);
299
+ return { enabled: true, step, samples: frames.length, findings };
300
+ }
301
+ function sampleStep(frames) {
302
+ const gaps = frames.slice(1).map((frame, index) => frame.time - (frames[index]?.time ?? frame.time)).filter((gap) => gap > 0).sort((a, b) => a - b);
303
+ return gaps[Math.floor(gaps.length / 2)] ?? 0;
304
+ }
305
+ function seconds(value) {
306
+ return `${value.toFixed(2)} s`;
307
+ }
308
+ function filmFinding(code, time, message, extra = {}) {
309
+ return {
310
+ code,
311
+ severity: "warning",
312
+ message,
313
+ selector: "[data-composition-id]",
314
+ dataAttributes: {},
315
+ sourceFile: "index.html",
316
+ bbox: ZERO_BBOX,
317
+ time,
318
+ ...extra
319
+ };
320
+ }
321
+ function detectFirstFrameEmpty(frames) {
322
+ const first = frames[0];
323
+ if (!first || first.time > 1e-3 || first.painted) return [];
324
+ return [
325
+ filmFinding(
326
+ "first_frame_empty",
327
+ 0,
328
+ "Frame 0 shows only the background, and the first frame is the thumbnail on social players: have the title or key image already on screen at 0 s instead of entering from nothing; see references/review.md."
329
+ )
330
+ ];
331
+ }
332
+ function detectFilmSilent(duration, soundSources) {
333
+ if (soundSources === null || soundSources > 0 || duration <= T.silentFilmSec) return [];
334
+ return [
335
+ filmFinding(
336
+ "film_silent",
337
+ 0,
338
+ `This ${Math.round(duration)} s film has no sound source (no <audio>, no <video> with sound): add a music bed and sounds on the visible actions unless the person asked for silence; see references/sound.md.`
339
+ )
340
+ ];
341
+ }
342
+ function detectDeadStretches(frames, duration) {
343
+ const endCardStart = duration - T.endCardSec;
344
+ const findings = [];
345
+ let start = null;
346
+ let end = 0;
347
+ const flush = () => {
348
+ if (start !== null && end - start > T.deadStretchSec) findings.push(deadStretch(start, end));
349
+ start = null;
350
+ };
351
+ for (let index = 1; index < frames.length; index += 1) {
352
+ const before = frames[index - 1];
353
+ const after = frames[index];
354
+ if (!before || !after) continue;
355
+ const still = before.signature !== "" && before.signature === after.signature && !before.held && !after.held && before.time < endCardStart;
356
+ if (!still) {
357
+ flush();
358
+ continue;
359
+ }
360
+ start ??= before.time;
361
+ end = Math.min(after.time, endCardStart);
362
+ }
363
+ flush();
364
+ return findings;
365
+ }
366
+ function deadStretch(start, end) {
367
+ const length = end - start;
368
+ return filmFinding(
369
+ "dead_stretch",
370
+ start,
371
+ `Nothing on screen changes for ${seconds(length)} (${start.toFixed(2)}\u2013${end.toFixed(2)} s): start the next move within ${T.deadStretchSec} s, or put data-hold on the element whose stillness is deliberate; see references/motion.md.`,
372
+ { seconds: round(length), floorSeconds: T.deadStretchSec, until: round(end) }
373
+ );
374
+ }
375
+ function readableBlocks(frame, readHeight) {
376
+ const blocks = /* @__PURE__ */ new Map();
377
+ for (const part of frame.parts) {
378
+ if (part.captions || part.opacity < T.readOpacity) continue;
379
+ const rect = {
380
+ left: part.rect.left,
381
+ top: part.rect.top,
382
+ right: part.rect.left + part.rect.width,
383
+ bottom: part.rect.top + part.rect.height
384
+ };
385
+ const block = blocks.get(part.block);
386
+ if (!block) {
387
+ blocks.set(part.block, {
388
+ text: part.text,
389
+ rect,
390
+ selector: part.blockSelector,
391
+ sourceFile: part.sourceFile
392
+ });
393
+ continue;
394
+ }
395
+ block.text = `${block.text} ${part.text}`;
396
+ block.rect = {
397
+ left: Math.min(block.rect.left, rect.left),
398
+ top: Math.min(block.rect.top, rect.top),
399
+ right: Math.max(block.rect.right, rect.right),
400
+ bottom: Math.max(block.rect.bottom, rect.bottom)
401
+ };
402
+ }
403
+ for (const [key, block] of blocks) {
404
+ if (block.rect.bottom - block.rect.top < readHeight) blocks.delete(key);
405
+ }
406
+ return blocks;
407
+ }
408
+ function trackBlocks(frames, readHeight) {
409
+ const tracks = /* @__PURE__ */ new Map();
410
+ frames.forEach((frame, index) => {
411
+ for (const [key, block] of readableBlocks(frame, readHeight)) {
412
+ const track = tracks.get(key) ?? {
413
+ key,
414
+ selector: block.selector,
415
+ sourceFile: block.sourceFile,
416
+ seen: /* @__PURE__ */ new Map()
417
+ };
418
+ track.seen.set(index, { text: block.text, rect: block.rect });
419
+ tracks.set(key, track);
420
+ }
421
+ });
422
+ return [...tracks.values()];
423
+ }
424
+ function indexRuns(indices) {
425
+ const sorted = [...indices].sort((a, b) => a - b);
426
+ const runs = [];
427
+ for (const index of sorted) {
428
+ const last = runs[runs.length - 1];
429
+ if (last && index === last[1] + 1) last[1] = index;
430
+ else runs.push([index, index]);
431
+ }
432
+ return runs;
433
+ }
434
+ function windowSeconds(window2) {
435
+ return (window2.endIn + window2.endOut) / 2 - (window2.startOut + window2.startIn) / 2;
436
+ }
437
+ function runWindow(frames, [first, last], duration) {
438
+ const timeAt = (index) => frames[Math.min(Math.max(index, 0), frames.length - 1)]?.time ?? 0;
439
+ const reachesEnd = last === frames.length - 1;
440
+ return {
441
+ startOut: timeAt(first - 1),
442
+ startIn: timeAt(first),
443
+ endIn: reachesEnd ? duration : timeAt(last),
444
+ endOut: reachesEnd ? duration : timeAt(last + 1)
445
+ };
446
+ }
447
+ function lineCandidates(frames, duration, canvas) {
448
+ const readHeight = T.readHeightAt1080 * (Math.min(canvas.width, canvas.height) / 1080);
449
+ const tracks = trackBlocks(frames, readHeight);
450
+ const lastEnd = (track) => Math.max(...track.seen.keys());
451
+ const latest = Math.max(-1, ...tracks.map(lastEnd));
452
+ const candidates = [];
453
+ for (const track of tracks) {
454
+ const runs = indexRuns([...track.seen.keys()]);
455
+ const finalLine = lastEnd(track) === latest;
456
+ const windows = runs.map((run2) => runWindow(frames, run2, duration));
457
+ const chosen = finalLine ? windows.length - 1 : windows.reduce((best, window3, index) => {
458
+ const bestWindow = windows[best];
459
+ return bestWindow && windowSeconds(window3) > windowSeconds(bestWindow) ? index : best;
460
+ }, 0);
461
+ const window2 = windows[chosen];
462
+ const run = runs[chosen];
463
+ if (!window2 || !run) continue;
464
+ const texts = [...track.seen.values()].map((seen) => seen.text);
465
+ const text = texts.reduce(
466
+ (longest, next) => readingWords(next) > readingWords(longest) ? next : longest
467
+ );
468
+ const words = readingWords(text);
469
+ if (words === 0) continue;
470
+ const floor = lineFloorSec(words, finalLine);
471
+ if (window2.endIn - window2.startIn >= floor) continue;
472
+ candidates.push({ track, words, text, finalLine, floor, window: window2, firstIndex: run[0] });
473
+ }
474
+ return candidates;
475
+ }
476
+ async function refineWindow(candidate, probe, canvas) {
477
+ const readHeight = T.readHeightAt1080 * (Math.min(canvas.width, canvas.height) / 1080);
478
+ const visibleAt = async (time) => readableBlocks(await probe(time), readHeight).has(candidate.track.key);
479
+ const window2 = { ...candidate.window };
480
+ for (let step = 0; step < T.refineSteps; step += 1) {
481
+ if (window2.startIn > window2.startOut) {
482
+ const mid = (window2.startOut + window2.startIn) / 2;
483
+ if (await visibleAt(mid)) window2.startIn = mid;
484
+ else window2.startOut = mid;
485
+ }
486
+ if (window2.endOut > window2.endIn) {
487
+ const mid = (window2.endIn + window2.endOut) / 2;
488
+ if (await visibleAt(mid)) window2.endIn = mid;
489
+ else window2.endOut = mid;
490
+ }
491
+ }
492
+ return window2;
493
+ }
494
+ async function detectLineUnread(frames, duration, canvas, probe) {
495
+ const findings = [];
496
+ const candidates = lineCandidates(frames, duration, canvas);
497
+ for (const [index, candidate] of candidates.entries()) {
498
+ const window2 = probe && index < T.maxRefinedLines ? await refineWindow(candidate, probe, canvas) : candidate.window;
499
+ const held = windowSeconds(window2);
500
+ if (held >= candidate.floor - T.measureToleranceSec) continue;
501
+ const error = Math.max(window2.startIn - window2.startOut, window2.endOut - window2.endIn) / 2;
502
+ findings.push(lineUnreadFinding(candidate, window2, held, error));
503
+ }
504
+ return findings;
505
+ }
506
+ function lineUnreadFinding(candidate, window2, held, error) {
507
+ const { track, words, text, finalLine, floor } = candidate;
508
+ const quoted = quote(text, 60);
509
+ const wordLabel = `${words} word${words === 1 ? "" : "s"}`;
510
+ const measured = `${seconds(held)} (\xB1${Math.max(0.01, error).toFixed(2)} s)`;
511
+ const target = lineTargetSec(words);
512
+ const message = finalLine ? `The film's last line ${quoted} (${wordLabel}) is on screen ${measured}, under the ${seconds(floor)} a closing line needs: hold it about 0.5 s + 0.3 s a word and at least 2 s before the film ends; see references/motion.md.` : `${quoted} (${wordLabel}) is on screen ${measured}, under its ${seconds(floor)} floor: hold it about 0.5 s + 0.3 s a word (${seconds(target)} here) before it leaves or changes; see references/motion.md.`;
513
+ const firstSeen = track.seen.get(candidate.firstIndex);
514
+ const rect = firstSeen?.rect;
515
+ return {
516
+ code: "line_unread",
517
+ severity: "warning",
518
+ message,
519
+ text,
520
+ selector: track.selector,
521
+ dataAttributes: {},
522
+ sourceFile: track.sourceFile,
523
+ bbox: rect ? { x: rect.left, y: rect.top, width: rect.right - rect.left, height: rect.bottom - rect.top } : ZERO_BBOX,
524
+ time: round(window2.startIn),
525
+ words,
526
+ seconds: round(held),
527
+ floorSeconds: round(floor)
528
+ };
529
+ }
530
+ function typeFloorPx(canvas) {
531
+ return Math.round(T.typeFloorFraction * Math.min(canvas.width, canvas.height));
532
+ }
533
+ function detectTextBelowFloor(frames, duration, canvas) {
534
+ const floorPx = typeFloorPx(canvas);
535
+ const step = sampleStep(frames);
536
+ const sightings = /* @__PURE__ */ new Map();
537
+ frames.forEach((frame, index) => {
538
+ for (const part of frame.parts) {
539
+ if (part.captions || part.opacity < T.readOpacity) continue;
540
+ if (part.fontPx >= floorPx - 0.5 || readingWords(part.text) === 0) continue;
541
+ const seen = sightings.get(part.key) ?? /* @__PURE__ */ new Map();
542
+ seen.set(index, part);
543
+ sightings.set(part.key, seen);
544
+ }
545
+ });
546
+ const small = [];
547
+ for (const seen of sightings.values()) {
548
+ const [first, last] = longestRun(indexRuns([...seen.keys()]));
549
+ const start = frames[first]?.time ?? 0;
550
+ const end = last === frames.length - 1 ? duration : (frames[last]?.time ?? start) + step;
551
+ if (end - start < T.typeFloorHoldSec) continue;
552
+ const parts = [...seen.values()];
553
+ const part = parts[0];
554
+ if (!part) continue;
555
+ small.push({
556
+ selector: part.selector,
557
+ sourceFile: part.sourceFile,
558
+ text: part.text,
559
+ fontPx: Math.min(...parts.map((entry) => entry.fontPx)),
560
+ seconds: end - start,
561
+ time: start,
562
+ bbox: {
563
+ x: part.rect.left,
564
+ y: part.rect.top,
565
+ width: part.rect.width,
566
+ height: part.rect.height
567
+ }
568
+ });
569
+ }
570
+ const shortSide = Math.min(canvas.width, canvas.height);
571
+ return groupBySourceFile(small).map((group) => smallTextFinding(group, floorPx, shortSide));
572
+ }
573
+ function longestRun(runs) {
574
+ return runs.reduce((best, run) => run[1] - run[0] > best[1] - best[0] ? run : best, [0, -1]);
575
+ }
576
+ function groupBySourceFile(small) {
577
+ const groups = /* @__PURE__ */ new Map();
578
+ for (const entry of small)
579
+ groups.set(entry.sourceFile, [...groups.get(entry.sourceFile) ?? [], entry]);
580
+ return [...groups.values()].map(
581
+ (group) => group.sort((a, b) => a.fontPx - b.fontPx || a.time - b.time)
582
+ );
583
+ }
584
+ function quote(text, max) {
585
+ return `"${text.length > max ? `${text.slice(0, max - 3)}...` : text}"`;
586
+ }
587
+ function smallTextFinding(group, floorPx, shortSide) {
588
+ const [smallest] = group;
589
+ if (!smallest) throw new Error("text_below_floor group is empty");
590
+ const subject = group.length === 1 ? `${quote(smallest.text, 48)} renders at ${Math.round(smallest.fontPx)} px for ${seconds(smallest.seconds)}` : `${group.length} text elements stay under it for 0.5 s or more, the smallest ${quote(smallest.text, 40)} at ${Math.round(smallest.fontPx)} px`;
591
+ const byTime = [...group].sort((a, b) => a.time - b.time);
592
+ return {
593
+ code: "text_below_floor",
594
+ severity: "warning",
595
+ message: `Type floor is ${floorPx} px (2.6% of the ${shortSide} px short side, what a phone can read) and ${subject}: set reading text to at least ${floorPx} px, or put data-layout-ignore on text that is texture rather than reading; see references/look.md.`,
596
+ text: smallest.text,
597
+ selector: smallest.selector,
598
+ dataAttributes: {},
599
+ sourceFile: smallest.sourceFile,
600
+ bbox: smallest.bbox,
601
+ time: round(smallest.time),
602
+ times: byTime.map((entry) => round(entry.time)),
603
+ fontPx: round(smallest.fontPx),
604
+ floorPx,
605
+ seconds: round(smallest.seconds),
606
+ elements: group.map((entry) => ({
607
+ selector: entry.selector,
608
+ text: entry.text,
609
+ fontPx: round(entry.fontPx),
610
+ time: round(entry.time),
611
+ seconds: round(entry.seconds)
612
+ }))
613
+ };
614
+ }
615
+ function round(value) {
616
+ return Math.round(value * 1e3) / 1e3;
617
+ }
618
+
619
+ // src/utils/hdrPromotion.ts
620
+ async function bundleProjectHtml(projectDir) {
621
+ const { bundleToSingleHtml } = await import("./compiler-TM3W6GYF.js");
622
+ return bundleToSingleHtml(projectDir);
623
+ }
624
+ async function collectBundledRenderMedia(html) {
625
+ const producer = await loadProducer();
626
+ if (!producer.collectRenderMedia) throw new Error("Render media collector unavailable");
627
+ return producer.collectRenderMedia(html);
628
+ }
629
+ var DEFAULT_DEPENDENCIES = {
630
+ bundleHtml: bundleProjectHtml,
631
+ collectMedia: collectBundledRenderMedia
632
+ };
633
+ async function inspectHdrAutoPromotion(project, extractMetadata = extractMediaMetadata, dependencies = DEFAULT_DEPENDENCIES) {
634
+ const assets = [];
635
+ const media = await dependencies.collectMedia(await dependencies.bundleHtml(project.dir));
636
+ for (const { src } of [...media.videos, ...media.images]) {
637
+ const path = /^https?:\/\//i.test(src) ? src : resolveProjectRelativeSrc(src, project.dir, project.dir);
638
+ try {
639
+ assets.push({
640
+ asset: src,
641
+ colorSpace: (await extractMetadata(path)).colorSpace
642
+ });
643
+ } catch {
644
+ continue;
645
+ }
646
+ }
647
+ return findHdrAutoPromotion(assets);
648
+ }
649
+
650
+ // src/commands/contrast-bg.ts
651
+ function relativeLuminance(color) {
652
+ const channel = (value) => {
653
+ const srgb = value / 255;
654
+ return srgb <= 0.03928 ? srgb / 12.92 : ((srgb + 0.055) / 1.055) ** 2.4;
655
+ };
656
+ return 0.2126 * channel(color[0]) + 0.7152 * channel(color[1]) + 0.0722 * channel(color[2]);
657
+ }
658
+ function contrastRatio(first, second) {
659
+ const firstLuminance = relativeLuminance(first);
660
+ const secondLuminance = relativeLuminance(second);
661
+ const lighter = Math.max(firstLuminance, secondLuminance);
662
+ const darker = Math.min(firstLuminance, secondLuminance);
663
+ return (lighter + 0.05) / (darker + 0.05);
664
+ }
665
+ function requiredContrastRatio(large) {
666
+ return large ? 3 : 4.5;
667
+ }
668
+ function suggestCompliantForegroundColor(foreground, background, requiredRatio) {
669
+ if (contrastRatio(foreground, background) >= requiredRatio) return [...foreground];
670
+ const black = [0, 0, 0];
671
+ const white = [255, 255, 255];
672
+ const target = contrastRatio(white, background) >= contrastRatio(black, background) ? white : black;
673
+ for (let step = 1; step <= 255; step += 1) {
674
+ const amount = step / 255;
675
+ const candidate = [
676
+ Math.round(foreground[0] + (target[0] - foreground[0]) * amount),
677
+ Math.round(foreground[1] + (target[1] - foreground[1]) * amount),
678
+ Math.round(foreground[2] + (target[2] - foreground[2]) * amount)
679
+ ];
680
+ if (contrastRatio(candidate, background) >= requiredRatio) return candidate;
681
+ }
682
+ return [...target];
683
+ }
684
+ function parseColorRGBA(color) {
685
+ const body = /rgba?\(([^)]+)\)/.exec(color ?? "")?.[1];
686
+ if (body == null) return null;
687
+ const p = body.split(",").map((s) => parseFloat(s.trim()));
688
+ if (p.length < 3 || p.some((n) => Number.isNaN(n))) return null;
689
+ const [r, g, b, a] = p;
690
+ if (r == null || g == null || b == null) return null;
691
+ return [r, g, b, a ?? 1];
692
+ }
693
+
694
+ // src/utils/checkPipeline.ts
695
+ var MOTION_FPS = 20;
696
+ var MOTION_MAX_SAMPLES = 300;
697
+ var ZERO_BBOX2 = { x: 0, y: 0, width: 0, height: 0 };
698
+ var FRAME_BREACH_FLOOR_PX = 120;
699
+ var FRAME_BREACH_FLOOR_FRACTION = 0.06;
700
+ var DEFAULT_CHECK_OPTIONS = {
701
+ samples: 9,
702
+ atTransitions: false,
703
+ maxIssues: 80,
704
+ collapseStatic: true,
705
+ tolerance: 2,
706
+ timeout: 3e3,
707
+ contrast: true,
708
+ strict: false,
709
+ snapshots: false,
710
+ browserGpuMode: "auto"
711
+ };
712
+ function selectContrastTimes(grid) {
713
+ if (grid.length <= 5) return [...grid];
714
+ return Array.from({ length: 5 }, (_, index) => {
715
+ const selected = Math.floor(index * (grid.length - 1) / 4);
716
+ return grid[selected] ?? grid[0] ?? 0;
717
+ });
718
+ }
719
+ function buildMotionSampleTimes(duration) {
720
+ if (!Number.isFinite(duration) || duration <= 0) return [];
721
+ const count = Math.min(MOTION_MAX_SAMPLES, Math.max(2, Math.ceil(duration * MOTION_FPS) + 1));
722
+ const step = duration / (count - 1);
723
+ return Array.from({ length: count }, (_, index) => Math.round(index * step * 1e3) / 1e3);
724
+ }
725
+ function gateSampleTimes(duration, seeks, fallback) {
726
+ if (!Number.isFinite(duration) || duration <= 0) return [];
727
+ const fractions = seeks && seeks.length > 0 ? seeks : [fallback];
728
+ return mergeSampleTimes(fractions.map((fraction) => fraction * duration));
729
+ }
730
+ function validateSampleRange([start, end], duration) {
731
+ if (!Number.isFinite(start + end) || start < 0 || end <= start || end > duration)
732
+ throw new Error(`Check range must lie within the composition duration (${duration} seconds)`);
733
+ }
734
+ function gateSamplesInRange(flag, samples, inRange, range) {
735
+ if (!range) return samples;
736
+ const selected = samples.filter(inRange);
737
+ if (samples.length > 0 && selected.length === 0) {
738
+ throw new Error(`No ${flag} seek samples lie within --range`);
739
+ }
740
+ return selected;
741
+ }
742
+ function layoutSamplesInRange(options, duration) {
743
+ if (!options.range)
744
+ return {
745
+ inRange: (_t) => true,
746
+ baseSamples: buildLayoutSampleTimes({ duration, samples: options.samples, at: options.at })
747
+ };
748
+ validateSampleRange(options.range, duration);
749
+ const [start, end] = options.range;
750
+ const inRange = (t) => t >= start && t <= end;
751
+ const selected = (options.at ?? []).filter(inRange);
752
+ if (options.at?.length && !selected.length) throw new Error("No --at samples lie within --range");
753
+ const baseSamples = buildLayoutSampleTimes({
754
+ duration: end - start,
755
+ samples: options.samples,
756
+ at: selected.map((t) => t - start)
757
+ }).map((t) => t + start);
758
+ return { inRange, baseSamples };
759
+ }
760
+ async function buildSampleGrid(driver, options) {
761
+ const duration = await driver.getDuration();
762
+ const { inRange, baseSamples } = layoutSamplesInRange(options, duration);
763
+ const transitions = options.atTransitions ? buildTransitionSampleTimes({
764
+ duration,
765
+ boundaries: (await driver.getTransitionBoundaries()).filter(inRange),
766
+ cap: options.maxTransitionSamples
767
+ }) : { times: [], dropped: 0 };
768
+ const captionSamples = gateSamplesInRange(
769
+ "--caption-zone",
770
+ options.captionZone ? gateSampleTimes(duration, options.captionZone.seek, 1) : [],
771
+ inRange,
772
+ options.range
773
+ );
774
+ const frameSamples = gateSamplesInRange(
775
+ "--frame-check",
776
+ options.frameCheck ? gateSampleTimes(duration, options.frameCheck.seek, 0.5) : [],
777
+ inRange,
778
+ options.range
779
+ );
780
+ transitions.times = transitions.times.filter(inRange);
781
+ const auditSamples = mergeSampleTimes(baseSamples, transitions.times).filter(inRange);
782
+ const layoutSamples = mergeSampleTimes(auditSamples, captionSamples, frameSamples).filter(
783
+ inRange
784
+ );
785
+ if (layoutSamples.length === 0) {
786
+ throw new Error("Could not determine composition duration \u2014 no layout samples run");
787
+ }
788
+ return {
789
+ duration,
790
+ layoutSamples,
791
+ userPickedSamples: options.at?.length ? baseSamples : [],
792
+ captionSamples,
793
+ frameSamples,
794
+ transitionSamples: transitions.times,
795
+ transitionSamplesDropped: transitions.dropped,
796
+ contrastSamples: options.contrast ? selectContrastTimes(auditSamples) : []
797
+ };
798
+ }
799
+ async function planMotionSampling(driver, motion, duration) {
800
+ if (motion.kind !== "valid") {
801
+ return { times: [], selectors: [], livenessScopes: [], preflightIssues: [], assertions: [] };
802
+ }
803
+ const preflightIssues = await driver.findAmbiguousSelectors(
804
+ collectSamplingTargets(motion.spec.assertions).selectors
805
+ );
806
+ const ambiguous = new Set(preflightIssues.map((issue) => issue.selector));
807
+ const assertions = motion.spec.assertions.filter(
808
+ (assertion) => !assertionTargets(assertion).selectors.some((selector) => ambiguous.has(selector))
809
+ );
810
+ noteSkippedAssertions(preflightIssues, motion.spec.assertions);
811
+ const targets = collectSamplingTargets(assertions);
812
+ const times = assertions.length > 0 ? buildMotionSampleTimes(motion.spec.duration ?? duration) : [];
813
+ return { times, ...targets, preflightIssues, assertions };
814
+ }
815
+ function noteSkippedAssertions(issues, assertions) {
816
+ for (const issue of issues) {
817
+ const skipped = assertions.filter(
818
+ (assertion) => assertionTargets(assertion).selectors.includes(issue.selector)
819
+ ).length;
820
+ if (skipped > 0) issue.message += ` ${skipped} assertion(s) naming it were not evaluated.`;
821
+ }
822
+ }
823
+ function geometryRequest(time, grid, options) {
824
+ const text = grid.captionSamples.includes(time);
825
+ const media = grid.frameSamples.includes(time);
826
+ if (!text && !media) return null;
827
+ const configuredTolerance = options.frameCheck?.tol;
828
+ const tolerance = typeof configuredTolerance === "number" ? configuredTolerance : 2;
829
+ return { text, media, tolerance };
830
+ }
831
+ function candidateIsSized(candidate, canvas) {
832
+ if (candidate.elementRect.width < 4 || candidate.elementRect.height < 4) return false;
833
+ return !(candidate.elementRect.width >= 0.95 * canvas.width && candidate.elementRect.height >= 0.95 * canvas.height);
834
+ }
835
+ function geometryIssueAnchor(candidate, time) {
836
+ return {
837
+ selector: candidate.selector,
838
+ dataAttributes: candidate.dataAttributes,
839
+ sourceFile: candidate.sourceFile,
840
+ bbox: candidate.bbox,
841
+ time,
842
+ rect: candidate.rect
843
+ };
844
+ }
845
+ function captionBoxOverlapsZone(box, zone, canvas) {
846
+ const zx0 = zone.x0 * canvas.width;
847
+ const zy0 = zone.y0 * canvas.height;
848
+ const zx1 = zone.x1 * canvas.width;
849
+ const zy1 = zone.y1 * canvas.height;
850
+ const bx1 = box.left + box.width;
851
+ const by1 = box.top + box.height;
852
+ const overlaps = box.left < zx1 && bx1 > zx0 && box.top < zy1 && by1 > zy0;
853
+ return { overlaps, cy: box.top + box.height / 2 };
854
+ }
855
+ function captionFinding(candidate, options, canvas, time) {
856
+ const zone = options.captionZone;
857
+ if (!zone || candidate.kind !== "text" || !candidateIsSized(candidate, canvas)) return null;
858
+ if ("data-layout-allow-caption-zone" in candidate.dataAttributes) return null;
859
+ const { overlaps, cy } = captionBoxOverlapsZone(candidate.elementRect, zone, canvas);
860
+ if (!overlaps) return null;
861
+ const text = candidate.text.slice(0, 48);
862
+ const pctFromBottom = Math.round((canvas.height - cy) / canvas.height * 100);
863
+ return {
864
+ ...geometryIssueAnchor(candidate, time),
865
+ code: "caption_zone_collision",
866
+ severity: zone.severity === "error" ? "error" : "warning",
867
+ text,
868
+ message: `<${candidate.tag}> "${text}" overlaps the reserved caption band (~${pctFromBottom}% up from the bottom).`,
869
+ fixHint: "Keep main content outside the configured caption band, or mark intentional lower-third copy with data-layout-allow-caption-zone."
870
+ };
871
+ }
872
+ function maxOverflow(candidate) {
873
+ if (!candidate.overflow) return 0;
874
+ return Math.max(
875
+ candidate.overflow.left ?? 0,
876
+ candidate.overflow.top ?? 0,
877
+ candidate.overflow.right ?? 0,
878
+ candidate.overflow.bottom ?? 0
879
+ );
880
+ }
881
+ function overflowMessage(candidate) {
882
+ const overflow = candidate.overflow ?? {};
883
+ const edges = [];
884
+ if (overflow.left) edges.push(`${overflow.left}px past the left`);
885
+ if (overflow.top) edges.push(`${overflow.top}px past the top`);
886
+ if (overflow.right) edges.push(`${overflow.right}px past the right`);
887
+ if (overflow.bottom) edges.push(`${overflow.bottom}px past the bottom`);
888
+ return `<${candidate.tag}> "${candidate.text.slice(0, 48)}" spills outside the frame (${edges.join(", ")}).`;
889
+ }
890
+ function frameFinding(candidate, options, canvas, time) {
891
+ if (!options.frameCheck || candidate.kind !== "media" || !candidateIsSized(candidate, canvas)) {
892
+ return null;
893
+ }
894
+ const floor = Math.max(
895
+ FRAME_BREACH_FLOOR_PX,
896
+ FRAME_BREACH_FLOOR_FRACTION * Math.min(canvas.width, canvas.height)
897
+ );
898
+ if (maxOverflow(candidate) < floor) return null;
899
+ const text = candidate.text.slice(0, 48);
900
+ return {
901
+ ...geometryIssueAnchor(candidate, time),
902
+ code: "frame_out_of_frame",
903
+ severity: options.frameCheck.severity === "error" ? "error" : "warning",
904
+ text,
905
+ overflow: candidate.overflow,
906
+ message: overflowMessage(candidate),
907
+ fixHint: "Keep media within the composition frame's safe area."
908
+ };
909
+ }
910
+ async function collectGeometryAt(driver, options, grid, canvas, time) {
911
+ const request = geometryRequest(time, grid, options);
912
+ if (!request) return [];
913
+ const candidates = await driver.collectGeometryCandidates(time, request);
914
+ const issues = [];
915
+ for (const candidate of candidates) {
916
+ if (request.text) {
917
+ const finding = captionFinding(candidate, options, canvas, time);
918
+ if (finding) issues.push(finding);
919
+ }
920
+ if (request.media) {
921
+ const finding = frameFinding(candidate, options, canvas, time);
922
+ if (finding) issues.push(finding);
923
+ }
924
+ }
925
+ return issues;
926
+ }
927
+ function emptyGridSamples() {
928
+ return {
929
+ layoutIssues: [],
930
+ motionFrames: [],
931
+ contrastEntries: [],
932
+ screenshots: [],
933
+ contrastMs: 0,
934
+ layoutStateSignatures: [],
935
+ rotationSamples: [],
936
+ indicatorFrames: [],
937
+ craftFrames: []
938
+ };
939
+ }
940
+ function appendGridSamples(into, from) {
941
+ into.layoutIssues.push(...from.layoutIssues);
942
+ into.motionFrames.push(...from.motionFrames);
943
+ into.contrastEntries.push(...from.contrastEntries);
944
+ into.screenshots.push(...from.screenshots);
945
+ into.contrastMs += from.contrastMs;
946
+ into.layoutStateSignatures.push(...from.layoutStateSignatures);
947
+ into.rotationSamples.push(...from.rotationSamples);
948
+ into.indicatorFrames.push(...from.indicatorFrames);
949
+ into.craftFrames.push(...from.craftFrames);
950
+ }
951
+ async function collectSampleAt(driver, options, grid, motion, sets, time) {
952
+ const collected = emptyGridSamples();
953
+ await driver.seek(time, sets.contrast.has(time));
954
+ const issuesAtTime = [];
955
+ if (sets.layout.has(time)) {
956
+ const layoutIssues = await driver.collectLayout(time, options.tolerance, options.layout);
957
+ collected.layoutIssues.push(...layoutIssues);
958
+ issuesAtTime.push(...layoutIssues);
959
+ collected.layoutStateSignatures.push({
960
+ time,
961
+ signature: await driver.collectLayoutGeometry(),
962
+ clock: await driver.collectSeekClock()
963
+ });
964
+ collected.rotationSamples.push(...await driver.collectRotationSample(time));
965
+ collected.indicatorFrames.push(await driver.collectOffPivotRotationSample(time));
966
+ }
967
+ if (sets.canvas) {
968
+ const geometryIssues = await collectGeometryAt(driver, options, grid, sets.canvas, time);
969
+ collected.layoutIssues.push(...geometryIssues);
970
+ issuesAtTime.push(...geometryIssues);
971
+ }
972
+ if (sets.motion.has(time)) {
973
+ collected.motionFrames.push(
974
+ await driver.collectMotionFrame(time, motion.selectors, motion.livenessScopes)
975
+ );
976
+ }
977
+ if (sets.contrast.has(time))
978
+ await collectContrastAt(driver, options, time, issuesAtTime, collected);
979
+ return collected;
980
+ }
981
+ async function collectGridSamples(driver, options, grid, motion, helpers, enter, report, keep) {
982
+ const geometryEnabled = grid.captionSamples.length > 0 || grid.frameSamples.length > 0;
983
+ const sets = {
984
+ layout: new Set(grid.layoutSamples),
985
+ motion: new Set(motion.times),
986
+ contrast: new Set(grid.contrastSamples),
987
+ canvas: geometryEnabled ? await driver.getCanvas() : null
988
+ };
989
+ const times = mergeSampleTimes(grid.layoutSamples, motion.times);
990
+ const pending = times.map((_, index) => index);
991
+ const perTime = [];
992
+ keep(() => {
993
+ const so = emptyGridSamples();
994
+ for (const samples of perTime) if (samples) appendGridSamples(so, samples);
995
+ const contrast = buildContrastResults(so.contrastEntries);
996
+ return {
997
+ layoutIssues: so.layoutIssues,
998
+ contrastFindings: contrast.findings,
999
+ contrastChecked: so.contrastEntries.length,
1000
+ contrastPassed: contrast.passed
1001
+ };
1002
+ });
1003
+ let finished = 0;
1004
+ const work = async (worker, taken = []) => {
1005
+ for (let index = pending.shift(); index !== void 0; index = pending.shift()) {
1006
+ taken.push(index);
1007
+ const time = times[index] ?? 0;
1008
+ perTime[index] = await collectSampleAt(worker, options, grid, motion, sets, time);
1009
+ enter("samples");
1010
+ report("samples", ++finished, times.length);
1011
+ }
1012
+ };
1013
+ const helpWith = async (helper) => {
1014
+ const taken = [];
1015
+ try {
1016
+ await helper.initialize(options.contrast);
1017
+ helper.markSamples?.("start");
1018
+ await work(helper, taken);
1019
+ helper.markSamples?.("end");
1020
+ } catch {
1021
+ helper.markSamples?.("drop");
1022
+ for (const index of taken) delete perTime[index];
1023
+ pending.push(...taken);
1024
+ }
1025
+ };
1026
+ let firstPageDone = false;
1027
+ const running = [];
1028
+ void helpers.then((ready) => {
1029
+ for (const helper of ready) if (!firstPageDone) running.push(helpWith(helper));
1030
+ });
1031
+ driver.markSamples?.("start");
1032
+ await work(driver);
1033
+ firstPageDone = true;
1034
+ await Promise.all(running);
1035
+ await work(driver);
1036
+ driver.markSamples?.("end");
1037
+ const collected = emptyGridSamples();
1038
+ for (const samples of perTime) appendGridSamples(collected, samples);
1039
+ enter("overlap");
1040
+ await collectMotionOverlapSamples(
1041
+ driver,
1042
+ grid,
1043
+ collected,
1044
+ options.range,
1045
+ craftEnabled(driver, options),
1046
+ () => enter("overlap"),
1047
+ report
1048
+ );
1049
+ enter("samples");
1050
+ return collected;
1051
+ }
1052
+ function craftEnabled(driver, options) {
1053
+ return !options.range && typeof driver.collectCraftFrame === "function";
1054
+ }
1055
+ async function collectContrastAt(driver, options, time, issuesAtTime, collected) {
1056
+ const contrastStart = Date.now();
1057
+ const capture = options.snapshots ? await driver.collectContrast(time, annotationBoxesFrom(issuesAtTime)) : await driver.collectContrast(time);
1058
+ collected.contrastMs += Date.now() - contrastStart;
1059
+ collected.contrastEntries.push(...capture.entries);
1060
+ if (capture.pngBase64) collected.screenshots.push({ time, pngBase64: capture.pngBase64 });
1061
+ }
1062
+ var OVERLAP_SAMPLE_FPS = 8;
1063
+ var OVERLAP_MAX_SAMPLES = 240;
1064
+ var OVERLAP_HOLD_PROBES = 60;
1065
+ var OVERLAP_BATCH = 8;
1066
+ function evenlyPicked(times, count) {
1067
+ if (times.length <= count) return times;
1068
+ const last = Math.max(1, count - 1);
1069
+ return Array.from(
1070
+ { length: count },
1071
+ (_, i) => times[Math.floor(i * (times.length - 1) / last)] ?? 0
1072
+ );
1073
+ }
1074
+ function uniformTimes(duration, fill) {
1075
+ const step = duration / Math.max(1, fill - 1);
1076
+ return Array.from({ length: fill }, (_, i) => i * step);
1077
+ }
1078
+ function buildOverlapSampleTimes(duration, clipEdges) {
1079
+ if (!Number.isFinite(duration) || duration <= 0) return [];
1080
+ const count = Math.min(
1081
+ OVERLAP_MAX_SAMPLES - OVERLAP_HOLD_PROBES,
1082
+ Math.max(2, Math.ceil(duration * OVERLAP_SAMPLE_FPS) + 1)
1083
+ );
1084
+ const cuts = [];
1085
+ for (const time of mergeSampleTimes(
1086
+ [0, duration],
1087
+ clipEdges.filter((t) => t > 0 && t < duration)
1088
+ ))
1089
+ if (time - (cuts.at(-1) ?? -Infinity) >= CONTENT_OVERLAP_HELD_ERROR_MS / 1e3) cuts.push(time);
1090
+ const uncovered = (fill2) => {
1091
+ const step = duration / Math.max(1, fill2 - 1);
1092
+ return cuts.slice(1).flatMap((end, i) => {
1093
+ const start = cuts[i] ?? 0;
1094
+ return Math.floor(start / step) === Math.floor(end / step) ? [(start + end) / 2] : [];
1095
+ });
1096
+ };
1097
+ let fill = count - Math.floor(count / 2);
1098
+ let windows = uncovered(fill);
1099
+ if (windows.length < count - fill) {
1100
+ fill = count - windows.length;
1101
+ windows = uncovered(fill);
1102
+ }
1103
+ const picked = evenlyPicked(windows, count - fill);
1104
+ return mergeSampleTimes(
1105
+ [...picked, ...uniformTimes(duration, fill)].map((t) => Math.round(t * 1e3) / 1e3)
1106
+ );
1107
+ }
1108
+ function holdProber(driver, collected, end) {
1109
+ const holdS = CONTENT_OVERLAP_HELD_ERROR_MS / 1e3;
1110
+ const held = /* @__PURE__ */ new Set();
1111
+ const keyOf = (issue) => `${issue.code} ${issue.selector}`;
1112
+ let probes = 0;
1113
+ return async (time, next, issues) => {
1114
+ const unproven = new Set(issues.map(keyOf).filter((key) => !held.has(key)));
1115
+ const probe = Math.round((time + holdS) * 1e3) / 1e3;
1116
+ const sparse = next - time > 2 * holdS && probe <= end;
1117
+ if (!unproven.size || !sparse || probes >= OVERLAP_HOLD_PROBES) return;
1118
+ probes += 1;
1119
+ await driver.seekGeometry(probe);
1120
+ const later = await driver.collectOverlap(probe);
1121
+ collected.layoutIssues.push(...later);
1122
+ for (const key of later.map(keyOf)) if (unproven.has(key)) held.add(key);
1123
+ };
1124
+ }
1125
+ async function denseOverlapTimes(driver, grid, range) {
1126
+ const offset = range?.[0] ?? 0;
1127
+ const edges = (await driver.getClipEdges()).map((time) => time - offset);
1128
+ const overlapTimes = buildOverlapSampleTimes(range ? range[1] - range[0] : grid.duration, edges);
1129
+ return range ? mergeSampleTimes(overlapTimes.map((time) => time + range[0])) : overlapTimes;
1130
+ }
1131
+ async function collectOverlapGridInBatches(driver, times, progress, report) {
1132
+ const issues = [];
1133
+ const batches = Math.ceil(times.length / OVERLAP_BATCH);
1134
+ for (let from = 0; from < times.length; from += OVERLAP_BATCH) {
1135
+ issues.push(...await driver.collectOverlapGrid(times.slice(from, from + OVERLAP_BATCH)));
1136
+ progress();
1137
+ report("overlap", from / OVERLAP_BATCH + 1, batches);
1138
+ }
1139
+ return issues;
1140
+ }
1141
+ async function collectMotionOverlapSamples(driver, grid, collected, range, craft, progress, report) {
1142
+ const baseTimes = new Set(grid.layoutSamples);
1143
+ const shiftedTimes = await denseOverlapTimes(driver, grid, range);
1144
+ if (craft) {
1145
+ const craftTimes = buildCraftSampleTimes(grid.duration).filter((time) => time < grid.duration);
1146
+ for (const [index, time] of craftTimes.entries()) {
1147
+ await driver.seekGeometry(time);
1148
+ collected.craftFrames.push(...await collectCraftFrame(driver, time));
1149
+ progress();
1150
+ report("craft", index + 1, craftTimes.length);
1151
+ }
1152
+ }
1153
+ const probeHold = holdProber(driver, collected, range ? range[1] : grid.duration);
1154
+ const gridTimes = shiftedTimes.filter((t) => !baseTimes.has(t));
1155
+ const gridIssues = await collectOverlapGridInBatches(driver, gridTimes, progress, report);
1156
+ let sample = 0;
1157
+ for (const [index, time] of shiftedTimes.entries()) {
1158
+ if (baseTimes.has(time)) continue;
1159
+ const issues = gridIssues[sample++] ?? [];
1160
+ collected.layoutIssues.push(...issues);
1161
+ await probeHold(time, shiftedTimes[index + 1] ?? Number.POSITIVE_INFINITY, issues);
1162
+ progress();
1163
+ }
1164
+ }
1165
+ var CRAFT_SAMPLE_FPS = 8;
1166
+ var CRAFT_MAX_SAMPLES = 600;
1167
+ function buildCraftSampleTimes(duration) {
1168
+ if (!Number.isFinite(duration) || duration <= 0) return [];
1169
+ const count = Math.min(
1170
+ CRAFT_MAX_SAMPLES,
1171
+ Math.max(2, Math.ceil(duration * CRAFT_SAMPLE_FPS) + 1)
1172
+ );
1173
+ const step = duration / (count - 1);
1174
+ return mergeSampleTimes(
1175
+ Array.from({ length: count }, (_, index) => Math.round(index * step * 1e3) / 1e3)
1176
+ );
1177
+ }
1178
+ async function collectCraftFrame(driver, time) {
1179
+ return driver.collectCraftFrame ? [await driver.collectCraftFrame(time)] : [];
1180
+ }
1181
+ var SWEEP_STATIC_MIN_DURATION_SEC = 3;
1182
+ var ZERO_LAYOUT_RECT = {
1183
+ left: 0,
1184
+ top: 0,
1185
+ right: 0,
1186
+ bottom: 0,
1187
+ width: 0,
1188
+ height: 0
1189
+ };
1190
+ function detectSweepStatic(duration, samples, motionIssues, hasNoTimelineDeclaration) {
1191
+ if (hasNoTimelineDeclaration) return [];
1192
+ if (duration < SWEEP_STATIC_MIN_DURATION_SEC) return [];
1193
+ if (samples.length < 2) return [];
1194
+ if (motionIssues.some((issue) => issue.code === "motion_frozen")) return [];
1195
+ const signatures = samples.map((sample) => sample.signature);
1196
+ if (allSame(signatures))
1197
+ return [sweepStaticIssue(anAnimationIsStuck(samples) ? "error" : "still")];
1198
+ if (allSame(signatures.map(seenPart))) return [sweepStaticIssue("audio")];
1199
+ return [];
1200
+ }
1201
+ async function framesMoved(driver, times) {
1202
+ const [firstTime, ...rest] = times;
1203
+ if (firstTime === void 0) return false;
1204
+ const first = await frameAt(driver, firstTime);
1205
+ for (const time of rest) {
1206
+ if (!await driver.framesDiffer(first, await frameAt(driver, time))) continue;
1207
+ return !await driver.framesDiffer(first, await frameAt(driver, firstTime));
1208
+ }
1209
+ return false;
1210
+ }
1211
+ async function frameAt(driver, time, scope) {
1212
+ await driver.seek(time, true);
1213
+ return driver.captureFrame(scope);
1214
+ }
1215
+ async function withFrameMotion(driver, frames, assertions) {
1216
+ const marked = frames.map((frame) => ({ ...frame, liveness: { ...frame.liveness } }));
1217
+ for (const assertion of assertions) {
1218
+ if (assertion.kind !== "keepsMoving") continue;
1219
+ const scope = assertion.withinSelector ?? "*";
1220
+ if (frames.every((frame) => !frame.liveness[scope])) continue;
1221
+ const maxStaticSec = assertion.maxStaticSec ?? DEFAULT_MAX_STATIC_SEC;
1222
+ for (const span of frozenSpans(frames, scope, maxStaticSec)) {
1223
+ const held = frames.find((frame) => frame.time === span.start)?.liveness[scope];
1224
+ const inSpan = marked.filter(
1225
+ (frame, index) => frame.time >= span.start && frame.time <= span.end && frames[index]?.liveness[scope] === held
1226
+ );
1227
+ await markFrameChanges(driver, inSpan, assertion.withinSelector, maxStaticSec / 2);
1228
+ }
1229
+ }
1230
+ return marked;
1231
+ }
1232
+ async function markFrameChanges(driver, frames, within, captureEverySec) {
1233
+ const scope = within ?? "*";
1234
+ let reference;
1235
+ let mark = 0;
1236
+ let nextCapture = Number.NEGATIVE_INFINITY;
1237
+ for (const frame of frames) {
1238
+ if (frame.time >= nextCapture) {
1239
+ nextCapture = frame.time + captureEverySec;
1240
+ const capture = await frameAt(driver, frame.time, within);
1241
+ if (reference === void 0) reference = capture;
1242
+ else if (await driver.framesDiffer(reference, capture)) {
1243
+ reference = capture;
1244
+ mark++;
1245
+ }
1246
+ }
1247
+ frame.liveness[scope] = `${frame.liveness[scope] ?? ""}|frame${mark}`;
1248
+ }
1249
+ }
1250
+ function anAnimationIsStuck(samples) {
1251
+ const seenById = /* @__PURE__ */ new Map();
1252
+ for (const sample of samples) {
1253
+ for (const clock of new Map(sample.clock.map((clock2) => [clock2.id, clock2])).values()) {
1254
+ seenById.set(clock.id, [...seenById.get(clock.id) ?? [], clock]);
1255
+ }
1256
+ }
1257
+ return [...seenById.values()].some(
1258
+ (seen) => seen.length > 1 && allSame(seen.map((clock) => clock.time)) && seen.every((clock) => !clock.done)
1259
+ );
1260
+ }
1261
+ var AUDIO_TIME_SEPARATOR = "";
1262
+ function seenPart(signature) {
1263
+ return signature.split(AUDIO_TIME_SEPARATOR)[0] ?? signature;
1264
+ }
1265
+ function allSame(values) {
1266
+ return values.every((value) => value === values[0]);
1267
+ }
1268
+ var STILL_FIX_HINT = "If the composition is meant to be still, add `data-no-timeline` to the element with `data-composition-id`. Otherwise confirm it seeks a paused GSAP/CSS timeline under `data-*` timing attributes rather than only autoplaying.";
1269
+ var SWEEP_STATIC_MESSAGES = {
1270
+ error: "Timeline did not advance under seek; every green verdict on this run is unreliable.",
1271
+ still: "Nothing on screen moved under seek.",
1272
+ audio: "Only the audio advanced under seek; nothing on screen moved."
1273
+ };
1274
+ function sweepStaticIssue(kind) {
1275
+ return {
1276
+ code: "sweep_static",
1277
+ severity: kind === "error" ? "error" : "warning",
1278
+ time: 0,
1279
+ selector: "[data-composition-id]",
1280
+ dataAttributes: {},
1281
+ sourceFile: "index.html",
1282
+ bbox: ZERO_BBOX2,
1283
+ rect: ZERO_LAYOUT_RECT,
1284
+ message: SWEEP_STATIC_MESSAGES[kind],
1285
+ fixHint: kind === "error" ? "An animation on the page never moved under seek. Build it on the paused GSAP timeline registered in `window.__timelines[compositionId]`, or as a CSS animation, so the seek drives it." : STILL_FIX_HINT
1286
+ };
1287
+ }
1288
+ var ROTATION_MIN_SAMPLES = 3;
1289
+ var ROTATION_MIN_ANGLE_SPREAD_DEG = 20;
1290
+ var ROTATION_MAX_SIZE_RATIO = 1.6;
1291
+ var ROTATION_RIGID_AABB_RATIO = 1.15;
1292
+ var ROTATION_RIGID_ESTIMATE_MIN_DET = 0.15;
1293
+ var ROTATION_MIN_MEDIAN_AREA_PX = 2500;
1294
+ var ROTATION_DRIFT_SIZE_FRACTION = 0.1;
1295
+ var ROTATION_DRIFT_VIEWPORT_FRACTION = 0.02;
1296
+ function median(values) {
1297
+ if (values.length === 0) return 0;
1298
+ const sorted = [...values].sort((a, b) => a - b);
1299
+ const mid = Math.floor(sorted.length / 2);
1300
+ const upper = sorted[mid] ?? 0;
1301
+ return sorted.length % 2 === 0 ? ((sorted[mid - 1] ?? 0) + upper) / 2 : upper;
1302
+ }
1303
+ function maxAngleSpread(angles) {
1304
+ let max = 0;
1305
+ for (let i = 0; i < angles.length; i++) {
1306
+ const a = angles[i];
1307
+ if (a === void 0) continue;
1308
+ for (let j = i + 1; j < angles.length; j++) {
1309
+ const b = angles[j];
1310
+ if (b === void 0) continue;
1311
+ const raw = Math.abs(a - b) % 360;
1312
+ max = Math.max(max, Math.min(raw, 360 - raw));
1313
+ }
1314
+ }
1315
+ return max;
1316
+ }
1317
+ function maxCenterDrift(samples) {
1318
+ let max = 0;
1319
+ for (let i = 0; i < samples.length; i++) {
1320
+ const first = samples[i];
1321
+ if (!first) continue;
1322
+ for (let j = i + 1; j < samples.length; j++) {
1323
+ const second = samples[j];
1324
+ if (!second) continue;
1325
+ max = Math.max(max, Math.hypot(first.cx - second.cx, first.cy - second.cy));
1326
+ }
1327
+ }
1328
+ return max;
1329
+ }
1330
+ function rotationDriftFinding(samples, drift) {
1331
+ const last = samples[samples.length - 1];
1332
+ if (!last) return null;
1333
+ const rect = {
1334
+ left: last.cx - last.w / 2,
1335
+ top: last.cy - last.h / 2,
1336
+ right: last.cx + last.w / 2,
1337
+ bottom: last.cy + last.h / 2,
1338
+ width: last.w,
1339
+ height: last.h
1340
+ };
1341
+ return {
1342
+ code: "rotation_pivot_drift",
1343
+ // EF promotes this to error separately; keep it a warning here.
1344
+ severity: "warning",
1345
+ time: last.time,
1346
+ selector: last.selector,
1347
+ dataAttributes: {},
1348
+ sourceFile: "index.html",
1349
+ bbox: rectToBbox(rect),
1350
+ rect,
1351
+ message: `Rotating element's bounding-box center drifts ${Math.round(drift)}px across rotation \u2014 it is not spinning about its own center (check transformOrigin/svgOrigin).`,
1352
+ fixHint: "The bounding-box center should stay fixed while the element spins; check its transformOrigin/svgOrigin so rotation pivots about the element's own center rather than a point in a coordinate space it was resized out of."
1353
+ };
1354
+ }
1355
+ function groupBy(items, keyOf) {
1356
+ const byKey = /* @__PURE__ */ new Map();
1357
+ for (const item of items) {
1358
+ const group = byKey.get(keyOf(item));
1359
+ if (group) group.push(item);
1360
+ else byKey.set(keyOf(item), [item]);
1361
+ }
1362
+ return byKey;
1363
+ }
1364
+ function groupBySelector(samples) {
1365
+ return groupBy(samples, (sample) => sample.selector);
1366
+ }
1367
+ function hasEnoughRotationSamples(group) {
1368
+ return group.length >= ROTATION_MIN_SAMPLES;
1369
+ }
1370
+ function isActuallySpinning(group) {
1371
+ return maxAngleSpread(group.map((s) => s.angle)) > ROTATION_MIN_ANGLE_SPREAD_DEG;
1372
+ }
1373
+ function aabbForRotatedRect(elemW, elemH, angleDeg) {
1374
+ const rad = angleDeg * Math.PI / 180;
1375
+ const cosAbs = Math.abs(Math.cos(rad));
1376
+ const sinAbs = Math.abs(Math.sin(rad));
1377
+ return { w: elemW * cosAbs + elemH * sinAbs, h: elemW * sinAbs + elemH * cosAbs };
1378
+ }
1379
+ function unrotatedSizeFromSample(sample) {
1380
+ const rad = sample.angle * Math.PI / 180;
1381
+ const cosAbs = Math.abs(Math.cos(rad));
1382
+ const sinAbs = Math.abs(Math.sin(rad));
1383
+ const det = cosAbs * cosAbs - sinAbs * sinAbs;
1384
+ if (Math.abs(det) < ROTATION_RIGID_ESTIMATE_MIN_DET) return null;
1385
+ const elemW = (cosAbs * sample.w - sinAbs * sample.h) / det;
1386
+ const elemH = (cosAbs * sample.h - sinAbs * sample.w) / det;
1387
+ if (!(elemW > 0) || !(elemH > 0)) return null;
1388
+ return { w: elemW, h: elemH };
1389
+ }
1390
+ function aabbMatchesSample(expected, sample) {
1391
+ if (expected.w <= 0 || expected.h <= 0 || sample.w <= 0 || sample.h <= 0) return false;
1392
+ return Math.max(expected.w, sample.w) / Math.min(expected.w, sample.w) <= ROTATION_RIGID_AABB_RATIO && Math.max(expected.h, sample.h) / Math.min(expected.h, sample.h) <= ROTATION_RIGID_AABB_RATIO;
1393
+ }
1394
+ function isSingularRotationAngle(angleDeg) {
1395
+ const rad = angleDeg * Math.PI / 180;
1396
+ const cosAbs = Math.abs(Math.cos(rad));
1397
+ const sinAbs = Math.abs(Math.sin(rad));
1398
+ return Math.abs(cosAbs * cosAbs - sinAbs * sinAbs) < ROTATION_RIGID_ESTIMATE_MIN_DET;
1399
+ }
1400
+ function isNearSquareAabb(sample) {
1401
+ if (sample.w <= 0 || sample.h <= 0) return false;
1402
+ return Math.max(sample.w, sample.h) / Math.min(sample.w, sample.h) <= ROTATION_RIGID_AABB_RATIO;
1403
+ }
1404
+ function fitsSingularPhaseRigidProjection(group) {
1405
+ if (group.length === 0 || !group.every((s) => isSingularRotationAngle(s.angle))) return false;
1406
+ if (!group.every(isNearSquareAabb)) return false;
1407
+ const ref = group[0];
1408
+ if (!ref) return false;
1409
+ return group.every((sample) => aabbMatchesSample({ w: ref.w, h: ref.h }, sample));
1410
+ }
1411
+ function fitsOneRigidRectangle(group) {
1412
+ for (const ref of group) {
1413
+ const size = unrotatedSizeFromSample(ref);
1414
+ if (!size) continue;
1415
+ if (group.every(
1416
+ (sample) => aabbMatchesSample(aabbForRotatedRect(size.w, size.h, sample.angle), sample)
1417
+ )) {
1418
+ return true;
1419
+ }
1420
+ }
1421
+ return fitsSingularPhaseRigidProjection(group);
1422
+ }
1423
+ function isRotationSizeStable(group) {
1424
+ if (group.some((s) => s.w <= 0 || s.h <= 0)) return false;
1425
+ const longSides = group.map((s) => Math.max(s.w, s.h));
1426
+ const minLong = Math.min(...longSides);
1427
+ if (minLong <= 0) return false;
1428
+ if (Math.max(...longSides) / minLong > ROTATION_MAX_SIZE_RATIO) return false;
1429
+ return fitsOneRigidRectangle(group);
1430
+ }
1431
+ function isSizableRotation(group) {
1432
+ return median(group.map((s) => s.w * s.h)) >= ROTATION_MIN_MEDIAN_AREA_PX;
1433
+ }
1434
+ function isRotationDriftCandidate(group) {
1435
+ return hasEnoughRotationSamples(group) && isActuallySpinning(group) && isRotationSizeStable(group) && isSizableRotation(group);
1436
+ }
1437
+ function detectRotationPivotDrift(samples, canvas) {
1438
+ const findings = [];
1439
+ const viewportFloor = ROTATION_DRIFT_VIEWPORT_FRACTION * Math.min(canvas.width, canvas.height);
1440
+ for (const group of groupBySelector(samples).values()) {
1441
+ if (!isRotationDriftCandidate(group)) continue;
1442
+ const medianSize = median(group.map((s) => Math.max(s.w, s.h)));
1443
+ const threshold = Math.max(ROTATION_DRIFT_SIZE_FRACTION * medianSize, viewportFloor);
1444
+ const drift = maxCenterDrift(group);
1445
+ if (drift <= threshold) continue;
1446
+ const finding = rotationDriftFinding(group, drift);
1447
+ if (finding) findings.push(finding);
1448
+ }
1449
+ return findings;
1450
+ }
1451
+ var INDICATOR_MIN_SAMPLES = 5;
1452
+ var INDICATOR_MIN_ANGLE_SPREAD_DEG = 20;
1453
+ var INDICATOR_MIN_HUB_CIRCLES = 2;
1454
+ var INDICATOR_MAX_FIT_RESIDUAL_FRACTION = 0.05;
1455
+ var INDICATOR_MAX_RIGID_LEN_VARIATION = 0.15;
1456
+ var INDICATOR_DRIFT_LENGTH_FRACTION = 0.35;
1457
+ function fitCircle(points) {
1458
+ const count = points.length;
1459
+ if (count < 3) return null;
1460
+ const meanX = points.reduce((sum, p) => sum + p.x, 0) / count;
1461
+ const meanY = points.reduce((sum, p) => sum + p.y, 0) / count;
1462
+ let suu = 0;
1463
+ let svv = 0;
1464
+ let suv = 0;
1465
+ let suuu = 0;
1466
+ let svvv = 0;
1467
+ let suvv = 0;
1468
+ let svuu = 0;
1469
+ for (const point of points) {
1470
+ const u = point.x - meanX;
1471
+ const v = point.y - meanY;
1472
+ suu += u * u;
1473
+ svv += v * v;
1474
+ suv += u * v;
1475
+ suuu += u * u * u;
1476
+ svvv += v * v * v;
1477
+ suvv += u * v * v;
1478
+ svuu += v * u * u;
1479
+ }
1480
+ const determinant = suu * svv - suv * suv;
1481
+ if (Math.abs(determinant) < 1e-6) return null;
1482
+ const rhsU = (suuu + suvv) / 2;
1483
+ const rhsV = (svvv + svuu) / 2;
1484
+ const uc = (rhsU * svv - rhsV * suv) / determinant;
1485
+ const vc = (rhsV * suu - rhsU * suv) / determinant;
1486
+ const cx = uc + meanX;
1487
+ const cy = vc + meanY;
1488
+ const radius = Math.sqrt(uc * uc + vc * vc + (suu + svv) / count);
1489
+ let squaredError = 0;
1490
+ for (const point of points) {
1491
+ const delta = Math.hypot(point.x - cx, point.y - cy) - radius;
1492
+ squaredError += delta * delta;
1493
+ }
1494
+ return { cx, cy, radius, residual: Math.sqrt(squaredError / count) };
1495
+ }
1496
+ var INDICATOR_MIN_HUB_PRESENCE_FRACTION = 0.6;
1497
+ function resolveHub(group) {
1498
+ const present = group.filter(
1499
+ (s) => s.hx !== null && s.hy !== null && s.hubCount >= INDICATOR_MIN_HUB_CIRCLES
1500
+ );
1501
+ if (present.length < INDICATOR_MIN_HUB_PRESENCE_FRACTION * group.length) return null;
1502
+ const hx = median(present.map((s) => s.hx));
1503
+ const hy = median(present.map((s) => s.hy));
1504
+ return { hx, hy };
1505
+ }
1506
+ function isRigidBody(group) {
1507
+ const lengths = group.map((s) => s.len).filter((len) => len > 0);
1508
+ if (lengths.length < group.length) return false;
1509
+ const mid = median(lengths);
1510
+ if (mid <= 0) return false;
1511
+ const spread = Math.max(...lengths) - Math.min(...lengths);
1512
+ return spread <= INDICATOR_MAX_RIGID_LEN_VARIATION * mid;
1513
+ }
1514
+ function recoverPivot(group) {
1515
+ if (group.length < INDICATOR_MIN_SAMPLES) return null;
1516
+ const endpointA = group.map((s) => ({ x: s.ax, y: s.ay }));
1517
+ const endpointB = group.map((s) => ({ x: s.bx, y: s.by }));
1518
+ const fits = [fitCircle(endpointA), fitCircle(endpointB)].filter(
1519
+ (fit) => fit !== null
1520
+ );
1521
+ if (fits.length === 0) return null;
1522
+ const best = fits.reduce((widest, fit) => fit.radius > widest.radius ? fit : widest);
1523
+ if (best.radius <= 0) return null;
1524
+ if (best.residual > INDICATOR_MAX_FIT_RESIDUAL_FRACTION * best.radius) return null;
1525
+ return best;
1526
+ }
1527
+ function offPivotRotationFinding(group, drift) {
1528
+ const last = group[group.length - 1];
1529
+ if (!last) return null;
1530
+ const rect = {
1531
+ left: Math.min(last.ax, last.bx) - 4,
1532
+ top: Math.min(last.ay, last.by) - 4,
1533
+ right: Math.max(last.ax, last.bx) + 4,
1534
+ bottom: Math.max(last.ay, last.by) + 4,
1535
+ width: Math.abs(last.bx - last.ax) + 8,
1536
+ height: Math.abs(last.by - last.ay) + 8
1537
+ };
1538
+ return {
1539
+ code: "off_pivot_rotation",
1540
+ severity: "warning",
1541
+ time: last.time,
1542
+ selector: last.selector,
1543
+ dataAttributes: {},
1544
+ sourceFile: "index.html",
1545
+ bbox: rectToBbox(rect),
1546
+ rect,
1547
+ message: `Gauge/dial pointer rotates about a point ${Math.round(drift)}px from the dial hub \u2014 its center-of-rotation is off the hub, so it points wrong or overshoots the track (check the rotating group's pivot: translate to the hub before rotate, or set svgOrigin/transform-origin to the hub).`,
1548
+ fixHint: "The pointer's center-of-rotation must coincide with the dial hub. Rotate the group about the hub (e.g. transform-origin/svgOrigin set to the hub center, or translate the group so its rotation pivot lands on the hub) rather than about the element's own bbox center."
1549
+ };
1550
+ }
1551
+ function countAngularBodies(angles) {
1552
+ const clusters = [];
1553
+ for (const angle of angles) {
1554
+ if (!clusters.some((rep) => Math.min(Math.abs(angle - rep), 360 - Math.abs(angle - rep)) <= 25)) {
1555
+ clusters.push(angle);
1556
+ }
1557
+ }
1558
+ return clusters.length;
1559
+ }
1560
+ function angleAboutHub(group, hub) {
1561
+ const midX = median(group.map((s) => (s.ax + s.bx) / 2));
1562
+ const midY = median(group.map((s) => (s.ay + s.by) / 2));
1563
+ return Math.atan2(midY - hub.hy, midX - hub.hx) * 180 / Math.PI;
1564
+ }
1565
+ function buildIndicatorCandidate(group) {
1566
+ if (group.length < INDICATOR_MIN_SAMPLES) return null;
1567
+ if (maxAngleSpread(group.map((s) => s.angle)) <= INDICATOR_MIN_ANGLE_SPREAD_DEG) return null;
1568
+ const hub = resolveHub(group);
1569
+ if (!hub) return null;
1570
+ if (!isRigidBody(group)) return null;
1571
+ const pivot = recoverPivot(group);
1572
+ if (!pivot) return null;
1573
+ const drift = Math.hypot(pivot.cx - hub.hx, pivot.cy - hub.hy);
1574
+ const length = median(group.map((s) => s.len));
1575
+ const eligible = drift > INDICATOR_DRIFT_LENGTH_FRACTION * length;
1576
+ return {
1577
+ // Scoped-known blind spot: hubs bucketed by rounded screen coords, so two
1578
+ // separate dials whose screen hubs land in the same 5px bucket merge into
1579
+ // one multi-body group (rare — distinct dials stacked at ~identical screen
1580
+ // position). Key by owning <svg> identity if that pattern shows up.
1581
+ hubKey: `${Math.round(hub.hx / 5)}:${Math.round(hub.hy / 5)}`,
1582
+ angleAboutHub: angleAboutHub(group, hub),
1583
+ drift,
1584
+ length,
1585
+ finding: eligible ? offPivotRotationFinding(group, drift) : null
1586
+ };
1587
+ }
1588
+ function selectHubFindings(candidates) {
1589
+ const byHub = groupBy(candidates, (candidate) => candidate.hubKey);
1590
+ const findings = [];
1591
+ for (const group of byHub.values()) {
1592
+ const eligible = group.filter((c2) => c2.finding !== null);
1593
+ if (countAngularBodies(eligible.map((c2) => c2.angleAboutHub)) >= 2) continue;
1594
+ const best = group.filter((c2) => c2.finding !== null).reduce(
1595
+ (max, c2) => !max || c2.length > max.length ? c2 : max,
1596
+ null
1597
+ );
1598
+ if (best?.finding) findings.push(best.finding);
1599
+ }
1600
+ return findings;
1601
+ }
1602
+ function detectOffPivotRotation(frames) {
1603
+ const timed = frames.flatMap(
1604
+ (frame) => frame.samples.map((sample) => ({ ...sample, time: frame.time }))
1605
+ );
1606
+ const candidates = [];
1607
+ for (const group of groupBySelector(timed).values()) {
1608
+ const candidate = buildIndicatorCandidate(group);
1609
+ if (candidate) candidates.push(candidate);
1610
+ }
1611
+ return selectHubFindings(candidates);
1612
+ }
1613
+ function annotationBoxesFrom(issues) {
1614
+ return issues.filter((issue) => issue.severity === "error" && issue.bbox.width > 0 && issue.bbox.height > 0).map((issue, index) => ({ label: `${index + 1} ${issue.code}`, bbox: issue.bbox }));
1615
+ }
1616
+ function wholeCompositionKeepsMoving(assertions) {
1617
+ return assertions.some(
1618
+ (assertion) => assertion.kind === "keepsMoving" && assertion.withinSelector === void 0
1619
+ );
1620
+ }
1621
+ function disabledCraft() {
1622
+ return { enabled: false, step: 0, samples: 0, findings: [] };
1623
+ }
1624
+ async function runAuditGrid(driver, options, motion, helpers = Promise.resolve([]), enter = () => {
1625
+ }, report = () => {
1626
+ }, keep = () => {
1627
+ }) {
1628
+ enter("samples");
1629
+ await driver.initialize(options.contrast);
1630
+ const grid = await buildSampleGrid(driver, options);
1631
+ const plan = await planMotionSampling(
1632
+ driver,
1633
+ options.range ? { kind: "none" } : motion,
1634
+ grid.duration
1635
+ );
1636
+ const seekLoopStart = Date.now();
1637
+ const collected = await collectGridSamples(
1638
+ driver,
1639
+ options,
1640
+ grid,
1641
+ plan,
1642
+ helpers,
1643
+ enter,
1644
+ report,
1645
+ keep
1646
+ );
1647
+ const seekLoopMs = Date.now() - seekLoopStart;
1648
+ let motionIssues = plan.preflightIssues;
1649
+ if (motion.kind === "valid" && plan.assertions.length > 0 && collected.motionFrames.length > 0) {
1650
+ const canvas = await driver.getCanvas();
1651
+ let evaluated = evaluateMotion(collected.motionFrames, plan.assertions, canvas, grid.duration);
1652
+ if (evaluated.some((issue) => issue.code === "motion_frozen")) {
1653
+ const frames = await withFrameMotion(driver, collected.motionFrames, plan.assertions);
1654
+ evaluated = evaluateMotion(frames, plan.assertions, canvas, grid.duration);
1655
+ }
1656
+ motionIssues = [...motionIssues, ...await driver.anchorMotionIssues(evaluated)];
1657
+ }
1658
+ const userPicked = new Set(grid.userPickedSamples);
1659
+ const noTimeline = await driver.hasNoTimelineDeclaration();
1660
+ const sweepSamples = collected.layoutStateSignatures.filter(
1661
+ (sample) => !userPicked.has(sample.time)
1662
+ );
1663
+ const sweepVerdict = options.range ? [] : detectSweepStatic(grid.duration, sweepSamples, motionIssues, noTimeline);
1664
+ const sweepFindings = sweepVerdict.length > 0 && await framesMoved(
1665
+ driver,
1666
+ sweepSamples.map((sample) => sample.time)
1667
+ ) ? [] : sweepVerdict;
1668
+ const rotationFindings = detectRotationPivotDrift(
1669
+ collected.rotationSamples,
1670
+ await driver.getCanvas()
1671
+ );
1672
+ const offPivotFindings = detectOffPivotRotation(collected.indicatorFrames);
1673
+ const contrast = buildContrastResults(collected.contrastEntries);
1674
+ const craft = craftEnabled(driver, options) ? await auditCraft(
1675
+ {
1676
+ frames: collected.craftFrames,
1677
+ duration: grid.duration,
1678
+ canvas: await driver.getCanvas(),
1679
+ soundSources: driver.collectCraftFilm ? (await driver.collectCraftFilm()).soundSources : null,
1680
+ stillnessJudgedElsewhere: noTimeline || sweepFindings.length > 0 || wholeCompositionKeepsMoving(plan.assertions)
1681
+ },
1682
+ async (time) => {
1683
+ await driver.seekGeometry(time);
1684
+ if (!driver.collectCraftFrame) throw new Error("craft sampling unavailable");
1685
+ return driver.collectCraftFrame(time);
1686
+ }
1687
+ ) : disabledCraft();
1688
+ if (options.components?.length) {
1689
+ if (!driver.matchComponents)
1690
+ throw new Error("Component filtering unavailable in this check driver");
1691
+ const findings = [
1692
+ ...collected.layoutIssues,
1693
+ ...sweepFindings,
1694
+ ...rotationFindings,
1695
+ ...offPivotFindings,
1696
+ ...motionIssues,
1697
+ ...contrast.findings,
1698
+ ...craft.findings
1699
+ ];
1700
+ const matches = await driver.matchComponents(options.components, findings);
1701
+ findings.forEach((finding, i) => {
1702
+ finding.componentMatch = matches[i];
1703
+ });
1704
+ }
1705
+ return {
1706
+ duration: grid.duration,
1707
+ layoutSamples: grid.layoutSamples,
1708
+ transitionSamples: grid.transitionSamples,
1709
+ transitionSamplesDropped: grid.transitionSamplesDropped,
1710
+ runtimeFindings: [],
1711
+ layoutIssues: [
1712
+ ...collected.layoutIssues,
1713
+ ...sweepFindings,
1714
+ ...rotationFindings,
1715
+ ...offPivotFindings
1716
+ ],
1717
+ motionIssues,
1718
+ motionSampleCount: collected.motionFrames.length,
1719
+ contrastSamples: grid.contrastSamples,
1720
+ contrastFindings: contrast.findings,
1721
+ contrastChecked: collected.contrastEntries.length,
1722
+ contrastPassed: contrast.passed,
1723
+ craft,
1724
+ screenshots: collected.screenshots,
1725
+ timings: { launchSettleMs: 0, seekLoopMs, contrastMs: collected.contrastMs },
1726
+ skipped: false
1727
+ };
1728
+ }
1729
+ async function runCheckPipeline(project, options, dependencies = DEFAULT_DEPENDENCIES2) {
1730
+ beginCheckSnapshots(project.dir);
1731
+ const prelaunched = dependencies.prelaunchBrowser?.(project, options).catch(() => void 0);
1732
+ try {
1733
+ let lintResult;
1734
+ const lintStartedAt = Date.now();
1735
+ try {
1736
+ lintResult = await dependencies.lintProject(project.dir);
1737
+ } catch (error) {
1738
+ return failureReport(options, runtimeFailure(error, "check_lint_failure"));
1739
+ }
1740
+ trackLintRun(project.dir, lintResult, {
1741
+ command: "check",
1742
+ durationMs: Date.now() - lintStartedAt,
1743
+ ...getRunId() !== void 0 ? { runId: getRunId() } : {}
1744
+ });
1745
+ const lint = buildLintSection(lintResult);
1746
+ let hdrPromotion = null;
1747
+ let hdrInspection = "available";
1748
+ try {
1749
+ hdrPromotion = await (dependencies.inspectHdrAutoPromotion ?? inspectHdrAutoPromotion)(
1750
+ project
1751
+ );
1752
+ } catch {
1753
+ hdrInspection = "unavailable";
1754
+ }
1755
+ if (shouldBlockRender(true, false, lintResult.totalErrors, lintResult.totalWarnings)) {
1756
+ return buildReport(
1757
+ options,
1758
+ lint,
1759
+ emptyBrowserResult(),
1760
+ { kind: "none" },
1761
+ [],
1762
+ [],
1763
+ hdrPromotion,
1764
+ hdrInspection
1765
+ );
1766
+ }
1767
+ const motion = dependencies.resolveMotionSpec(project.dir);
1768
+ const specFindings = motion.kind === "invalid" ? [
1769
+ findingAtRoot(
1770
+ "motion_spec_invalid",
1771
+ "error",
1772
+ motion.message,
1773
+ relative(project.dir, motion.path) || "index.motion.json"
1774
+ )
1775
+ ] : [];
1776
+ const deadline = new CheckDeadline();
1777
+ let outcome;
1778
+ try {
1779
+ let browser;
1780
+ try {
1781
+ const browserAhead = await prelaunched;
1782
+ browser = await deadline.run(
1783
+ () => dependencies.runBrowserCheck(project, options, motion, deadline, browserAhead)
1784
+ );
1785
+ } catch (error) {
1786
+ const found = deadline.partialResult();
1787
+ browser = { ...emptyBrowserResult(), ...found, skipped: Object.keys(found).length === 0 };
1788
+ browser.runtimeFindings = [...browser.runtimeFindings, runtimeFailure(error)];
1789
+ }
1790
+ const snapshotFiles = options.snapshots ? await writeContrastSnapshots(dependencies, project.dir, browser) : [];
1791
+ const report = buildReport(
1792
+ options,
1793
+ lint,
1794
+ browser,
1795
+ motion,
1796
+ specFindings,
1797
+ snapshotFiles,
1798
+ hdrPromotion,
1799
+ hdrInspection
1800
+ );
1801
+ outcome = {
1802
+ kind: "report",
1803
+ report: options.snapshots ? await withFindingCrops(dependencies, project, options, report, deadline) : report
1804
+ };
1805
+ } catch (error) {
1806
+ outcome = { kind: "failed", error };
1807
+ }
1808
+ try {
1809
+ await deadline.close();
1810
+ } catch (error) {
1811
+ if (outcome.kind === "failed") {
1812
+ throw new BrowserCleanupError(
1813
+ [outcome.error, error],
1814
+ `${normalizeErrorMessage(outcome.error)}; ${normalizeErrorMessage(error)}`
1815
+ );
1816
+ }
1817
+ const report = outcome.report;
1818
+ return {
1819
+ ...report,
1820
+ ok: false,
1821
+ runtime: section([
1822
+ ...report.runtime.findings,
1823
+ runtimeFailure(error, "check_cleanup_failure")
1824
+ ])
1825
+ };
1826
+ }
1827
+ if (outcome.kind === "failed") throw outcome.error;
1828
+ return outcome.report;
1829
+ } finally {
1830
+ await (await prelaunched)?.discard();
1831
+ await finishCheckSnapshots(project.dir).catch(() => void 0);
1832
+ }
1833
+ }
1834
+ async function writeContrastSnapshots(dependencies, projectDir, browser) {
1835
+ const files = [];
1836
+ for (let index = 0; index < browser.screenshots.length; index += 1) {
1837
+ const shot = browser.screenshots[index];
1838
+ if (!shot) continue;
1839
+ try {
1840
+ files.push(await dependencies.writeSnapshot(projectDir, index, shot.time, shot.pngBase64));
1841
+ } catch (error) {
1842
+ browser.runtimeFindings.push(runtimeFailure(error, "snapshot_write_failed"));
1843
+ }
1844
+ }
1845
+ return files;
1846
+ }
1847
+ async function withFindingCrops(dependencies, project, options, report, deadline) {
1848
+ const cropRequests = selectFindingCropRequests(report);
1849
+ if (cropRequests.length === 0) return report;
1850
+ try {
1851
+ deadline.enter("crops");
1852
+ const findingFiles = await deadline.run(
1853
+ () => dependencies.captureFindingCrops(project, options, cropRequests, deadline)
1854
+ );
1855
+ return { ...report, snapshots: { ...report.snapshots, findingFiles } };
1856
+ } catch (error) {
1857
+ if (error instanceof CheckDeadlineError) {
1858
+ const finding = runtimeFailure(error);
1859
+ return { ...report, ok: false, runtime: section([...report.runtime.findings, finding]) };
1860
+ }
1861
+ console.error(" finding crops skipped: " + normalizeErrorMessage(error));
1862
+ trackCommandFailure("check-finding-crops", error);
1863
+ return report;
1864
+ }
1865
+ }
1866
+ var MAX_FINDING_CROPS = 12;
1867
+ function selectFindingCropRequests(report) {
1868
+ const candidates = [
1869
+ ...report.layout.findings,
1870
+ ...report.motion.findings,
1871
+ ...report.contrast.findings,
1872
+ ...report.runtime.findings
1873
+ ];
1874
+ const requests = [];
1875
+ for (const finding of candidates) {
1876
+ if (requests.length >= MAX_FINDING_CROPS) break;
1877
+ if (finding.severity !== "error" || !hasRealBbox(finding.bbox)) continue;
1878
+ requests.push({
1879
+ filename: findingCropFilename(requests.length, finding.code),
1880
+ time: finding.time,
1881
+ bbox: finding.bbox
1882
+ });
1883
+ }
1884
+ return requests;
1885
+ }
1886
+ function hasRealBbox(bbox) {
1887
+ return bbox.width > 0 && bbox.height > 0;
1888
+ }
1889
+ function findingCropFilename(index, code) {
1890
+ const safeCode = code.replace(/[^a-zA-Z0-9_-]/g, "_");
1891
+ return `finding-${String(index).padStart(2, "0")}-${safeCode}.png`;
1892
+ }
1893
+ function checkExitCode(report) {
1894
+ return report.ok ? 0 : 1;
1895
+ }
1896
+ function contrastFailureHeld(entries) {
1897
+ const sampledTimes = new Set(entries.map((entry) => entry.time)).size;
1898
+ const failureSamples = /* @__PURE__ */ new Map();
1899
+ for (const entry of entries) {
1900
+ if (entry.wcagAA) continue;
1901
+ const key = findingElementKey(entry);
1902
+ const times = failureSamples.get(key) ?? /* @__PURE__ */ new Set();
1903
+ times.add(entry.time);
1904
+ failureSamples.set(key, times);
1905
+ }
1906
+ return (entry) => sampledTimes < 2 || (failureSamples.get(findingElementKey(entry))?.size ?? 0) >= 2;
1907
+ }
1908
+ function buildContrastResults(entries) {
1909
+ const findings = [];
1910
+ let passed = 0;
1911
+ const isHeld = contrastFailureHeld(entries);
1912
+ for (const entry of entries) {
1913
+ if (entry.wcagAA) {
1914
+ passed += 1;
1915
+ continue;
1916
+ }
1917
+ const held = isHeld(entry);
1918
+ const requiredRatio = requiredContrastRatio(entry.large);
1919
+ findings.push({
1920
+ code: "contrast_aa_failure",
1921
+ severity: held ? "error" : "warning",
1922
+ message: `Contrast is ${entry.ratio}:1; WCAG AA requires ${requiredRatio}:1.`,
1923
+ text: entry.text,
1924
+ fg: entry.fg,
1925
+ bg: entry.bg,
1926
+ ratio: entry.ratio,
1927
+ requiredRatio,
1928
+ suggestedColor: suggestedColor(entry.fg, entry.bg, requiredRatio),
1929
+ large: entry.large,
1930
+ selector: entry.selector,
1931
+ dataAttributes: entry.dataAttributes,
1932
+ sourceFile: entry.sourceFile,
1933
+ bbox: entry.bbox,
1934
+ time: entry.time
1935
+ });
1936
+ }
1937
+ return {
1938
+ findings: groupSampledFindings(
1939
+ findings,
1940
+ (next, current) => current.ratio / current.requiredRatio - next.ratio / next.requiredRatio
1941
+ ),
1942
+ passed
1943
+ };
1944
+ }
1945
+ function suggestedColor(fg, bg, requiredRatio) {
1946
+ const foreground = parseColorRGBA(fg);
1947
+ const background = parseColorRGBA(bg);
1948
+ if (!foreground || !background) return fg;
1949
+ const fgRgb = [foreground[0], foreground[1], foreground[2]];
1950
+ const bgRgb = [background[0], background[1], background[2]];
1951
+ const suggested = suggestCompliantForegroundColor(fgRgb, bgRgb, requiredRatio);
1952
+ return `rgb(${suggested[0]},${suggested[1]},${suggested[2]})`;
1953
+ }
1954
+ function buildLintSection(result) {
1955
+ const findings = result.results.flatMap(
1956
+ ({ file, result: fileResult }) => fileResult.findings.map((finding) => ({
1957
+ code: finding.code,
1958
+ severity: finding.severity,
1959
+ message: finding.message,
1960
+ selector: finding.selector ?? (finding.elementId ? `#${finding.elementId}` : "[data-composition-id]"),
1961
+ dataAttributes: {},
1962
+ sourceFile: finding.file ?? file,
1963
+ bbox: ZERO_BBOX2,
1964
+ time: 0,
1965
+ fixHint: finding.fixHint
1966
+ }))
1967
+ );
1968
+ return { ...section(findings), filesScanned: result.results.length };
1969
+ }
1970
+ function buildReport(options, lint, browser, motion, extraMotionFindings, snapshotFiles, hdrPromotion = null, hdrInspection = "available") {
1971
+ const shaping = options.components?.length ? { ...options, maxIssues: Number.MAX_SAFE_INTEGER } : options;
1972
+ const layout = shapeLayoutSection(browser.layoutIssues, browser, shaping);
1973
+ const shapedMotion = shapeLayoutFindings(browser.motionIssues, shaping);
1974
+ const motionFindings = [...shapedMotion.findings, ...extraMotionFindings];
1975
+ const runtime = section(browser.runtimeFindings);
1976
+ const motionSection = section(motionFindings);
1977
+ const contrastSection = section(browser.contrastFindings);
1978
+ const craftSection = section(browser.craft.findings);
1979
+ const warningCount = lint.warningCount + runtime.warningCount + layout.warningCount + motionSection.warningCount + contrastSection.warningCount + craftSection.warningCount;
1980
+ const errorCount = lint.errorCount + runtime.errorCount + layout.errorCount + motionSection.errorCount + contrastSection.errorCount + craftSection.errorCount;
1981
+ const strictWarningCount = warningCount - craftSection.warningCount;
1982
+ const report = {
1983
+ ok: errorCount === 0 && (!options.strict || strictWarningCount === 0),
1984
+ strict: options.strict,
1985
+ browserSkipped: browser.skipped,
1986
+ lint,
1987
+ runtime,
1988
+ layout,
1989
+ motion: {
1990
+ ...motionSection,
1991
+ enabled: motion.kind !== "none" && !options.range,
1992
+ specPath: motion.kind === "none" ? void 0 : motion.path,
1993
+ samples: browser.motionSampleCount
1994
+ },
1995
+ contrast: {
1996
+ ...contrastSection,
1997
+ enabled: options.contrast,
1998
+ samples: browser.contrastSamples,
1999
+ checked: browser.contrastChecked,
2000
+ passed: browser.contrastPassed
2001
+ },
2002
+ craft: {
2003
+ ...craftSection,
2004
+ enabled: browser.craft.enabled,
2005
+ step: browser.craft.step,
2006
+ samples: browser.craft.samples
2007
+ },
2008
+ hdr: { autoPromotion: hdrPromotion, inspection: hdrInspection },
2009
+ snapshots: {
2010
+ enabled: options.snapshots,
2011
+ files: snapshotFiles,
2012
+ times: options.snapshots ? browser.screenshots.map((shot) => shot.time) : [],
2013
+ findingFiles: []
2014
+ }
2015
+ };
2016
+ trackCheckReport({
2017
+ contrastGate: options.contrast,
2018
+ motionGate: motion.kind !== "none" && !options.range,
2019
+ captionZoneGate: options.captionZone !== void 0,
2020
+ frameCheckGate: options.frameCheck !== void 0,
2021
+ snapshotsGate: options.snapshots,
2022
+ lintErrors: lint.errorCount,
2023
+ lintWarnings: lint.warningCount,
2024
+ runtimeErrors: runtime.errorCount,
2025
+ runtimeWarnings: runtime.warningCount,
2026
+ layoutErrors: layout.errorCount,
2027
+ layoutWarnings: layout.warningCount,
2028
+ motionErrors: motionSection.errorCount,
2029
+ motionWarnings: motionSection.warningCount,
2030
+ contrastErrors: contrastSection.errorCount,
2031
+ contrastWarnings: contrastSection.warningCount,
2032
+ craftWarnings: craftSection.warningCount,
2033
+ launchSettleMs: browser.timings.launchSettleMs,
2034
+ seekLoopMs: browser.timings.seekLoopMs,
2035
+ contrastMs: browser.timings.contrastMs,
2036
+ gridPoints: browser.layoutSamples.length,
2037
+ contrastPoints: browser.contrastChecked,
2038
+ ok: report.ok,
2039
+ exitCode: checkExitCode(report),
2040
+ runId: getRunId()
2041
+ });
2042
+ return filterComponentReport(report, options);
2043
+ }
2044
+ function filterComponentReport(report, options) {
2045
+ if (!options.range && !options.components?.length) return report;
2046
+ {
2047
+ report.scope = {
2048
+ range: options.range,
2049
+ components: options.components,
2050
+ hiddenFindings: 0,
2051
+ notes: [
2052
+ "Static lint and unscoped runtime failures remain project-wide.",
2053
+ ...options.range ? [
2054
+ "Whole-timeline motion assertions, frozen-sweep detection and craft warnings are skipped for range checks."
2055
+ ] : []
2056
+ ]
2057
+ };
2058
+ for (const group of [report.layout, report.motion, report.contrast, report.craft]) {
2059
+ const before = group.findings.length;
2060
+ group.findings = group.findings.filter((f) => f.componentMatch !== false);
2061
+ report.scope.hiddenFindings += before - group.findings.length;
2062
+ if (group !== report.contrast && group.findings.length > options.maxIssues) {
2063
+ group.findings = group.findings.slice(0, options.maxIssues);
2064
+ report.scope.notes.push(
2065
+ "Selected findings exceed --max-issues; increase it to see the remainder."
2066
+ );
2067
+ if (group === report.layout) report.layout.truncated = true;
2068
+ }
2069
+ }
2070
+ }
2071
+ return report;
2072
+ }
2073
+ function shapeLayoutSection(issues, browser, options) {
2074
+ const shaped = shapeLayoutFindings(issues, options, browser.layoutSamples.length);
2075
+ return {
2076
+ ...section(shaped.findings),
2077
+ duration: browser.duration,
2078
+ samples: browser.layoutSamples,
2079
+ transitionSamples: browser.transitionSamples,
2080
+ transitionSamplesDropped: browser.transitionSamplesDropped,
2081
+ tolerance: options.tolerance,
2082
+ totalIssueCount: shaped.totalIssueCount,
2083
+ truncated: shaped.truncated
2084
+ };
2085
+ }
2086
+ function shapeLayoutFindings(issues, options, totalSampleCount) {
2087
+ const deduped = dedupeLayoutIssues(issues);
2088
+ const all = options.collapseStatic ? collapseStaticLayoutIssues(deduped, totalSampleCount) : deduped;
2089
+ const anchored = all.map(ensureAnchoredLayoutIssue);
2090
+ let grouped = anchored;
2091
+ if (options.collapseStatic) {
2092
+ grouped = groupSampledFindings(anchored).map((finding) => ({
2093
+ ...finding,
2094
+ firstSeen: finding.times[0] ?? finding.time,
2095
+ lastSeen: finding.times.at(-1) ?? finding.time,
2096
+ occurrences: finding.times.length,
2097
+ heldMs: longestContiguousRunMs(finding.times)
2098
+ }));
2099
+ }
2100
+ const limited = limitLayoutIssues(grouped, options.maxIssues);
2101
+ return {
2102
+ findings: limited.issues.map(ensureAnchoredLayoutIssue),
2103
+ totalIssueCount: limited.totalIssueCount,
2104
+ truncated: limited.truncated
2105
+ };
2106
+ }
2107
+ function ensureAnchoredLayoutIssue(issue) {
2108
+ const sourceFile = Reflect.get(issue, "sourceFile");
2109
+ const dataAttributes = Reflect.get(issue, "dataAttributes");
2110
+ const bbox = Reflect.get(issue, "bbox");
2111
+ if (typeof sourceFile === "string" && isStringRecord(dataAttributes) && isBbox(bbox)) {
2112
+ return { ...issue, sourceFile, dataAttributes, bbox };
2113
+ }
2114
+ return {
2115
+ ...issue,
2116
+ sourceFile: "index.html",
2117
+ dataAttributes: {},
2118
+ bbox: rectToBbox(issue.rect)
2119
+ };
2120
+ }
2121
+ function section(findings) {
2122
+ const errorCount = findings.filter((finding) => finding.severity === "error").length;
2123
+ const warningCount = findings.filter((finding) => finding.severity === "warning").length;
2124
+ const infoCount = findings.filter((finding) => finding.severity === "info").length;
2125
+ return { ok: errorCount === 0, errorCount, warningCount, infoCount, findings };
2126
+ }
2127
+ function emptyBrowserResult() {
2128
+ return {
2129
+ duration: 0,
2130
+ layoutSamples: [],
2131
+ transitionSamples: [],
2132
+ transitionSamplesDropped: 0,
2133
+ runtimeFindings: [],
2134
+ layoutIssues: [],
2135
+ motionIssues: [],
2136
+ motionSampleCount: 0,
2137
+ contrastSamples: [],
2138
+ contrastFindings: [],
2139
+ contrastChecked: 0,
2140
+ contrastPassed: 0,
2141
+ craft: disabledCraft(),
2142
+ screenshots: [],
2143
+ timings: { launchSettleMs: 0, seekLoopMs: 0, contrastMs: 0 },
2144
+ skipped: true
2145
+ };
2146
+ }
2147
+ function runtimeFailure(error, code = "check_runtime_failure") {
2148
+ return findingAtRoot(
2149
+ code,
2150
+ "error",
2151
+ normalizeErrorMessage(error),
2152
+ error instanceof CheckDeadlineError ? error.sourceFile ?? "index.html" : "index.html"
2153
+ );
2154
+ }
2155
+ function findingAtRoot(code, severity, message, sourceFile) {
2156
+ return {
2157
+ code,
2158
+ severity,
2159
+ message,
2160
+ selector: "[data-composition-id]",
2161
+ dataAttributes: {},
2162
+ sourceFile,
2163
+ bbox: ZERO_BBOX2,
2164
+ time: 0
2165
+ };
2166
+ }
2167
+ function failureReport(options, finding) {
2168
+ const lint = { ...section([]), filesScanned: 0 };
2169
+ const browser = emptyBrowserResult();
2170
+ browser.runtimeFindings.push(finding);
2171
+ return buildReport(options, lint, browser, { kind: "none" }, [], []);
2172
+ }
2173
+ function isBbox(value) {
2174
+ if (typeof value !== "object" || value === null) return false;
2175
+ return ["x", "y", "width", "height"].every((key) => typeof Reflect.get(value, key) === "number");
2176
+ }
2177
+ function isStringRecord(value) {
2178
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
2179
+ return Object.keys(value).every((key) => typeof Reflect.get(value, key) === "string");
2180
+ }
2181
+ function resolveMotionSpec(projectDir) {
2182
+ const path = findMotionSpec(projectDir);
2183
+ if (!path) return { kind: "none" };
2184
+ const result = readMotionSpec(path);
2185
+ return result.ok ? { kind: "valid", path, spec: result.spec } : {
2186
+ kind: "invalid",
2187
+ path,
2188
+ message: `Invalid motion spec ${path}: ${result.errors.join("; ")}`
2189
+ };
2190
+ }
2191
+ async function runBrowserCheck(project, options, motion, deadline, prelaunched) {
2192
+ const module = await import("./checkBrowser-LZ66CMJG.js");
2193
+ return module.runBrowserCheck(project, options, motion, runAuditGrid, deadline, prelaunched);
2194
+ }
2195
+ var adoptedBrowser;
2196
+ async function prelaunchBrowserForNextCheck(project, options) {
2197
+ adoptedBrowser = await prelaunchBrowser(project, options);
2198
+ return adoptedBrowser;
2199
+ }
2200
+ async function prelaunchBrowser(project, options) {
2201
+ const adopted = adoptedBrowser;
2202
+ adoptedBrowser = void 0;
2203
+ if (adopted) return adopted;
2204
+ const html = await readFile(project.indexPath, "utf8").catch(() => void 0);
2205
+ if (html === void 0) return void 0;
2206
+ const { prelaunchCompositionBrowser } = await import("./captureCompositionFrame-4DA6SEH2.js");
2207
+ return prelaunchCompositionBrowser(html, options.browserGpuMode);
2208
+ }
2209
+ async function writeSnapshot(projectDir, index, time, pngBase64) {
2210
+ const snapshotDir = checkSnapshotDirectory(projectDir);
2211
+ mkdirSync(snapshotDir, { recursive: true });
2212
+ const filename = `frame-${String(index).padStart(2, "0")}-at-${time.toFixed(1)}s.png`;
2213
+ const path = join(snapshotDir, filename);
2214
+ writeFileSync(path, Buffer.from(pngBase64, "base64"));
2215
+ return relative(projectDir, path);
2216
+ }
2217
+ async function captureFindingCrops(project, options, requests, deadline) {
2218
+ const module = await import("./checkBrowser-LZ66CMJG.js");
2219
+ return module.captureFindingCrops(project, options, requests, deadline);
2220
+ }
2221
+ var DEFAULT_DEPENDENCIES2 = {
2222
+ lintProject,
2223
+ resolveMotionSpec,
2224
+ runBrowserCheck,
2225
+ prelaunchBrowser,
2226
+ writeSnapshot,
2227
+ captureFindingCrops,
2228
+ inspectHdrAutoPromotion
2229
+ };
2230
+
2231
+ // src/commands/check.ts
2232
+ var examples = [
2233
+ ["Run the full verification gate", "framey check"],
2234
+ ["Output one agent-readable envelope", "framey check --json"],
2235
+ ["Persist the five audited contrast frames", "framey check --snapshots"],
2236
+ ["Also fail on warnings", "framey check --strict"],
2237
+ ["Make only the browser-playable video copies, then exit", "framey check --prepare-media"]
2238
+ ];
2239
+ var DEFAULT_COMMAND_DEPENDENCIES = {
2240
+ resolveProject,
2241
+ async runPipeline(project, options) {
2242
+ const capturing = captureArtifactRevision(project.dir).catch(warnNotRecorded);
2243
+ const report = await runCheckPipeline(project, options);
2244
+ const images = (files) => files.map((file) => resolve(project.dir, file));
2245
+ reportArtifacts("check", "frame", images(report.snapshots.files));
2246
+ reportArtifacts("check", "finding", images(report.snapshots.findingFiles));
2247
+ const capture = await capturing;
2248
+ if (!capture) return report;
2249
+ try {
2250
+ return { ...report, saved: await saveCheckReport(project, options, report, capture) };
2251
+ } catch (error) {
2252
+ warnNotRecorded(error);
2253
+ return report;
2254
+ }
2255
+ },
2256
+ async prepareMedia(project, options) {
2257
+ const { bundleWithLocalizedFonts } = await import("./bundleWithLocalizedFonts-ALYBQEQT.js");
2258
+ const html = await bundleWithLocalizedFonts(project.dir);
2259
+ return (await preResolveHostileMediaProxies(project.dir, html, options.autoProxy)).files;
2260
+ },
2261
+ withMeta
2262
+ };
2263
+ function warnNotRecorded(error) {
2264
+ console.warn(`${c.warn("\u26A0")} Check report was not recorded: ${normalizeErrorMessage(error)}`);
2265
+ return void 0;
2266
+ }
2267
+ async function saveCheckReport(project, options, report, capture) {
2268
+ const id = randomUUID();
2269
+ const directory = join2(project.dir, ".hyperframes", "artifacts", "checks");
2270
+ mkdirSync2(directory, { recursive: true });
2271
+ const path = join2(directory, `${id}.json`);
2272
+ writeFileSync2(path, JSON.stringify(report, null, 2) + "\n", { flag: "wx" });
2273
+ const reportScope = "scope" in report && typeof report.scope === "object" && report.scope !== null ? report.scope : void 0;
2274
+ const artifacts = [
2275
+ {
2276
+ kind: "check",
2277
+ id,
2278
+ scope: reportScope ? "scoped" : "project",
2279
+ report: resolve(path),
2280
+ metadata: {
2281
+ ok: report.ok,
2282
+ browserSkipped: report.browserSkipped,
2283
+ scope: reportScope,
2284
+ options
2285
+ }
2286
+ }
2287
+ ];
2288
+ for (const [index, image] of report.snapshots.files.entries()) {
2289
+ artifacts.push({
2290
+ kind: "snapshot",
2291
+ id: `${id}-${index}`,
2292
+ scope: "full",
2293
+ image: resolve(project.dir, image),
2294
+ time: report.snapshots.times[index],
2295
+ metadata: {
2296
+ checkId: id,
2297
+ ...pngDimensions(resolve(project.dir, image))
2298
+ }
2299
+ });
2300
+ }
2301
+ const crops = selectFindingCropRequests(report);
2302
+ for (const [index, image] of report.snapshots.findingFiles.entries()) {
2303
+ const crop = crops.find((request) => image.endsWith(request.filename));
2304
+ artifacts.push({
2305
+ kind: "snapshot",
2306
+ id: `${id}-crop-${index}`,
2307
+ scope: "finding",
2308
+ image: resolve(project.dir, image),
2309
+ time: crop?.time,
2310
+ metadata: {
2311
+ checkId: id,
2312
+ finding: crop,
2313
+ ...pngDimensions(resolve(project.dir, image))
2314
+ }
2315
+ });
2316
+ }
2317
+ const [saved] = await recordArtifacts(project.dir, capture, artifacts);
2318
+ if (!saved) throw new Error("no history record was written");
2319
+ reportArtifacts("check", "check-report", [resolve(path)]);
2320
+ return {
2321
+ id: saved.id,
2322
+ revision: saved.revision,
2323
+ report: saved.report,
2324
+ attribution: saved.attribution
2325
+ };
2326
+ }
2327
+ var CHECK_COMMAND_ARGS = {
2328
+ dir: { type: "positional", description: "Project directory", required: false },
2329
+ json: { type: "boolean", description: "Output agent-readable JSON", default: false },
2330
+ samples: {
2331
+ type: "string",
2332
+ description: "Number of midpoint samples across the duration (default: 9)",
2333
+ default: "9"
2334
+ },
2335
+ range: {
2336
+ type: "string",
2337
+ description: "Runtime sample interval in global seconds: start,end. Static lint remains project-wide."
2338
+ },
2339
+ components: {
2340
+ type: "string",
2341
+ description: "CSS selector list: filter findings involving these components or descendants; preserve full-check status."
2342
+ },
2343
+ at: {
2344
+ type: "string",
2345
+ description: "Comma-separated timestamps in seconds (e.g., --at 1.5,4,7.25)"
2346
+ },
2347
+ "at-transitions": {
2348
+ type: "boolean",
2349
+ description: "Also sample at every tween start/end boundary (plus segment midpoints) to catch transient overlaps at transition seams",
2350
+ default: false
2351
+ },
2352
+ "max-transition-samples": {
2353
+ type: "string",
2354
+ description: "Optional cap on transition-derived samples; when it truncates, the omitted count is reported (default: unlimited)"
2355
+ },
2356
+ "max-issues": {
2357
+ type: "string",
2358
+ description: "Maximum issues to print or return after static collapse (default: 80)",
2359
+ default: "80"
2360
+ },
2361
+ "collapse-static": {
2362
+ type: "boolean",
2363
+ description: "Collapse repeated static issues across samples (default: true)",
2364
+ default: true
2365
+ },
2366
+ tolerance: {
2367
+ type: "string",
2368
+ description: "Allowed pixel overflow before reporting an issue (default: 2)",
2369
+ default: "2"
2370
+ },
2371
+ timeout: {
2372
+ type: "string",
2373
+ description: "Initial render-ready timeout in ms; also sets the navigation minimum (10s floor, default: 3000)",
2374
+ default: "3000"
2375
+ },
2376
+ contrast: {
2377
+ type: "boolean",
2378
+ description: "Run the WCAG AA contrast pass (enabled by default)",
2379
+ default: true
2380
+ },
2381
+ strict: {
2382
+ type: "boolean",
2383
+ description: "Exit non-zero on warnings too",
2384
+ default: false
2385
+ },
2386
+ "prepare-media": {
2387
+ type: "boolean",
2388
+ description: "Only make the browser-playable copies of videos Chrome cannot decode, then exit (no browser, no report). Stopped copies resume on the next run",
2389
+ default: false
2390
+ },
2391
+ proxy: {
2392
+ type: "boolean",
2393
+ description: "Auto-transcode browser-hostile video codecs (default: hyperframes.json media.autoProxy, which defaults on)",
2394
+ default: void 0
2395
+ },
2396
+ "browser-gpu": {
2397
+ type: "boolean",
2398
+ description: "Use hardware browser GPU capture; pass --no-browser-gpu for deterministic SwiftShader (default: auto-detect, PRODUCER_BROWSER_GPU_MODE overrides)",
2399
+ default: void 0
2400
+ },
2401
+ snapshots: {
2402
+ type: "boolean",
2403
+ description: "Save contrast-pass PNGs under .hyperframes/artifacts/snapshots/",
2404
+ default: false
2405
+ },
2406
+ "caption-zone": {
2407
+ type: "string",
2408
+ description: 'Caption band "x0=0;y0=.82;x1=1;y1=1[;severity=warning|error][;seek=.5,1]" (fractions 0-1; defaults: warning, seek=1)'
2409
+ },
2410
+ "frame-check": {
2411
+ type: "string",
2412
+ description: 'Bare --frame-check uses defaults (tol=2px, severity=warning, seek=.5; breach floor=max(120px, 6% of shorter canvas edge)); or pass "severity=error;seek=.25,.75;tol=4" to tune'
2413
+ },
2414
+ layout: {
2415
+ type: "string",
2416
+ description: 'Layout knobs: "proseCoverageFloor=0.05" (0\u20131; default 0.15).'
2417
+ }
2418
+ };
2419
+ function createCheckCommand(dependencies = DEFAULT_COMMAND_DEPENDENCIES) {
2420
+ return defineCommand({
2421
+ meta: {
2422
+ name: "check",
2423
+ description: "Run lint, runtime, layout, motion, and WCAG contrast verification in one browser session"
2424
+ },
2425
+ args: CHECK_COMMAND_ARGS,
2426
+ async run({ rawArgs }) {
2427
+ const args = parseArgs(rawArgs, CHECK_COMMAND_ARGS);
2428
+ const asJson = args.json === true;
2429
+ try {
2430
+ const project = dependencies.resolveProject(args.dir);
2431
+ const options = parseCheckOptions(args);
2432
+ if (!asJson) {
2433
+ console.log(`${c.accent("\u25C6")} Checking ${c.accent(project.name)}`);
2434
+ }
2435
+ if (args["prepare-media"] === true) {
2436
+ await runPrepareMedia(dependencies, project, options, asJson);
2437
+ return;
2438
+ }
2439
+ const report = await withProjectLock(
2440
+ project.dir,
2441
+ "check",
2442
+ () => dependencies.runPipeline(project, options)
2443
+ );
2444
+ if (asJson) {
2445
+ console.log(JSON.stringify(dependencies.withMeta(report), null, 2));
2446
+ } else {
2447
+ printHumanReport(report);
2448
+ if (report.saved) {
2449
+ console.log(`Report: ${report.saved.report}`);
2450
+ console.log(
2451
+ `Revision: ${report.saved.revision ?? "none"} (${report.saved.attribution})`
2452
+ );
2453
+ }
2454
+ }
2455
+ setCommandExitCode(checkExitCode(report));
2456
+ } catch (error) {
2457
+ const message = normalizeErrorMessage(error);
2458
+ if (asJson) {
2459
+ console.log(
2460
+ JSON.stringify(dependencies.withMeta({ ok: false, error: message }), null, 2)
2461
+ );
2462
+ } else {
2463
+ console.error(`${c.error("\u2717")} Check failed: ${message}`);
2464
+ }
2465
+ setCommandExitCode(1);
2466
+ }
2467
+ }
2468
+ });
2469
+ }
2470
+ async function runPrepareMedia(dependencies, project, options, asJson) {
2471
+ const files = await withProjectLock(
2472
+ project.dir,
2473
+ "check",
2474
+ () => dependencies.prepareMedia(project, options)
2475
+ );
2476
+ const failed = files.filter((file) => file.state === "failed");
2477
+ if (asJson) {
2478
+ console.log(
2479
+ JSON.stringify(
2480
+ dependencies.withMeta({
2481
+ ok: failed.length === 0,
2482
+ prepared: files,
2483
+ total: files.length,
2484
+ ready: files.length - failed.length,
2485
+ failed: failed.length
2486
+ }),
2487
+ null,
2488
+ 2
2489
+ )
2490
+ );
2491
+ } else {
2492
+ const mark = failed.length === 0 ? c.success("\u2713") : c.error("\u2717");
2493
+ console.log(
2494
+ `${mark} Browser-playable copies: ${files.length - failed.length}/${files.length} ready`
2495
+ );
2496
+ for (const file of failed) console.log(` ${file.file}: ${file.reason}`);
2497
+ }
2498
+ setCommandExitCode(failed.length === 0 ? 0 : 1);
2499
+ }
2500
+ function parseCheckOptions(args) {
2501
+ const maxTransitionSamples = positiveInteger(args["max-transition-samples"], 0);
2502
+ return {
2503
+ samples: positiveInteger(args.samples, DEFAULT_CHECK_OPTIONS.samples),
2504
+ range: parseCheckRange(args.range),
2505
+ components: typeof args.components === "string" ? [args.components] : void 0,
2506
+ at: parseAt(args.at),
2507
+ atTransitions: args["at-transitions"] === true,
2508
+ maxTransitionSamples: maxTransitionSamples > 0 ? maxTransitionSamples : void 0,
2509
+ maxIssues: positiveInteger(args["max-issues"], DEFAULT_CHECK_OPTIONS.maxIssues),
2510
+ collapseStatic: args["collapse-static"] !== false,
2511
+ tolerance: nonNegativeNumber(args.tolerance, DEFAULT_CHECK_OPTIONS.tolerance),
2512
+ timeout: Math.max(500, positiveInteger(args.timeout, DEFAULT_CHECK_OPTIONS.timeout)),
2513
+ contrast: args.contrast !== false,
2514
+ strict: args.strict === true,
2515
+ snapshots: args.snapshots === true,
2516
+ captionZone: parseCaptionZone(args["caption-zone"]),
2517
+ frameCheck: parseFrameCheck(args["frame-check"]),
2518
+ layout: parseLayout(args.layout),
2519
+ autoProxy: args.proxy,
2520
+ browserGpuMode: resolveLocalBrowserGpuMode(args["browser-gpu"])
2521
+ };
2522
+ }
2523
+ function prelaunchBrowserForCheck(rawArgs) {
2524
+ const args = parseArgs(rawArgs, CHECK_COMMAND_ARGS);
2525
+ return prelaunchBrowserForNextCheck(resolveProject(args.dir), parseCheckOptions(args));
2526
+ }
2527
+ function parseCheckRange(value) {
2528
+ if (value === void 0) return void 0;
2529
+ const parts = String(value).split(/[,:]/);
2530
+ const start = Number(parts[0]), end = Number(parts[1]);
2531
+ if (parts.length !== 2 || parts.some((p) => !p.trim()) || !Number.isFinite(start) || !Number.isFinite(end) || start < 0 || end <= start)
2532
+ throw new Error("Invalid --range: use non-negative start,end with end > start");
2533
+ return [start, end];
2534
+ }
2535
+ var CAPTION_ZONE_FIELDS = /* @__PURE__ */ new Set(["x0", "y0", "x1", "y1", "severity", "seek"]);
2536
+ var FRAME_CHECK_FIELDS = /* @__PURE__ */ new Set(["severity", "seek", "tol"]);
2537
+ var LAYOUT_FIELDS = /* @__PURE__ */ new Set(["proseCoverageFloor"]);
2538
+ function parseFrameCheck(value) {
2539
+ if (value === void 0 || value === null || value === false) return void 0;
2540
+ if (value === true || value === "") return {};
2541
+ if (typeof value !== "string") throw frameCheckError();
2542
+ if (value.startsWith("-")) throw swallowedFlagUsageError("frame-check", value);
2543
+ const fields = parseFrameCheckFields(value);
2544
+ const severity = captionSeverity(fields.get("severity"), frameCheckError);
2545
+ const seek = captionSeeks(fields.get("seek"), frameCheckError);
2546
+ const tol = parseFrameCheckTolerance(fields.get("tol"));
2547
+ return {
2548
+ ...severity ? { severity } : {},
2549
+ ...seek ? { seek } : {},
2550
+ ...tol !== void 0 ? { tol } : {}
2551
+ };
2552
+ }
2553
+ function parseFrameCheckFields(value) {
2554
+ const fields = /* @__PURE__ */ new Map();
2555
+ for (const part of value.split(";")) {
2556
+ const { key, entry } = parseCaptionField(part, frameCheckError);
2557
+ if (!FRAME_CHECK_FIELDS.has(key) || fields.has(key)) throw frameCheckError();
2558
+ fields.set(key, entry);
2559
+ }
2560
+ return fields;
2561
+ }
2562
+ function parseFrameCheckTolerance(raw) {
2563
+ if (raw === void 0) return void 0;
2564
+ const tol = parseNumberStrict(raw);
2565
+ if (tol === null || tol < 0) throw frameCheckError();
2566
+ return tol;
2567
+ }
2568
+ function frameCheckError() {
2569
+ return new Error(
2570
+ 'Invalid --frame-check: use bare --frame-check or "severity=warning|error;seek=.25,.75;tol=4" (all fields optional)'
2571
+ );
2572
+ }
2573
+ function parseLayout(value) {
2574
+ if (value === void 0 || value === null || value === false) return void 0;
2575
+ if (value === true || value === "") throw layoutError();
2576
+ if (typeof value !== "string") throw layoutError();
2577
+ const fields = parseLayoutFields(value);
2578
+ const proseCoverageFloor = parseProseCoverageFloor(fields.get("proseCoverageFloor"));
2579
+ if (proseCoverageFloor === void 0) throw layoutError();
2580
+ return { proseCoverageFloor };
2581
+ }
2582
+ function parseLayoutFields(value) {
2583
+ const fields = /* @__PURE__ */ new Map();
2584
+ for (const part of value.split(";")) {
2585
+ const trimmed = part.trim();
2586
+ if (!trimmed) continue;
2587
+ const separator = trimmed.indexOf("=");
2588
+ if (separator <= 0) throw layoutError();
2589
+ const key = trimmed.slice(0, separator).trim();
2590
+ const entry = trimmed.slice(separator + 1).trim();
2591
+ if (!LAYOUT_FIELDS.has(key) || fields.has(key)) throw layoutError();
2592
+ fields.set(key, entry);
2593
+ }
2594
+ return fields;
2595
+ }
2596
+ function parseProseCoverageFloor(raw) {
2597
+ if (raw === void 0) return void 0;
2598
+ const floor = parseNumberStrict(raw);
2599
+ if (floor === null || floor < 0 || floor > 1) throw layoutError();
2600
+ return floor;
2601
+ }
2602
+ function layoutError() {
2603
+ return new Error(
2604
+ 'Invalid --layout: use "proseCoverageFloor=0.05" with a fraction from 0 to 1 (inclusive)'
2605
+ );
2606
+ }
2607
+ function parseNumberStrict(raw) {
2608
+ if (raw === "") return null;
2609
+ const value = Number(raw);
2610
+ return Number.isFinite(value) ? value : null;
2611
+ }
2612
+ function parseCaptionZone(value) {
2613
+ if (value === void 0 || value === null) return void 0;
2614
+ const fields = parseCaptionFields(captionZoneString(value));
2615
+ const { x0, y0, x1, y1 } = parseCaptionBounds(fields);
2616
+ const severity = captionSeverity(fields.get("severity"));
2617
+ const seek = captionSeeks(fields.get("seek"));
2618
+ return {
2619
+ x0,
2620
+ y0,
2621
+ x1,
2622
+ y1,
2623
+ ...severity ? { severity } : {},
2624
+ ...seek ? { seek } : {}
2625
+ };
2626
+ }
2627
+ function captionZoneString(value) {
2628
+ if (typeof value !== "string" || value.trim() === "") throw captionZoneError();
2629
+ return value;
2630
+ }
2631
+ function parseCaptionFields(value) {
2632
+ const fields = /* @__PURE__ */ new Map();
2633
+ for (const part of value.split(";")) {
2634
+ const { key, entry } = parseCaptionField(part);
2635
+ if (!CAPTION_ZONE_FIELDS.has(key) || fields.has(key)) throw captionZoneError();
2636
+ fields.set(key, entry);
2637
+ }
2638
+ return fields;
2639
+ }
2640
+ function parseCaptionField(part, errorFactory = captionZoneError) {
2641
+ const separator = part.indexOf("=");
2642
+ if (separator <= 0) throw errorFactory();
2643
+ return {
2644
+ key: part.slice(0, separator).trim(),
2645
+ entry: part.slice(separator + 1).trim()
2646
+ };
2647
+ }
2648
+ function parseCaptionBounds(fields) {
2649
+ const x0 = requiredCaptionFraction(fields, "x0");
2650
+ const y0 = requiredCaptionFraction(fields, "y0");
2651
+ const x1 = requiredCaptionFraction(fields, "x1");
2652
+ const y1 = requiredCaptionFraction(fields, "y1");
2653
+ if (x0 > x1 || y0 > y1) throw captionZoneError();
2654
+ return { x0, y0, x1, y1 };
2655
+ }
2656
+ function requiredCaptionFraction(fields, key) {
2657
+ const value = captionFraction(fields.get(key));
2658
+ if (value === null) throw captionZoneError();
2659
+ return value;
2660
+ }
2661
+ function captionFraction(value) {
2662
+ if (value === void 0 || value === "") return null;
2663
+ const parsed = parseNumberStrict(value);
2664
+ return parsed !== null && parsed >= 0 && parsed <= 1 ? parsed : null;
2665
+ }
2666
+ function captionSeverity(value, errorFactory = captionZoneError) {
2667
+ if (value === void 0) return void 0;
2668
+ if (value === "error" || value === "warning") return value;
2669
+ throw errorFactory();
2670
+ }
2671
+ function captionSeeks(value, errorFactory = captionZoneError) {
2672
+ if (value === void 0) return void 0;
2673
+ if (value === "") return [];
2674
+ const values = value.split(",").map(captionFraction);
2675
+ if (values.some((entry) => entry === null)) throw errorFactory();
2676
+ return values.flatMap((entry) => entry === null ? [] : entry);
2677
+ }
2678
+ function captionZoneError() {
2679
+ return new Error(
2680
+ 'Invalid --caption-zone; use "x0=0;y0=.82;x1=1;y1=1[;severity=warning|error][;seek=.5,1]" with fractions from 0 to 1.'
2681
+ );
2682
+ }
2683
+ function positiveInteger(value, fallback) {
2684
+ const parsed = parseInt(String(value ?? ""), 10);
2685
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
2686
+ }
2687
+ function nonNegativeNumber(value, fallback) {
2688
+ const parsed = parseFloat(String(value ?? ""));
2689
+ return Number.isFinite(parsed) && parsed >= 0 ? parsed : fallback;
2690
+ }
2691
+ function printHumanReport(report) {
2692
+ if (report.scope) {
2693
+ console.log(
2694
+ `Scope: ${report.scope.range ? report.scope.range.join("\u2013") + "s" : "full timeline"}; ${report.scope.hiddenFindings} findings hidden by component filter. Counts/status include hidden findings.`
2695
+ );
2696
+ for (const note of report.scope.notes) console.log(note);
2697
+ }
2698
+ printSection("Lint", report.lint);
2699
+ if (report.browserSkipped) {
2700
+ console.log();
2701
+ console.log(
2702
+ ` ${c.warn("\u26A0")} Browser session never ran \u2014 layout, motion, and contrast below are empty placeholders, not a clean pass.`
2703
+ );
2704
+ }
2705
+ printSection("Runtime", report.runtime);
2706
+ printLayoutSection("Layout", report.layout);
2707
+ printSection("Motion", report.motion);
2708
+ printContrastSection(report);
2709
+ printCraftSection(report);
2710
+ printSnapshotSection(report);
2711
+ console.log();
2712
+ const label = report.ok ? c.success("Check passed") : c.error("Check failed");
2713
+ console.log(`${report.ok ? c.success("\u25C7") : c.error("\u25C7")} ${label}`);
2714
+ }
2715
+ function isEmptySection(section2) {
2716
+ return section2.findings.length === 0 && section2.errorCount + section2.warningCount + section2.infoCount === 0;
2717
+ }
2718
+ function printSection(title, section2) {
2719
+ console.log();
2720
+ console.log(c.bold(title));
2721
+ if (isEmptySection(section2)) {
2722
+ console.log(` ${c.success("\u25C7")} 0 errors, 0 warnings`);
2723
+ return;
2724
+ }
2725
+ for (const finding of section2.findings) printFinding(finding);
2726
+ printCounts(section2);
2727
+ }
2728
+ function printLayoutSection(title, section2) {
2729
+ console.log();
2730
+ console.log(c.bold(title));
2731
+ if (isEmptySection(section2)) {
2732
+ console.log(` ${c.success("\u25C7")} 0 issues across ${section2.samples.length} sample(s)`);
2733
+ } else {
2734
+ for (const finding of section2.findings) {
2735
+ const formatted = formatLayoutIssue(finding).replace(/\n/g, "\n ");
2736
+ console.log(` ${findingIcon(finding)} ${formatted}`);
2737
+ }
2738
+ printCounts(section2);
2739
+ }
2740
+ if (section2.transitionSamplesDropped > 0) {
2741
+ console.log(
2742
+ ` ${c.warn("\u26A0")} ${section2.transitionSamplesDropped} transition sample(s) omitted`
2743
+ );
2744
+ }
2745
+ }
2746
+ function printContrastSection(report) {
2747
+ const section2 = report.contrast;
2748
+ console.log();
2749
+ console.log(c.bold("Contrast"));
2750
+ if (!section2.enabled) {
2751
+ console.log(` ${c.dim("\u25C7")} skipped`);
2752
+ return;
2753
+ }
2754
+ if (isEmptySection(section2)) {
2755
+ console.log(
2756
+ ` ${c.success("\u25C7")} ${section2.passed}/${section2.checked} text checks pass WCAG AA`
2757
+ );
2758
+ return;
2759
+ }
2760
+ for (const finding of section2.findings) {
2761
+ console.log(
2762
+ ` ${c.error("\u2717")} ${finding.selector} ${finding.ratio}:1 (need ${finding.requiredRatio}:1, ${formatFindingTimes(finding)})`
2763
+ );
2764
+ console.log(` ${c.dim(`Try ${finding.suggestedColor}; source ${finding.sourceFile}`)}`);
2765
+ }
2766
+ printCounts(section2);
2767
+ }
2768
+ function printCraftSection(report) {
2769
+ const section2 = report.craft;
2770
+ if (!section2.enabled) {
2771
+ console.log();
2772
+ console.log(c.bold("Craft"));
2773
+ console.log(` ${c.dim("\u25C7")} skipped`);
2774
+ return;
2775
+ }
2776
+ printSection("Craft", section2);
2777
+ }
2778
+ function printSnapshotSection(report) {
2779
+ console.log();
2780
+ console.log(c.bold("Snapshots"));
2781
+ if (!report.snapshots.enabled) {
2782
+ console.log(` ${c.dim("\u25C7")} disabled`);
2783
+ } else {
2784
+ console.log(` ${c.success("\u25C7")} ${report.snapshots.files.length} PNG(s) saved`);
2785
+ for (const file of report.snapshots.files) console.log(` ${c.dim(file)}`);
2786
+ if (report.snapshots.findingFiles.length > 0) {
2787
+ console.log(
2788
+ ` ${c.success("\u25C7")} ${report.snapshots.findingFiles.length} finding crop(s) saved`
2789
+ );
2790
+ for (const file of report.snapshots.findingFiles) console.log(` ${c.dim(file)}`);
2791
+ }
2792
+ }
2793
+ }
2794
+ function printFinding(finding) {
2795
+ const where = `${finding.sourceFile} ${finding.selector} ${formatFindingTimes(finding)}`;
2796
+ console.log(` ${findingIcon(finding)} ${finding.code}: ${finding.message}`);
2797
+ console.log(` ${c.dim(where)}`);
2798
+ if (finding.fixHint) console.log(` ${c.dim(`Fix: ${finding.fixHint}`)}`);
2799
+ }
2800
+ function findingIcon(finding) {
2801
+ if (finding.severity === "error") return c.error("\u2717");
2802
+ if (finding.severity === "warning") return c.warn("\u26A0");
2803
+ return c.dim("\u2139");
2804
+ }
2805
+ function printCounts(section2) {
2806
+ console.log(
2807
+ ` ${c.dim(`${section2.errorCount} error(s), ${section2.warningCount} warning(s), ${section2.infoCount} info(s)`)}`
2808
+ );
2809
+ }
2810
+ var check_default = createCheckCommand();
2811
+
2812
+ export {
2813
+ examples,
2814
+ createCheckCommand,
2815
+ prelaunchBrowserForCheck,
2816
+ parseCheckRange,
2817
+ parseFrameCheck,
2818
+ parseLayout,
2819
+ check_default
2820
+ };