@ggui-ai/ui-gen 0.27.0 → 0.29.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (315) hide show
  1. package/dist/{adapter-h4_V69LI.d.ts → adapter-DLQ-5COm.d.ts} +1 -1
  2. package/dist/adapters/base.js +2 -27
  3. package/dist/adapters/base.js.map +1 -1
  4. package/dist/adapters/generation-dispatch.d.ts +14 -6
  5. package/dist/adapters/generation-dispatch.js +40 -19095
  6. package/dist/adapters/generation-dispatch.js.map +1 -1
  7. package/dist/adapters/index.d.ts +1 -1
  8. package/dist/adapters/index.js +25 -3561
  9. package/dist/adapters/index.js.map +1 -1
  10. package/dist/adapters/types.js +2 -9
  11. package/dist/adapters/types.js.map +1 -1
  12. package/dist/advanced/index.d.ts +13 -12
  13. package/dist/advanced/index.js +63 -21789
  14. package/dist/advanced/index.js.map +1 -1
  15. package/dist/{bank-Ctdp3FUA.d.ts → bank-LWaiAtaA.d.ts} +1 -1
  16. package/dist/blueprint-validator.js +6 -2301
  17. package/dist/blueprint-validator.js.map +1 -1
  18. package/dist/boilerplate.d.ts +3 -2
  19. package/dist/boilerplate.js +6 -1867
  20. package/dist/boilerplate.js.map +1 -1
  21. package/dist/check/index.d.ts +3 -2
  22. package/dist/check/index.js +6 -1923
  23. package/dist/check/index.js.map +1 -1
  24. package/dist/chunk-33B3BAWS.js +24 -0
  25. package/dist/chunk-33B3BAWS.js.map +1 -0
  26. package/dist/chunk-3HSGA6NI.js +591 -0
  27. package/dist/chunk-3HSGA6NI.js.map +1 -0
  28. package/dist/chunk-3NNNGNTO.js +1707 -0
  29. package/dist/chunk-3NNNGNTO.js.map +1 -0
  30. package/dist/chunk-3T2HMTON.js +79 -0
  31. package/dist/chunk-3T2HMTON.js.map +1 -0
  32. package/dist/chunk-3ZSSDV3L.js +123 -0
  33. package/dist/chunk-3ZSSDV3L.js.map +1 -0
  34. package/dist/chunk-4MBR5GA3.js +99 -0
  35. package/dist/chunk-4MBR5GA3.js.map +1 -0
  36. package/dist/chunk-56NJFD55.js +337 -0
  37. package/dist/chunk-56NJFD55.js.map +1 -0
  38. package/dist/chunk-5AGTY555.js +26 -0
  39. package/dist/chunk-5AGTY555.js.map +1 -0
  40. package/dist/chunk-5EJM7EGY.js +115 -0
  41. package/dist/chunk-5EJM7EGY.js.map +1 -0
  42. package/dist/chunk-5KXCQLLR.js +529 -0
  43. package/dist/chunk-5KXCQLLR.js.map +1 -0
  44. package/dist/chunk-5VBXHVEH.js +171 -0
  45. package/dist/chunk-5VBXHVEH.js.map +1 -0
  46. package/dist/chunk-6KDDEING.js +515 -0
  47. package/dist/chunk-6KDDEING.js.map +1 -0
  48. package/dist/chunk-6VDRWAP6.js +123 -0
  49. package/dist/chunk-6VDRWAP6.js.map +1 -0
  50. package/dist/chunk-732IBFEJ.js +2400 -0
  51. package/dist/chunk-732IBFEJ.js.map +1 -0
  52. package/dist/chunk-73AURS3I.js +46 -0
  53. package/dist/chunk-73AURS3I.js.map +1 -0
  54. package/dist/chunk-77H5SKLX.js +8 -0
  55. package/dist/chunk-77H5SKLX.js.map +1 -0
  56. package/dist/chunk-7QE3AHLE.js +35 -0
  57. package/dist/chunk-7QE3AHLE.js.map +1 -0
  58. package/dist/chunk-A375NNWN.js +11 -0
  59. package/dist/chunk-A375NNWN.js.map +1 -0
  60. package/dist/chunk-AOU33EFS.js +140 -0
  61. package/dist/chunk-AOU33EFS.js.map +1 -0
  62. package/dist/chunk-BA4QYA6E.js +461 -0
  63. package/dist/chunk-BA4QYA6E.js.map +1 -0
  64. package/dist/chunk-DBP764GL.js +3 -0
  65. package/dist/chunk-DBP764GL.js.map +1 -0
  66. package/dist/chunk-DUU3ZJDQ.js +41 -0
  67. package/dist/chunk-DUU3ZJDQ.js.map +1 -0
  68. package/dist/chunk-DXKRBXNU.js +30 -0
  69. package/dist/chunk-DXKRBXNU.js.map +1 -0
  70. package/dist/chunk-FB7OEEAH.js +1467 -0
  71. package/dist/chunk-FB7OEEAH.js.map +1 -0
  72. package/dist/chunk-FLRNMZV2.js +18 -0
  73. package/dist/chunk-FLRNMZV2.js.map +1 -0
  74. package/dist/chunk-FWJIMKFZ.js +2032 -0
  75. package/dist/chunk-FWJIMKFZ.js.map +1 -0
  76. package/dist/chunk-FXRHWYIQ.js +163 -0
  77. package/dist/chunk-FXRHWYIQ.js.map +1 -0
  78. package/dist/chunk-G3PMV62Z.js +33 -0
  79. package/dist/chunk-G3PMV62Z.js.map +1 -0
  80. package/dist/chunk-GAJI7Y3P.js +13 -0
  81. package/dist/chunk-GAJI7Y3P.js.map +1 -0
  82. package/dist/chunk-GYINWMPR.js +2052 -0
  83. package/dist/chunk-GYINWMPR.js.map +1 -0
  84. package/dist/chunk-HD6ABIK7.js +484 -0
  85. package/dist/chunk-HD6ABIK7.js.map +1 -0
  86. package/dist/chunk-HDQVHYO6.js +100 -0
  87. package/dist/chunk-HDQVHYO6.js.map +1 -0
  88. package/dist/chunk-IHUT3SIW.js +7 -0
  89. package/dist/chunk-IHUT3SIW.js.map +1 -0
  90. package/dist/chunk-JDH5EGNZ.js +126 -0
  91. package/dist/chunk-JDH5EGNZ.js.map +1 -0
  92. package/dist/chunk-JMD4RGSV.js +37 -0
  93. package/dist/chunk-JMD4RGSV.js.map +1 -0
  94. package/dist/chunk-KJWGZL5B.js +33 -0
  95. package/dist/chunk-KJWGZL5B.js.map +1 -0
  96. package/dist/chunk-MKMDQFZ7.js +86 -0
  97. package/dist/chunk-MKMDQFZ7.js.map +1 -0
  98. package/dist/chunk-NF6JUZWF.js +85 -0
  99. package/dist/chunk-NF6JUZWF.js.map +1 -0
  100. package/dist/chunk-O2YC6XDC.js +1933 -0
  101. package/dist/chunk-O2YC6XDC.js.map +1 -0
  102. package/dist/chunk-OVLH5ICV.js +29 -0
  103. package/dist/chunk-OVLH5ICV.js.map +1 -0
  104. package/dist/chunk-PBOYHGSD.js +106 -0
  105. package/dist/chunk-PBOYHGSD.js.map +1 -0
  106. package/dist/chunk-PJFH7QWA.js +81 -0
  107. package/dist/chunk-PJFH7QWA.js.map +1 -0
  108. package/dist/chunk-PUH4CXGZ.js +284 -0
  109. package/dist/chunk-PUH4CXGZ.js.map +1 -0
  110. package/dist/chunk-QLXXPGTV.js +18 -0
  111. package/dist/chunk-QLXXPGTV.js.map +1 -0
  112. package/dist/chunk-R5GIGPW2.js +2845 -0
  113. package/dist/chunk-R5GIGPW2.js.map +1 -0
  114. package/dist/chunk-RH4IUSTO.js +83 -0
  115. package/dist/chunk-RH4IUSTO.js.map +1 -0
  116. package/dist/chunk-S2GVXKKY.js +153 -0
  117. package/dist/chunk-S2GVXKKY.js.map +1 -0
  118. package/dist/chunk-TVHGKFJS.js +58 -0
  119. package/dist/chunk-TVHGKFJS.js.map +1 -0
  120. package/dist/chunk-U7GALWLG.js +66 -0
  121. package/dist/chunk-U7GALWLG.js.map +1 -0
  122. package/dist/chunk-VTC263LT.js +256 -0
  123. package/dist/chunk-VTC263LT.js.map +1 -0
  124. package/dist/chunk-WEOXFEVE.js +739 -0
  125. package/dist/chunk-WEOXFEVE.js.map +1 -0
  126. package/dist/chunk-WYHP46LT.js +28 -0
  127. package/dist/chunk-WYHP46LT.js.map +1 -0
  128. package/dist/chunk-XGLO5LGG.js +274 -0
  129. package/dist/chunk-XGLO5LGG.js.map +1 -0
  130. package/dist/chunk-XRBCRKA2.js +29 -0
  131. package/dist/chunk-XRBCRKA2.js.map +1 -0
  132. package/dist/chunk-XW7ADFFV.js +340 -0
  133. package/dist/chunk-XW7ADFFV.js.map +1 -0
  134. package/dist/chunk-YIQ42IUX.js +34 -0
  135. package/dist/chunk-YIQ42IUX.js.map +1 -0
  136. package/dist/chunk-YJZTUY7B.js +101 -0
  137. package/dist/chunk-YJZTUY7B.js.map +1 -0
  138. package/dist/chunk-Z3CXME3K.js +107 -0
  139. package/dist/chunk-Z3CXME3K.js.map +1 -0
  140. package/dist/classifier/index.js +2 -527
  141. package/dist/classifier/index.js.map +1 -1
  142. package/dist/client-JBPCXU7W.js +4 -0
  143. package/dist/client-JBPCXU7W.js.map +1 -0
  144. package/dist/compose.js +3 -367
  145. package/dist/compose.js.map +1 -1
  146. package/dist/cost-tracker-HHGQ4FTW.js +67 -0
  147. package/dist/cost-tracker-HHGQ4FTW.js.map +1 -0
  148. package/dist/{create-ui-generator-BrZHF0cf.d.ts → create-ui-generator-BY-WeaPi.d.ts} +3 -3
  149. package/dist/{design-mode-DGWN_Gkm.d.ts → design-mode-XyOQmzE0.d.ts} +4 -3
  150. package/dist/{dispatch-B07TI30d.d.ts → dispatch-8LzTddKw.d.ts} +3 -2
  151. package/dist/dist-UALN6ZVB.js +543 -0
  152. package/dist/dist-UALN6ZVB.js.map +1 -0
  153. package/dist/dupe-break-X6K7QFIS.js +4 -0
  154. package/dist/dupe-break-X6K7QFIS.js.map +1 -0
  155. package/dist/evaluation/axis-checks/index.d.ts +5 -4
  156. package/dist/evaluation/axis-checks/index.js +6 -1818
  157. package/dist/evaluation/axis-checks/index.js.map +1 -1
  158. package/dist/evaluation/axis-checks/registry.d.ts +3 -2
  159. package/dist/evaluation/axis-checks/registry.js +3 -1748
  160. package/dist/evaluation/axis-checks/registry.js.map +1 -1
  161. package/dist/evaluation/index.d.ts +49 -132
  162. package/dist/evaluation/index.js +302 -6368
  163. package/dist/evaluation/index.js.map +1 -1
  164. package/dist/evaluation/types.d.ts +3 -2
  165. package/dist/evaluation/types.js +2 -5
  166. package/dist/evaluation/types.js.map +1 -1
  167. package/dist/fragments/index.js +2 -338
  168. package/dist/fragments/index.js.map +1 -1
  169. package/dist/{generate-tSO_shQ0.d.ts → generate-DQ4ci90s.d.ts} +1 -1
  170. package/dist/harness/check/runtime-render/index.d.ts +35 -15
  171. package/dist/harness/check/runtime-render/index.js +6 -2390
  172. package/dist/harness/check/runtime-render/index.js.map +1 -1
  173. package/dist/harness/check/runtime-render/render-check-worker.js +4 -1739
  174. package/dist/harness/check/runtime-render/render-check-worker.js.map +1 -1
  175. package/dist/harness/index.d.ts +3 -2
  176. package/dist/harness/index.js +23 -5124
  177. package/dist/harness/index.js.map +1 -1
  178. package/dist/harness/llm-trace-sink.js +2 -24
  179. package/dist/harness/llm-trace-sink.js.map +1 -1
  180. package/dist/harness/prompts.d.ts +2 -2
  181. package/dist/harness/prompts.js +8 -324
  182. package/dist/harness/prompts.js.map +1 -1
  183. package/dist/harness/result-types.d.ts +18 -6
  184. package/dist/harness/result-types.js +2 -0
  185. package/dist/harness/result-types.js.map +1 -1
  186. package/dist/harness/runtime.d.ts +22 -5
  187. package/dist/harness/runtime.js +10 -2313
  188. package/dist/harness/runtime.js.map +1 -1
  189. package/dist/harness/types-public.d.ts +3 -2
  190. package/dist/harness/types-public.js +16 -2954
  191. package/dist/harness/types-public.js.map +1 -1
  192. package/dist/harness/validator-trace-sink.d.ts +3 -2
  193. package/dist/harness/validator-trace-sink.js +2 -26
  194. package/dist/harness/validator-trace-sink.js.map +1 -1
  195. package/dist/hash.js +2 -39
  196. package/dist/hash.js.map +1 -1
  197. package/dist/hashline-5OKREIRM.js +4 -0
  198. package/dist/hashline-5OKREIRM.js.map +1 -0
  199. package/dist/index.d.ts +51 -12
  200. package/dist/index.js +57 -21850
  201. package/dist/index.js.map +1 -1
  202. package/dist/llm-evaluator-PN3WFCS4.js +653 -0
  203. package/dist/llm-evaluator-PN3WFCS4.js.map +1 -0
  204. package/dist/magic-string.es-2NZTC4GZ.js +1307 -0
  205. package/dist/magic-string.es-2NZTC4GZ.js.map +1 -0
  206. package/dist/patch.d.ts +3 -2
  207. package/dist/patch.js +2 -56
  208. package/dist/patch.js.map +1 -1
  209. package/dist/policy-VCYKGSAC.js +5 -0
  210. package/dist/policy-VCYKGSAC.js.map +1 -0
  211. package/dist/policy.js +2 -35
  212. package/dist/policy.js.map +1 -1
  213. package/dist/primitive-index-2OF6F4TZ.js +131 -0
  214. package/dist/primitive-index-2OF6F4TZ.js.map +1 -0
  215. package/dist/primitive-slice-WTMBLYFJ.js +235 -0
  216. package/dist/primitive-slice-WTMBLYFJ.js.map +1 -0
  217. package/dist/{prompts-BhNUuRoZ.d.ts → prompts-CLZGywMP.d.ts} +1 -1
  218. package/dist/provider-adapter-contract.js +10407 -14204
  219. package/dist/provider-adapter-contract.js.map +1 -1
  220. package/dist/provider-adapter.js +2 -44
  221. package/dist/provider-adapter.js.map +1 -1
  222. package/dist/providers/index.d.ts +89 -1
  223. package/dist/providers/index.js +115 -44
  224. package/dist/providers/index.js.map +1 -1
  225. package/dist/raw-C6KBH5I3.js +141 -0
  226. package/dist/raw-C6KBH5I3.js.map +1 -0
  227. package/dist/raw-FXSBXQTZ.js +7 -0
  228. package/dist/raw-FXSBXQTZ.js.map +1 -0
  229. package/dist/raw-MURDUPGK.js +8 -0
  230. package/dist/raw-MURDUPGK.js.map +1 -0
  231. package/dist/raw-T3GW3UVX.js +8 -0
  232. package/dist/raw-T3GW3UVX.js.map +1 -0
  233. package/dist/render-check-host-ZNIS6BJ5.js +5 -0
  234. package/dist/render-check-host-ZNIS6BJ5.js.map +1 -0
  235. package/dist/sdk-4JNSDNSR.js +6 -0
  236. package/dist/sdk-4JNSDNSR.js.map +1 -0
  237. package/dist/sdk-DIVJMHCX.js +7 -0
  238. package/dist/sdk-DIVJMHCX.js.map +1 -0
  239. package/dist/sdk-QRJOCRTS.js +7 -0
  240. package/dist/sdk-QRJOCRTS.js.map +1 -0
  241. package/dist/tag-balance-CTHCTRJ7.js +123 -0
  242. package/dist/tag-balance-CTHCTRJ7.js.map +1 -0
  243. package/dist/tools/get-primitives-ts.d.ts +1 -1
  244. package/dist/tools/get-primitives-ts.js +3 -1
  245. package/dist/tools/get-primitives-ts.js.map +1 -1
  246. package/dist/tools/render-check-worker.js +3 -40
  247. package/dist/tools/render-check-worker.js.map +1 -1
  248. package/dist/{types-public-h8IMnYuh.d.ts → types-public-hYlrerZh.d.ts} +35 -2
  249. package/dist/validation/index.d.ts +2 -2
  250. package/dist/validation/index.js +3 -498
  251. package/dist/validation/index.js.map +1 -1
  252. package/dist/{visual-evaluator-C2strxqD.d.ts → visual-evaluator-KskajxD5.d.ts} +14 -4
  253. package/dist/visual-evaluator-TGEXYQTP.js +12 -0
  254. package/dist/visual-evaluator-TGEXYQTP.js.map +1 -0
  255. package/dist/workflows.d.ts +3 -2
  256. package/dist/workflows.js +2 -151
  257. package/dist/workflows.js.map +1 -1
  258. package/package.json +21 -17
  259. package/src/adapters/claude/sdk.ts +4 -2
  260. package/src/adapters/claude/tool-bridge.ts +2 -1
  261. package/src/adapters/generation-dispatch.ts +9 -0
  262. package/src/adapters/openai/raw.ts +4 -0
  263. package/src/adapters/openai/sdk.ts +6 -1
  264. package/src/adapters/openai/tokens.ts +21 -7
  265. package/src/adapters/provider-router.ts +1 -0
  266. package/src/adapters/tool-bridge.ts +1 -1
  267. package/src/adapters/tools.ts +1 -1
  268. package/src/advanced/feedback.ts +1 -2
  269. package/src/advanced/generator.ts +4 -3
  270. package/src/boilerplate/free-design-prompt.ts +7 -0
  271. package/src/boilerplate/generate.ts +18 -5
  272. package/src/boilerplate/hard-sections.ts +8 -3
  273. package/src/boilerplate/react-hooks.ts +56 -0
  274. package/src/boilerplate/system-prompt.ts +12 -3
  275. package/src/check/run-tier0.ts +233 -8
  276. package/src/check/type-checker.ts +128 -16
  277. package/src/coding-agent/tools.ts +26 -3
  278. package/src/create-ui-generator.ts +4 -2
  279. package/src/design-mode.ts +4 -9
  280. package/src/evaluation/axis-checks/checks/state-merge.ts +3 -5
  281. package/src/evaluation/axis-checks/checks/state-payload.ts +1 -28
  282. package/src/evaluation/axis-checks/checks/tint.ts +197 -0
  283. package/src/evaluation/axis-checks/helpers.ts +246 -0
  284. package/src/evaluation/axis-checks/registry.ts +2 -0
  285. package/src/evaluation/criteria/__fixtures__/edge-v2-draw2.json +4629 -0
  286. package/src/evaluation/criteria/bank.ts +1 -1
  287. package/src/evaluation/criteria/edge-instruments.ts +14 -0
  288. package/src/evaluation/criteria/edge-probe-v2.ts +352 -0
  289. package/src/evaluation/criteria/edge-v2.ts +242 -0
  290. package/src/evaluation/criteria/resolve.ts +24 -5
  291. package/src/evaluation/evaluator.ts +2 -2
  292. package/src/evaluation/index.ts +3 -5
  293. package/src/evaluation/llm-evaluator.ts +2 -2
  294. package/src/evaluation/mcp-server.ts +0 -50
  295. package/src/evaluation/types-public.ts +6 -1
  296. package/src/evaluation/visual-evaluator.ts +162 -31
  297. package/src/harness/check/runtime-render/index.ts +28 -4
  298. package/src/harness/check/runtime-render/render-check.ts +20 -21
  299. package/src/harness/claude-agent-sdk-loader.ts +93 -0
  300. package/src/harness/claude-code-login-query.ts +1 -1
  301. package/src/harness/coding/assemble-result.ts +1 -0
  302. package/src/harness/coding/dupe-break.ts +70 -0
  303. package/src/harness/coding/generate-task-runner.ts +6 -1
  304. package/src/harness/coding/run-coding-turn.ts +59 -1
  305. package/src/harness/coding/run-eval-round.ts +9 -0
  306. package/src/harness/llm-router.ts +50 -2
  307. package/src/harness/result-types.ts +12 -0
  308. package/src/harness/runtime.ts +17 -0
  309. package/src/index.ts +10 -1
  310. package/src/providers/anthropic-tool-call.ts +263 -0
  311. package/src/providers/index.ts +13 -0
  312. package/src/tools/get-primitives-ts.ts +1 -1
  313. package/src/tools/render-check.ts +4 -4
  314. package/src/validation/primitives.ts +2 -1
  315. package/src/evaluation/loop.ts +0 -218
@@ -0,0 +1,2400 @@
1
+ import { createVisionAgent } from './chunk-O2YC6XDC.js';
2
+ import { buildStylingProfileJudgeBlock } from './chunk-NF6JUZWF.js';
3
+ import { displayModeForCanvas, CANVAS_VIEWPORTS } from './chunk-3T2HMTON.js';
4
+ import { createHash } from 'crypto';
5
+ import * as z from 'zod';
6
+ import { build } from 'esbuild';
7
+ import { EXPANDED_FRAME, getCssTokens, FRAMED_CARD_GROUND, expandedFrameScrimDecls, mountFitCss, expandedFramePanelRule, expandedFramePanelDecls, GGUI_MOUNT_CANVAS_EXPORT, resolveMountFit, inlineCardRadius } from '@ggui-ai/design/rendering';
8
+ import { inflateSync } from 'zlib';
9
+ import { readFileSync, existsSync, mkdtempSync, writeFileSync, rmSync, readdirSync, statSync } from 'fs';
10
+ import { resolve, relative, dirname } from 'path';
11
+ import { fileURLToPath } from 'url';
12
+ import { createRequire } from 'module';
13
+ import { tmpdir } from 'os';
14
+
15
+ var CRITERION_ID = /^[a-z][a-z0-9-]*(?:\.[a-zA-Z][a-zA-Z0-9-]*)+$/;
16
+ var criteriaScopeSchema = z.object({
17
+ kind: z.string().min(1).optional(),
18
+ canvas: z.string().min(1).optional(),
19
+ preset: z.string().min(1).optional()
20
+ });
21
+ var bankRowSchema = z.object({
22
+ id: z.string().regex(CRITERION_ID, "a criterion id is family.property[.facet], lower-case family"),
23
+ group: z.string().optional(),
24
+ scope: criteriaScopeSchema.default({}),
25
+ /** Importance, independent of whether anything automated checks it yet. */
26
+ severity: z.enum(["must", "should"]),
27
+ /** The method that produces the verdict. */
28
+ evaluation: z.enum(["instrument", "judge", "human"]),
29
+ /** The method's readiness — the only source of an `n/a`. */
30
+ status: z.enum(["live", "reference", "planned", "manual"]),
31
+ /** The question, answerable yes/no from the frame. */
32
+ text: z.string().min(1),
33
+ /** The evidence rule: what the one-sentence evidence must name (a region or element). */
34
+ evidence: z.string().min(1),
35
+ /** The templated fix line a feedback channel may consume later; never read in v1. */
36
+ fix: z.string().optional()
37
+ });
38
+ var criteriaBankSchema = z.object({
39
+ version: z.string().min(1),
40
+ applies: z.object({
41
+ /** `["*"]` = every kind. */
42
+ kind: z.array(z.string().min(1)).optional(),
43
+ canvases: z.array(z.string().min(1)).optional()
44
+ }).optional(),
45
+ criteria: z.array(bankRowSchema).min(1),
46
+ recipes: z.array(bankRowSchema).optional()
47
+ }).superRefine((bank, ctx) => {
48
+ const seen = /* @__PURE__ */ new Set();
49
+ const check = (rows, key) => {
50
+ rows.forEach((c, i) => {
51
+ if (seen.has(c.id)) ctx.addIssue({ code: "custom", path: [key, i, "id"], message: `duplicate criterion id ${c.id}` });
52
+ seen.add(c.id);
53
+ });
54
+ };
55
+ check(bank.criteria, "criteria");
56
+ check(bank.recipes ?? [], "recipes");
57
+ });
58
+ function bankRows(bank) {
59
+ return [...bank.criteria, ...bank.recipes ?? []];
60
+ }
61
+ function parseCriteriaBank(raw) {
62
+ const parsed = criteriaBankSchema.safeParse(raw);
63
+ if (!parsed.success) {
64
+ const issues = parsed.error.issues.map((i) => `${i.path.map(String).join(".") || "<root>"}: ${i.message}`).join("; ");
65
+ throw new Error(`criteria bank invalid \u2014 ${issues}`);
66
+ }
67
+ return parsed.data;
68
+ }
69
+
70
+ // src/evaluation/criteria/select.ts
71
+ var CRITERIA_SELECTOR_VERSION = "selector@2";
72
+ function canonicalCriteriaContext(ctx) {
73
+ const sortKeys = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? Object.fromEntries(
74
+ Object.entries(value).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([k, v]) => [k, sortKeys(v)])
75
+ ) : value;
76
+ return JSON.stringify(sortKeys(ctx));
77
+ }
78
+ function criteriaSetIdFor(bankVersion, ctx) {
79
+ return createHash("sha256").update(`${bankVersion}|${CRITERIA_SELECTOR_VERSION}|${canonicalCriteriaContext(ctx)}`, "utf8").digest("hex").slice(0, 16);
80
+ }
81
+ function matchedScope(row, ctx) {
82
+ const s = row.scope;
83
+ const matched = [];
84
+ if (s.kind !== void 0) {
85
+ if (ctx.kind !== s.kind) return null;
86
+ matched.push(`kind=${s.kind}`);
87
+ }
88
+ if (s.canvas !== void 0) {
89
+ if (ctx.canvas !== s.canvas) return null;
90
+ matched.push(`canvas=${s.canvas}`);
91
+ }
92
+ if (s.preset !== void 0) {
93
+ if (ctx.preset !== s.preset) return null;
94
+ matched.push(`preset=${s.preset}`);
95
+ }
96
+ return matched;
97
+ }
98
+ function selectCriteria(bank, ctx) {
99
+ const criteriaSetId = criteriaSetIdFor(bank.version, ctx);
100
+ const applies = bank.applies;
101
+ if (applies?.canvases !== void 0 && !applies.canvases.includes(ctx.canvas)) return { criteriaSetId, selection: [] };
102
+ if (applies?.kind !== void 0 && ctx.kind !== void 0 && !applies.kind.includes("*") && !applies.kind.includes(ctx.kind)) {
103
+ return { criteriaSetId, selection: [] };
104
+ }
105
+ const selection = [];
106
+ for (const row of bankRows(bank)) {
107
+ const matched = matchedScope(row, ctx);
108
+ if (matched === null) continue;
109
+ selection.push(matched.length === 0 ? { id: row.id, source: "static", reason: "static" } : { id: row.id, source: "context", reason: `context: ${matched.join(", ")}` });
110
+ }
111
+ return { criteriaSetId, selection };
112
+ }
113
+
114
+ // src/evaluation/criteria/edge-probe.ts
115
+ var EDGE_BLOCKS_EXPRESSION = String.raw`(() => {
116
+ const root = document.getElementById('root');
117
+ if (!root) return null;
118
+ const rootBox = root.getBoundingClientRect();
119
+ const x0 = rootBox.left;
120
+ const px = (v) => Math.round((v - x0) * 2) / 2;
121
+ const CONTROL = 'button,input,select,textarea,[role="button"],[role="tab"],[role="switch"],[role="checkbox"],[role="radio"],[role="option"],[role="link"],a[href]';
122
+ const ALNUM = /[\p{L}\p{N}]/u;
123
+ const css = (el) => getComputedStyle(el);
124
+ const clear = (c) => !c || c === 'transparent' || /rgba\([^)]*,\s*0(\.0+)?\)$/.test(c);
125
+ // Visible through every ancestor (an ancestor at opacity 0 or visibility hidden hides it), and not degenerate.
126
+ const shown = (el) => {
127
+ const s = css(el);
128
+ if (s.display === 'none' || s.visibility === 'hidden' || Number(s.opacity) === 0) return false;
129
+ if (typeof el.checkVisibility === 'function' && !el.checkVisibility({ opacityProperty: true, visibilityProperty: true })) return false;
130
+ const r = el.getBoundingClientRect();
131
+ return r.width > 1 && r.height > 1;
132
+ };
133
+ const inline = (s) => /^inline/.test(s.display) && s.display !== 'inline-block' && s.display !== 'inline-flex' && s.display !== 'inline-grid';
134
+ const groundOf = (el) => {
135
+ for (let p = el.parentElement; p; p = p.parentElement) { const b = css(p).backgroundColor; if (!clear(b)) return b; }
136
+ return 'rgb(255, 255, 255)';
137
+ };
138
+ const sides = (s) => ['Top', 'Right', 'Bottom', 'Left'].filter((k) => parseFloat(s['border' + k + 'Width']) > 0 && s['border' + k + 'Style'] !== 'none' && !clear(s['border' + k + 'Color'])).length;
139
+ const bounded = (el) => { const s = css(el); return sides(s) >= 1 || (!clear(s.backgroundColor) && s.backgroundColor !== groundOf(el)); };
140
+ // A control: a native or ARIA one, or a bounded box the visitor clicks (a pointer cursor or a tab stop) that holds no
141
+ // control of its own and is not the whole card. Its contents are not blocks of the column (decided 2026-10-02).
142
+ const isControl = (el) => {
143
+ const s = css(el);
144
+ if (inline(s)) return false;
145
+ if (el.matches(CONTROL)) return true;
146
+ if (el === root) return false;
147
+ const clickable = s.cursor === 'pointer' || (el.hasAttribute('tabindex') && el.tabIndex >= 0);
148
+ if (!clickable || !bounded(el) || el.querySelector(CONTROL)) return false;
149
+ const r = el.getBoundingClientRect();
150
+ return r.width * r.height < 0.6 * rootBox.width * rootBox.height;
151
+ };
152
+ const isTable = (el) => el.matches('table,[role="table"],[role="grid"]');
153
+ const isSurface = (el) => {
154
+ if (el === root || isControl(el)) return false;
155
+ const s = css(el);
156
+ if (/^inline/.test(s.display) || s.display === 'contents') return false;
157
+ if (isTable(el)) return true;
158
+ return sides(s) >= 3 || (!clear(s.backgroundColor) && s.backgroundColor !== groundOf(el)) || (s.boxShadow && s.boxShadow !== 'none');
159
+ };
160
+ const path = (el) => {
161
+ const parts = [];
162
+ for (let e = el; e && e !== root && parts.length < 4; e = e.parentElement) {
163
+ const i = e.parentElement ? Array.prototype.indexOf.call(e.parentElement.children, e) + 1 : 1;
164
+ parts.unshift(e.tagName.toLowerCase() + ':' + i);
165
+ }
166
+ return parts.join('>');
167
+ };
168
+ const label = (el) => { const t = (el.innerText || '').replace(/\s+/g, ' ').trim(); return path(el) + (t ? ' "' + t.slice(0, 24) + '"' : ''); };
169
+ const surfaces = [];
170
+ const surfaceOf = (el, self) => { for (let p = self ? el : el.parentElement; p && p !== root; p = p.parentElement) if (surfaces.indexOf(p) >= 0) return p; return null; };
171
+ const inTable = (el) => { for (let p = el.parentElement; p && p !== root; p = p.parentElement) if (isTable(p)) return true; return false; };
172
+ const inControl = (el) => { for (let p = el; p && p !== root; p = p.parentElement) if (isControl(p)) return p; return null; };
173
+ const all = Array.prototype.slice.call(root.querySelectorAll('*')).filter(shown);
174
+ for (const el of all) if (isSurface(el) && !inTable(el) && !inControl(el)) surfaces.push(el);
175
+ const ids = new Map();
176
+ const idOf = (el) => { if (!ids.has(el)) ids.set(el, label(el)); return ids.get(el); };
177
+ const blocks = [];
178
+ // Per block: the element it stands for and its first line's box, for rows.
179
+ const owners = [];
180
+ const lines = [];
181
+ // Per block: its raw line boxes and left, so a row reads the line two items share, not a wrapped item's widest line.
182
+ const rects = [];
183
+ const rawLeft = [];
184
+ const words = [];
185
+ const within = (el, self) => { const s = surfaceOf(el, self); return s ? { within: idOf(s) } : {}; };
186
+ for (const s of surfaces) {
187
+ const r = s.getBoundingClientRect();
188
+ owners.push(s); lines.push({ top: r.top, bottom: r.bottom }); rects.push([r]); rawLeft.push(r.left); words.push('');
189
+ // A placeholder: a box with no text, no control and no image or icon inside it (a loading bar). Not a block.
190
+ const holds = Array.prototype.some.call(s.querySelectorAll('*'), (el) => shown(el) && (el.matches(CONTROL) || el.tagName === 'IMG' || el.tagName.toLowerCase() === 'svg')) || ALNUM.test(s.innerText || '');
191
+ blocks.push(Object.assign({ id: idOf(s), kind: 'surface', edge: px(r.left), left: px(r.left), right: px(r.right), top: Math.round(r.top) }, within(s, false), isTable(s) ? { role: 'table' } : holds ? {} : { role: 'placeholder' }));
192
+ }
193
+ const blockOf = (node) => { for (let p = node.parentElement; p && p !== root; p = p.parentElement) if (!inline(css(p))) return p; return root; };
194
+ const groups = new Map();
195
+ const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
196
+ for (let n = walker.nextNode(); n; n = walker.nextNode()) {
197
+ if (!/\S/.test(n.data) || !n.parentElement || n.parentElement.closest('style,script,noscript,template') || !shown(n.parentElement) || inControl(n.parentElement) || inTable(n.parentElement)) continue;
198
+ const b = blockOf(n);
199
+ if (!groups.has(b)) groups.set(b, []);
200
+ groups.get(b).push(n);
201
+ }
202
+ // Ink: text whose own colour is not transparent. The edge is the first visible character in ink (decided 2026-10-02):
203
+ // leading invisible text, private-use icon-font glyphs and formatting characters are not characters of the block,
204
+ // while a currency sign or a quotation mark is.
205
+ const inked = (n) => !clear(css(n.parentElement).color) && ALNUM.test(n.data);
206
+ const textRects = (nodes) => {
207
+ const rs = [];
208
+ for (const n of nodes) { if (!inked(n)) continue; const range = document.createRange(); range.selectNodeContents(n); for (const r of range.getClientRects()) if (r.width > 0 && r.height > 0) rs.push(r); }
209
+ return rs;
210
+ };
211
+ const glyphAt = (n, i) => { const range = document.createRange(); range.setStart(n, i); range.setEnd(n, i + 1); const r = range.getClientRects()[0]; return r || null; };
212
+ const firstGlyph = (nodes) => {
213
+ for (const n of nodes) { if (!inked(n)) continue; const i = n.data.search(/[^\s\p{Co}\p{Cf}]/u); if (i < 0) continue; const r = glyphAt(n, i); if (r) return r; }
214
+ return null;
215
+ };
216
+ // A marker paints: a loaded image, an SVG with a filled or stroked shape, or an element with its own fill, border
217
+ // or visible text. An element that paints nothing is not a marker.
218
+ const painted = (m) => {
219
+ if (!shown(m)) return false;
220
+ if (m.tagName === 'IMG') return m.complete && m.naturalWidth > 0;
221
+ if (m.tagName.toLowerCase() === 'svg' || m.closest('svg')) {
222
+ const shapes = m.querySelectorAll('path,circle,rect,line,polyline,polygon,ellipse,use,text');
223
+ return Array.prototype.some.call(shapes, (sh) => { const st = css(sh); return (st.fill !== 'none' && !clear(st.fill)) || (st.stroke !== 'none' && !clear(st.stroke)); });
224
+ }
225
+ const s = css(m);
226
+ if (!clear(s.backgroundColor) || sides(s) >= 1 || ((m.textContent || '').trim().length > 0 && !clear(s.color))) return true;
227
+ // A wrapper that paints nothing itself is a marker when an image or icon inside it paints.
228
+ return Array.prototype.some.call(m.querySelectorAll('svg,img'), (k) => painted(k));
229
+ };
230
+ const markerBefore = (b, textLeft, lineTop, lineBottom) => {
231
+ const cands = [];
232
+ const consider = (m) => { if (!painted(m)) return; const r = m.getBoundingClientRect(); if (r.left < textLeft - 2 && r.bottom > lineTop && r.top < lineBottom) cands.push(r.left); };
233
+ for (const m of b.querySelectorAll('svg,img,[aria-hidden="true"],i')) if ((m.textContent || '').trim().length <= 2) consider(m);
234
+ const p = b.parentElement;
235
+ if (p && p !== root && /flex/.test(css(p).display) && !/column/.test(css(p).flexDirection)) for (const sib of p.children) { if (sib === b) break; if (sib.matches('svg,img,i,[aria-hidden="true"]') || !(sib.textContent || '').trim()) consider(sib); }
236
+ return cands.length ? Math.min.apply(null, cands) : null;
237
+ };
238
+ let texts = 0;
239
+ let ornaments = 0;
240
+ for (const [b, nodes] of groups) {
241
+ if (!nodes.some((n) => ALNUM.test(n.data))) { ornaments += 1; continue; }
242
+ const rs = textRects(nodes);
243
+ const g = firstGlyph(nodes);
244
+ if (!rs.length || g === null) continue;
245
+ texts += 1;
246
+ const first = g.left;
247
+ const left = Math.min.apply(null, rs.map((r) => r.left));
248
+ const right = Math.max.apply(null, rs.map((r) => r.right));
249
+ const top = Math.min.apply(null, rs.map((r) => r.top));
250
+ const marker = markerBefore(b, first, g.top, g.bottom);
251
+ const s = css(b);
252
+ const box = b.getBoundingClientRect();
253
+ const parent = b.parentElement ? css(b.parentElement) : null;
254
+ const centredText = s.textAlign === 'center' && right - left < box.width * 0.9;
255
+ const centredBox = parent && /flex/.test(parent.display) && /column/.test(parent.flexDirection) && parent.alignItems === 'center';
256
+ const list = b.matches('li') || s.display === 'list-item' ? b.closest('ul,ol') : null;
257
+ const inProse = list && list.previousElementSibling && list.previousElementSibling.matches('p');
258
+ const block = { id: idOf(b), kind: 'text', edge: px(marker !== null ? marker : first), left: px(Math.min(left, marker !== null ? marker : left)), right: px(right), top: Math.round(top) };
259
+ if (marker !== null) block.textEdge = px(first);
260
+ if (centredText || centredBox) block.role = 'centred';
261
+ else if (inProse) block.role = 'list-item';
262
+ owners.push(b); lines.push({ top: g.top, bottom: g.bottom }); rects.push(rs); rawLeft.push(Math.min(left, marker !== null ? marker : left)); words.push(nodes.map((n) => n.data).join('').trim());
263
+ blocks.push(Object.assign(block, within(b, true)));
264
+ }
265
+ const ctlGroups = new Map();
266
+ for (const el of all) { if (!isControl(el) || inTable(el) || (el.parentElement && inControl(el.parentElement))) continue; const p = el.parentElement || root; if (!ctlGroups.has(p)) ctlGroups.set(p, []); ctlGroups.get(p).push(el); }
267
+ let controls = 0;
268
+ for (const [p, els] of ctlGroups) {
269
+ const rs = els.map((e) => e.getBoundingClientRect());
270
+ const topRow = Math.min.apply(null, rs.map((r) => r.top));
271
+ let firstEl = null; let firstR = null;
272
+ els.forEach((e, i) => { const r = rs[i]; if (r.top - topRow < r.height / 2 && (!firstR || r.left < firstR.left)) { firstEl = e; firstR = r; } });
273
+ let edge = firstR.left;
274
+ if (!bounded(firstEl)) { const tw = document.createTreeWalker(firstEl, NodeFilter.SHOW_TEXT); const ns = []; for (let n = tw.nextNode(); n; n = tw.nextNode()) ns.push(n); const g = firstGlyph(ns); if (g) edge = g.left; }
275
+ controls += 1;
276
+ const ps = css(p);
277
+ const centred = ps.justifyContent === 'center' || (ps.textAlign === 'center' && !/flex|grid/.test(ps.display));
278
+ const block = { id: idOf(p) + ' [controls]', kind: 'controls', edge: px(edge), left: px(Math.min.apply(null, rs.map((r) => r.left))), right: px(Math.max.apply(null, rs.map((r) => r.right))), top: Math.round(topRow) };
279
+ if (centred) block.role = 'centred';
280
+ owners.push(firstEl); lines.push({ top: firstR.top, bottom: firstR.bottom }); rects.push(rs); rawLeft.push(Math.min.apply(null, rs.map((r) => r.left))); words.push('');
281
+ blocks.push(Object.assign(block, within(firstEl, true)));
282
+ }
283
+ // Rows (decided 2026-10-01, read by geometry 2026-10-02): items set on one line, however the CSS sets them. An item
284
+ // after another measured item on its line that holds one block (a text, a control, or a box holding at most one
285
+ // text) belongs to the row and is not measured; an item with a stack inside it is a column.
286
+ const dropped = new Set();
287
+ const sameLine = (a, b) => Math.min(a.bottom, b.bottom) - Math.max(a.top, b.top) > Math.min(a.bottom - a.top, b.bottom - b.top) / 2;
288
+ const contains = (o, el) => o === el || o.contains(el);
289
+ const innerTexts = (i) => owners.map((el, k) => k).filter((k) => k !== i && blocks[k].kind !== 'surface' && contains(owners[i], owners[k]));
290
+ const isPill = (i) => blocks[i].kind === 'surface' && innerTexts(i).length <= 1 && !owners.some((el, k) => k !== i && blocks[k].kind === 'surface' && contains(owners[i], el));
291
+ const itemOf = (el, other) => { let e = el; while (e.parentElement && e.parentElement !== root && !e.parentElement.contains(other)) e = e.parentElement; return e; };
292
+ for (let i = 0; i < blocks.length; i += 1) {
293
+ if (blocks[i].kind === 'surface' && !isPill(i)) continue;
294
+ const before = blocks.findIndex((bj, j) => j !== i && bj.within === blocks[i].within && !(bj.kind === 'surface' && !isPill(j)) && !contains(owners[j], owners[i]) && !contains(owners[i], owners[j]) && rects[j].some((r) => sameLine(r, lines[i]) && r.right <= rawLeft[i] + 1));
295
+ if (before < 0) continue;
296
+ // A number or bullet in front of a text on its line is a hanging marker, not a row: the pair is one block with two
297
+ // edges, the marker's and the text's.
298
+ if (blocks[i].kind === 'text' && blocks[before].kind === 'text' && /^[\p{N}\p{P}\p{S}]{1,3}$/u.test(words[before])) {
299
+ blocks[i] = Object.assign({}, blocks[i], { edge: blocks[before].edge, left: Math.min(blocks[i].left, blocks[before].left), textEdge: blocks[i].edge });
300
+ delete blocks[i].role;
301
+ dropped.add(before);
302
+ continue;
303
+ }
304
+ const item = itemOf(owners[i], owners[before]);
305
+ const inside = owners.map((el, k) => k).filter((k) => contains(item, owners[k]) && blocks[k].kind !== 'surface');
306
+ const boxes = owners.map((el, k) => k).filter((k) => contains(item, owners[k]) && blocks[k].kind === 'surface' && k !== i);
307
+ if (blocks[i].kind === 'surface') { if (boxes.length === 0) { dropped.add(i); for (const k of innerTexts(i)) dropped.add(k); } continue; }
308
+ if (inside.length === 1 && boxes.length === 0) dropped.add(i);
309
+ }
310
+ const kept = blocks.filter((_, i) => !dropped.has(i));
311
+ // Placeholders: a frame with no text in ink shows nothing to evaluate, whatever controls or boxes it draws.
312
+ return { blocks: kept, placeholdersOnly: texts === 0, skipped: { rowItems: dropped.size, ornaments } };
313
+ })()`;
314
+ var KINDS = ["text", "controls", "surface"];
315
+ var ROLES = ["list-item", "table", "centred", "placeholder"];
316
+ var isKind = (v) => KINDS.some((k) => k === v);
317
+ var isRole = (v) => ROLES.some((r) => r === v);
318
+ var num = (v) => typeof v === "number" && Number.isFinite(v);
319
+ var field = (o, key) => Reflect.get(o, key);
320
+ function parseEdgeProbe(value) {
321
+ if (typeof value !== "object" || value === null) return null;
322
+ const raw = field(value, "blocks");
323
+ const placeholdersOnly = field(value, "placeholdersOnly");
324
+ if (!Array.isArray(raw) || typeof placeholdersOnly !== "boolean") return null;
325
+ const skippedRaw = field(value, "skipped");
326
+ if (typeof skippedRaw !== "object" || skippedRaw === null) return null;
327
+ const rowItems = field(skippedRaw, "rowItems");
328
+ const ornaments = field(skippedRaw, "ornaments");
329
+ if (!num(rowItems) || !num(ornaments)) return null;
330
+ const blocks = [];
331
+ for (const entry of raw) {
332
+ if (typeof entry !== "object" || entry === null) return null;
333
+ const [id, kind, edge, left, right, top] = ["id", "kind", "edge", "left", "right", "top"].map((k) => field(entry, k));
334
+ const [within, textEdge, role] = ["within", "textEdge", "role"].map((k) => field(entry, k));
335
+ if (typeof id !== "string" || !isKind(kind) || !num(edge) || !num(left) || !num(right) || !num(top)) return null;
336
+ if (within !== void 0 && typeof within !== "string") return null;
337
+ if (textEdge !== void 0 && !num(textEdge)) return null;
338
+ if (role !== void 0 && !isRole(role)) return null;
339
+ blocks.push({
340
+ id,
341
+ kind,
342
+ edge,
343
+ left,
344
+ right,
345
+ top,
346
+ ...within !== void 0 ? { within } : {},
347
+ ...textEdge !== void 0 ? { textEdge } : {},
348
+ ...role !== void 0 ? { role } : {}
349
+ });
350
+ }
351
+ return { blocks, placeholdersOnly, skipped: { rowItems, ornaments } };
352
+ }
353
+
354
+ // src/evaluation/criteria/edge-probe-v2.ts
355
+ var EDGE_BLOCKS_V2_EXPRESSION = String.raw`(() => {
356
+ const root = document.getElementById('root');
357
+ if (!root) return null;
358
+ const rootBox = root.getBoundingClientRect();
359
+ const x0 = rootBox.left;
360
+ const px = (v) => Math.round((v - x0) * 2) / 2;
361
+ const CONTROL = 'button,input,select,textarea,[role="button"],[role="tab"],[role="switch"],[role="checkbox"],[role="radio"],[role="option"],[role="link"],a[href]';
362
+ const ALNUM = /[\p{L}\p{N}]/u;
363
+ const css = (el) => getComputedStyle(el);
364
+ const clear = (c) => !c || c === 'transparent' || /rgba\([^)]*,\s*0(\.0+)?\)$/.test(c);
365
+ // Visible through every ancestor (an ancestor at opacity 0 or visibility hidden hides it), and not degenerate.
366
+ const shown = (el) => {
367
+ const s = css(el);
368
+ if (s.display === 'none' || s.visibility === 'hidden' || Number(s.opacity) === 0) return false;
369
+ if (typeof el.checkVisibility === 'function' && !el.checkVisibility({ opacityProperty: true, visibilityProperty: true })) return false;
370
+ const r = el.getBoundingClientRect();
371
+ return r.width > 1 && r.height > 1;
372
+ };
373
+ const inline = (s) => /^inline/.test(s.display) && s.display !== 'inline-block' && s.display !== 'inline-flex' && s.display !== 'inline-grid';
374
+ const groundOf = (el) => {
375
+ for (let p = el.parentElement; p; p = p.parentElement) { const b = css(p).backgroundColor; if (!clear(b)) return b; }
376
+ return 'rgb(255, 255, 255)';
377
+ };
378
+ const sides = (s) => ['Top', 'Right', 'Bottom', 'Left'].filter((k) => parseFloat(s['border' + k + 'Width']) > 0 && s['border' + k + 'Style'] !== 'none' && !clear(s['border' + k + 'Color'])).length;
379
+ const bounded = (el) => { const s = css(el); return sides(s) >= 1 || (!clear(s.backgroundColor) && s.backgroundColor !== groundOf(el)); };
380
+ // A control: a native or ARIA one, or a bounded box the visitor clicks (a pointer cursor or a tab stop) that holds no
381
+ // control of its own and is not the whole card. Its contents are not blocks of the column (decided 2026-10-02).
382
+ const isControl = (el) => {
383
+ const s = css(el);
384
+ if (inline(s)) return false;
385
+ if (el.matches(CONTROL)) return true;
386
+ if (el === root) return false;
387
+ const clickable = s.cursor === 'pointer' || (el.hasAttribute('tabindex') && el.tabIndex >= 0);
388
+ if (!clickable || !bounded(el) || el.querySelector(CONTROL)) return false;
389
+ const r = el.getBoundingClientRect();
390
+ return r.width * r.height < 0.6 * rootBox.width * rootBox.height;
391
+ };
392
+ const isTable = (el) => el.matches('table,[role="table"],[role="grid"]');
393
+ const isSurface = (el) => {
394
+ if (el === root || isControl(el)) return false;
395
+ const s = css(el);
396
+ if (/^inline/.test(s.display) || s.display === 'contents') return false;
397
+ if (isTable(el)) return true;
398
+ // v2 (the design lead, #1765): what paints owns its edge — any border side (a single rule counts), a fill, a shadow.
399
+ return sides(s) >= 1 || (!clear(s.backgroundColor) && s.backgroundColor !== groundOf(el)) || (s.boxShadow && s.boxShadow !== 'none');
400
+ };
401
+ const path = (el) => {
402
+ const parts = [];
403
+ // v2: the whole path from the root, so nested boxes with the same tail and text never share an id (v1 kept four
404
+ // steps, and three nested surfaces of one card read as one container).
405
+ for (let e = el; e && e !== root; e = e.parentElement) {
406
+ const i = e.parentElement ? Array.prototype.indexOf.call(e.parentElement.children, e) + 1 : 1;
407
+ parts.unshift(e.tagName.toLowerCase() + ':' + i);
408
+ }
409
+ return parts.join('>');
410
+ };
411
+ const label = (el) => { const t = (el.innerText || '').replace(/\s+/g, ' ').trim(); return path(el) + (t ? ' "' + t.slice(0, 24) + '"' : ''); };
412
+ const surfaces = [];
413
+ const surfaceOf = (el, self) => { for (let p = self ? el : el.parentElement; p && p !== root; p = p.parentElement) if (surfaces.indexOf(p) >= 0) return p; return null; };
414
+ const inTable = (el) => { for (let p = el.parentElement; p && p !== root; p = p.parentElement) if (isTable(p)) return true; return false; };
415
+ const inControl = (el) => { for (let p = el; p && p !== root; p = p.parentElement) if (isControl(p)) return p; return null; };
416
+ const all = Array.prototype.slice.call(root.querySelectorAll('*')).filter(shown);
417
+ for (const el of all) if (isSurface(el) && !inTable(el) && !inControl(el)) surfaces.push(el);
418
+ const ids = new Map();
419
+ const idOf = (el) => { if (!ids.has(el)) ids.set(el, label(el)); return ids.get(el); };
420
+ const blocks = [];
421
+ // Per block: the element it stands for and its first line's box, for rows.
422
+ const owners = [];
423
+ const lines = [];
424
+ // Per block: its raw line boxes and left, so a row reads the line two items share, not a wrapped item's widest line.
425
+ const rects = [];
426
+ const rawLeft = [];
427
+ const words = [];
428
+ const within = (el, self) => { const s = surfaceOf(el, self); return s ? { within: idOf(s) } : {}; };
429
+ for (const s of surfaces) {
430
+ const r = s.getBoundingClientRect();
431
+ owners.push(s); lines.push({ top: r.top, bottom: r.bottom }); rects.push([r]); rawLeft.push(r.left); words.push('');
432
+ // A placeholder: a box with no text, no control and no image or icon inside it (a loading bar). Not a block.
433
+ const holds = Array.prototype.some.call(s.querySelectorAll('*'), (el) => shown(el) && (el.matches(CONTROL) || el.tagName === 'IMG' || el.tagName.toLowerCase() === 'svg')) || ALNUM.test(s.innerText || '');
434
+ const ss = css(s);
435
+ const cl = r.left + parseFloat(ss.borderLeftWidth) + parseFloat(ss.paddingLeft);
436
+ const cr = r.right - parseFloat(ss.borderRightWidth) - parseFloat(ss.paddingRight);
437
+ blocks.push(Object.assign({ id: idOf(s), kind: 'surface', edge: px(r.left), left: px(r.left), right: px(r.right), top: Math.round(r.top), bottom: Math.round(r.bottom), contentLeft: px(cl), contentRight: px(cr) }, within(s, false), isTable(s) ? { role: 'table' } : holds ? {} : { role: 'placeholder' }));
438
+ }
439
+ const blockOf = (node) => { for (let p = node.parentElement; p && p !== root; p = p.parentElement) if (!inline(css(p))) return p; return root; };
440
+ const groups = new Map();
441
+ const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
442
+ for (let n = walker.nextNode(); n; n = walker.nextNode()) {
443
+ if (!/\S/.test(n.data) || !n.parentElement || n.parentElement.closest('style,script,noscript,template') || !shown(n.parentElement) || inControl(n.parentElement) || inTable(n.parentElement)) continue;
444
+ const b = blockOf(n);
445
+ if (!groups.has(b)) groups.set(b, []);
446
+ groups.get(b).push(n);
447
+ }
448
+ // Ink: text whose own colour is not transparent. The edge is the first visible character in ink (decided 2026-10-02):
449
+ // leading invisible text, private-use icon-font glyphs and formatting characters are not characters of the block,
450
+ // while a currency sign or a quotation mark is.
451
+ const inked = (n) => !clear(css(n.parentElement).color) && ALNUM.test(n.data);
452
+ const textRects = (nodes) => {
453
+ const rs = [];
454
+ for (const n of nodes) { if (!inked(n)) continue; const range = document.createRange(); range.selectNodeContents(n); for (const r of range.getClientRects()) if (r.width > 0 && r.height > 0) rs.push(r); }
455
+ return rs;
456
+ };
457
+ const glyphAt = (n, i) => { const range = document.createRange(); range.setStart(n, i); range.setEnd(n, i + 1); const r = range.getClientRects()[0]; return r || null; };
458
+ const firstGlyph = (nodes) => {
459
+ for (const n of nodes) { if (!inked(n)) continue; const i = n.data.search(/[^\s\p{Co}\p{Cf}]/u); if (i < 0) continue; const r = glyphAt(n, i); if (r) return r; }
460
+ return null;
461
+ };
462
+ // A marker paints: a loaded image, an SVG with a filled or stroked shape, or an element with its own fill, border
463
+ // or visible text. An element that paints nothing is not a marker.
464
+ const painted = (m) => {
465
+ if (!shown(m)) return false;
466
+ if (m.tagName === 'IMG') return m.complete && m.naturalWidth > 0;
467
+ if (m.tagName.toLowerCase() === 'svg' || m.closest('svg')) {
468
+ const shapes = m.querySelectorAll('path,circle,rect,line,polyline,polygon,ellipse,use,text');
469
+ return Array.prototype.some.call(shapes, (sh) => { const st = css(sh); return (st.fill !== 'none' && !clear(st.fill)) || (st.stroke !== 'none' && !clear(st.stroke)); });
470
+ }
471
+ const s = css(m);
472
+ if (!clear(s.backgroundColor) || sides(s) >= 1 || ((m.textContent || '').trim().length > 0 && !clear(s.color))) return true;
473
+ // A wrapper that paints nothing itself is a marker when an image or icon inside it paints.
474
+ return Array.prototype.some.call(m.querySelectorAll('svg,img'), (k) => painted(k));
475
+ };
476
+ const markerEls = [];
477
+ const markerBefore = (b, textLeft, lineTop, lineBottom) => {
478
+ const cands = [];
479
+ const consider = (m) => { if (!painted(m)) return; const r = m.getBoundingClientRect(); if (r.left < textLeft - 2 && r.bottom > lineTop && r.top < lineBottom) { cands.push(r.left); markerEls.push(m); } };
480
+ for (const m of b.querySelectorAll('svg,img,[aria-hidden="true"],i')) if ((m.textContent || '').trim().length <= 2) consider(m);
481
+ const p = b.parentElement;
482
+ if (p && p !== root && /flex/.test(css(p).display) && !/column/.test(css(p).flexDirection)) for (const sib of p.children) { if (sib === b) break; if (sib.matches('svg,img,i,[aria-hidden="true"]') || !(sib.textContent || '').trim()) consider(sib); }
483
+ return cands.length ? Math.min.apply(null, cands) : null;
484
+ };
485
+ let texts = 0;
486
+ let ornaments = 0;
487
+ for (const [b, nodes] of groups) {
488
+ if (!nodes.some((n) => ALNUM.test(n.data))) { ornaments += 1; continue; }
489
+ const rs = textRects(nodes);
490
+ const g = firstGlyph(nodes);
491
+ if (!rs.length || g === null) continue;
492
+ texts += 1;
493
+ const first = g.left;
494
+ const left = Math.min.apply(null, rs.map((r) => r.left));
495
+ const right = Math.max.apply(null, rs.map((r) => r.right));
496
+ const top = Math.min.apply(null, rs.map((r) => r.top));
497
+ const marker = markerBefore(b, first, g.top, g.bottom);
498
+ const s = css(b);
499
+ const box = b.getBoundingClientRect();
500
+ const parent = b.parentElement ? css(b.parentElement) : null;
501
+ const centredText = s.textAlign === 'center' && right - left < box.width * 0.9;
502
+ const centredBox = parent && /flex/.test(parent.display) && /column/.test(parent.flexDirection) && parent.alignItems === 'center';
503
+ const list = b.matches('li') || s.display === 'list-item' ? b.closest('ul,ol') : null;
504
+ const inProse = list && list.previousElementSibling && list.previousElementSibling.matches('p');
505
+ const bottom = Math.max.apply(null, rs.map((r) => r.bottom));
506
+ const block = { id: idOf(b), kind: 'text', edge: px(marker !== null ? marker : first), left: px(Math.min(left, marker !== null ? marker : left)), right: px(right), top: Math.round(top), bottom: Math.round(bottom) };
507
+ if (marker !== null) block.textEdge = px(first);
508
+ if (centredText || centredBox) block.role = 'centred';
509
+ else if (inProse) block.role = 'list-item';
510
+ owners.push(b); lines.push({ top: g.top, bottom: g.bottom }); rects.push(rs); rawLeft.push(Math.min(left, marker !== null ? marker : left)); words.push(nodes.map((n) => n.data).join('').trim());
511
+ blocks.push(Object.assign(block, within(b, true)));
512
+ }
513
+ const ctlGroups = new Map();
514
+ for (const el of all) { if (!isControl(el) || inTable(el) || (el.parentElement && inControl(el.parentElement))) continue; const p = el.parentElement || root; if (!ctlGroups.has(p)) ctlGroups.set(p, []); ctlGroups.get(p).push(el); }
515
+ let controls = 0;
516
+ for (const [p, els] of ctlGroups) {
517
+ const rs = els.map((e) => e.getBoundingClientRect());
518
+ const topRow = Math.min.apply(null, rs.map((r) => r.top));
519
+ let firstEl = null; let firstR = null;
520
+ els.forEach((e, i) => { const r = rs[i]; if (r.top - topRow < r.height / 2 && (!firstR || r.left < firstR.left)) { firstEl = e; firstR = r; } });
521
+ let edge = firstR.left;
522
+ const tw = document.createTreeWalker(firstEl, NodeFilter.SHOW_TEXT); const ns = []; for (let n = tw.nextNode(); n; n = tw.nextNode()) ns.push(n);
523
+ const cg = firstGlyph(ns);
524
+ if (!bounded(firstEl) && cg) edge = cg.left;
525
+ // v2: a control's edge is its first painted content — its first character or a painted icon inside its box,
526
+ // whichever is leftmost; an icon drawn over a native field counts. A field with neither reads its padding edge.
527
+ const fs = css(firstEl);
528
+ const fieldEdge = firstEl.matches('input,select,textarea') ? firstR.left + parseFloat(fs.borderLeftWidth) + parseFloat(fs.paddingLeft) : null;
529
+ const iconLefts = all.filter((k) => k.matches('svg,img,i,[aria-hidden="true"]') && !(k.parentElement && k.parentElement.closest('svg')) && painted(k)).map((k) => k.getBoundingClientRect()).filter((r) => r.left >= firstR.left - 1 && r.right <= firstR.right + 1 && r.top >= firstR.top - 1 && r.bottom <= firstR.bottom + 1).map((r) => r.left);
530
+ const iconLeft = iconLefts.length ? Math.min.apply(null, iconLefts) : null;
531
+ controls += 1;
532
+ const ps = css(p);
533
+ const centred = ps.justifyContent === 'center' || (ps.textAlign === 'center' && !/flex|grid/.test(ps.display));
534
+ const block = { id: idOf(p) + ' [controls]', kind: 'controls', edge: px(edge), left: px(Math.min.apply(null, rs.map((r) => r.left))), right: px(Math.max.apply(null, rs.map((r) => r.right))), top: Math.round(topRow), bottom: Math.round(Math.max.apply(null, rs.map((r) => r.bottom))) };
535
+ const firstPaint = [cg ? cg.left : null, iconLeft].filter((v) => v !== null);
536
+ if (firstPaint.length) block.textEdge = px(Math.min.apply(null, firstPaint));
537
+ else if (fieldEdge !== null) block.textEdge = px(fieldEdge);
538
+ if (centred) block.role = 'centred';
539
+ owners.push(firstEl); lines.push({ top: firstR.top, bottom: firstR.bottom }); rects.push(rs); rawLeft.push(Math.min.apply(null, rs.map((r) => r.left))); words.push('');
540
+ blocks.push(Object.assign(block, within(firstEl, true)));
541
+ }
542
+ // v2: media — a painted image, video or icon that is not a text's marker and sits in no text, control or table.
543
+ const textOwners = Array.from(groups.keys());
544
+ for (const el of all) {
545
+ if (!el.matches('img,video,picture,svg') || (el.parentElement && el.parentElement.closest('svg')) || !painted(el) || inControl(el) || inTable(el)) continue;
546
+ if (markerEls.some((m) => m === el || m.contains(el))) continue;
547
+ if (textOwners.some((t) => t.contains(el))) continue;
548
+ const r = el.getBoundingClientRect();
549
+ // An icon drawn inside a control's box (a search field's glass) is the control's adornment, not a block.
550
+ if (all.some((k) => isControl(k) && (() => { const kr = k.getBoundingClientRect(); return r.left >= kr.left - 1 && r.right <= kr.right + 1 && r.top >= kr.top - 1 && r.bottom <= kr.bottom + 1; })())) continue;
551
+ owners.push(el); lines.push({ top: r.top, bottom: r.bottom }); rects.push([r]); rawLeft.push(r.left); words.push('');
552
+ blocks.push(Object.assign({ id: idOf(el) + ' [media]', kind: 'media', edge: px(r.left), left: px(r.left), right: px(r.right), top: Math.round(r.top), bottom: Math.round(r.bottom) }, within(el, false)));
553
+ }
554
+ // v2 rule 2, trailing: the right content edge of the box a block sits in (its nearest wider, non-inline ancestor
555
+ // inside its container), where a right-anchored item ends.
556
+ for (let i = 0; i < blocks.length; i += 1) {
557
+ const el = owners[i];
558
+ const w = el.getBoundingClientRect().width;
559
+ for (let p = el.parentElement; p && p !== root.parentElement; p = p.parentElement) {
560
+ const ps = css(p);
561
+ if (inline(ps) || ps.display === 'contents') continue;
562
+ const pr = p.getBoundingClientRect();
563
+ if (pr.width <= w + 2) continue;
564
+ blocks[i].parentRight = px(pr.right - parseFloat(ps.borderRightWidth) - parseFloat(ps.paddingRight));
565
+ break;
566
+ }
567
+ }
568
+ // v2 rule 2: a block in a row item that is not the row's leading item on its line follows that leader. A row is a
569
+ // horizontal flex or grid box; the leading item is the leftmost of the shown children that share the item's line.
570
+ const isRowBox = (el) => { const s = css(el); return (/flex/.test(s.display) && !/column/.test(s.flexDirection)) || /grid/.test(s.display); };
571
+ const vOverlap = (a, b) => Math.min(a.bottom, b.bottom) - Math.max(a.top, b.top) > 1;
572
+ for (let i = 0; i < blocks.length; i += 1) {
573
+ const el = owners[i];
574
+ const c = (blocks[i].kind === 'surface' ? surfaceOf(el, false) : surfaceOf(el, true)) || root;
575
+ for (let e = el; e && e !== c && e !== root; e = e.parentElement) {
576
+ const p = e.parentElement;
577
+ if (!p) break;
578
+ if (isRowBox(p)) {
579
+ const er = e.getBoundingClientRect();
580
+ const line = Array.prototype.filter.call(p.children, (k) => shown(k) && vOverlap(k.getBoundingClientRect(), er));
581
+ if (line.length >= 2) {
582
+ const lead = line.reduce((a, k) => (k.getBoundingClientRect().left < a.getBoundingClientRect().left ? k : a));
583
+ // A leader that paints nothing (an empty icon slot) leads nothing: the item after it stands alone on its line.
584
+ const leads = painted(lead) || ALNUM.test(lead.innerText || '');
585
+ if (lead !== e && leads) { blocks[i].follows = label(lead); blocks[i].item = label(e); break; }
586
+ }
587
+ }
588
+ if (p === c) break;
589
+ }
590
+ }
591
+ // Rows (decided 2026-10-01, read by geometry 2026-10-02): items set on one line, however the CSS sets them. An item
592
+ // after another measured item on its line that holds one block (a text, a control, or a box holding at most one
593
+ // text) belongs to the row and is not measured; an item with a stack inside it is a column.
594
+ const dropped = new Set();
595
+ const sameLine = (a, b) => Math.min(a.bottom, b.bottom) - Math.max(a.top, b.top) > Math.min(a.bottom - a.top, b.bottom - b.top) / 2;
596
+ const contains = (o, el) => o === el || o.contains(el);
597
+ const innerTexts = (i) => owners.map((el, k) => k).filter((k) => k !== i && blocks[k].kind !== 'surface' && contains(owners[i], owners[k]));
598
+ const isPill = (i) => blocks[i].kind === 'surface' && innerTexts(i).length <= 1 && !owners.some((el, k) => k !== i && blocks[k].kind === 'surface' && contains(owners[i], el));
599
+ const itemOf = (el, other) => { let e = el; while (e.parentElement && e.parentElement !== root && !e.parentElement.contains(other)) e = e.parentElement; return e; };
600
+ for (let i = 0; i < blocks.length; i += 1) {
601
+ if (blocks[i].kind === 'surface' && !isPill(i)) continue;
602
+ const before = blocks.findIndex((bj, j) => j !== i && bj.within === blocks[i].within && !(bj.kind === 'surface' && !isPill(j)) && !contains(owners[j], owners[i]) && !contains(owners[i], owners[j]) && rects[j].some((r) => sameLine(r, lines[i]) && r.right <= rawLeft[i] + 1));
603
+ if (before < 0) continue;
604
+ // A number or bullet in front of a text on its line is a hanging marker, not a row: the pair is one block with two
605
+ // edges, the marker's and the text's.
606
+ if (blocks[i].kind === 'text' && blocks[before].kind === 'text' && /^[\p{N}\p{P}\p{S}]{1,3}$/u.test(words[before])) {
607
+ blocks[i] = Object.assign({}, blocks[i], { edge: blocks[before].edge, left: Math.min(blocks[i].left, blocks[before].left), textEdge: blocks[i].edge });
608
+ delete blocks[i].role;
609
+ // v2: the merged block's edge is its marker's, which leads the row, so it stands in its container's column.
610
+ delete blocks[i].follows;
611
+ delete blocks[i].item;
612
+ dropped.add(before);
613
+ continue;
614
+ }
615
+ const item = itemOf(owners[i], owners[before]);
616
+ const inside = owners.map((el, k) => k).filter((k) => contains(item, owners[k]) && blocks[k].kind !== 'surface');
617
+ const boxes = owners.map((el, k) => k).filter((k) => contains(item, owners[k]) && blocks[k].kind === 'surface' && k !== i);
618
+ if (blocks[i].kind === 'surface') { if (boxes.length === 0) { dropped.add(i); for (const k of innerTexts(i)) dropped.add(k); } continue; }
619
+ if (inside.length === 1 && boxes.length === 0) dropped.add(i);
620
+ }
621
+ const kept = blocks.filter((_, i) => !dropped.has(i));
622
+ // Placeholders: a frame with no text in ink shows nothing to evaluate, whatever controls or boxes it draws.
623
+ const rs0 = css(root);
624
+ const cardContent = { left: px(rootBox.left + parseFloat(rs0.borderLeftWidth) + parseFloat(rs0.paddingLeft)), right: px(rootBox.right - parseFloat(rs0.borderRightWidth) - parseFloat(rs0.paddingRight)) };
625
+ return { blocks: kept, cardContent, placeholdersOnly: texts === 0, skipped: { rowItems: dropped.size, ornaments } };
626
+ })()`;
627
+ var KINDS2 = ["text", "controls", "surface", "media"];
628
+ var ROLES2 = ["list-item", "table", "centred", "placeholder"];
629
+ var isKind2 = (v) => KINDS2.some((k) => k === v);
630
+ var isRole2 = (v) => ROLES2.some((r) => r === v);
631
+ var num2 = (v) => typeof v === "number" && Number.isFinite(v);
632
+ var field2 = (o, key) => Reflect.get(o, key);
633
+ var optNum = (v) => v === void 0 || num2(v);
634
+ var optStr = (v) => v === void 0 || typeof v === "string";
635
+ function parseEdgeProbeV2(value) {
636
+ if (typeof value !== "object" || value === null) return null;
637
+ const raw = field2(value, "blocks");
638
+ const placeholdersOnly = field2(value, "placeholdersOnly");
639
+ const cc = field2(value, "cardContent");
640
+ if (!Array.isArray(raw) || typeof placeholdersOnly !== "boolean" || typeof cc !== "object" || cc === null) return null;
641
+ const ccLeft = field2(cc, "left");
642
+ const ccRight = field2(cc, "right");
643
+ if (!num2(ccLeft) || !num2(ccRight)) return null;
644
+ const skippedRaw = field2(value, "skipped");
645
+ if (typeof skippedRaw !== "object" || skippedRaw === null) return null;
646
+ const rowItems = field2(skippedRaw, "rowItems");
647
+ const ornaments = field2(skippedRaw, "ornaments");
648
+ if (!num2(rowItems) || !num2(ornaments)) return null;
649
+ const blocks = [];
650
+ for (const entry of raw) {
651
+ if (typeof entry !== "object" || entry === null) return null;
652
+ const [id, kind, edge, left, right, top, bottom] = ["id", "kind", "edge", "left", "right", "top", "bottom"].map((k) => field2(entry, k));
653
+ const [within, textEdge, role, contentLeft, contentRight, follows, item, parentRight] = ["within", "textEdge", "role", "contentLeft", "contentRight", "follows", "item", "parentRight"].map((k) => field2(entry, k));
654
+ if (typeof id !== "string" || !isKind2(kind) || !num2(edge) || !num2(left) || !num2(right) || !num2(top) || !num2(bottom)) return null;
655
+ if (!optStr(within) || !optNum(textEdge) || !optNum(contentLeft) || !optNum(contentRight) || !optStr(follows) || !optStr(item) || !optNum(parentRight)) return null;
656
+ if (role !== void 0 && !isRole2(role)) return null;
657
+ blocks.push({
658
+ id,
659
+ kind,
660
+ edge,
661
+ left,
662
+ right,
663
+ top,
664
+ bottom,
665
+ ...typeof within === "string" ? { within } : {},
666
+ ...num2(textEdge) ? { textEdge } : {},
667
+ ...role !== void 0 ? { role } : {},
668
+ ...num2(contentLeft) ? { contentLeft } : {},
669
+ ...num2(contentRight) ? { contentRight } : {},
670
+ ...typeof follows === "string" ? { follows } : {},
671
+ ...typeof item === "string" ? { item } : {},
672
+ ...num2(parentRight) ? { parentRight } : {}
673
+ });
674
+ }
675
+ return { blocks, cardContent: { left: ccLeft, right: ccRight }, placeholdersOnly, skipped: { rowItems, ornaments } };
676
+ }
677
+
678
+ // src/evaluation/criteria/edge-instruments.ts
679
+ var digest = (expression) => createHash("sha256").update(expression).digest("hex").slice(0, 12);
680
+ var EDGE_INSTRUMENT_IDS = {
681
+ v1: `space.edge@${digest(EDGE_BLOCKS_EXPRESSION)}`,
682
+ v2: `space.edge.v2@${digest(EDGE_BLOCKS_V2_EXPRESSION)}`
683
+ };
684
+
685
+ // src/evaluation/criteria/resolve.ts
686
+ var INSTRUMENT_BY_ID = {
687
+ "comp.fit": "fit",
688
+ "floor.fit": "fit",
689
+ "finish.canvas.inhabited": "fill"
690
+ };
691
+ var INSTRUMENT_BESIDE_JUDGE = {
692
+ "space.edge": "edge"
693
+ };
694
+ var REQUIRES_POST_ACTION_CAPTURE = /* @__PURE__ */ new Set(["state.feedback"]);
695
+ var NO_POST_ACTION = "not evaluated: no post-action capture";
696
+ function besideRead(kind, m) {
697
+ return edgeRead(m.edge);
698
+ }
699
+ function edgeRead(e) {
700
+ if (e === void 0) return { verdict: "n/a", evidence: "not measured: this capture had no page to read" };
701
+ if (e === null) return { verdict: "n/a", evidence: "edge probe failed on this capture" };
702
+ return { verdict: e.verdict, evidence: e.evidence };
703
+ }
704
+ function majority(id, answers, unansweredReason) {
705
+ const votes = answers.flatMap((a) => a.filter((x) => x.id === id));
706
+ if (votes.length === 0) return { verdict: "n/a", evidence: unansweredReason ?? "not answered" };
707
+ const counts = /* @__PURE__ */ new Map();
708
+ for (const v of votes) counts.set(v.verdict, (counts.get(v.verdict) ?? 0) + 1);
709
+ const ranked = [...counts.entries()].sort((a, b) => b[1] - a[1]);
710
+ const top = ranked[0];
711
+ if (ranked.length > 1 && ranked[1][1] === top[1]) return { verdict: "n/a", evidence: "no majority" };
712
+ const evidence = votes.find((v) => v.verdict === top[0])?.evidence ?? "";
713
+ return { verdict: top[0], evidence };
714
+ }
715
+ function instrumentRead(kind, m) {
716
+ if (kind === "fit") {
717
+ if (m.contentHeight === null) return { verdict: "n/a", evidence: "content height unmeasurable" };
718
+ return { verdict: m.overflow ? "fail" : "pass", evidence: `content ${m.contentHeight}px against a ${m.viewportHeight}px box` };
719
+ }
720
+ if (m.inkRatio === null) return { verdict: "n/a", evidence: "capture unreadable" };
721
+ return { verdict: "n/a", evidence: `fill ${m.inkRatio.toFixed(3)} reported; no floor ruled` };
722
+ }
723
+ function readRow(row, answers, m, unansweredReason) {
724
+ if (row.evaluation === "judge" && REQUIRES_POST_ACTION_CAPTURE.has(row.id) && m.postActionCapture !== true) return { verdict: "n/a", evidence: NO_POST_ACTION };
725
+ if (row.evaluation === "judge") return majority(row.id, answers, unansweredReason);
726
+ if (row.evaluation === "human") return { verdict: "n/a", evidence: `reader's column (status ${row.status})` };
727
+ if (row.status !== "live") return { verdict: "n/a", evidence: `not implemented (status ${row.status})` };
728
+ const kind = INSTRUMENT_BY_ID[row.id];
729
+ return kind === void 0 ? { verdict: "n/a", evidence: "instrument not wired (status live, no binding)" } : instrumentRead(kind, m);
730
+ }
731
+ function resolveCriteriaBlock(args) {
732
+ const byId = new Map(bankRows(args.bank).map((r) => [r.id, r]));
733
+ const verdicts = [];
734
+ for (const s of args.selected.selection) {
735
+ const row = byId.get(s.id);
736
+ if (row === void 0) continue;
737
+ const read = readRow(row, args.answers, args.measurements, args.unansweredReason);
738
+ const beside = INSTRUMENT_BESIDE_JUDGE[row.id];
739
+ verdicts.push({
740
+ id: row.id,
741
+ severity: row.severity,
742
+ method: row.evaluation,
743
+ status: row.status,
744
+ source: s.source,
745
+ verdict: read.verdict,
746
+ evidence: read.evidence,
747
+ ...beside !== void 0 && row.evaluation === "judge" ? {
748
+ instrument: { ...besideRead(beside, args.measurements), id: EDGE_INSTRUMENT_IDS.v1 },
749
+ // #1765 — v2 beside v1 on the same capture, report-only, until its own fresh sealed draw.
750
+ instrumentV2: { ...edgeRead(args.measurements.edgeV2), id: EDGE_INSTRUMENT_IDS.v2 }
751
+ } : {}
752
+ });
753
+ }
754
+ return {
755
+ criteriaSetId: args.selected.criteriaSetId,
756
+ bankVersion: args.bank.version,
757
+ selectorVersion: CRITERIA_SELECTOR_VERSION,
758
+ context: args.context,
759
+ selection: args.selected.selection,
760
+ verdicts,
761
+ ...args.call !== void 0 ? {
762
+ call: {
763
+ answer: args.call.answer,
764
+ // Kept answers whose id names no row of the bank: the judge answered something nobody asked.
765
+ unknownIds: args.answers.flat().filter((a) => !byId.has(a.id)).length,
766
+ ...args.call.finishReason !== void 0 ? { finishReason: args.call.finishReason } : {}
767
+ }
768
+ } : {}
769
+ };
770
+ }
771
+ var CRITERIA_PROPS_MAX_CHARS = 2e3;
772
+ function criteriaFrameLine(frame) {
773
+ const box = `${frame.width}\xD7${frame.height}`;
774
+ if (frame.fit === "framed-card") {
775
+ return `This frame is ${box}${frame.hostLabel !== void 0 ? ` (${frame.hostLabel})` : ""} with the card at its natural height at the top, inside the card's own frame \u2014 its surface, a thin ring of its ink and rounded corners \u2014 on the host's ground: the card is sized to its content, so judge composition and space within the card's own extent, and read the ground round it as the host's, not the card's.`;
776
+ }
777
+ if (frame.fit === "card") {
778
+ const framedBy = frame.hostLabel !== void 0 ? `inside the host's own frame (${frame.hostLabel}), on the host's ground` : "inside a generic stand-in for the host's frame \u2014 the card's surface with a thin ring and rounded corners, on the page ground";
779
+ return `This frame is an inline chat card captured at its natural height (at most ${box}, the host's ceiling), ${framedBy}: the host sizes the card to its content and draws that frame, so judge composition and space within the card's own extent, and read the frame and the ground round it as the host's, not the card's.`;
780
+ }
781
+ return `This frame is ${box}, a fixed full-screen box the card fills${frame.hostLabel !== void 0 ? ` (${frame.hostLabel})` : ""}: judge composition and space against the whole box, edge to edge.`;
782
+ }
783
+ function criteriaPropsSection(propsJson) {
784
+ const text = propsJson.length > CRITERIA_PROPS_MAX_CHARS ? `${propsJson.slice(0, CRITERIA_PROPS_MAX_CHARS)}\u2026 (truncated)` : propsJson;
785
+ return `The one input besides the frame \u2014 the props this card was rendered with. Read them ONLY for rows whose evidence names props (for example, whether a rendered string came from them); every other row is read off the frame alone:
786
+ ${text}
787
+ `;
788
+ }
789
+ function buildCriteriaJudgeBlock(bank, selected, inputs = {}) {
790
+ const byId = new Map(bankRows(bank).map((r) => [r.id, r]));
791
+ const asked = selected.selection.map((s) => byId.get(s.id)).filter((r) => r !== void 0 && r.evaluation === "judge").filter((r) => inputs.postActionCapture === true || !REQUIRES_POST_ACTION_CAPTURE.has(r.id));
792
+ if (asked.length === 0) return "";
793
+ const lines = asked.map((r) => `- ${r.id} (${r.severity}): ${r.text} Evidence must name: ${r.evidence}`);
794
+ return "## Criteria \u2014 answer each one from the FRAME only\nRead these off the screenshot. Never infer them from the request, the styling profile or any direction text above.\n" + (inputs.frame !== void 0 ? `${criteriaFrameLine(inputs.frame)}
795
+ ` : "") + (inputs.propsJson !== void 0 ? criteriaPropsSection(inputs.propsJson) : "") + `${lines.join("\n")}
796
+ Add to your JSON a top-level "criteria" array with exactly these ids: [{ "id": "<id>", "verdict": "pass" | "fail" | "n/a", "evidence": "<one sentence naming the region or element you read it from>" }]. Every row here was selected as applicable to this card, so "n/a" never means "does not apply": use it ONLY when this frame cannot show the criterion \u2014 it is then recorded as not evaluated.`;
797
+ }
798
+ var INK_TOLERANCE = 8;
799
+ var PNG_SIGNATURE = [137, 80, 78, 71, 13, 10, 26, 10];
800
+ function decodePng(png) {
801
+ const buf = Buffer.from(png.buffer, png.byteOffset, png.byteLength);
802
+ if (buf.length < 8 + 25) return { reason: `not a PNG: ${buf.length} bytes` };
803
+ for (let i = 0; i < PNG_SIGNATURE.length; i++) {
804
+ if (buf[i] !== PNG_SIGNATURE[i]) return { reason: "not a PNG: signature mismatch" };
805
+ }
806
+ let width = 0;
807
+ let height = 0;
808
+ let channels = null;
809
+ const idat = [];
810
+ let offset = 8;
811
+ while (offset + 8 <= buf.length) {
812
+ const length = buf.readUInt32BE(offset);
813
+ const type = buf.toString("latin1", offset + 4, offset + 8);
814
+ const start = offset + 8;
815
+ if (start + length + 4 > buf.length) return { reason: `truncated PNG: ${type} chunk runs past the end` };
816
+ const data = buf.subarray(start, start + length);
817
+ if (type === "IHDR") {
818
+ if (length !== 13) return { reason: `malformed IHDR: ${length} bytes` };
819
+ width = data.readUInt32BE(0);
820
+ height = data.readUInt32BE(4);
821
+ const bitDepth = data[8];
822
+ const colorType = data[9];
823
+ const interlace = data[12];
824
+ if (bitDepth !== 8) return { reason: `unsupported PNG: bit depth ${bitDepth}` };
825
+ if (interlace !== 0) return { reason: "unsupported PNG: interlaced" };
826
+ if (colorType === 2) channels = 3;
827
+ else if (colorType === 6) channels = 4;
828
+ else return { reason: `unsupported PNG: colour type ${colorType}` };
829
+ } else if (type === "IDAT") {
830
+ idat.push(data);
831
+ } else if (type === "IEND") {
832
+ break;
833
+ }
834
+ offset = start + length + 4;
835
+ }
836
+ if (channels === null) return { reason: "malformed PNG: no IHDR" };
837
+ if (width === 0 || height === 0) return { reason: `empty PNG: ${width}\xD7${height}` };
838
+ if (idat.length === 0) return { reason: "malformed PNG: no IDAT" };
839
+ let raw;
840
+ try {
841
+ raw = inflateSync(Buffer.concat(idat));
842
+ } catch (error) {
843
+ return { reason: `corrupt IDAT stream: ${error instanceof Error ? error.message : String(error)}` };
844
+ }
845
+ const stride = width * channels;
846
+ if (raw.length !== (stride + 1) * height) {
847
+ return { reason: `malformed PNG: ${raw.length} raw bytes for ${width}\xD7${height}\xD7${channels}` };
848
+ }
849
+ const pixels = new Uint8Array(stride * height);
850
+ const bpp = channels;
851
+ for (let y = 0; y < height; y++) {
852
+ const filter = raw[y * (stride + 1)];
853
+ const inRow = y * (stride + 1) + 1;
854
+ const outRow = y * stride;
855
+ const prevRow = outRow - stride;
856
+ for (let i = 0; i < stride; i++) {
857
+ const x = raw[inRow + i];
858
+ const a = i >= bpp ? pixels[outRow + i - bpp] : 0;
859
+ const b = y > 0 ? pixels[prevRow + i] : 0;
860
+ const c = y > 0 && i >= bpp ? pixels[prevRow + i - bpp] : 0;
861
+ let value;
862
+ switch (filter) {
863
+ case 0:
864
+ value = x;
865
+ break;
866
+ case 1:
867
+ value = x + a;
868
+ break;
869
+ case 2:
870
+ value = x + b;
871
+ break;
872
+ case 3:
873
+ value = x + (a + b >> 1);
874
+ break;
875
+ case 4: {
876
+ const p = a + b - c;
877
+ const pa = Math.abs(p - a);
878
+ const pb = Math.abs(p - b);
879
+ const pc = Math.abs(p - c);
880
+ value = x + (pa <= pb && pa <= pc ? a : pb <= pc ? b : c);
881
+ break;
882
+ }
883
+ default:
884
+ return { reason: `malformed PNG: scanline filter ${filter} at row ${y}` };
885
+ }
886
+ pixels[outRow + i] = value & 255;
887
+ }
888
+ }
889
+ return { width, height, channels, pixels };
890
+ }
891
+ function readInkExtent(png, insetPx = 0) {
892
+ const image = decodePng(png);
893
+ if ("reason" in image) return image;
894
+ const region = { x: insetPx, y: insetPx, width: image.width - 2 * insetPx, height: image.height - 2 * insetPx };
895
+ if (insetPx < 0 || region.width <= 0 || region.height <= 0) {
896
+ return { reason: `an inset of ${insetPx}px leaves nothing of a ${image.width}\xD7${image.height} image` };
897
+ }
898
+ const { channels, pixels, width } = image;
899
+ const counts = /* @__PURE__ */ new Map();
900
+ let ground = 0;
901
+ let groundCount = 0;
902
+ for (let y = region.y; y < region.y + region.height; y++) {
903
+ for (let x = region.x; x < region.x + region.width; x++) {
904
+ const i = (y * width + x) * channels;
905
+ const key = pixels[i] << 16 | pixels[i + 1] << 8 | pixels[i + 2];
906
+ const n = (counts.get(key) ?? 0) + 1;
907
+ counts.set(key, n);
908
+ if (n > groundCount) {
909
+ groundCount = n;
910
+ ground = key;
911
+ }
912
+ }
913
+ }
914
+ const gr = ground >> 16 & 255;
915
+ const gg = ground >> 8 & 255;
916
+ const gb = ground & 255;
917
+ let lastInkRow = null;
918
+ for (let y = region.y; y < region.y + region.height; y++) {
919
+ for (let x = region.x; x < region.x + region.width; x++) {
920
+ const i = (y * width + x) * channels;
921
+ if (Math.abs(pixels[i] - gr) > INK_TOLERANCE || Math.abs(pixels[i + 1] - gg) > INK_TOLERANCE || Math.abs(pixels[i + 2] - gb) > INK_TOLERANCE) {
922
+ lastInkRow = y - region.y;
923
+ break;
924
+ }
925
+ }
926
+ }
927
+ const ratio = lastInkRow === null ? 0 : Math.round((lastInkRow + 1) / region.height * 1e3) / 1e3;
928
+ return { ratio, lastInkRow, region, ground: [gr, gg, gb] };
929
+ }
930
+ function isDesignRenderingInput(relativePath) {
931
+ const rel = relativePath.split("\\").join("/");
932
+ if (rel.startsWith("__tests__/") || rel.includes("/__tests__/")) return false;
933
+ if (/\.test\.[cm]?[jt]sx?$/.test(rel)) return false;
934
+ if (/\.stories\.[cm]?[jt]sx?$/.test(rel)) return false;
935
+ if (rel.endsWith(".md")) return false;
936
+ return true;
937
+ }
938
+ function designTreeSha256(root) {
939
+ const base = resolve(root);
940
+ const files = [];
941
+ const walk = (dir) => {
942
+ for (const name of readdirSync(dir).sort()) {
943
+ const full = resolve(dir, name);
944
+ if (statSync(full).isDirectory()) walk(full);
945
+ else if (isDesignRenderingInput(relative(base, full))) files.push(full);
946
+ }
947
+ };
948
+ walk(base);
949
+ const h = createHash("sha256");
950
+ for (const f of files) {
951
+ h.update(relative(base, f) + "\n");
952
+ h.update(readFileSync(f));
953
+ h.update("\n");
954
+ }
955
+ return h.digest("hex");
956
+ }
957
+ var identityCache = /* @__PURE__ */ new Map();
958
+ function judgeDesignIdentity(src) {
959
+ const key = resolve(src);
960
+ const cached = identityCache.get(key);
961
+ if (cached !== void 0) return cached;
962
+ let identity;
963
+ try {
964
+ identity = { src: key, srcSha256: designTreeSha256(key) };
965
+ } catch (e) {
966
+ const reason = e instanceof Error ? e.message : String(e);
967
+ console.warn(`[visual-eval] design tree unreadable at ${key} \u2014 the judgement carries no design identity (${reason})`);
968
+ identity = null;
969
+ }
970
+ identityCache.set(key, identity);
971
+ return identity;
972
+ }
973
+ function resetJudgeDesignIdentityCache() {
974
+ identityCache.clear();
975
+ }
976
+
977
+ // src/evaluation/criteria/edge.ts
978
+ var EDGE_TOLERANCE_PX = 2;
979
+ var CARD = "card";
980
+ var same = (a, b) => Math.abs(a - b) <= EDGE_TOLERANCE_PX;
981
+ var overlaps = (a, b) => a.left < b.right && b.left < a.right;
982
+ var axis = (b) => (b.left + b.right) / 2;
983
+ function contentEdge(surface, blocks) {
984
+ const texts = blocks.filter((b) => b.within === surface.id && b.kind !== "surface" && b.role !== "centred");
985
+ if (texts.length === 0) return void 0;
986
+ const first = texts.reduce((a, b) => b.top < a.top ? b : a);
987
+ return first.textEdge ?? first.edge;
988
+ }
989
+ function columns(direct) {
990
+ const all = direct.filter((b) => b.kind !== "surface");
991
+ const bridges = (b) => {
992
+ const under = all.filter((x) => x !== b && overlaps(x, b));
993
+ return under.some((x, i) => under.slice(i + 1).some((y) => !overlaps(x, y)));
994
+ };
995
+ const flow = all.filter((b) => !bridges(b));
996
+ const parentPool = all.filter((b) => bridges(b));
997
+ const parent = flow.map((_, i) => i);
998
+ const find = (i) => parent[i] === i ? i : parent[i] = find(parent[i]);
999
+ for (let i = 0; i < flow.length; i += 1) for (let j = i + 1; j < flow.length; j += 1) if (overlaps(flow[i], flow[j])) parent[find(i)] = find(j);
1000
+ const groups = /* @__PURE__ */ new Map();
1001
+ flow.forEach((b, i) => groups.set(find(i), [...groups.get(find(i)) ?? [], b]));
1002
+ const cols = [...groups.values()];
1003
+ for (const s of direct.filter((b) => b.kind === "surface")) {
1004
+ const hit = cols.filter((c) => c.some((b) => b.kind !== "surface" && overlaps(b, s)));
1005
+ if (hit.length === 1) hit[0].push(s);
1006
+ else parentPool.push(s);
1007
+ }
1008
+ const lp = parentPool.map((_, i) => i);
1009
+ const lfind = (i) => lp[i] === i ? i : lp[i] = lfind(lp[i]);
1010
+ for (let i = 0; i < parentPool.length; i += 1) for (let j = i + 1; j < parentPool.length; j += 1) if (overlaps(parentPool[i], parentPool[j])) lp[lfind(i)] = lfind(j);
1011
+ const lgroups = /* @__PURE__ */ new Map();
1012
+ parentPool.forEach((b, i) => lgroups.set(lfind(i), [...lgroups.get(lfind(i)) ?? [], b]));
1013
+ cols.push(...lgroups.values());
1014
+ return cols;
1015
+ }
1016
+ function checkColumn(container, col, blocks) {
1017
+ const surfaces = col.filter((b) => b.kind === "surface");
1018
+ const grounds = surfaces.flatMap((s) => {
1019
+ const c = s.role === "table" ? void 0 : contentEdge(s, blocks);
1020
+ return c === void 0 ? [] : [c];
1021
+ });
1022
+ const flow = col.filter((b) => b.kind !== "surface" && b.role !== "centred" && b.role !== "list-item");
1023
+ const listItems = col.filter((b) => b.role === "list-item");
1024
+ const candidates = [...flow.flatMap((b) => [b.edge, ...b.textEdge !== void 0 ? [b.textEdge] : []]), ...surfaces.flatMap((s) => [s.edge]), ...grounds];
1025
+ const blockEdges = (b) => [b.edge, ...b.textEdge !== void 0 ? [b.textEdge] : []];
1026
+ const endEdgesBesides = (b) => [...surfaces.map((s) => s.right), ...flow.filter((x) => x !== b && x.kind === "controls").map((x) => x.right)];
1027
+ let best = candidates.length === 0 ? [] : null;
1028
+ for (const E of candidates) {
1029
+ const allowed = [E, ...grounds];
1030
+ const off = [];
1031
+ for (const b of flow) {
1032
+ if (blockEdges(b).some((x) => allowed.some((a) => same(x, a)))) continue;
1033
+ if (b.kind === "controls" && endEdgesBesides(b).some((r) => same(b.right, r))) continue;
1034
+ const nearest = allowed.reduce((n, a) => Math.abs(a - b.edge) < Math.abs(n - b.edge) ? a : n);
1035
+ off.push({ container, block: b.id, at: b.edge, nearest, offPx: Math.abs(b.edge - nearest) });
1036
+ }
1037
+ for (const s of surfaces) {
1038
+ const c = s.role === "table" ? void 0 : contentEdge(s, blocks);
1039
+ if (same(s.edge, E) || c !== void 0 && same(c, E)) continue;
1040
+ const nearest = c !== void 0 && Math.abs(c - E) < Math.abs(s.edge - E) ? c : s.edge;
1041
+ off.push({ container, block: s.id, at: nearest, nearest: E, offPx: Math.abs(nearest - E) });
1042
+ }
1043
+ if (best === null || off.length < best.length || off.length === best.length && sum(off) < sum(best)) best = off;
1044
+ if (best.length === 0) break;
1045
+ }
1046
+ const offsets = best ?? [];
1047
+ if (listItems.length > 1) {
1048
+ const e0 = listItems[0].edge;
1049
+ for (const li of listItems.slice(1)) if (!same(li.edge, e0)) offsets.push({ container, block: li.id, at: li.edge, nearest: e0, offPx: Math.abs(li.edge - e0) });
1050
+ }
1051
+ const centred = col.filter((b) => b.role === "centred");
1052
+ const edgeAligned = col.filter((b) => b.role !== "centred");
1053
+ const groupTop = centred.length > 0 ? Math.min(...centred.map((c) => c.top)) : 0;
1054
+ const groupEndsColumn = centred.length > 1 && edgeAligned.every((b) => b.top < groupTop);
1055
+ if (centred.length > 0 && edgeAligned.some((b) => b.kind !== "surface") && !groupEndsColumn) {
1056
+ const E = flow[0]?.edge ?? edgeAligned[0].edge;
1057
+ for (const c of centred) offsets.push({ container, block: c.id, at: c.edge, nearest: E, offPx: Math.abs(c.edge - E), reason: "a centred block in an edge-aligned column" });
1058
+ } else if (centred.length > 1) {
1059
+ const a0 = axis(centred[0]);
1060
+ for (const c of centred.slice(1)) if (!same(axis(c), a0)) offsets.push({ container, block: c.id, at: axis(c), nearest: a0, offPx: Math.abs(axis(c) - a0) });
1061
+ }
1062
+ return offsets;
1063
+ }
1064
+ var sum = (o) => o.reduce((s, x) => s + x.offPx, 0);
1065
+ function judgeEdges(measured, opts = {}) {
1066
+ if (opts.placeholdersOnly === true) return { verdict: "n/a", offsets: [], evidence: "only loading placeholders are shown; not evaluated" };
1067
+ let blocks = measured;
1068
+ const kept = blocks.filter((b) => b.role !== "placeholder");
1069
+ const content = kept.filter((b) => b.kind !== "surface").length;
1070
+ if (content < 2) return { verdict: "n/a", offsets: [], evidence: kept.length === 0 ? "no blocks were measured" : "fewer than two blocks once placeholders are set aside; not evaluated" };
1071
+ blocks = kept;
1072
+ const offsets = [];
1073
+ const containers = [CARD, ...blocks.filter((b) => b.kind === "surface" && b.role !== "table").map((b) => b.id)];
1074
+ for (const container of containers) {
1075
+ const direct = blocks.filter((b) => (b.within ?? CARD) === container);
1076
+ for (const col of columns(direct)) offsets.push(...checkColumn(container, col, blocks));
1077
+ }
1078
+ if (offsets.length === 0) return { verdict: "pass", offsets, evidence: "every column shares its edge, or sits on a stacked surface\u2019s content edge, within 2 px" };
1079
+ const worst = offsets.reduce((a, b) => b.offPx > a.offPx ? b : a);
1080
+ const lines = offsets.map((o) => `${o.block} at x${round(o.at)} is ${round(o.offPx)} px from x${round(o.nearest)} (in ${o.container}${o.reason !== void 0 ? `; ${o.reason}` : ""})`);
1081
+ return { verdict: "fail", offsets, evidence: `${offsets.length} block(s) off the allowed edges, largest ${round(worst.offPx)} px: ${lines.join("; ")}` };
1082
+ }
1083
+ var round = (n) => (Math.round(n * 10) / 10).toString();
1084
+
1085
+ // src/evaluation/criteria/edge-v2.ts
1086
+ var CARD2 = "card";
1087
+ var same2 = (a, b) => Math.abs(a - b) <= EDGE_TOLERANCE_PX;
1088
+ var overlaps2 = (a, b) => a.left < b.right && b.left < a.right;
1089
+ var axis2 = (b) => (b.left + b.right) / 2;
1090
+ function contentEdge2(surface, blocks) {
1091
+ const texts = blocks.filter((b) => b.within === surface.id && b.kind !== "surface" && b.role !== "centred");
1092
+ if (texts.length === 0) return void 0;
1093
+ const first = texts.reduce((a, b) => b.top < a.top ? b : a);
1094
+ return first.textEdge ?? first.edge;
1095
+ }
1096
+ function columns2(direct) {
1097
+ const all = direct.filter((b) => b.kind !== "surface");
1098
+ const bridges = (b) => {
1099
+ const under = all.filter((x) => x !== b && overlaps2(x, b));
1100
+ return under.some((x, i) => under.slice(i + 1).some((y) => !overlaps2(x, y)));
1101
+ };
1102
+ const flow = all.filter((b) => !bridges(b));
1103
+ const parentPool = all.filter((b) => bridges(b));
1104
+ const parent = flow.map((_, i) => i);
1105
+ const find = (i) => parent[i] === i ? i : parent[i] = find(parent[i]);
1106
+ for (let i = 0; i < flow.length; i += 1) for (let j = i + 1; j < flow.length; j += 1) if (overlaps2(flow[i], flow[j])) parent[find(i)] = find(j);
1107
+ const groups = /* @__PURE__ */ new Map();
1108
+ flow.forEach((b, i) => groups.set(find(i), [...groups.get(find(i)) ?? [], b]));
1109
+ const cols = [...groups.values()];
1110
+ for (const s of direct.filter((b) => b.kind === "surface")) {
1111
+ const hit = cols.filter((c) => c.some((b) => b.kind !== "surface" && overlaps2(b, s)));
1112
+ if (hit.length === 1) hit[0].push(s);
1113
+ else parentPool.push(s);
1114
+ }
1115
+ const lp = parentPool.map((_, i) => i);
1116
+ const lfind = (i) => lp[i] === i ? i : lp[i] = lfind(lp[i]);
1117
+ for (let i = 0; i < parentPool.length; i += 1) for (let j = i + 1; j < parentPool.length; j += 1) if (overlaps2(parentPool[i], parentPool[j])) lp[lfind(i)] = lfind(j);
1118
+ const lgroups = /* @__PURE__ */ new Map();
1119
+ parentPool.forEach((b, i) => lgroups.set(lfind(i), [...lgroups.get(lfind(i)) ?? [], b]));
1120
+ cols.push(...lgroups.values());
1121
+ return cols;
1122
+ }
1123
+ function checkColumn2(container, col, blocks) {
1124
+ const surfaces = col.filter((b) => b.kind === "surface");
1125
+ const grounds = surfaces.flatMap((s) => {
1126
+ const c = s.role === "table" ? void 0 : contentEdge2(s, blocks);
1127
+ return c === void 0 ? [] : [c];
1128
+ });
1129
+ const flow = col.filter((b) => b.kind !== "surface" && b.role !== "centred" && b.role !== "list-item");
1130
+ const listItems = col.filter((b) => b.role === "list-item");
1131
+ const candidates = [...flow.flatMap((b) => [b.edge, ...b.textEdge !== void 0 ? [b.textEdge] : []]), ...surfaces.flatMap((s) => [s.edge]), ...grounds];
1132
+ const blockEdges = (b) => [b.edge, ...b.textEdge !== void 0 ? [b.textEdge] : []];
1133
+ const endEdgesBesides = (b) => [...surfaces.map((s) => s.right), ...flow.filter((x) => x !== b && x.kind === "controls").map((x) => x.right)];
1134
+ let best = candidates.length === 0 ? [] : null;
1135
+ for (const E of candidates) {
1136
+ const allowed = [E, ...grounds];
1137
+ const off = [];
1138
+ for (const b of flow) {
1139
+ if (blockEdges(b).some((x) => allowed.some((a) => same2(x, a)))) continue;
1140
+ if (b.kind === "controls" && endEdgesBesides(b).some((r) => same2(b.right, r))) continue;
1141
+ const nearest = allowed.reduce((n, a) => Math.abs(a - b.edge) < Math.abs(n - b.edge) ? a : n);
1142
+ off.push({ container, block: b.id, at: b.edge, nearest, offPx: Math.abs(b.edge - nearest) });
1143
+ }
1144
+ for (const s of surfaces) {
1145
+ const c = s.role === "table" ? void 0 : contentEdge2(s, blocks);
1146
+ if (same2(s.edge, E) || c !== void 0 && same2(c, E)) continue;
1147
+ const nearest = c !== void 0 && Math.abs(c - E) < Math.abs(s.edge - E) ? c : s.edge;
1148
+ off.push({ container, block: s.id, at: nearest, nearest: E, offPx: Math.abs(nearest - E) });
1149
+ }
1150
+ if (best === null || off.length < best.length || off.length === best.length && sum2(off) < sum2(best)) best = off;
1151
+ if (best.length === 0) break;
1152
+ }
1153
+ const offsets = best ?? [];
1154
+ if (listItems.length > 1) {
1155
+ const e0 = listItems[0].edge;
1156
+ for (const li of listItems.slice(1)) if (!same2(li.edge, e0)) offsets.push({ container, block: li.id, at: li.edge, nearest: e0, offPx: Math.abs(li.edge - e0) });
1157
+ }
1158
+ const centred = col.filter((b) => b.role === "centred");
1159
+ const edgeAligned = col.filter((b) => b.role !== "centred");
1160
+ const groupTop = centred.length > 0 ? Math.min(...centred.map((c) => c.top)) : 0;
1161
+ const groupEndsColumn = centred.length > 1 && edgeAligned.every((b) => b.top < groupTop);
1162
+ if (centred.length > 0 && edgeAligned.some((b) => b.kind !== "surface") && !groupEndsColumn) {
1163
+ const E = flow[0]?.edge ?? edgeAligned[0].edge;
1164
+ for (const c of centred) offsets.push({ container, block: c.id, at: c.edge, nearest: E, offPx: Math.abs(c.edge - E), reason: "a centred block in an edge-aligned column" });
1165
+ } else if (centred.length > 1) {
1166
+ const a0 = axis2(centred[0]);
1167
+ for (const c of centred.slice(1)) if (!same2(axis2(c), a0)) offsets.push({ container, block: c.id, at: axis2(c), nearest: a0, offPx: Math.abs(axis2(c) - a0) });
1168
+ }
1169
+ return offsets;
1170
+ }
1171
+ var sum2 = (o) => o.reduce((s, x) => s + x.offPx, 0);
1172
+ var follower = (b) => b.follows !== void 0;
1173
+ var vOverlap = (a, b) => Math.min(a.bottom, b.bottom) - Math.max(a.top, b.top) > 1;
1174
+ function judgeEdgesV2(measured, opts) {
1175
+ if (opts.placeholdersOnly === true) return { verdict: "n/a", offsets: [], evidence: "only loading placeholders are shown; not evaluated" };
1176
+ const content = measured.filter((b) => b.role !== "placeholder" && (b.kind === "text" || b.kind === "controls")).length;
1177
+ if (content < 2) return { verdict: "n/a", offsets: [], evidence: measured.length === 0 ? "no blocks were measured" : "fewer than two blocks once placeholders are set aside; not evaluated" };
1178
+ const byId = new Map(measured.map((b) => [b.id, b]));
1179
+ const containerOf = (b) => b.within ?? CARD2;
1180
+ const contentBoxOf = (container) => {
1181
+ if (container === CARD2) return opts.cardContent;
1182
+ const s = byId.get(container);
1183
+ return s?.contentLeft !== void 0 && s.contentRight !== void 0 ? { left: s.contentLeft, right: s.contentRight } : void 0;
1184
+ };
1185
+ const leadsARow = (p) => measured.some((b) => b !== p && containerOf(b) === containerOf(p) && follower(b) && b.left >= p.right - 1 && vOverlap(b, p));
1186
+ const holdsControl = (s) => {
1187
+ const inside = (b) => {
1188
+ for (let w = b.within; w !== void 0; w = byId.get(w)?.within) if (w === s.id) return true;
1189
+ return false;
1190
+ };
1191
+ return measured.some((b) => b.kind === "controls" && inside(b));
1192
+ };
1193
+ const aloneOnItsLine = (m) => !measured.some((b) => b !== m && containerOf(b) === containerOf(m) && b.role !== "placeholder" && vOverlap(b, m));
1194
+ const centredInContainer = (m) => {
1195
+ const box = contentBoxOf(containerOf(m));
1196
+ return box !== void 0 && same2((m.left + m.right) / 2, (box.left + box.right) / 2) && !same2(m.left, box.left);
1197
+ };
1198
+ const mediaKind = (b) => b.kind === "media" || b.kind === "surface" && b.role !== "table" && !holdsControl(b);
1199
+ const blocks = measured.filter((b) => {
1200
+ if (b.role === "placeholder") return leadsARow(b);
1201
+ if (b.kind === "media" && centredInContainer(b)) return false;
1202
+ return true;
1203
+ });
1204
+ const bleeds = (b) => b.kind === "surface" && containerOf(b) === CARD2 && b.left <= EDGE_TOLERANCE_PX && b.right >= opts.cardContent.right - EDGE_TOLERANCE_PX;
1205
+ const judged = blocks.map((b) => bleeds(b) && b.contentLeft !== void 0 ? { ...b, edge: b.contentLeft } : b);
1206
+ const rightAnchored = (b) => {
1207
+ const box = contentBoxOf(containerOf(b));
1208
+ const ends = (r) => r !== void 0 && same2(b.right, r);
1209
+ return (ends(box?.right) || ends(b.parentRight)) && box !== void 0 && b.left >= (box.left + box.right) / 2 && !bleeds(b);
1210
+ };
1211
+ const offsets = [];
1212
+ const containers = [CARD2, ...judged.filter((b) => b.kind === "surface" && b.role !== "table").map((b) => b.id)];
1213
+ for (const container of containers) {
1214
+ const inContainer = judged.filter((b) => containerOf(b) === container);
1215
+ const direct = inContainer.filter((b) => !follower(b) && !rightAnchored(b));
1216
+ for (const col of columns2(direct)) {
1217
+ const only = col.length === 1 ? col[0] : void 0;
1218
+ if (only !== void 0 && mediaKind(only) && aloneOnItsLine(only) && centredInContainer(only)) continue;
1219
+ offsets.push(...checkColumn2(container, col, judged));
1220
+ }
1221
+ const items = /* @__PURE__ */ new Map();
1222
+ for (const b of inContainer.filter(follower)) items.set(b.item ?? b.id, [...items.get(b.item ?? b.id) ?? [], b]);
1223
+ for (const [item, group] of items) for (const col of columns2(group)) offsets.push(...checkColumn2(`${container} \u203A ${item}`, col, judged));
1224
+ }
1225
+ if (offsets.length === 0) return { verdict: "pass", offsets, evidence: "every column shares its content edge, every row item after its leader shares its own, within 2 px" };
1226
+ const worst = offsets.reduce((a, b) => b.offPx > a.offPx ? b : a);
1227
+ const lines = offsets.map((o) => `${o.block} at x${round2(o.at)} is ${round2(o.offPx)} px from x${round2(o.nearest)} (in ${o.container}${o.reason !== void 0 ? `; ${o.reason}` : ""})`);
1228
+ return { verdict: "fail", offsets, evidence: `${offsets.length} block(s) off the allowed edges, largest ${round2(worst.offPx)} px: ${lines.join("; ")}` };
1229
+ }
1230
+ var round2 = (n) => (Math.round(n * 10) / 10).toString();
1231
+
1232
+ // src/evaluation/visual-evaluator.ts
1233
+ function canvasFitPolicy(canvas) {
1234
+ switch (canvas) {
1235
+ case "xs-chat-card":
1236
+ return { capture: "natural", overflow: "fail" };
1237
+ case "mobile-fullscreen-small":
1238
+ return { capture: "full-page", overflow: "warn" };
1239
+ default:
1240
+ return { capture: "full-page", overflow: "none" };
1241
+ }
1242
+ }
1243
+ var JUDGE_SCOPE_CLASS = "ggui-judge-scope";
1244
+ var JUDGE_PANEL_CLASS = "ggui-judge-panel";
1245
+ var JUDGE_INK_INSET_PX = EXPANDED_FRAME.insetPx + 24 + 1;
1246
+ function canvasFit(canvas) {
1247
+ return displayModeForCanvas(canvas) === "fullscreen" ? "fill" : void 0;
1248
+ }
1249
+ var JUDGE_GROUND_MARGIN_PX = EXPANDED_FRAME.insetPx;
1250
+ var JUDGE_GROUND_CLASS = "ggui-judge-ground";
1251
+ var JUDGE_INLINE_FRAME_CLASS = "ggui-judge-inline-frame";
1252
+ var JUDGE_INLINE_FRAME_BORDER_PX = 1;
1253
+ var JUDGE_INLINE_FRAME_RADIUS_PX = 16;
1254
+ var JUDGE_INLINE_PAD_PX = JUDGE_GROUND_MARGIN_PX + JUDGE_INLINE_FRAME_BORDER_PX;
1255
+ var JUDGE_INLINE_INK_INSET_PX = JUDGE_INLINE_PAD_PX + JUDGE_INLINE_FRAME_RADIUS_PX;
1256
+ var HOST_COLOUR_PATTERN = /^#[0-9a-f]{6}$/;
1257
+ var HOST_FRAME_BOUNDS = {
1258
+ ringWidthPx: { min: 0, max: 8 },
1259
+ ringAlpha: { min: 0, max: 1 },
1260
+ radiusPx: { min: 0, max: 64 },
1261
+ minHeightPx: { min: 0, max: 2560 }
1262
+ };
1263
+ function resolvePresentation(canvas, p) {
1264
+ if (p === void 0) return void 0;
1265
+ const ignored = (reason, why) => {
1266
+ console.warn(`[visual-eval] host presentation for ${canvas} ignored (${reason}): ${why}`);
1267
+ return { status: "ignored", reason };
1268
+ };
1269
+ const inline = canvasFitPolicy(canvas).capture === "natural";
1270
+ if (typeof p.label !== "string" || p.label.length === 0) return ignored("label_missing", "no label");
1271
+ if (p.ground !== void 0 && !HOST_COLOUR_PATTERN.test(p.ground)) return ignored("color_not_hex", "ground is not #rrggbb");
1272
+ if (inline && p.ground !== void 0) return ignored("ground_on_inline", "the inline card's room is its frame's ground");
1273
+ const f = p.frame;
1274
+ if (f !== void 0) {
1275
+ if (!inline) return ignored("frame_off_inline", "a frame on a canvas that is not the inline card");
1276
+ if (![f.surface, f.ground, f.ring.color].every((c) => HOST_COLOUR_PATTERN.test(c))) return ignored("color_not_hex", "a frame colour is not #rrggbb");
1277
+ const B = HOST_FRAME_BOUNDS;
1278
+ const inRange = (n, b) => Number.isFinite(n) && n >= b.min && n <= b.max;
1279
+ if (!inRange(f.ring.widthPx, B.ringWidthPx) || !inRange(f.ring.alpha, B.ringAlpha) || !inRange(f.radiusPx, B.radiusPx)) return ignored("frame_malformed", "a frame number out of range");
1280
+ if (f.minHeightPx !== void 0 && !inRange(f.minHeightPx, B.minHeightPx)) return ignored("frame_malformed", "minHeightPx out of range");
1281
+ }
1282
+ return { status: "applied", applied: p };
1283
+ }
1284
+ function inlineGeometry(frame) {
1285
+ if (frame === void 0) return { padPx: JUDGE_INLINE_PAD_PX, inkInsetPx: JUDGE_INLINE_INK_INSET_PX, floorPx: void 0 };
1286
+ const padPx = JUDGE_GROUND_MARGIN_PX + frame.ring.widthPx;
1287
+ return { padPx, inkInsetPx: padPx + frame.radiusPx, floorPx: frame.minHeightPx };
1288
+ }
1289
+ function canvasChrome(canvas) {
1290
+ if (canvasFitPolicy(canvas).capture === "natural") return "ground";
1291
+ return canvasFit(canvas) === "fill" && canvas !== "mobile-fullscreen-small" ? "panel" : void 0;
1292
+ }
1293
+ function judgeWindow(viewport, chrome, inlinePadPx = JUDGE_INLINE_PAD_PX) {
1294
+ if (chrome === void 0) return viewport;
1295
+ const gap = 2 * (chrome === "ground" ? inlinePadPx : EXPANDED_FRAME.insetPx);
1296
+ return { width: viewport.width + gap, height: viewport.height + gap };
1297
+ }
1298
+ function cardHeight(documentHeight, chrome) {
1299
+ if (documentHeight === null || chrome === void 0 || chrome === "ground") return documentHeight;
1300
+ const gap = 2 * EXPANDED_FRAME.insetPx;
1301
+ return documentHeight < gap ? null : documentHeight - gap;
1302
+ }
1303
+ function integerBox(viewport) {
1304
+ return { width: Math.round(viewport.width), height: Math.round(viewport.height) };
1305
+ }
1306
+ function isDeclaredViewport(canvas, viewport) {
1307
+ const classBox = CANVAS_VIEWPORTS[canvas];
1308
+ return viewport.width !== classBox.width || viewport.height !== classBox.height;
1309
+ }
1310
+ function canvasOverflowIssue(canvas, viewport, contentHeight, verdict) {
1311
+ const hidden = contentHeight - viewport.height;
1312
+ const classBox = CANVAS_VIEWPORTS[canvas];
1313
+ const box = isDeclaredViewport(canvas, viewport) ? `declared ${viewport.width}\xD7${viewport.height}; class box ${classBox.width}\xD7${classBox.height}` : `${viewport.width}\xD7${viewport.height}`;
1314
+ return {
1315
+ dimension: "canvas-overflow",
1316
+ origin: "instrument",
1317
+ severity: verdict === "fail" ? "critical" : "major",
1318
+ description: `Rendered content is ${contentHeight}px tall on the ${canvas} canvas (${box}) \u2014 ${hidden}px ${verdict === "fail" ? "is cut off: the inline card does not scroll" : "sits below the first screen"}.`,
1319
+ fix: "Fit the composition to the canvas: fewer and shorter sections, one compact row of chips, no hero taller than a third of the box, no fixed min-heights or tall paddings \u2014 measure against the canvas, not the page."
1320
+ };
1321
+ }
1322
+ function canvasOverflowXIssue(canvas, viewport, hiddenPx, verdict) {
1323
+ return {
1324
+ dimension: "canvas-overflow-x",
1325
+ origin: "instrument",
1326
+ severity: verdict === "fail" ? "critical" : "major",
1327
+ description: `Content runs ${hiddenPx}px past the ${canvas} card's ${viewport.width}px width \u2014 the card's frame clips it, so a visitor sees a cut-off row or a sideways scroll.`,
1328
+ fix: "Keep every row within the card: wrap long text (overflow-wrap: anywhere), let a wide table or code block scroll inside its own container, and give no child a fixed width wider than the card."
1329
+ };
1330
+ }
1331
+ function canvasBlankIssue(canvas, viewport) {
1332
+ const classBox = CANVAS_VIEWPORTS[canvas];
1333
+ const box = isDeclaredViewport(canvas, viewport) ? `declared ${viewport.width}\xD7${viewport.height}; class box ${classBox.width}\xD7${classBox.height}` : `${viewport.width}\xD7${viewport.height}`;
1334
+ return {
1335
+ dimension: "canvas-blank",
1336
+ origin: "instrument",
1337
+ severity: "critical",
1338
+ description: `Nothing painted on the ${canvas} canvas (${box}): the capture is one flat colour \u2014 the component mounted and rendered no pixels.`,
1339
+ fix: "Return and paint the component tree: a root that renders nothing is a blank, not an empty state. Check that the default export returns its JSX, that no early return yields null on the sample props, and that the first paint shows visible content."
1340
+ };
1341
+ }
1342
+ function resolveUiGenPackageDir() {
1343
+ return findPackageRootAbove(dirname(fileURLToPath(import.meta.url)), "@ggui-ai/ui-gen");
1344
+ }
1345
+ function findPackageRootAbove(start, name) {
1346
+ for (let dir = start; ; dir = dirname(dir)) {
1347
+ const manifest = resolve(dir, "package.json");
1348
+ if (existsSync(manifest) && readPackageName(manifest) === name) return dir;
1349
+ if (dirname(dir) === dir) throw new Error(`${name}: no package.json named ${name} at or above ${start}`);
1350
+ }
1351
+ }
1352
+ function readPackageName(manifest) {
1353
+ const parsed = JSON.parse(readFileSync(manifest, "utf8"));
1354
+ return typeof parsed === "object" && parsed !== null && "name" in parsed && typeof parsed.name === "string" ? parsed.name : void 0;
1355
+ }
1356
+ function resolveWirePackageDir() {
1357
+ const uiGenDir = resolveUiGenPackageDir();
1358
+ const candidates = [
1359
+ resolve(uiGenDir, "node_modules", "@ggui-ai", "wire"),
1360
+ // workspace / nested install
1361
+ resolve(uiGenDir, "..", "wire"),
1362
+ // pnpm deploy: `.pnpm/<ui-gen>@<ver>/node_modules/@ggui-ai/{ui-gen,wire}`
1363
+ resolve(uiGenDir, "..", "..", "@ggui-ai", "wire")
1364
+ // hoisted flat install beside the scope dir
1365
+ ];
1366
+ for (const dir of candidates) {
1367
+ if (existsSync(resolve(dir, "package.json"))) return dir;
1368
+ }
1369
+ try {
1370
+ const entry = fileURLToPath(import.meta.resolve("@ggui-ai/wire"));
1371
+ let dir = dirname(entry);
1372
+ for (let i = 0; i < 6; i++) {
1373
+ if (existsSync(resolve(dir, "package.json"))) return dir;
1374
+ dir = dirname(dir);
1375
+ }
1376
+ } catch {
1377
+ }
1378
+ return null;
1379
+ }
1380
+ function resolveDesignPackageDir() {
1381
+ try {
1382
+ return dirname(createRequire(import.meta.url).resolve("@ggui-ai/design/package.json"));
1383
+ } catch {
1384
+ const selfDir = dirname(fileURLToPath(import.meta.url));
1385
+ return resolve(selfDir, "..", "..", "..", "design");
1386
+ }
1387
+ }
1388
+ async function bundleForRendering(compiledCode, sampleProps, designSrc = resolve(resolveDesignPackageDir(), "src")) {
1389
+ const tmpDir = mkdtempSync(resolve(tmpdir(), "ggui-visual-eval-"));
1390
+ const componentFile = resolve(tmpDir, "component.tsx");
1391
+ const entryFile = resolve(tmpDir, "entry.tsx");
1392
+ const designPkgDir = resolve(designSrc, "..");
1393
+ const wirePkgDir = resolveWirePackageDir();
1394
+ writeFileSync(componentFile, compiledCode);
1395
+ const entryCode = `
1396
+ import React from 'react';
1397
+ import { createRoot } from 'react-dom/client';
1398
+ import { GguiWireProvider } from '@ggui-ai/wire';
1399
+ import Component from './component.tsx';
1400
+
1401
+ const props = ${JSON.stringify(sampleProps)};
1402
+ const wireConfig = {
1403
+ app: { appId: 'visual-eval', appName: 'Visual evaluation' },
1404
+ render: { sessionId: 'visual-eval', isConnected: true },
1405
+ auth: { isAuthenticated: false },
1406
+ dispatch: () => {},
1407
+ subscribe: () => () => {},
1408
+ };
1409
+ class RenderErrorBoundary extends React.Component<{ children: React.ReactNode }, { message: string | null }> {
1410
+ state = { message: null as string | null };
1411
+ static getDerivedStateFromError(err: unknown) { return { message: err instanceof Error ? err.message : String(err) }; }
1412
+ render() {
1413
+ return this.state.message === null
1414
+ ? this.props.children
1415
+ : React.createElement('div', { className: 'error' }, 'Render error: ' + this.state.message);
1416
+ }
1417
+ }
1418
+ const root = createRoot(document.getElementById('root')!);
1419
+ try {
1420
+ root.render(
1421
+ React.createElement(GguiWireProvider, { config: wireConfig },
1422
+ React.createElement(RenderErrorBoundary, null, React.createElement(Component, props))),
1423
+ );
1424
+ } catch (err) {
1425
+ root.render(React.createElement('div', { className: 'error' }, 'Render error: ' + (err as Error).message));
1426
+ }
1427
+ `;
1428
+ writeFileSync(entryFile, entryCode);
1429
+ try {
1430
+ const result = await build({
1431
+ entryPoints: [entryFile],
1432
+ bundle: true,
1433
+ write: false,
1434
+ format: "esm",
1435
+ platform: "browser",
1436
+ target: "es2020",
1437
+ jsx: "automatic",
1438
+ jsxImportSource: "react",
1439
+ external: ["react", "react-dom", "react-dom/client", "react/jsx-runtime"],
1440
+ alias: {
1441
+ // The wire hooks: resolved from ui-gen's own module graph so the
1442
+ // alias holds in a workspace checkout AND in a `pnpm deploy` image
1443
+ // (where deps are siblings, not children, of this package).
1444
+ ...wirePkgDir !== null ? { "@ggui-ai/wire": wirePkgDir } : {},
1445
+ // D1: generated code imports the single `@ggui-ai/design` barrel.
1446
+ // The subpath aliases stay for any first-party / legacy code.
1447
+ "@ggui-ai/design": resolve(designPkgDir, "src", "index.ts"),
1448
+ "@ggui-ai/design/primitives": resolve(designPkgDir, "src", "primitives", "index.ts"),
1449
+ "@ggui-ai/design/components": resolve(designPkgDir, "src", "components", "index.ts"),
1450
+ "@ggui-ai/design/compositions": resolve(designPkgDir, "src", "compositions", "index.ts"),
1451
+ "@ggui-ai/design/interact": resolve(designPkgDir, "src", "interact", "index.ts")
1452
+ },
1453
+ // Bare imports in the generated component resolve against ui-gen's
1454
+ // own node_modules — the temp entry dir has none. Without this every
1455
+ // wire-bearing component (`@ggui-ai/wire`) failed to bundle.
1456
+ // Workspace layout (deps under this package's node_modules) and the
1457
+ // `pnpm deploy` layout (deps as siblings of this package) — both
1458
+ // searched for any other allowlisted bare import.
1459
+ nodePaths: [resolve(resolveUiGenPackageDir(), "node_modules"), resolve(resolveUiGenPackageDir(), "..")],
1460
+ logLevel: "silent"
1461
+ });
1462
+ return result.outputFiles[0]?.text ?? "";
1463
+ } finally {
1464
+ rmSync(tmpDir, { recursive: true, force: true });
1465
+ }
1466
+ }
1467
+ function inlineFrameDecls(frame) {
1468
+ if (frame === void 0) {
1469
+ return `background: var(--ggui-color-container); border: ${JUDGE_INLINE_FRAME_BORDER_PX}px solid color-mix(in srgb, var(--ggui-color-onContainer) 8%, transparent); border-radius: ${JUDGE_INLINE_FRAME_RADIUS_PX}px; overflow: hidden;`;
1470
+ }
1471
+ const alphaPct = Math.round(frame.ring.alpha * 1e3) / 10;
1472
+ return `background: ${frame.surface}; border: ${frame.ring.widthPx}px solid color-mix(in srgb, ${frame.ring.color} ${alphaPct}%, transparent); border-radius: ${frame.radiusPx}px; overflow: hidden;`;
1473
+ }
1474
+ function buildRenderHTML(bundledCode, cssTokens, fit, chrome, presentation, framedCardRadiusPx) {
1475
+ const framedCard = framedCardRadiusPx !== void 0;
1476
+ if (chrome === "panel" && fit !== "fill" && !framedCard) throw new Error("the judge's panel frames a fill canvas only (ggui#1083 cut 3)");
1477
+ if (chrome === "ground" && fit === "fill") throw new Error("the host ground frames an inline card only \u2014 a fill canvas has no natural height (ggui#1475)");
1478
+ const tokens = cssTokens ?? getCssTokens();
1479
+ return `<!DOCTYPE html>
1480
+ <html>
1481
+ <head>
1482
+ <meta charset="utf-8">
1483
+ <meta name="viewport" content="width=device-width, initial-scale=1">
1484
+ <style>
1485
+ ${tokens}
1486
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
1487
+ body {
1488
+ font-family: var(--ggui-font-family-sans, system-ui, -apple-system, sans-serif);
1489
+ /* ggui#1083 \u2014 the host stand-in (neutral-50) under the theme's scrim (its tint at
1490
+ its opacity): the ground a frosted host puts under the card, so the judge scores
1491
+ the card where the visitor sees it. */
1492
+ ${presentation?.frame !== void 0 ? `background: ${presentation.frame.ground};` : presentation?.ground !== void 0 ? `background: ${presentation.ground};` : expandedFrameScrimDecls()}
1493
+ color: var(--ggui-color-neutral-900, #111827);
1494
+ }
1495
+ .error { color: #dc2626; padding: 16px; font-family: monospace; white-space: pre-wrap; }
1496
+ ${/* ggui#1765 — the fit's rules come from the ONE selection the runtime's composeThemeCss uses. */
1497
+ mountFitCss(JUDGE_SCOPE_CLASS, fit === "fill" ? "fill" : framedCard ? "framed-card" : "card", framedCardRadiusPx ?? null)}
1498
+ ${fit === "fill" && chrome === "panel" ? expandedFramePanelRule(JUDGE_PANEL_CLASS, JUDGE_SCOPE_CLASS) : ""}
1499
+ ${framedCard ? `.${JUDGE_PANEL_CLASS} { ${expandedFramePanelDecls()} background: ${FRAMED_CARD_GROUND}; display: flow-root; } body > .${JUDGE_PANEL_CLASS} { margin: ${EXPANDED_FRAME.insetPx}px; height: calc(100vh - ${2 * EXPANDED_FRAME.insetPx}px); }` : ""}
1500
+ ${framedCard && chrome !== "panel" ? `body { background: ${FRAMED_CARD_GROUND}; }` : ""}
1501
+ ${chrome === "ground" ? `.${JUDGE_GROUND_CLASS} { padding: ${JUDGE_GROUND_MARGIN_PX}px; } .${JUDGE_INLINE_FRAME_CLASS} { ${inlineFrameDecls(presentation?.frame)} }` : ""}
1502
+ </style>
1503
+ </head>
1504
+ <body>
1505
+ ${chrome === "panel" ? `<div class="${JUDGE_PANEL_CLASS}">` : chrome === "ground" ? `<div class="${JUDGE_GROUND_CLASS}"><div class="${JUDGE_INLINE_FRAME_CLASS}">` : ""}<div id="root" class="${JUDGE_SCOPE_CLASS}"></div>${chrome === "panel" ? "</div>" : chrome === "ground" ? "</div></div>" : ""}
1506
+ <script type="importmap">
1507
+ {
1508
+ "imports": {
1509
+ "react": "https://esm.sh/react@18",
1510
+ "react-dom": "https://esm.sh/react-dom@18",
1511
+ "react-dom/client": "https://esm.sh/react-dom@18/client",
1512
+ "react/jsx-runtime": "https://esm.sh/react@18/jsx-runtime"
1513
+ }
1514
+ }
1515
+ </script>
1516
+ <script type="module">
1517
+ ${bundledCode}
1518
+ </script>
1519
+ </body>
1520
+ </html>`;
1521
+ }
1522
+ var EXECUTABLE_PATH_LAUNCH_ARGS = ["--no-sandbox", "--disable-dev-shm-usage"];
1523
+ var DEFAULT_SCREENSHOT_VIEWPORT = { width: 1280, height: 800 };
1524
+ async function loadSparticuzChromium() {
1525
+ const chromium = await import('@sparticuz/chromium');
1526
+ return chromium.default;
1527
+ }
1528
+ async function resolveLaunchOptions(viewport, deps = {}) {
1529
+ const env = deps.env ?? process.env;
1530
+ const override = env.PUPPETEER_EXECUTABLE_PATH?.trim();
1531
+ const defaultViewport = integerBox(viewport);
1532
+ if (override) {
1533
+ return {
1534
+ executablePath: override,
1535
+ args: [...EXECUTABLE_PATH_LAUNCH_ARGS],
1536
+ defaultViewport,
1537
+ headless: true
1538
+ };
1539
+ }
1540
+ const chromium = await (deps.loadChromium ?? loadSparticuzChromium)();
1541
+ return {
1542
+ args: chromium.args,
1543
+ defaultViewport,
1544
+ executablePath: await chromium.executablePath(),
1545
+ headless: true
1546
+ };
1547
+ }
1548
+ async function launchWithPuppeteer(options) {
1549
+ const puppeteer = await import('puppeteer-core');
1550
+ return puppeteer.default.launch(options);
1551
+ }
1552
+ var CONTENT_HEIGHT_EXPRESSION = "Math.max(document.documentElement.scrollHeight, document.body ? document.body.scrollHeight : 0)";
1553
+ function cardHeightExpression(padPx) {
1554
+ return `(() => { const el = document.documentElement; const prev = el.style.height; el.style.height = 'max-content'; const h = Math.ceil(el.getBoundingClientRect().height); el.style.height = prev; return h - ${2 * padPx}; })()`;
1555
+ }
1556
+ var CARD_HEIGHT_EXPRESSION = cardHeightExpression(JUDGE_INLINE_PAD_PX);
1557
+ var OVERFLOW_X_ROUNDING_PX = 1;
1558
+ var CARD_OVERFLOW_X_EXPRESSION = "(() => { const r = document.getElementById('root'); return r ? Math.max(0, Math.ceil(r.scrollWidth - r.clientWidth)) : null; })()";
1559
+ function naturalClip(window, cardHeightPx, padPx = JUDGE_INLINE_PAD_PX) {
1560
+ const m = padPx;
1561
+ const box = window.height - 2 * m;
1562
+ return { x: 0, y: 0, width: window.width, height: Math.max(1, Math.min(Math.ceil(cardHeightPx), box)) + 2 * m };
1563
+ }
1564
+ var CARD_BOX_EXPRESSION = `(() => { const r = document.querySelector('.${JUDGE_SCOPE_CLASS}'); if (!r) return null; const b = r.getBoundingClientRect(); return { x: b.x, y: b.y, width: b.width, height: b.height }; })()`;
1565
+ async function measureCardBox(page) {
1566
+ try {
1567
+ const v = await page.evaluate(CARD_BOX_EXPRESSION);
1568
+ if (typeof v !== "object" || v === null) return null;
1569
+ const n = (k) => {
1570
+ const x2 = Reflect.get(v, k);
1571
+ return typeof x2 === "number" && Number.isFinite(x2) ? x2 : null;
1572
+ };
1573
+ const [x, y, width, height] = [n("x"), n("y"), n("width"), n("height")];
1574
+ return x === null || y === null || width === null || height === null || width <= 0 || height <= 0 ? null : { x, y, width, height };
1575
+ } catch (e) {
1576
+ console.warn(`[visual-eval] card box unavailable: ${e instanceof Error ? e.message : String(e)}`);
1577
+ return null;
1578
+ }
1579
+ }
1580
+ function cardBoxClip(window, box) {
1581
+ const m = JUDGE_GROUND_MARGIN_PX;
1582
+ const x = Math.max(0, Math.floor(box.x - m));
1583
+ const y = Math.max(0, Math.floor(box.y - m));
1584
+ return { x, y, width: Math.min(window.width - x, Math.ceil(box.width + 2 * m)), height: Math.max(1, Math.min(window.height - y, Math.ceil(box.height + 2 * m))) };
1585
+ }
1586
+ async function measureContentHeight(page, expression = CONTENT_HEIGHT_EXPRESSION) {
1587
+ try {
1588
+ const h = await page.evaluate(expression);
1589
+ return typeof h === "number" && Number.isFinite(h) ? Math.round(h) : null;
1590
+ } catch (e) {
1591
+ console.warn(`[visual-eval] content height unavailable: ${e instanceof Error ? e.message : String(e)}`);
1592
+ return null;
1593
+ }
1594
+ }
1595
+ async function measureEdges(page) {
1596
+ return { edge: await measureEdgesV1(page), edgeV2: await measureEdgesV2(page) };
1597
+ }
1598
+ async function measureEdgesV2(page) {
1599
+ try {
1600
+ const probe = parseEdgeProbeV2(await page.evaluate(EDGE_BLOCKS_V2_EXPRESSION));
1601
+ if (probe === null) {
1602
+ console.warn("[visual-eval] edge probe v2 unavailable: the page returned no readable blocks");
1603
+ return null;
1604
+ }
1605
+ return judgeEdgesV2(probe.blocks, { cardContent: probe.cardContent, placeholdersOnly: probe.placeholdersOnly });
1606
+ } catch (e) {
1607
+ console.warn(`[visual-eval] edge probe v2 unavailable: ${e instanceof Error ? e.message : String(e)}`);
1608
+ return null;
1609
+ }
1610
+ }
1611
+ async function measureEdgesV1(page) {
1612
+ try {
1613
+ const probe = parseEdgeProbe(await page.evaluate(EDGE_BLOCKS_EXPRESSION));
1614
+ if (probe === null) {
1615
+ console.warn("[visual-eval] edge probe unavailable: the page returned no readable blocks");
1616
+ return null;
1617
+ }
1618
+ return judgeEdges(probe.blocks, { placeholdersOnly: probe.placeholdersOnly });
1619
+ } catch (e) {
1620
+ console.warn(`[visual-eval] edge probe unavailable: ${e instanceof Error ? e.message : String(e)}`);
1621
+ return null;
1622
+ }
1623
+ }
1624
+ async function captureScreenshotDetailed(html, viewport = DEFAULT_SCREENSHOT_VIEWPORT, deps = {}, capture = "full-page", natural = { padPx: JUDGE_INLINE_PAD_PX }) {
1625
+ try {
1626
+ const launchOptions = await resolveLaunchOptions(viewport, deps);
1627
+ const browser = await (deps.launch ?? launchWithPuppeteer)(launchOptions);
1628
+ try {
1629
+ const page = await browser.newPage();
1630
+ await page.setContent(html, { waitUntil: "load", timeout: 15e3 });
1631
+ await page.waitForNetworkIdle({ idleTime: 500, timeout: 15e3 }).catch(() => {
1632
+ });
1633
+ await page.waitForSelector("#root > *", { timeout: 1e4 }).catch(() => {
1634
+ });
1635
+ await new Promise((r) => setTimeout(r, deps.settleMs ?? 1e3));
1636
+ if (capture === "natural") {
1637
+ const cardHeightPx = await measureContentHeight(page, natural.padPx === JUDGE_INLINE_PAD_PX ? CARD_HEIGHT_EXPRESSION : cardHeightExpression(natural.padPx));
1638
+ const overflowX = await measureContentHeight(page, CARD_OVERFLOW_X_EXPRESSION);
1639
+ const floorPx = natural.floorPx;
1640
+ if (floorPx !== void 0 && cardHeightPx !== null && cardHeightPx < floorPx) {
1641
+ await page.evaluate(`(() => { const f = document.querySelector('.${JUDGE_INLINE_FRAME_CLASS}'); if (f) f.style.minHeight = '${floorPx}px'; })()`);
1642
+ }
1643
+ const framed = cardHeightPx === null ? viewport.height - 2 * natural.padPx : floorPx !== void 0 ? Math.max(cardHeightPx, floorPx) : cardHeightPx;
1644
+ const clip = naturalClip(viewport, framed, natural.padPx);
1645
+ const edges2 = await measureEdges(page);
1646
+ const screenshot2 = await page.screenshot({ type: "png", fullPage: false, clip });
1647
+ return { png: Buffer.from(screenshot2), contentHeight: cardHeightPx, overflowX, ...edges2 };
1648
+ }
1649
+ if (capture === "card-box") {
1650
+ const box = await measureCardBox(page);
1651
+ if (box === null) return { png: null, reason: `card-box capture: the card's own box (.${JUDGE_SCOPE_CLASS}) could not be measured`, contentHeight: null };
1652
+ const clip = cardBoxClip(viewport, box);
1653
+ const edges2 = await measureEdges(page);
1654
+ const screenshot2 = await page.screenshot({ type: "png", fullPage: false, clip });
1655
+ return { png: Buffer.from(screenshot2), contentHeight: Math.round(box.height), ...edges2 };
1656
+ }
1657
+ const contentHeight = await measureContentHeight(page);
1658
+ const edges = await measureEdges(page);
1659
+ const screenshot = await page.screenshot({ type: "png", fullPage: capture === "full-page" });
1660
+ return { png: Buffer.from(screenshot), contentHeight, ...edges };
1661
+ } finally {
1662
+ await browser.close();
1663
+ }
1664
+ } catch (e) {
1665
+ const reason = `screenshot failed: ${e instanceof Error ? e.message : String(e)}`;
1666
+ console.warn(`[visual-eval] ${reason}`);
1667
+ return { png: null, reason, contentHeight: null };
1668
+ }
1669
+ }
1670
+ async function captureScreenshot(html, viewport = DEFAULT_SCREENSHOT_VIEWPORT, deps = {}) {
1671
+ return (await captureScreenshotDetailed(html, viewport, deps)).png;
1672
+ }
1673
+ var VISUAL_EVAL_PROMPT = `You are a visual quality evaluator for ggui \u2014 a platform where AI agents generate React UI components on demand.
1674
+
1675
+ ## Context
1676
+ The screenshot shows a generated React component built from a design system with CSS variable theming. The component was created by an LLM to fulfill a user's request. Your job is to evaluate whether the generated UI achieves the user's goal AND looks professionally designed.
1677
+
1678
+ ## Scoring Dimensions (0-100 each)
1679
+
1680
+ 1. **completeness** (25%): Does the screenshot show ALL features the user requested? Are all sections, controls, data fields, and interactive elements present? Missing features or sections = major deduction. Compare against the original request carefully.
1681
+
1682
+ 2. **layout** (25%): Is the layout well-composed? Clear sections and grouping? No overlapping elements? Proper spacing between elements? Responsive-looking? Does it look like a professional app UI, not a homework project?
1683
+
1684
+ 3. **hierarchy** (25%): Clear visual hierarchy? Headings are visually distinct from body text? Primary actions (buttons) are prominent? Information grouped logically with clear section breaks? Color and size used to guide the eye?
1685
+
1686
+ 4. **aesthetics** (25%): Professional, polished appearance? Consistent color palette? Good use of whitespace \u2014 not too sparse, not too cluttered? Cards, shadows, and borders used effectively? Does it look like something a designer would approve? Would you be comfortable showing this to a client?
1687
+
1688
+ ## Scoring Guide
1689
+ - 90-100: Production-ready. Would ship to real users. Polished layout, clear hierarchy, professional aesthetics.
1690
+ - 80-89: Good quality. Minor improvements needed (slightly tight spacing, one section could be better grouped).
1691
+ - 70-79: Acceptable but generic. Works correctly but lacks design polish or visual refinement.
1692
+ - 60-69: Below standard. Noticeable problems \u2014 cramped layout, poor hierarchy, or missing sections.
1693
+ - Below 60: Broken or fundamentally incomplete. Error messages visible, blank areas, or critical features missing.
1694
+
1695
+ ## Important
1696
+ - Check the screenshot against the "Original Request" \u2014 does it actually deliver what was asked for?
1697
+ - Error messages like "Render error" or blank white space = rendering score 0
1698
+ - A component that renders but is just a wall of text with no structure should score low on layout and hierarchy
1699
+
1700
+ List AT MOST 6 issues, most severe first, one sentence each for "description" and "fix" \u2014 a longer list is cut off and lost.
1701
+
1702
+ Respond with ONLY a JSON object (no markdown, no explanation):
1703
+ {
1704
+ "completeness": <0-100>,
1705
+ "layout": <0-100>,
1706
+ "hierarchy": <0-100>,
1707
+ "aesthetics": <0-100>,
1708
+ "issues": [
1709
+ { "dimension": "<dimension>", "severity": "<critical|major|minor>", "description": "<what's wrong>", "fix": "<specific fix>" }
1710
+ ],
1711
+ "critique": "<2-3 sentences: what's good, what needs improvement, does it achieve the user's goal?>"
1712
+ }`;
1713
+ var VISUAL_JUDGE_MAX_OUTPUT_TOKENS = 4096;
1714
+ var VISUAL_JUDGE_PROMPT_VERSION = "v2-bounded-issues";
1715
+ var VISUAL_JUDGE_PROMPT_DIGEST = createHash("sha256").update(VISUAL_EVAL_PROMPT, "utf8").digest("hex");
1716
+ async function callMultimodalLLM(config, model, prompt, screenshot, originalPrompt, profileBlock = "", criteriaBlock = "") {
1717
+ const userPrompt = `## Original Request
1718
+ ${originalPrompt}
1719
+
1720
+ ` + (profileBlock.length > 0 ? `${profileBlock}
1721
+
1722
+ ` : "") + (criteriaBlock.length > 0 ? `${criteriaBlock}
1723
+
1724
+ ` : "") + "Evaluate the screenshot of the generated component.";
1725
+ const agent = createVisionAgent({
1726
+ provider: config.provider === "claude" ? "anthropic" : config.provider,
1727
+ model,
1728
+ routeOverride: config.routeOverride,
1729
+ onRetry: config.onRetry
1730
+ });
1731
+ return agent.callVision(
1732
+ model,
1733
+ prompt,
1734
+ userPrompt,
1735
+ { mediaType: "image/png", base64: screenshot.toString("base64") },
1736
+ VISUAL_JUDGE_MAX_OUTPUT_TOKENS
1737
+ );
1738
+ }
1739
+ function mountCanvasOf(compiledCode) {
1740
+ const name = GGUI_MOUNT_CANVAS_EXPORT;
1741
+ const binding = new RegExp(`([A-Za-z_$][\\w$]*)\\s+as\\s+${name}\\b`).exec(compiledCode)?.[1] ?? name;
1742
+ return new RegExp(`(?:^|[^\\w$.])${binding.replace(/\$/g, "\\$")}\\s*=\\s*"([^"\\\\]*)"`).exec(compiledCode)?.[1];
1743
+ }
1744
+ function themeVarsOf(css) {
1745
+ const vars = {};
1746
+ for (const [, name, value] of css.matchAll(/(--ggui-[\w-]+)\s*:\s*([^;}]+)/g)) {
1747
+ if (name !== void 0 && value !== void 0) vars[name] = value.trim();
1748
+ }
1749
+ return vars;
1750
+ }
1751
+ function readInk(png, chrome, canvas, inlineInkInsetPx = JUDGE_INLINE_INK_INSET_PX) {
1752
+ if (png === null) return null;
1753
+ const ink = readInkExtent(png, chrome === "panel" ? JUDGE_INK_INSET_PX : chrome === "ground" ? inlineInkInsetPx : 0);
1754
+ if ("reason" in ink) {
1755
+ console.warn(`[visual-eval] ink extent unreadable at canvas ${canvas}: ${ink.reason}`);
1756
+ return null;
1757
+ }
1758
+ return ink;
1759
+ }
1760
+ function blankVerdict(canvas, frame) {
1761
+ if (frame.chrome === "ground" && frame.attempt.contentHeight !== null && frame.attempt.contentHeight <= 0) {
1762
+ return canvasBlankIssue(canvas, frame.viewport);
1763
+ }
1764
+ return frame.ink !== null && frame.ink.lastInkRow === null ? canvasBlankIssue(canvas, frame.viewport) : null;
1765
+ }
1766
+ async function frameCanvas(bundledCode, context, canvas, canvasViewports, deps, hostPresentations) {
1767
+ const declaredBox = canvasViewports?.[canvas];
1768
+ const viewport = declaredBox !== void 0 ? integerBox(declaredBox) : CANVAS_VIEWPORTS[canvas];
1769
+ const mountFit = resolveMountFit({ canvas: mountCanvasOf(context.compiledCode), displayMode: displayModeForCanvas(canvas) });
1770
+ const policy = mountFit === "framed-card" ? { capture: "card-box", overflow: "fail" } : canvasFitPolicy(canvas);
1771
+ const fit = mountFit === "framed-card" ? void 0 : canvasFit(canvas);
1772
+ const chrome = canvasChrome(canvas);
1773
+ const outcome = resolvePresentation(canvas, hostPresentations?.[canvas]);
1774
+ const presentation = outcome?.status === "applied" ? outcome.applied : void 0;
1775
+ const geometry = inlineGeometry(presentation?.frame);
1776
+ const framedCardRadiusPx = mountFit === "framed-card" ? inlineCardRadius(themeVarsOf(context.cssTokens ?? getCssTokens())).radiusPx : void 0;
1777
+ const canvasHtml = buildRenderHTML(bundledCode, context.cssTokens, fit, chrome, presentation, framedCardRadiusPx);
1778
+ const capture = policy.capture;
1779
+ const captured = await captureScreenshotDetailed(canvasHtml, judgeWindow(viewport, chrome, geometry.padPx), deps, capture, {
1780
+ padPx: geometry.padPx,
1781
+ ...geometry.floorPx !== void 0 ? { floorPx: geometry.floorPx } : {}
1782
+ });
1783
+ const attempt = { ...captured, contentHeight: capture === "card-box" ? captured.contentHeight : cardHeight(captured.contentHeight, chrome) };
1784
+ const cardBoxInk = JUDGE_GROUND_MARGIN_PX + JUDGE_INLINE_FRAME_BORDER_PX + (framedCardRadiusPx ?? JUDGE_INLINE_FRAME_RADIUS_PX);
1785
+ return {
1786
+ viewport,
1787
+ policy,
1788
+ mountFit,
1789
+ fit,
1790
+ chrome,
1791
+ declared: declaredBox !== void 0,
1792
+ attempt,
1793
+ ink: capture === "card-box" ? readInk(captured.png, "ground", canvas, cardBoxInk) : readInk(captured.png, chrome, canvas, geometry.inkInsetPx),
1794
+ ...outcome !== void 0 ? { presentation: outcome } : {}
1795
+ };
1796
+ }
1797
+ function fitCeiling(frame) {
1798
+ return frame.mountFit === "framed-card" ? { width: frame.viewport.width, height: frame.viewport.height - 2 * EXPANDED_FRAME.insetPx } : frame.viewport;
1799
+ }
1800
+ function fitVerdict(canvas, frame) {
1801
+ const contentHeight = frame.attempt.contentHeight;
1802
+ const ceiling = fitCeiling(frame);
1803
+ if (contentHeight === null || contentHeight <= ceiling.height || frame.policy.overflow === "none") return null;
1804
+ return canvasOverflowIssue(canvas, ceiling, contentHeight, frame.policy.overflow);
1805
+ }
1806
+ function overflowXVerdict(canvas, frame) {
1807
+ const px = frame.attempt.overflowX;
1808
+ if (px === void 0 || px === null || px <= OVERFLOW_X_ROUNDING_PX || frame.policy.overflow === "none") return null;
1809
+ return canvasOverflowXIssue(canvas, frame.viewport, px, frame.policy.overflow);
1810
+ }
1811
+ function judgeSalvageOf(error, text, finishReason) {
1812
+ const cause = finishReason === void 0 ? "unknown" : finishReason === "length" ? "output-cap" : finishReason === "stop" ? "malformed" : "stopped";
1813
+ const message = error instanceof Error ? error.message : String(error);
1814
+ const position = /position (\d+)/.exec(message);
1815
+ const start = text.indexOf("{");
1816
+ const offset = position !== null ? (start > 0 ? start : 0) + Number(position[1]) : void 0;
1817
+ const parseError = message.startsWith("Visual evaluator did not return valid JSON") ? "no complete JSON object in the answer" : message.replace(/"(?:[^"\\]|\\.)*"/g, '"\u2026"').slice(0, 160);
1818
+ return { cause, ...finishReason !== void 0 ? { finishReason } : {}, parseError, ...offset !== void 0 ? { offset } : {}, chars: text.length };
1819
+ }
1820
+ function judgeRecordOf(k, parsed, samples, criteria, requestedModel) {
1821
+ const salvaged = parsed.flatMap((a) => a.salvage !== void 0 ? [a.salvage.cause] : []);
1822
+ const answered = [...parsed, ...criteria?.kind === "ok" ? [criteria] : []];
1823
+ const served = [...new Set(answered.flatMap((a) => a.servedModel !== void 0 ? [a.servedModel] : []))];
1824
+ const criteriaSalvaged = criteria?.kind === "ok" ? criteria.salvage?.cause : void 0;
1825
+ return {
1826
+ k,
1827
+ rule: "median",
1828
+ samples,
1829
+ sigma: populationSigma(samples),
1830
+ notes: parsed.map((a) => a.result.critique ?? ""),
1831
+ ...salvaged.length > 0 ? { salvaged } : {},
1832
+ ...criteriaSalvaged !== void 0 ? { criteriaSalvaged } : {},
1833
+ models: { requested: requestedModel, served }
1834
+ };
1835
+ }
1836
+ function callFacts(response) {
1837
+ return {
1838
+ ...response.finishReason !== void 0 ? { finishReason: response.finishReason } : {},
1839
+ ...response.servedModel !== void 0 ? { servedModel: response.servedModel } : {}
1840
+ };
1841
+ }
1842
+ var NO_CRITERIA_ARRAY = { array: false, received: 0, kept: 0, dropped: { notObject: 0, badId: 0, badVerdict: 0 } };
1843
+ function criteriaCallOf(criteria) {
1844
+ if (criteria?.kind !== "ok") return {};
1845
+ const answer = criteria.result.criteriaCounts ?? NO_CRITERIA_ARRAY;
1846
+ return { call: { answer, ...criteria.finishReason !== void 0 ? { finishReason: criteria.finishReason } : {} } };
1847
+ }
1848
+ async function judgeFrame(judge, config, model, png, originalPrompt, profileBlock, canvas, k, criteriaBlock) {
1849
+ const [scoring, criteria] = await Promise.all([
1850
+ Promise.all(Array.from({ length: k }, () => judgeAndParse(judge, config, model, png, originalPrompt, profileBlock, canvas))),
1851
+ criteriaBlock.length > 0 ? judgeAndParse(judge, config, model, png, originalPrompt, profileBlock, canvas, criteriaBlock) : void 0
1852
+ ]);
1853
+ return { scoring, criteria };
1854
+ }
1855
+ function criteriaCallTokens(criteria) {
1856
+ return criteria?.kind === "ok" ? { inputTokens: criteria.inputTokens, outputTokens: criteria.outputTokens } : void 0;
1857
+ }
1858
+ function criteriaReads(criteria) {
1859
+ return criteria?.kind === "ok" && criteria.salvage === void 0 ? [criteria.result.criteriaAnswers ?? []] : [];
1860
+ }
1861
+ function criteriaUnansweredReason(criteria) {
1862
+ return criteria?.kind === "ok" && criteria.salvage !== void 0 ? { unansweredReason: `not answered: the criteria answer was salvaged (${criteria.salvage.cause})` } : {};
1863
+ }
1864
+ function judgeCountFor(config, canvas) {
1865
+ const k = Math.max(1, Math.floor(config.judgeK ?? 1));
1866
+ if (k === 1) return 1;
1867
+ return config.judgeKCanvases === void 0 || config.judgeKCanvases.includes(canvas) ? k : 1;
1868
+ }
1869
+ function medianOf(values) {
1870
+ const sorted = [...values].sort((a, b) => a - b);
1871
+ return sorted[Math.floor((sorted.length - 1) / 2)];
1872
+ }
1873
+ function populationSigma(values) {
1874
+ if (values.length < 2) return 0;
1875
+ const mean = values.reduce((a, b) => a + b, 0) / values.length;
1876
+ const variance = values.reduce((a, b) => a + (b - mean) ** 2, 0) / values.length;
1877
+ return Math.round(Math.sqrt(variance) * 10) / 10;
1878
+ }
1879
+ async function judgeAndParse(judge, config, model, screenshot, originalPrompt, profileBlock, canvas, criteriaBlock = "") {
1880
+ let firstReason;
1881
+ for (let attempt = 1; attempt <= 2; attempt++) {
1882
+ const response = await judge(config, model, VISUAL_EVAL_PROMPT, screenshot, originalPrompt, profileBlock, criteriaBlock);
1883
+ try {
1884
+ const result = parseVisualResponse(response.text, config.passThreshold);
1885
+ return { kind: "ok", result, inputTokens: response.inputTokens, outputTokens: response.outputTokens, ...callFacts(response) };
1886
+ } catch (e) {
1887
+ const salvaged = salvageTruncatedVisualAnswer(response.text, config.passThreshold);
1888
+ if (salvaged !== null) {
1889
+ const salvage = judgeSalvageOf(e, response.text, response.finishReason);
1890
+ const token = criteriaBlock.length > 0 ? "visual_criteria_salvaged" : "visual_judge_salvaged";
1891
+ console.warn(
1892
+ `[visual-eval] ${token}${canvas ? ` canvas=${canvas}` : ""} cause=${salvage.cause} finish=${salvage.finishReason ?? "unreported"} at=${salvage.offset ?? "?"}/${salvage.chars} (${salvage.parseError}): score recovered from the closed prefix, issues list partial`
1893
+ );
1894
+ return { kind: "ok", result: salvaged, inputTokens: response.inputTokens, outputTokens: response.outputTokens, salvage, ...callFacts(response) };
1895
+ }
1896
+ const reason = `judge answer unparsable: ${e instanceof Error ? e.message : String(e)}`;
1897
+ if (attempt === 1) {
1898
+ firstReason = reason;
1899
+ console.warn(`[visual-eval] visual_judge_retry${canvas ? ` canvas=${canvas}` : ""}: ${reason}`);
1900
+ } else {
1901
+ console.warn(`[visual-eval] visual_judge_retry failed again${canvas ? ` canvas=${canvas}` : ""}: ${reason}`);
1902
+ }
1903
+ }
1904
+ }
1905
+ return { kind: "unparsable", reason: firstReason ?? "judge answer unparsable" };
1906
+ }
1907
+ async function runVisualEvaluation(context, config, deps = {}) {
1908
+ return (await runVisualEvaluationDetailed(context, config, deps)).result;
1909
+ }
1910
+ async function runVisualEvaluationDetailed(context, config, deps = {}) {
1911
+ const startTime = Date.now();
1912
+ const judge = deps.judge ?? callMultimodalLLM;
1913
+ const designSrc = config.designSrcDir ?? resolve(resolveDesignPackageDir(), "src");
1914
+ const design = judgeDesignIdentity(designSrc);
1915
+ const stamp = (r) => ({
1916
+ ...r,
1917
+ ...design !== null ? { design } : {},
1918
+ ...config.themeMode !== void 0 ? { themeMode: config.themeMode } : {}
1919
+ });
1920
+ let bundledCode;
1921
+ try {
1922
+ bundledCode = await bundleForRendering(context.compiledCode, config.sampleProps ?? {}, designSrc);
1923
+ } catch (e) {
1924
+ const unavailableReason = `bundle failed: ${e instanceof Error ? e.message : String(e)}`;
1925
+ console.warn(`[visual-eval] ${unavailableReason}`);
1926
+ return { result: null, unavailableReason };
1927
+ }
1928
+ console.log(`[visual-eval] bundled: ${bundledCode.length}B (${Date.now() - startTime}ms)`);
1929
+ const html = buildRenderHTML(bundledCode, context.cssTokens);
1930
+ const model = config.model ?? getDefaultVisualModel(config.provider);
1931
+ if (config.canvases !== void 0 && config.canvases.length > 0) {
1932
+ const perCanvas = [];
1933
+ const perCanvasResults = [];
1934
+ for (const canvas of config.canvases) {
1935
+ const frame = await frameCanvas(bundledCode, context, canvas, config.canvasViewports, deps, config.hostPresentations);
1936
+ const { viewport, policy, fit, attempt: attempt2 } = frame;
1937
+ const screenshot2 = attempt2.png;
1938
+ if (!screenshot2) {
1939
+ const unavailableReason = attempt2.reason ?? "no browser available";
1940
+ console.warn(`[visual-eval] ${unavailableReason} at canvas ${canvas} \u2014 skipping visual evaluation`);
1941
+ return { result: null, unavailableReason, canvas };
1942
+ }
1943
+ const k = judgeCountFor(config, canvas);
1944
+ const profileBlock = buildStylingProfileJudgeBlock(context.profile);
1945
+ const criteriaContext = context.criteria !== void 0 ? { ...context.criteria.context, canvas } : void 0;
1946
+ const criteriaSelected = context.criteria !== void 0 && criteriaContext !== void 0 ? selectCriteria(context.criteria.bank, criteriaContext) : void 0;
1947
+ const criteriaBlock = context.criteria !== void 0 && criteriaSelected !== void 0 ? buildCriteriaJudgeBlock(context.criteria.bank, criteriaSelected, {
1948
+ frame: { canvas, width: viewport.width, height: viewport.height, fit: frame.mountFit, ...frame.presentation?.status === "applied" ? { hostLabel: frame.presentation.applied.label } : {} },
1949
+ ...config.sampleProps !== void 0 ? { propsJson: JSON.stringify(config.sampleProps) } : {}
1950
+ }) : "";
1951
+ const { scoring: answers, criteria: criteriaAnswer } = await judgeFrame(
1952
+ judge,
1953
+ config,
1954
+ model,
1955
+ screenshot2,
1956
+ context.originalPrompt,
1957
+ profileBlock,
1958
+ canvas,
1959
+ k,
1960
+ criteriaBlock
1961
+ );
1962
+ const parsed = answers.filter((a) => a.kind === "ok");
1963
+ if (parsed.length === 0) {
1964
+ const first = answers.find((a) => a.kind === "unparsable");
1965
+ return { result: null, unavailableReason: first?.reason ?? "judge answer unparsable", canvas };
1966
+ }
1967
+ const samples = parsed.map((a) => a.result.finalScore);
1968
+ const median = medianOf(samples);
1969
+ const representative = parsed[samples.indexOf(median)];
1970
+ const result2 = representative.result;
1971
+ result2.finalScore = median;
1972
+ result2.passed = median >= config.passThreshold;
1973
+ result2.inputTokens = parsed.reduce((sum3, a) => sum3 + a.inputTokens, 0);
1974
+ result2.outputTokens = parsed.reduce((sum3, a) => sum3 + a.outputTokens, 0);
1975
+ const response2 = { inputTokens: result2.inputTokens, outputTokens: result2.outputTokens };
1976
+ const criteriaTokens = criteriaCallTokens(criteriaAnswer);
1977
+ const judgeRecord = judgeRecordOf(k, parsed, samples, criteriaAnswer, model);
1978
+ const contentHeight = attempt2.contentHeight;
1979
+ const overflow = contentHeight !== null && contentHeight > viewport.height;
1980
+ const fitIssue = fitVerdict(canvas, frame);
1981
+ if (fitIssue !== null) {
1982
+ result2.issues.push(fitIssue);
1983
+ if (policy.overflow === "fail") result2.passed = false;
1984
+ }
1985
+ const overflowXIssue = overflowXVerdict(canvas, frame);
1986
+ if (overflowXIssue !== null) {
1987
+ result2.issues.push(overflowXIssue);
1988
+ if (policy.overflow === "fail") result2.passed = false;
1989
+ }
1990
+ const blankIssue = blankVerdict(canvas, frame);
1991
+ if (blankIssue !== null) {
1992
+ result2.issues.push(blankIssue);
1993
+ result2.passed = false;
1994
+ }
1995
+ const inkRatio = frame.ink?.ratio ?? null;
1996
+ const criteria = context.criteria !== void 0 && criteriaContext !== void 0 && criteriaSelected !== void 0 ? resolveCriteriaBlock({
1997
+ bank: context.criteria.bank,
1998
+ context: criteriaContext,
1999
+ selected: criteriaSelected,
2000
+ answers: criteriaReads(criteriaAnswer),
2001
+ measurements: { overflow, contentHeight, viewportHeight: viewport.height, inkRatio, edge: attempt2.edge ?? null, edgeV2: attempt2.edgeV2 ?? null },
2002
+ ...criteriaUnansweredReason(criteriaAnswer),
2003
+ ...criteriaCallOf(criteriaAnswer)
2004
+ }) : void 0;
2005
+ perCanvasResults.push(result2);
2006
+ perCanvas.push({
2007
+ canvas,
2008
+ viewport,
2009
+ score: result2.finalScore,
2010
+ passed: result2.passed,
2011
+ screenshotPng: screenshot2,
2012
+ contentHeight,
2013
+ overflow,
2014
+ inkRatio,
2015
+ judge: judgeRecord,
2016
+ ...fit !== void 0 ? { fit } : {},
2017
+ ...frame.declared ? { declared: true } : {},
2018
+ ...criteria !== void 0 ? { criteria } : {},
2019
+ ...criteriaTokens !== void 0 ? { criteriaTokens } : {},
2020
+ ...frame.presentation !== void 0 ? { presentation: frame.presentation } : {}
2021
+ });
2022
+ console.log(
2023
+ `[visual-eval] canvas=${canvas} ${viewport.width}\xD7${viewport.height} score=${result2.finalScore}${k > 1 ? ` (median of ${judgeRecord.samples.length}/${k}: ${judgeRecord.samples.join(",")} \u03C3=${judgeRecord.sigma})` : ""} ${result2.passed ? "pass" : "FAIL"} | content=${contentHeight ?? "?"}px${overflow ? ` OVERFLOW (${policy.overflow})` : ""} ink=${inkRatio ?? "?"}${blankIssue !== null ? " BLANK" : ""} | in=${response2.inputTokens} out=${response2.outputTokens}` + (criteriaTokens !== void 0 ? ` | criteria in=${criteriaTokens.inputTokens} out=${criteriaTokens.outputTokens}` : "")
2024
+ );
2025
+ }
2026
+ const aggregate = aggregateCanvasResults(perCanvas, perCanvasResults);
2027
+ const elapsed2 = Date.now() - startTime;
2028
+ console.log(
2029
+ `[visual-eval] score=${aggregate.finalScore} (mean of ${perCanvas.length} canvases; ${aggregate.passed ? "every canvas passed" : `${perCanvas.filter((c) => !c.passed).length} failed`}) (${elapsed2}ms)`
2030
+ );
2031
+ return { result: stamp(aggregate) };
2032
+ }
2033
+ const attempt = await captureScreenshotDetailed(html, config.viewport, deps);
2034
+ const screenshot = attempt.png;
2035
+ if (!screenshot) {
2036
+ const unavailableReason = attempt.reason ?? "no browser available";
2037
+ console.warn(`[visual-eval] ${unavailableReason} \u2014 skipping visual evaluation`);
2038
+ return { result: null, unavailableReason };
2039
+ }
2040
+ console.log(`[visual-eval] screenshot: ${screenshot.length}B (${Date.now() - startTime}ms)`);
2041
+ const answer = await judgeAndParse(
2042
+ judge,
2043
+ config,
2044
+ model,
2045
+ screenshot,
2046
+ context.originalPrompt,
2047
+ buildStylingProfileJudgeBlock(context.profile)
2048
+ );
2049
+ if (answer.kind === "unparsable") {
2050
+ return { result: null, unavailableReason: answer.reason };
2051
+ }
2052
+ const result = answer.result;
2053
+ result.inputTokens = answer.inputTokens;
2054
+ result.outputTokens = answer.outputTokens;
2055
+ const response = { inputTokens: answer.inputTokens, outputTokens: answer.outputTokens };
2056
+ const elapsed = Date.now() - startTime;
2057
+ console.log(`[visual-eval] score=${result.finalScore} (${elapsed}ms) | in=${response.inputTokens} out=${response.outputTokens}`);
2058
+ return { result: stamp(result) };
2059
+ }
2060
+ function canvasScopedIssue(canvas, issue) {
2061
+ return { ...issue, description: `[${canvas}] ${issue.description}` };
2062
+ }
2063
+ function aggregateCanvasResults(perCanvas, results) {
2064
+ const n = results.length;
2065
+ const mean = (pick) => Math.round(results.reduce((sum3, r) => sum3 + pick(r), 0) / n);
2066
+ const dimensions = {
2067
+ completeness: mean((r) => r.dimensions.completeness),
2068
+ visualPolish: mean((r) => r.dimensions.visualPolish),
2069
+ interactivity: mean((r) => r.dimensions.interactivity),
2070
+ accessibility: mean((r) => r.dimensions.accessibility),
2071
+ codeQuality: mean((r) => r.dimensions.codeQuality)
2072
+ };
2073
+ const issues = [];
2074
+ const critiques = [];
2075
+ let inputTokens = 0;
2076
+ let outputTokens = 0;
2077
+ results.forEach((r, i) => {
2078
+ const canvas = perCanvas[i].canvas;
2079
+ for (const issue of r.issues) issues.push(canvasScopedIssue(canvas, issue));
2080
+ if (r.critique) critiques.push(`${canvas}: ${r.critique}`);
2081
+ inputTokens += r.inputTokens ?? 0;
2082
+ outputTokens += r.outputTokens ?? 0;
2083
+ });
2084
+ const criteriaSpent = perCanvas.flatMap((c) => c.criteriaTokens !== void 0 ? [c.criteriaTokens] : []);
2085
+ const judgeSalvage = {
2086
+ scoring: {
2087
+ salvaged: perCanvas.reduce((sum3, c) => sum3 + (c.judge.salvaged?.length ?? 0), 0),
2088
+ answers: perCanvas.reduce((sum3, c) => sum3 + c.judge.samples.length, 0)
2089
+ },
2090
+ criteria: {
2091
+ salvaged: perCanvas.filter((c) => c.judge.criteriaSalvaged !== void 0).length,
2092
+ answered: perCanvas.filter((c) => c.criteriaTokens !== void 0).length
2093
+ }
2094
+ };
2095
+ return {
2096
+ judgeSalvage,
2097
+ passed: perCanvas.every((c) => c.passed),
2098
+ finalScore: mean((r) => r.finalScore),
2099
+ dimensions,
2100
+ issues,
2101
+ ...critiques.length > 0 ? { critique: critiques.join("\n") } : {},
2102
+ inputTokens,
2103
+ outputTokens,
2104
+ ...criteriaSpent.length > 0 ? {
2105
+ criteriaTokens: {
2106
+ inputTokens: criteriaSpent.reduce((sum3, t) => sum3 + t.inputTokens, 0),
2107
+ outputTokens: criteriaSpent.reduce((sum3, t) => sum3 + t.outputTokens, 0)
2108
+ }
2109
+ } : {},
2110
+ canvases: [...perCanvas]
2111
+ };
2112
+ }
2113
+ function summarizeVisualResult(result) {
2114
+ if (result.canvases === void 0) return void 0;
2115
+ const canvases = result.canvases.map((c) => ({
2116
+ canvas: c.canvas,
2117
+ viewport: c.viewport,
2118
+ score: c.score,
2119
+ passed: c.passed,
2120
+ contentHeight: c.contentHeight,
2121
+ overflow: c.overflow,
2122
+ inkRatio: c.inkRatio,
2123
+ judge: c.judge,
2124
+ ...c.fit !== void 0 ? { fit: c.fit } : {},
2125
+ ...c.criteria !== void 0 ? { criteria: c.criteria } : {},
2126
+ ...c.presentation !== void 0 ? { presentation: c.presentation } : {}
2127
+ }));
2128
+ const blocks = result.canvases.map((c) => c.criteria).filter((b) => b !== void 0);
2129
+ const idsWhere = (severity, verdict) => [
2130
+ ...new Set(blocks.flatMap((b) => b.verdicts.filter((v) => v.severity === severity && v.verdict === verdict).map((v) => v.id)))
2131
+ ];
2132
+ const rollup = blocks.length > 0 ? { mustFailed: idsWhere("must", "fail"), shouldFailed: idsWhere("should", "fail"), mustNa: idsWhere("must", "n/a") } : void 0;
2133
+ const fitCanvas = result.canvases.find((c) => canvasFitPolicy(c.canvas).overflow === "fail" && c.contentHeight !== null);
2134
+ const fit = fitCanvas !== void 0 && fitCanvas.contentHeight !== null ? {
2135
+ canvas: fitCanvas.canvas,
2136
+ ceiling: { width: fitCanvas.viewport.width, height: fitCanvas.viewport.height },
2137
+ // Declared = the order carried a box, even one equal to the class box (the review's point).
2138
+ declared: fitCanvas.declared === true,
2139
+ overflowPx: Math.max(0, fitCanvas.contentHeight - fitCanvas.viewport.height)
2140
+ } : void 0;
2141
+ return {
2142
+ score: result.finalScore,
2143
+ passed: result.passed,
2144
+ canvases,
2145
+ ...fit !== void 0 ? { fit } : {},
2146
+ ...result.design !== void 0 ? { design: result.design } : {},
2147
+ ...result.themeMode !== void 0 ? { themeMode: result.themeMode } : {},
2148
+ ...rollup !== void 0 ? { criteria: rollup } : {}
2149
+ };
2150
+ }
2151
+ async function judgeStoredCapture(input, config, deps = {}) {
2152
+ const judge = deps.judge ?? callMultimodalLLM;
2153
+ const model = config.model ?? getDefaultVisualModel(config.provider);
2154
+ const { canvas, viewport, png } = input;
2155
+ const chrome = canvasChrome(canvas);
2156
+ const ink = readInk(png, chrome, canvas);
2157
+ const inkRatio = ink?.ratio ?? null;
2158
+ const k = judgeCountFor(config, canvas);
2159
+ const profileBlock = buildStylingProfileJudgeBlock(input.profile);
2160
+ const criteriaContext = input.criteria !== void 0 ? { ...input.criteria.context, canvas } : void 0;
2161
+ const criteriaSelected = input.criteria !== void 0 && criteriaContext !== void 0 ? selectCriteria(input.criteria.bank, criteriaContext) : void 0;
2162
+ const criteriaBlock = input.criteria !== void 0 && criteriaSelected !== void 0 ? buildCriteriaJudgeBlock(input.criteria.bank, criteriaSelected, {
2163
+ frame: { canvas, width: viewport.width, height: viewport.height, fit: input.mountFit ?? (canvasFit(canvas) === "fill" ? "fill" : "card") },
2164
+ ...input.sampleProps !== void 0 ? { propsJson: JSON.stringify(input.sampleProps) } : {}
2165
+ }) : "";
2166
+ const { scoring: answers, criteria: criteriaAnswer } = await judgeFrame(judge, config, model, png, input.originalPrompt, profileBlock, canvas, k, criteriaBlock);
2167
+ const parsed = answers.filter((a) => a.kind === "ok");
2168
+ if (parsed.length === 0) {
2169
+ const first = answers.find((a) => a.kind === "unparsable");
2170
+ return { kind: "unavailable", reason: first?.reason ?? "judge answer unparsable" };
2171
+ }
2172
+ const samples = parsed.map((a) => a.result.finalScore);
2173
+ const median = medianOf(samples);
2174
+ const result = parsed[samples.indexOf(median)].result;
2175
+ const issues = [...result.issues];
2176
+ let passed = median >= config.passThreshold;
2177
+ const contentHeight = input.contentHeight;
2178
+ const overflow = contentHeight !== null && contentHeight > viewport.height;
2179
+ const policy = canvasFitPolicy(canvas);
2180
+ if (overflow && contentHeight !== null && policy.overflow !== "none") {
2181
+ issues.push(canvasOverflowIssue(canvas, viewport, contentHeight, policy.overflow));
2182
+ if (policy.overflow === "fail") passed = false;
2183
+ }
2184
+ if (ink !== null && ink.lastInkRow === null) {
2185
+ issues.push(canvasBlankIssue(canvas, viewport));
2186
+ passed = false;
2187
+ }
2188
+ const criteria = input.criteria !== void 0 && criteriaContext !== void 0 && criteriaSelected !== void 0 ? resolveCriteriaBlock({
2189
+ bank: input.criteria.bank,
2190
+ context: criteriaContext,
2191
+ selected: criteriaSelected,
2192
+ answers: criteriaReads(criteriaAnswer),
2193
+ measurements: { overflow, contentHeight, viewportHeight: viewport.height, inkRatio },
2194
+ ...criteriaUnansweredReason(criteriaAnswer),
2195
+ ...criteriaCallOf(criteriaAnswer)
2196
+ }) : void 0;
2197
+ const criteriaTokens = criteriaCallTokens(criteriaAnswer);
2198
+ return {
2199
+ kind: "ok",
2200
+ verdict: {
2201
+ canvas,
2202
+ viewport,
2203
+ score: median,
2204
+ passed,
2205
+ issues,
2206
+ judge: judgeRecordOf(k, parsed, samples, criteriaAnswer, model),
2207
+ contentHeight,
2208
+ overflow,
2209
+ inkRatio,
2210
+ ...criteria !== void 0 ? { criteria } : {},
2211
+ inputTokens: parsed.reduce((sum3, a) => sum3 + a.inputTokens, 0),
2212
+ outputTokens: parsed.reduce((sum3, a) => sum3 + a.outputTokens, 0),
2213
+ ...criteriaTokens !== void 0 ? { criteriaTokens } : {}
2214
+ }
2215
+ };
2216
+ }
2217
+ function salvageTruncatedVisualAnswer(text, passThreshold) {
2218
+ const dim = (name) => {
2219
+ const m = text.match(new RegExp(`"${name}"\\s*:\\s*(\\d+(?:\\.\\d+)?)`));
2220
+ return m ? Number(m[1]) : null;
2221
+ };
2222
+ const completeness = dim("completeness");
2223
+ const layout = dim("layout");
2224
+ const hierarchy = dim("hierarchy");
2225
+ const aesthetics = dim("aesthetics");
2226
+ if (completeness === null || layout === null || hierarchy === null || aesthetics === null) return null;
2227
+ const issues = [];
2228
+ const issuesAt = text.indexOf('"issues"');
2229
+ if (issuesAt >= 0) {
2230
+ for (const m of text.slice(issuesAt).matchAll(/\{[^{}]*\}/g)) {
2231
+ try {
2232
+ const o = JSON.parse(m[0]);
2233
+ if (typeof o.description === "string") {
2234
+ issues.push({
2235
+ dimension: typeof o.dimension === "string" ? o.dimension : "layout",
2236
+ severity: typeof o.severity === "string" ? o.severity : "minor",
2237
+ description: o.description,
2238
+ fix: typeof o.fix === "string" ? o.fix : ""
2239
+ });
2240
+ }
2241
+ } catch {
2242
+ }
2243
+ }
2244
+ }
2245
+ const rebuilt = JSON.stringify({
2246
+ completeness,
2247
+ layout,
2248
+ hierarchy,
2249
+ aesthetics,
2250
+ issues,
2251
+ critique: `[salvaged judge answer \u2014 it did not parse; score recovered from the closed prefix; ${issues.length} issue(s) kept, the rest and the critique lost]`
2252
+ });
2253
+ return parseVisualResponse(rebuilt, passThreshold);
2254
+ }
2255
+ function parseVisualResponse(text, passThreshold) {
2256
+ const jsonMatch = text.match(/\{[\s\S]*\}/);
2257
+ if (!jsonMatch) {
2258
+ throw new Error("Visual evaluator did not return valid JSON: " + text.slice(0, 200));
2259
+ }
2260
+ const raw = JSON.parse(jsonMatch[0]);
2261
+ const completeness = raw.completeness ?? 0;
2262
+ const layout = raw.layout ?? 0;
2263
+ const hierarchy = raw.hierarchy ?? 0;
2264
+ const aesthetics = raw.aesthetics ?? 0;
2265
+ const finalScore = Math.round(
2266
+ completeness * 0.25 + layout * 0.25 + hierarchy * 0.25 + aesthetics * 0.25
2267
+ );
2268
+ const dimensions = {
2269
+ completeness,
2270
+ visualPolish: aesthetics,
2271
+ interactivity: 0,
2272
+ // can't assess interactivity from screenshot
2273
+ accessibility: 0,
2274
+ // can't assess a11y from screenshot
2275
+ codeQuality: 0
2276
+ // can't assess code from screenshot
2277
+ };
2278
+ const issues = (raw.issues || []).map((i) => ({
2279
+ dimension: i.dimension || "visual",
2280
+ origin: "judge",
2281
+ severity: i.severity || "minor",
2282
+ description: i.description || "",
2283
+ fix: i.fix || ""
2284
+ }));
2285
+ const criteria = parseCriteriaAnswers(raw.criteria);
2286
+ return {
2287
+ passed: finalScore >= passThreshold,
2288
+ finalScore,
2289
+ dimensions,
2290
+ issues,
2291
+ critique: raw.critique,
2292
+ ...criteria.answers !== void 0 ? { criteriaAnswers: criteria.answers } : {},
2293
+ criteriaCounts: criteria.counts
2294
+ };
2295
+ }
2296
+ function parseCriteriaAnswers(raw) {
2297
+ if (!Array.isArray(raw)) return { counts: NO_CRITERIA_ARRAY };
2298
+ const answers = [];
2299
+ const dropped = { notObject: 0, badId: 0, badVerdict: 0 };
2300
+ for (const entry of raw) {
2301
+ if (typeof entry !== "object" || entry === null) {
2302
+ dropped.notObject += 1;
2303
+ continue;
2304
+ }
2305
+ const o = entry;
2306
+ if (typeof o.id !== "string") {
2307
+ dropped.badId += 1;
2308
+ continue;
2309
+ }
2310
+ if (o.verdict !== "pass" && o.verdict !== "fail" && o.verdict !== "n/a") {
2311
+ dropped.badVerdict += 1;
2312
+ continue;
2313
+ }
2314
+ answers.push({ id: o.id, verdict: o.verdict, evidence: typeof o.evidence === "string" ? o.evidence : "" });
2315
+ }
2316
+ return { answers, counts: { array: true, received: raw.length, kept: answers.length, dropped } };
2317
+ }
2318
+ function getDefaultVisualModel(provider) {
2319
+ switch (provider) {
2320
+ case "claude":
2321
+ return "claude-haiku-5-5";
2322
+ case "google":
2323
+ return "gemini-3-flash-preview";
2324
+ }
2325
+ }
2326
+ async function runVisualEval(context, config, deps = {}) {
2327
+ const detailed = await runVisualEvaluationDetailed(context, config, deps);
2328
+ const result = detailed.result;
2329
+ if (!result) {
2330
+ const reason = detailed.unavailableReason ?? "no browser available";
2331
+ return { issues: [], coverage: { status: "skipped", reason } };
2332
+ }
2333
+ const issues = (result.issues || []).map(toEvalIssue);
2334
+ const summary = summarizeVisualResult(result);
2335
+ const coverage = { status: "ran" };
2336
+ const tokens = result.inputTokens !== void 0 && result.outputTokens !== void 0 ? {
2337
+ inputTokens: result.inputTokens,
2338
+ outputTokens: result.outputTokens,
2339
+ ...result.criteriaTokens !== void 0 ? { criteria: result.criteriaTokens } : {}
2340
+ } : void 0;
2341
+ return { issues, ...summary !== void 0 ? { summary } : {}, coverage, ...tokens !== void 0 ? { tokens } : {} };
2342
+ }
2343
+ function toEvalIssue(issue) {
2344
+ return {
2345
+ tier: 2,
2346
+ result: issue.severity === "critical" ? "fail" : "warn",
2347
+ category: "visual",
2348
+ subcategory: issue.dimension,
2349
+ severity: issue.severity === "critical" ? "critical" : "major",
2350
+ description: issue.description,
2351
+ fix: issue.fix || "",
2352
+ ...issue.origin !== void 0 ? { origin: issue.origin } : {}
2353
+ };
2354
+ }
2355
+ async function runVisualFit(context, config, deps = {}) {
2356
+ if (config.canvases === void 0 || config.canvases.length === 0) {
2357
+ return { status: "unavailable", reason: "no canvases \u2014 the fit verdict is taken per canvas" };
2358
+ }
2359
+ const designSrc = config.designSrcDir ?? resolve(resolveDesignPackageDir(), "src");
2360
+ let bundledCode;
2361
+ try {
2362
+ bundledCode = await bundleForRendering(context.compiledCode, config.sampleProps ?? {}, designSrc);
2363
+ } catch (e) {
2364
+ return { status: "unavailable", reason: `bundle failed: ${e instanceof Error ? e.message : String(e)}` };
2365
+ }
2366
+ const issues = [];
2367
+ const readings = [];
2368
+ for (const canvas of config.canvases) {
2369
+ const frame = await frameCanvas(bundledCode, context, canvas, config.canvasViewports, deps, config.hostPresentations);
2370
+ if (!frame.attempt.png) {
2371
+ return { status: "unavailable", reason: `${frame.attempt.reason ?? "no browser available"} at canvas ${canvas}` };
2372
+ }
2373
+ const contentHeight = frame.attempt.contentHeight;
2374
+ const overflow = contentHeight !== null && contentHeight > frame.viewport.height;
2375
+ const verdict = fitVerdict(canvas, frame);
2376
+ if (verdict !== null) issues.push(toEvalIssue(canvasScopedIssue(canvas, verdict)));
2377
+ const overflowXIssue = overflowXVerdict(canvas, frame);
2378
+ if (overflowXIssue !== null) issues.push(toEvalIssue(canvasScopedIssue(canvas, overflowXIssue)));
2379
+ const blank = blankVerdict(canvas, frame);
2380
+ if (blank !== null) issues.push(toEvalIssue(canvasScopedIssue(canvas, blank)));
2381
+ const inkRatio = frame.ink?.ratio ?? null;
2382
+ readings.push({
2383
+ canvas,
2384
+ viewport: frame.viewport,
2385
+ contentHeight,
2386
+ overflow,
2387
+ inkRatio,
2388
+ ...frame.declared ? { declared: true } : {},
2389
+ ...frame.presentation !== void 0 ? { presentation: frame.presentation } : {}
2390
+ });
2391
+ console.log(
2392
+ `[visual-fit] canvas=${canvas} ${frame.viewport.width}\xD7${frame.viewport.height} content=${contentHeight ?? "?"}px${overflow ? ` OVERFLOW (${frame.policy.overflow})` : ""} ink=${inkRatio ?? "?"}${blank !== null ? " BLANK" : ""} | no judge`
2393
+ );
2394
+ }
2395
+ return { status: "measured", issues, readings };
2396
+ }
2397
+
2398
+ export { CARD_BOX_EXPRESSION, CARD_HEIGHT_EXPRESSION, CARD_OVERFLOW_X_EXPRESSION, CONTENT_HEIGHT_EXPRESSION, CRITERIA_SELECTOR_VERSION, EDGE_BLOCKS_EXPRESSION, EDGE_TOLERANCE_PX, EXECUTABLE_PATH_LAUNCH_ARGS, HOST_COLOUR_PATTERN, HOST_FRAME_BOUNDS, INK_TOLERANCE, INSTRUMENT_BESIDE_JUDGE, INSTRUMENT_BY_ID, JUDGE_GROUND_CLASS, JUDGE_GROUND_MARGIN_PX, JUDGE_INK_INSET_PX, JUDGE_INLINE_FRAME_BORDER_PX, JUDGE_INLINE_FRAME_CLASS, JUDGE_INLINE_FRAME_RADIUS_PX, JUDGE_INLINE_INK_INSET_PX, JUDGE_INLINE_PAD_PX, JUDGE_PANEL_CLASS, JUDGE_SCOPE_CLASS, OVERFLOW_X_ROUNDING_PX, VISUAL_EVAL_PROMPT, VISUAL_JUDGE_MAX_OUTPUT_TOKENS, VISUAL_JUDGE_PROMPT_DIGEST, VISUAL_JUDGE_PROMPT_VERSION, bankRows, buildCriteriaJudgeBlock, buildRenderHTML, bundleForRendering, callMultimodalLLM, canonicalCriteriaContext, canvasBlankIssue, canvasChrome, canvasFit, canvasFitPolicy, canvasOverflowIssue, canvasOverflowXIssue, captureScreenshot, captureScreenshotDetailed, cardBoxClip, cardHeightExpression, criteriaBankSchema, criteriaSetIdFor, decodePng, designTreeSha256, findPackageRootAbove, fitCeiling, integerBox, isDeclaredViewport, judgeDesignIdentity, judgeEdges, judgeStoredCapture, judgeWindow, mountCanvasOf, naturalClip, parseCriteriaBank, parseEdgeProbe, readInkExtent, resetJudgeDesignIdentityCache, resolveCriteriaBlock, resolveDesignPackageDir, resolveLaunchOptions, resolveUiGenPackageDir, resolveWirePackageDir, runVisualEval, runVisualEvaluation, runVisualEvaluationDetailed, runVisualFit, salvageTruncatedVisualAnswer, selectCriteria, summarizeVisualResult };
2399
+ //# sourceMappingURL=chunk-732IBFEJ.js.map
2400
+ //# sourceMappingURL=chunk-732IBFEJ.js.map