@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
@@ -8,6 +8,8 @@
8
8
  * id; a tie or an unanswered id reads `n/a` (the CTO's A2) — a `must` at `n/a` is never a pass when
9
9
  * a gate later reads the set.
10
10
  */
11
+ import { EDGE_INSTRUMENT_IDS } from './edge-instruments.js';
12
+ import type { MountFit } from '@ggui-ai/design/rendering';
11
13
  import type { CriteriaAnswer } from '../types.js';
12
14
  import type { CriteriaAnswerCounts, CriteriaBlock, CriteriaContext, CriteriaVerdict, CriterionVerdict } from '../types-public.js';
13
15
  import type { VisionFinishReason } from '../../harness/llm-router.js';
@@ -50,14 +52,20 @@ export interface CriteriaMeasurements {
50
52
  * `null` when the probe ran and failed.
51
53
  */
52
54
  readonly edge?: EdgeVerdict | null;
55
+ /** #1765 — the v2 edge rule on the same capture, read beside v1: absent / `null` as for `edge`. */
56
+ readonly edgeV2?: EdgeVerdict | null;
53
57
  /** ggui#1711 — the capture includes a state after an action ran. Absent = no (every capture today). */
54
58
  readonly postActionCapture?: boolean;
55
59
  }
56
60
 
57
61
  function besideRead(kind: 'edge', m: CriteriaMeasurements): Read {
58
- if (m.edge === undefined) return { verdict: 'n/a', evidence: 'not measured: this capture had no page to read' };
59
- if (m.edge === null) return { verdict: 'n/a', evidence: 'edge probe failed on this capture' };
60
- return { verdict: m.edge.verdict, evidence: m.edge.evidence };
62
+ return edgeRead(m.edge);
63
+ }
64
+
65
+ function edgeRead(e: EdgeVerdict | null | undefined): Read {
66
+ if (e === undefined) return { verdict: 'n/a', evidence: 'not measured: this capture had no page to read' };
67
+ if (e === null) return { verdict: 'n/a', evidence: 'edge probe failed on this capture' };
68
+ return { verdict: e.verdict, evidence: e.evidence };
61
69
  }
62
70
 
63
71
  type Read = { verdict: CriteriaVerdict; evidence: string };
@@ -122,7 +130,13 @@ export function resolveCriteriaBlock(args: {
122
130
  source: s.source,
123
131
  verdict: read.verdict,
124
132
  evidence: read.evidence,
125
- ...(beside !== undefined && row.evaluation === 'judge' ? { instrument: besideRead(beside, args.measurements) } : {}),
133
+ ...(beside !== undefined && row.evaluation === 'judge'
134
+ ? {
135
+ instrument: { ...besideRead(beside, args.measurements), id: EDGE_INSTRUMENT_IDS.v1 },
136
+ // #1765 — v2 beside v1 on the same capture, report-only, until its own fresh sealed draw.
137
+ instrumentV2: { ...edgeRead(args.measurements.edgeV2), id: EDGE_INSTRUMENT_IDS.v2 },
138
+ }
139
+ : {}),
126
140
  });
127
141
  }
128
142
  return {
@@ -153,6 +167,8 @@ export interface CriteriaJudgeFrame {
153
167
  readonly height: number;
154
168
  /** ggui#1492 — the host whose presentation framed this canvas (e.g. "a chat widget, inline, reference theme"); absent ⇒ the judge's own stand-in. */
155
169
  readonly hostLabel?: string;
170
+ /** How the card was composed on this frame (`resolveMountFit`, the one rule the runtime serves by); the sentence follows it. */
171
+ readonly fit: MountFit;
156
172
  }
157
173
 
158
174
  /** What the block carries besides the selected rows: the frame's box, and the props the card was rendered with. */
@@ -178,7 +194,10 @@ export const CRITERIA_PROPS_MAX_CHARS = 2000;
178
194
  */
179
195
  export function criteriaFrameLine(frame: CriteriaJudgeFrame): string {
180
196
  const box = `${frame.width}×${frame.height}`;
181
- if (frame.canvas === 'xs-chat-card') {
197
+ if (frame.fit === 'framed-card') {
198
+ return `This frame is ${box}${frame.hostLabel !== undefined ? ` (${frame.hostLabel})` : ''} with the card at its natural height at the top, inside the card's own frame — its surface, a thin ring of its ink and rounded corners — 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.`;
199
+ }
200
+ if (frame.fit === 'card') {
182
201
  const framedBy =
183
202
  frame.hostLabel !== undefined
184
203
  ? `inside the host's own frame (${frame.hostLabel}), on the host's ground`
@@ -218,9 +218,9 @@ function buildEvalResult(
218
218
 
219
219
  function getDefaultEvalModel(provider: string): string {
220
220
  switch (provider) {
221
- case 'claude': return 'claude-haiku-4-5-20251001';
221
+ case 'claude': return 'claude-haiku-5-5';
222
222
  case 'openai': return 'gpt-5.6-luna';
223
223
  case 'google': return 'gemini-3.5-flash-lite';
224
- default: return 'claude-haiku-4-5-20251001';
224
+ default: return 'claude-haiku-5-5';
225
225
  }
226
226
  }
@@ -11,8 +11,8 @@
11
11
  // - `./types.js` — the post-eval result shape (`DimensionScores`,
12
12
  // `EvaluationResult`, `EvaluationIssue`, `QualityMetadata`,
13
13
  // `EvaluationConfig`, `EvaluationContext`).
14
- // Plus the runtime cluster (`runEvaluationLoop`, `runEvaluation`,
15
- // `runAxisChecks`, evaluator MCP server, prompts, message parsing).
14
+ // Plus the runtime cluster (`runEvaluation`, `runAxisChecks`, score
15
+ // arithmetic, prompts, message parsing).
16
16
 
17
17
  // Vocabulary the harness legs depend on (was: flat `packages/ui-gen/src/evaluation.ts`).
18
18
  export type {
@@ -55,10 +55,8 @@ export {
55
55
  } from './types-public.js';
56
56
 
57
57
  // Cloud-ported runtime cluster.
58
- export { runEvaluationLoop } from './loop.js';
59
- export type { EvaluationLoopOptions, EvaluationLoopResult } from './loop.js';
60
58
  export { runEvaluation } from './evaluator.js';
61
- export { createEvaluationToolsServer, computeEvaluationScore } from './mcp-server.js';
59
+ export { computeEvaluationScore } from './mcp-server.js';
62
60
  export type { EvaluateScoreInput } from './mcp-server.js';
63
61
  export { getEvaluatorSystemPrompt, buildFixPrompt } from './prompts.js';
64
62
  export {
@@ -568,13 +568,13 @@ Use 'fail' only for pervasive design system violations. Use 'warn' for isolated
568
568
  function getDefaultEvalModel(provider: string): string {
569
569
  switch (provider) {
570
570
  case 'claude':
571
- return 'claude-haiku-4-5-20251001';
571
+ return 'claude-haiku-5-5';
572
572
  case 'openai':
573
573
  return 'gpt-5.6-luna';
574
574
  case 'google':
575
575
  return 'gemini-3.5-flash-lite';
576
576
  default:
577
- return 'claude-haiku-4-5-20251001';
577
+ return 'claude-haiku-5-5';
578
578
  }
579
579
  }
580
580
 
@@ -1,7 +1,5 @@
1
1
  // packages/ui-gen/src/evaluation/mcp-server.ts
2
2
 
3
- import { createSdkMcpServer, tool } from '@anthropic-ai/claude-agent-sdk';
4
- import { z } from 'zod';
5
3
  import type { EvaluationIssue, EvaluationResult } from './types';
6
4
 
7
5
  /**
@@ -51,51 +49,3 @@ export function computeEvaluationScore(
51
49
  ...(args.critique && { critique: args.critique }),
52
50
  };
53
51
  }
54
-
55
- /**
56
- * Create the evaluation MCP server with the evaluate_score tool.
57
- *
58
- * The evaluator LLM provides qualitative scores per dimension.
59
- * This tool handles the arithmetic (average, pass/fail) so the LLM
60
- * doesn't need to do math.
61
- */
62
- export function createEvaluationToolsServer(passThreshold = 70) {
63
- return createSdkMcpServer({
64
- name: 'eval-tools',
65
- version: '1.0.0',
66
- tools: [
67
- tool(
68
- 'evaluate_score',
69
- 'Compute evaluation score from dimension ratings. Call this after analyzing the component code against all 5 dimensions.',
70
- {
71
- completeness: z.number().min(0).max(100).describe('Score for feature completeness (0-100)'),
72
- visualPolish: z.number().min(0).max(100).describe('Score for visual polish and design (0-100)'),
73
- interactivity: z.number().min(0).max(100).describe('Score for interactivity and state management (0-100)'),
74
- accessibility: z.number().min(0).max(100).describe('Score for accessibility (ARIA, keyboard nav, contrast) (0-100)'),
75
- codeQuality: z.number().min(0).max(100).describe('Score for code quality and structure (0-100)'),
76
- issues: z.array(
77
- z.object({
78
- dimension: z.string().describe('Which dimension this issue affects'),
79
- description: z.string().describe('What the issue is'),
80
- severity: z.enum(['critical', 'major', 'minor']).describe('Issue severity'),
81
- fix: z.string().describe('How to fix this issue'),
82
- })
83
- ).describe('List of specific issues found'),
84
- critique: z.string().optional().describe('Optional overall critique summary'),
85
- },
86
- async (args) => {
87
- const result = computeEvaluationScore(args, passThreshold);
88
-
89
- return {
90
- content: [
91
- {
92
- type: 'text',
93
- text: JSON.stringify(result, null, 2),
94
- },
95
- ],
96
- };
97
- }
98
- ),
99
- ],
100
- });
101
- }
@@ -477,7 +477,12 @@ export interface CriterionVerdict {
477
477
  * ggui#1663 — a measured reading beside a judge row, REPORT-ONLY: the row's `verdict` stays the judge's, so this
478
478
  * changes no verdict, score or bar. Present only on rows an instrument answers beside the judge (`space.edge`).
479
479
  */
480
- instrument?: { verdict: CriteriaVerdict; evidence: string };
480
+ instrument?: { verdict: CriteriaVerdict; evidence: string; id?: string };
481
+ /**
482
+ * #1765 — the rewritten edge rule (`space.edge.v2`), a SEPARATE instrument read beside v1 on the same capture,
483
+ * REPORT-ONLY like `instrument`; `id` names the probe that produced it. Present only beside `space.edge`.
484
+ */
485
+ instrumentV2?: { verdict: CriteriaVerdict; evidence: string; id: string };
481
486
  }
482
487
  /** ggui#1687 — the criteria call as it came back: its answer's counts, ids it answered outside the bank, its stop. */
483
488
  export interface CriteriaCallRecord {
@@ -21,13 +21,13 @@ import type { CriteriaContextInput } from './criteria/context.js';
21
21
  import { selectCriteria } from './criteria/select.js';
22
22
  import { buildCriteriaJudgeBlock, resolveCriteriaBlock } from './criteria/resolve.js';
23
23
  import { build } from 'esbuild';
24
- import { EXPANDED_FRAME, expandedFramePanelRule, expandedFrameScrimDecls, fillFitRule, getCssTokens } from '@ggui-ai/design/rendering';
24
+ import { EXPANDED_FRAME, FRAMED_CARD_GROUND, GGUI_MOUNT_CANVAS_EXPORT, mountFitCss, expandedFramePanelDecls, expandedFramePanelRule, expandedFrameScrimDecls, getCssTokens, inlineCardRadius, resolveMountFit, type MountFit } from '@ggui-ai/design/rendering';
25
25
  import { readInkExtent, type InkExtent } from './ink-extent.js';
26
26
  import { judgeDesignIdentity, type JudgeDesignIdentity } from './design-identity.js';
27
27
  import { resolve, dirname } from 'path';
28
28
  import { fileURLToPath } from 'url';
29
29
  import { createRequire } from 'node:module';
30
- import { existsSync, writeFileSync, mkdtempSync, rmSync } from 'fs';
30
+ import { existsSync, readFileSync, writeFileSync, mkdtempSync, rmSync } from 'fs';
31
31
  import { createHash } from 'node:crypto';
32
32
  import { tmpdir } from 'os';
33
33
  import { createVisionAgent, type AgentConfig, type VisionFinishReason } from '../harness/llm-router';
@@ -45,6 +45,8 @@ import type { LaunchOptions } from 'puppeteer-core';
45
45
  import { CANVAS_VIEWPORTS, displayModeForCanvas, type CanvasClass, type CanvasViewport } from '../design-mode.js';
46
46
  import { judgeEdges, type EdgeVerdict } from './criteria/edge.js';
47
47
  import { EDGE_BLOCKS_EXPRESSION, parseEdgeProbe } from './criteria/edge-probe.js';
48
+ import { EDGE_BLOCKS_V2_EXPRESSION, parseEdgeProbeV2 } from './criteria/edge-probe-v2.js';
49
+ import { judgeEdgesV2 } from './criteria/edge-v2.js';
48
50
 
49
51
  // ---------------------------------------------------------------------------
50
52
  // Types
@@ -176,7 +178,12 @@ export interface CanvasVisualResult {
176
178
  }
177
179
 
178
180
  /** How a canvas is captured for the judge and what an overflow means there (ggui#1027). */
179
- export type CaptureMode = 'viewport' | 'full-page' | 'natural';
181
+ /**
182
+ * How a canvas is captured. `card-box` (ggui#1953): a framed card (a chat-width card on a fullscreen surface,
183
+ * ggui#1924) is captured as the inline card is — its OWN box at its natural extent, plus the frame ground's margin on
184
+ * every side, ring and corners visible — never the pane round it, whose empty ground is the host's, not the card's.
185
+ */
186
+ export type CaptureMode = 'viewport' | 'full-page' | 'natural' | 'card-box';
180
187
  export interface CanvasFitPolicy {
181
188
  /**
182
189
  * `viewport` = the whole box; `full-page` = the whole scrolled document; `natural` (ggui#1475) = the
@@ -501,8 +508,24 @@ export interface VisualEvalContext {
501
508
  * same place the harness's own imports come from.
502
509
  */
503
510
  export function resolveUiGenPackageDir(): string {
504
- const selfDir = dirname(fileURLToPath(import.meta.url));
505
- return resolve(selfDir, '..', '..');
511
+ // ggui#1769: found by walking up to this package's own manifest, never by counting directories. The code moves
512
+ // with the bundle's layout: under `src/evaluation/` and the old per-entry `dist/evaluation/` the package root
513
+ // was two levels up, but a shared chunk sits at `dist/` itself, one level up.
514
+ return findPackageRootAbove(dirname(fileURLToPath(import.meta.url)), '@ggui-ai/ui-gen');
515
+ }
516
+
517
+ /** The nearest directory at or above `start` whose `package.json` is named `name`; throws when there is none. */
518
+ export function findPackageRootAbove(start: string, name: string): string {
519
+ for (let dir = start; ; dir = dirname(dir)) {
520
+ const manifest = resolve(dir, 'package.json');
521
+ if (existsSync(manifest) && readPackageName(manifest) === name) return dir;
522
+ if (dirname(dir) === dir) throw new Error(`${name}: no package.json named ${name} at or above ${start}`);
523
+ }
524
+ }
525
+
526
+ function readPackageName(manifest: string): string | undefined {
527
+ const parsed: unknown = JSON.parse(readFileSync(manifest, 'utf8'));
528
+ return typeof parsed === 'object' && parsed !== null && 'name' in parsed && typeof parsed.name === 'string' ? parsed.name : undefined;
506
529
  }
507
530
 
508
531
  /**
@@ -686,8 +709,11 @@ function buildRenderHTML(
686
709
  fit?: 'fill',
687
710
  chrome?: CanvasChrome,
688
711
  presentation?: CanvasHostPresentation,
712
+ /** Set for a `framed-card` mount: the card frame's radius ({@link inlineCardRadius}); `null` keeps the theme's `lg` token. */
713
+ framedCardRadiusPx?: number | null,
689
714
  ): string {
690
- if (chrome === 'panel' && fit !== 'fill') throw new Error("the judge's panel frames a fill canvas only (ggui#1083 cut 3)");
715
+ const framedCard = framedCardRadiusPx !== undefined;
716
+ if (chrome === 'panel' && fit !== 'fill' && !framedCard) throw new Error("the judge's panel frames a fill canvas only (ggui#1083 cut 3)");
691
717
  if (chrome === 'ground' && fit === 'fill') throw new Error("the host ground frames an inline card only — a fill canvas has no natural height (ggui#1475)");
692
718
  // Default to the design tokens production's no-theme branch injects
693
719
  // (getCssTokens → default theme, light) — ggui#613: under the s4
@@ -714,12 +740,15 @@ function buildRenderHTML(
714
740
  color: var(--ggui-color-neutral-900, #111827);
715
741
  }
716
742
  .error { color: #dc2626; padding: 16px; font-family: monospace; white-space: pre-wrap; }
717
- ${fit === 'fill' ? fillFitRule(JUDGE_SCOPE_CLASS) + (chrome === 'panel' ? expandedFramePanelRule(JUDGE_PANEL_CLASS, JUDGE_SCOPE_CLASS) : '') : ''}
743
+ ${/* ggui#1765 — the fit's rules come from the ONE selection the runtime's composeThemeCss uses. */ mountFitCss(JUDGE_SCOPE_CLASS, fit === 'fill' ? 'fill' : framedCard ? 'framed-card' : 'card', framedCardRadiusPx ?? null)}
744
+ ${fit === 'fill' && chrome === 'panel' ? expandedFramePanelRule(JUDGE_PANEL_CLASS, JUDGE_SCOPE_CLASS) : ''}
745
+ ${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); }` : ''}
746
+ ${framedCard && chrome !== 'panel' ? `body { background: ${FRAMED_CARD_GROUND}; }` : ''}
718
747
  ${chrome === 'ground' ? `.${JUDGE_GROUND_CLASS} { padding: ${JUDGE_GROUND_MARGIN_PX}px; } .${JUDGE_INLINE_FRAME_CLASS} { ${inlineFrameDecls(presentation?.frame)} }` : ''}
719
748
  </style>
720
749
  </head>
721
750
  <body>
722
- ${chrome === 'panel' ? `<div class="${JUDGE_PANEL_CLASS}">` : chrome === 'ground' ? `<div class="${JUDGE_GROUND_CLASS}"><div class="${JUDGE_INLINE_FRAME_CLASS}">` : ''}<div id="root"${fit === 'fill' ? ` class="${JUDGE_SCOPE_CLASS}"` : ''}></div>${chrome === 'panel' ? '</div>' : chrome === 'ground' ? '</div></div>' : ''}
751
+ ${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>' : ''}
723
752
  <script type="importmap">
724
753
  {
725
754
  "imports": {
@@ -856,6 +885,8 @@ export interface ScreenshotAttempt {
856
885
  * probe failed. Absent on a failed capture.
857
886
  */
858
887
  readonly edge?: EdgeVerdict | null;
888
+ /** #1765 — the v2 edge rule on the same DOM, read beside `edge` (report-only); `null` when its probe failed. */
889
+ readonly edgeV2?: EdgeVerdict | null;
859
890
  }
860
891
 
861
892
  /** The expression the fit measurement evaluates in the page — the taller of the two scroll heights. */
@@ -902,6 +933,33 @@ export function naturalClip(window: { width: number; height: number }, cardHeigh
902
933
  return { x: 0, y: 0, width: window.width, height: Math.max(1, Math.min(Math.ceil(cardHeightPx), box)) + 2 * m };
903
934
  }
904
935
 
936
+ /** The expression a card-box capture reads: the mount's own border box, in page CSS px (ggui#1953). */
937
+ export const 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 }; })()`;
938
+
939
+ async function measureCardBox(page: ScreenshotPage): Promise<{ x: number; y: number; width: number; height: number } | null> {
940
+ try {
941
+ const v: unknown = await page.evaluate(CARD_BOX_EXPRESSION);
942
+ if (typeof v !== 'object' || v === null) return null;
943
+ const n = (k: string): number | null => { const x = Reflect.get(v, k); return typeof x === 'number' && Number.isFinite(x) ? x : null; };
944
+ const [x, y, width, height] = [n('x'), n('y'), n('width'), n('height')];
945
+ return x === null || y === null || width === null || height === null || width <= 0 || height <= 0 ? null : { x, y, width, height };
946
+ } catch (e) {
947
+ console.warn(`[visual-eval] card box unavailable: ${e instanceof Error ? e.message : String(e)}`);
948
+ return null;
949
+ }
950
+ }
951
+
952
+ /**
953
+ * The card-box capture's region (ggui#1953): the card's own box with the frame ground's margin on every side, clamped
954
+ * to the window. The card is never cropped above its own bottom edge while the window can hold it.
955
+ */
956
+ export function cardBoxClip(window: { width: number; height: number }, box: { x: number; y: number; width: number; height: number }): ScreenshotClip {
957
+ const m = JUDGE_GROUND_MARGIN_PX;
958
+ const x = Math.max(0, Math.floor(box.x - m));
959
+ const y = Math.max(0, Math.floor(box.y - m));
960
+ 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))) };
961
+ }
962
+
905
963
  /** Measure the rendered document's height (or the card's, for a natural capture); a failed measurement is reported, never a failed capture. */
906
964
  async function measureContentHeight(page: ScreenshotPage, expression: string = CONTENT_HEIGHT_EXPRESSION): Promise<number | null> {
907
965
  try {
@@ -917,7 +975,26 @@ async function measureContentHeight(page: ScreenshotPage, expression: string = C
917
975
  * ggui#1663 — read the edge rule off the page the screenshot is about to take. The probe only reads layout (it never
918
976
  * writes to the DOM), so the frame is unchanged; a failed read is reported and returns `null`, never a failed capture.
919
977
  */
920
- async function measureEdges(page: ScreenshotPage): Promise<EdgeVerdict | null> {
978
+ async function measureEdges(page: ScreenshotPage): Promise<{ edge: EdgeVerdict | null; edgeV2: EdgeVerdict | null }> {
979
+ return { edge: await measureEdgesV1(page), edgeV2: await measureEdgesV2(page) };
980
+ }
981
+
982
+ /** #1765 — the v2 rule on the same page, beside v1; a failed read is reported and returns `null`, never a failed capture. */
983
+ async function measureEdgesV2(page: ScreenshotPage): Promise<EdgeVerdict | null> {
984
+ try {
985
+ const probe = parseEdgeProbeV2(await page.evaluate(EDGE_BLOCKS_V2_EXPRESSION));
986
+ if (probe === null) {
987
+ console.warn('[visual-eval] edge probe v2 unavailable: the page returned no readable blocks');
988
+ return null;
989
+ }
990
+ return judgeEdgesV2(probe.blocks, { cardContent: probe.cardContent, placeholdersOnly: probe.placeholdersOnly });
991
+ } catch (e) {
992
+ console.warn(`[visual-eval] edge probe v2 unavailable: ${e instanceof Error ? e.message : String(e)}`);
993
+ return null;
994
+ }
995
+ }
996
+
997
+ async function measureEdgesV1(page: ScreenshotPage): Promise<EdgeVerdict | null> {
921
998
  try {
922
999
  const probe = parseEdgeProbe(await page.evaluate(EDGE_BLOCKS_EXPRESSION));
923
1000
  if (probe === null) {
@@ -969,14 +1046,26 @@ export async function captureScreenshotDetailed(
969
1046
  }
970
1047
  const framed = cardHeightPx === null ? viewport.height - 2 * natural.padPx : floorPx !== undefined ? Math.max(cardHeightPx, floorPx) : cardHeightPx;
971
1048
  const clip = naturalClip(viewport, framed, natural.padPx);
972
- const edge = await measureEdges(page);
1049
+ const edges = await measureEdges(page);
973
1050
  const screenshot = await page.screenshot({ type: 'png', fullPage: false, clip });
974
- return { png: Buffer.from(screenshot), contentHeight: cardHeightPx, overflowX, edge };
1051
+ return { png: Buffer.from(screenshot), contentHeight: cardHeightPx, overflowX, ...edges };
1052
+ }
1053
+ if (capture === 'card-box') {
1054
+ // ggui#1953 — the card's own box (the mount, which wears the card frame), so a root that stretches itself still
1055
+ // shows its void, and the host's empty ground below a short card is never in the frame.
1056
+ const box = await measureCardBox(page);
1057
+ // Fail closed, named: a card-box capture that cannot find the card's own box never falls back to the pane, which
1058
+ // is the frame this capture exists to stop judging (and would read as the old defect, silently).
1059
+ if (box === null) return { png: null, reason: `card-box capture: the card's own box (.${JUDGE_SCOPE_CLASS}) could not be measured`, contentHeight: null };
1060
+ const clip = cardBoxClip(viewport, box);
1061
+ const edges = await measureEdges(page);
1062
+ const screenshot = await page.screenshot({ type: 'png', fullPage: false, clip });
1063
+ return { png: Buffer.from(screenshot), contentHeight: Math.round(box.height), ...edges };
975
1064
  }
976
1065
  const contentHeight = await measureContentHeight(page);
977
- const edge = await measureEdges(page);
1066
+ const edges = await measureEdges(page);
978
1067
  const screenshot = await page.screenshot({ type: 'png', fullPage: capture === 'full-page' });
979
- return { png: Buffer.from(screenshot), contentHeight, edge };
1068
+ return { png: Buffer.from(screenshot), contentHeight, ...edges };
980
1069
  } finally {
981
1070
  await browser.close();
982
1071
  }
@@ -1137,9 +1226,29 @@ export interface VisualEvalDetailed {
1137
1226
  * ({@link runVisualFit}) frame through this ONE function, so a fit verdict
1138
1227
  * reached without the judge is taken on the frame the judge would have seen.
1139
1228
  */
1229
+ /**
1230
+ * The canvas a compiled bundle declares in its {@link GGUI_MOUNT_CANVAS_EXPORT} export, read from the code as the runtime
1231
+ * reads it from the module: the binding exported under that name (`export{r as gguiMountCanvas}` minified, or the plain
1232
+ * `const gguiMountCanvas`) and its string literal. Untrusted: `resolveMountFit` narrows it, and anything unread is `undefined`.
1233
+ */
1234
+ export function mountCanvasOf(compiledCode: string): string | undefined {
1235
+ const name = GGUI_MOUNT_CANVAS_EXPORT;
1236
+ const binding = new RegExp(`([A-Za-z_$][\\w$]*)\\s+as\\s+${name}\\b`).exec(compiledCode)?.[1] ?? name;
1237
+ return new RegExp(`(?:^|[^\\w$.])${binding.replace(/\$/g, '\\$')}\\s*=\\s*"([^"\\\\]*)"`).exec(compiledCode)?.[1];
1238
+ }
1239
+ /** The `--ggui-*` custom properties a composed token block declares (the last declaration of a name wins, as in CSS). */
1240
+ function themeVarsOf(css: string): Readonly<Record<string, string>> {
1241
+ const vars: Record<string, string> = {};
1242
+ for (const [, name, value] of css.matchAll(/(--ggui-[\w-]+)\s*:\s*([^;}]+)/g)) {
1243
+ if (name !== undefined && value !== undefined) vars[name] = value.trim();
1244
+ }
1245
+ return vars;
1246
+ }
1140
1247
  interface CanvasFrame {
1141
1248
  readonly viewport: CanvasViewport;
1142
1249
  readonly policy: CanvasFitPolicy;
1250
+ /** How the runtime would compose this card on this canvas (`resolveMountFit`) — the capture and the frame statement follow it. */
1251
+ readonly mountFit: MountFit;
1143
1252
  readonly fit: 'fill' | undefined;
1144
1253
  /** The host stand-in drawn round the scope (ggui#1083 cut 3) — the window is padded by its gap; `viewport` stays the card's box. */
1145
1254
  readonly chrome: CanvasChrome;
@@ -1174,7 +1283,6 @@ function blankVerdict(canvas: CanvasClass, frame: CanvasFrame): VisualIssue | nu
1174
1283
  async function frameCanvas(
1175
1284
  bundledCode: string,
1176
1285
  context: VisualEvalContext,
1177
- html: string,
1178
1286
  canvas: CanvasClass,
1179
1287
  canvasViewports: VisualEvalConfig['canvasViewports'],
1180
1288
  deps: ScreenshotDeps,
@@ -1182,27 +1290,48 @@ async function frameCanvas(
1182
1290
  ): Promise<CanvasFrame> {
1183
1291
  const declaredBox = canvasViewports?.[canvas];
1184
1292
  const viewport = declaredBox !== undefined ? integerBox(declaredBox) : CANVAS_VIEWPORTS[canvas];
1185
- const policy = canvasFitPolicy(canvas);
1186
- const fit = canvasFit(canvas);
1293
+ // The ONE fit rule the runtime serves by: the bundle's canvas export + the display mode this canvas stands for.
1294
+ const mountFit = resolveMountFit({ canvas: mountCanvasOf(context.compiledCode), displayMode: displayModeForCanvas(canvas) });
1295
+ // ggui#1953 — the fit decides the overflow posture: a framed card is a card, seen whole in its room, so a card that
1296
+ // cannot fit its pane fails the canvas as the inline card does (#1475), never scrolls as a page.
1297
+ const policy: CanvasFitPolicy = mountFit === 'framed-card' ? { capture: 'card-box', overflow: 'fail' } : canvasFitPolicy(canvas);
1298
+ const fit = mountFit === 'framed-card' ? undefined : canvasFit(canvas);
1187
1299
  const chrome = canvasChrome(canvas);
1188
1300
  const outcome = resolvePresentation(canvas, hostPresentations?.[canvas]);
1189
1301
  const presentation = outcome?.status === 'applied' ? outcome.applied : undefined;
1190
1302
  const geometry = inlineGeometry(presentation?.frame);
1191
- const canvasHtml =
1192
- fit !== undefined || chrome !== undefined ? buildRenderHTML(bundledCode, context.cssTokens, fit, chrome, presentation) : html;
1193
- const captured = await captureScreenshotDetailed(canvasHtml, judgeWindow(viewport, chrome, geometry.padPx), deps, policy.capture, {
1303
+ const framedCardRadiusPx = mountFit === 'framed-card' ? inlineCardRadius(themeVarsOf(context.cssTokens ?? getCssTokens())).radiusPx : undefined;
1304
+ // Every canvas's page is composed by the one builder, from this canvas's fit, chrome and frame: a framed card on a
1305
+ // canvas with no chrome (the phone, ggui#1953) is still composed as the runtime serves it, never the plain page.
1306
+ const canvasHtml = buildRenderHTML(bundledCode, context.cssTokens, fit, chrome, presentation, framedCardRadiusPx);
1307
+ // ggui#1953 — a framed card is captured as a card, on its own box, at every canvas it is framed on.
1308
+ const capture: CaptureMode = policy.capture;
1309
+ const captured = await captureScreenshotDetailed(canvasHtml, judgeWindow(viewport, chrome, geometry.padPx), deps, capture, {
1194
1310
  padPx: geometry.padPx,
1195
1311
  ...(geometry.floorPx !== undefined ? { floorPx: geometry.floorPx } : {}),
1196
1312
  });
1197
1313
  // ggui#1083 cut 3 — on a panelled page the document is the panel plus its gap; every verdict reads the CARD's height.
1198
- const attempt: ScreenshotAttempt = { ...captured, contentHeight: cardHeight(captured.contentHeight, chrome) };
1314
+ // A card-box capture already measured the card itself; only the full-page document carries the panel's gap.
1315
+ const attempt: ScreenshotAttempt = { ...captured, contentHeight: capture === 'card-box' ? captured.contentHeight : cardHeight(captured.contentHeight, chrome) };
1316
+ // A card-box frame has the inline card's geometry (margin, ring, corner), so its ink is read past the same inset.
1317
+ const cardBoxInk = JUDGE_GROUND_MARGIN_PX + JUDGE_INLINE_FRAME_BORDER_PX + (framedCardRadiusPx ?? JUDGE_INLINE_FRAME_RADIUS_PX);
1199
1318
  return {
1200
- viewport, policy, fit, chrome, declared: declaredBox !== undefined, attempt,
1201
- ink: readInk(captured.png, chrome, canvas, geometry.inkInsetPx),
1319
+ viewport, policy, mountFit, fit, chrome, declared: declaredBox !== undefined, attempt,
1320
+ ink: capture === 'card-box' ? readInk(captured.png, 'ground', canvas, cardBoxInk) : readInk(captured.png, chrome, canvas, geometry.inkInsetPx),
1202
1321
  ...(outcome !== undefined ? { presentation: outcome } : {}),
1203
1322
  };
1204
1323
  }
1205
1324
 
1325
+ /**
1326
+ * The box a frame's card must fit (ggui#1953): the canvas box, except for a framed card, which sits in its pane with
1327
+ * the frame's gap above and below — so its ceiling is the pane less that gap on both sides (704 in a 736 pane).
1328
+ */
1329
+ export function fitCeiling(frame: Pick<CanvasFrame, 'viewport' | 'mountFit'>): { width: number; height: number } {
1330
+ return frame.mountFit === 'framed-card'
1331
+ ? { width: frame.viewport.width, height: frame.viewport.height - 2 * EXPANDED_FRAME.insetPx }
1332
+ : frame.viewport;
1333
+ }
1334
+
1206
1335
  /**
1207
1336
  * The deterministic fit verdict on one frame (ggui#1027): the issue the
1208
1337
  * canvas's policy gives an overflow, or `null` when the content fits, could
@@ -1210,8 +1339,9 @@ async function frameCanvas(
1210
1339
  */
1211
1340
  function fitVerdict(canvas: CanvasClass, frame: CanvasFrame): VisualIssue | null {
1212
1341
  const contentHeight = frame.attempt.contentHeight;
1213
- if (contentHeight === null || contentHeight <= frame.viewport.height || frame.policy.overflow === 'none') return null;
1214
- return canvasOverflowIssue(canvas, frame.viewport, contentHeight, frame.policy.overflow);
1342
+ const ceiling = fitCeiling(frame);
1343
+ if (contentHeight === null || contentHeight <= ceiling.height || frame.policy.overflow === 'none') return null;
1344
+ return canvasOverflowIssue(canvas, ceiling, contentHeight, frame.policy.overflow);
1215
1345
  }
1216
1346
 
1217
1347
  /**
@@ -1471,7 +1601,7 @@ export async function runVisualEvaluationDetailed(
1471
1601
  const perCanvas: CanvasVisualResult[] = [];
1472
1602
  const perCanvasResults: EvaluationResult[] = [];
1473
1603
  for (const canvas of config.canvases) {
1474
- const frame = await frameCanvas(bundledCode, context, html, canvas, config.canvasViewports, deps, config.hostPresentations);
1604
+ const frame = await frameCanvas(bundledCode, context, canvas, config.canvasViewports, deps, config.hostPresentations);
1475
1605
  const { viewport, policy, fit, attempt } = frame;
1476
1606
  const screenshot = attempt.png;
1477
1607
  if (!screenshot) {
@@ -1490,7 +1620,7 @@ export async function runVisualEvaluationDetailed(
1490
1620
  const criteriaBlock =
1491
1621
  context.criteria !== undefined && criteriaSelected !== undefined
1492
1622
  ? buildCriteriaJudgeBlock(context.criteria.bank, criteriaSelected, {
1493
- frame: { canvas, width: viewport.width, height: viewport.height, ...(frame.presentation?.status === 'applied' ? { hostLabel: frame.presentation.applied.label } : {}) },
1623
+ frame: { canvas, width: viewport.width, height: viewport.height, fit: frame.mountFit, ...(frame.presentation?.status === 'applied' ? { hostLabel: frame.presentation.applied.label } : {}) },
1494
1624
  ...(config.sampleProps !== undefined ? { propsJson: JSON.stringify(config.sampleProps) } : {}),
1495
1625
  })
1496
1626
  : '';
@@ -1541,7 +1671,7 @@ export async function runVisualEvaluationDetailed(
1541
1671
  context: criteriaContext,
1542
1672
  selected: criteriaSelected,
1543
1673
  answers: criteriaReads(criteriaAnswer),
1544
- measurements: { overflow, contentHeight, viewportHeight: viewport.height, inkRatio, edge: attempt.edge ?? null },
1674
+ measurements: { overflow, contentHeight, viewportHeight: viewport.height, inkRatio, edge: attempt.edge ?? null, edgeV2: attempt.edgeV2 ?? null },
1545
1675
  ...criteriaUnansweredReason(criteriaAnswer),
1546
1676
  ...criteriaCallOf(criteriaAnswer),
1547
1677
  })
@@ -1736,6 +1866,8 @@ export function summarizeVisualResult(result: VisualEvaluationResult): VisualEva
1736
1866
 
1737
1867
  /** One stored capture to judge: the frame the judge saw at capture time, plus what the capture measured. */
1738
1868
  export interface StoredCaptureInput {
1869
+ /** How the stored capture was composed. Absent ⇒ the canvas's composition before mount fits existed (`fill` on a fullscreen canvas, else `card`). */
1870
+ readonly mountFit?: MountFit;
1739
1871
  readonly canvas: CanvasClass;
1740
1872
  /** The capture as saved (`canvas-<class>.png`). */
1741
1873
  readonly png: Buffer;
@@ -1800,7 +1932,7 @@ export async function judgeStoredCapture(
1800
1932
  const criteriaBlock =
1801
1933
  input.criteria !== undefined && criteriaSelected !== undefined
1802
1934
  ? buildCriteriaJudgeBlock(input.criteria.bank, criteriaSelected, {
1803
- frame: { canvas, width: viewport.width, height: viewport.height },
1935
+ frame: { canvas, width: viewport.width, height: viewport.height, fit: input.mountFit ?? (canvasFit(canvas) === 'fill' ? 'fill' : 'card') },
1804
1936
  ...(input.sampleProps !== undefined ? { propsJson: JSON.stringify(input.sampleProps) } : {}),
1805
1937
  })
1806
1938
  : '';
@@ -2004,7 +2136,7 @@ function parseCriteriaAnswers(raw: unknown): { answers?: CriteriaAnswer[]; count
2004
2136
 
2005
2137
  function getDefaultVisualModel(provider: 'claude' | 'google'): string {
2006
2138
  switch (provider) {
2007
- case 'claude': return 'claude-haiku-4-5-20251001';
2139
+ case 'claude': return 'claude-haiku-5-5';
2008
2140
  case 'google': return 'gemini-3-flash-preview';
2009
2141
  }
2010
2142
  }
@@ -2151,11 +2283,10 @@ export async function runVisualFit(
2151
2283
  } catch (e) {
2152
2284
  return { status: 'unavailable', reason: `bundle failed: ${e instanceof Error ? e.message : String(e)}` };
2153
2285
  }
2154
- const html = buildRenderHTML(bundledCode, context.cssTokens);
2155
2286
  const issues: EvalIssue[] = [];
2156
2287
  const readings: CanvasFitReading[] = [];
2157
2288
  for (const canvas of config.canvases) {
2158
- const frame = await frameCanvas(bundledCode, context, html, canvas, config.canvasViewports, deps, config.hostPresentations);
2289
+ const frame = await frameCanvas(bundledCode, context, canvas, config.canvasViewports, deps, config.hostPresentations);
2159
2290
  if (!frame.attempt.png) {
2160
2291
  return { status: 'unavailable', reason: `${frame.attempt.reason ?? 'no browser available'} at canvas ${canvas}` };
2161
2292
  }
@@ -90,6 +90,28 @@ export async function warmupRuntimeRenderProbe(): Promise<{
90
90
  */
91
91
  const WARMUP_COMPONENT = "export default function Warmup() { return <div>warm</div>; }";
92
92
 
93
+ /**
94
+ * Why a worker warm-up failed (ggui#1839), one kind per exit of
95
+ * {@link warmupRuntimeRenderWorker}:
96
+ *
97
+ * - `incomplete`: the check started and ran out of its bound before it
98
+ * finished. The worker may be fine and the machine slow (a cold node's
99
+ * first spawn); it says nothing about whether a check can pass.
100
+ * - `blocking`: the check ran to an end and reported a `failed` or
101
+ * `unverified` issue: the worker cannot render a trivial component.
102
+ * - `spawn`: the spawn layer refused to run the worker at all (an invalid
103
+ * bound, a missing binary).
104
+ *
105
+ * Branch on this, never on `reason`, which is prose for a human.
106
+ */
107
+ export const RUNTIME_RENDER_WARMUP_FAILURE_KINDS = ["incomplete", "blocking", "spawn"] as const;
108
+ export type RuntimeRenderWarmupFailureKind = (typeof RUNTIME_RENDER_WARMUP_FAILURE_KINDS)[number];
109
+
110
+ /** The result of {@link warmupRuntimeRenderWorker}. */
111
+ export type RuntimeRenderWorkerWarmup =
112
+ | { ok: true; ms: number }
113
+ | { ok: false; ms: number; kind: RuntimeRenderWarmupFailureKind; reason: string };
114
+
93
115
  /**
94
116
  * Warm the PROCESS that runs every probe (ggui#1380): spawn one real check
95
117
  * worker on a trivial component through the same host every probe uses.
@@ -100,12 +122,12 @@ const WARMUP_COMPONENT = "export default function Warmup() { return <div>warm</d
100
122
  *
101
123
  * `bounds` are the subprocess bounds for the one run; a serving deployment
102
124
  * passes the same bounds it will probe with. Never throws: a worker that
103
- * cannot run is `{ ok: false, reason }`, so a boot sequence can log it and
104
- * decide, instead of dying in a fire-and-forget.
125
+ * cannot run is `{ ok: false, kind, reason }`, so a boot sequence can log
126
+ * it and decide by `kind`, instead of dying in a fire-and-forget.
105
127
  */
106
128
  export async function warmupRuntimeRenderWorker(
107
129
  bounds?: Partial<RenderCheckHostBounds>
108
- ): Promise<{ ok: true; ms: number } | { ok: false; ms: number; reason: string }> {
130
+ ): Promise<RuntimeRenderWorkerWarmup> {
109
131
  const start = Date.now();
110
132
  try {
111
133
  // Lazy: the host pulls `@ggui-ai/sandbox` (spawn machinery); keep it off
@@ -120,6 +142,7 @@ export async function warmupRuntimeRenderWorker(
120
142
  return {
121
143
  ok: false,
122
144
  ms,
145
+ kind: "incomplete",
123
146
  reason: `the warm-up check did not finish within ${result.incomplete.boundMs} ms (stopped at ${result.incomplete.elapsedMs} ms)`,
124
147
  };
125
148
  }
@@ -127,7 +150,7 @@ export async function warmupRuntimeRenderWorker(
127
150
  (i) => i.outcome === "failed" || i.outcome === "unverified"
128
151
  );
129
152
  if (blocking !== undefined) {
130
- return { ok: false, ms, reason: `${blocking.check} ${blocking.outcome}: ${blocking.reason}` };
153
+ return { ok: false, ms, kind: "blocking", reason: `${blocking.check} ${blocking.outcome}: ${blocking.reason}` };
131
154
  }
132
155
  return { ok: true, ms };
133
156
  } catch (err) {
@@ -136,6 +159,7 @@ export async function warmupRuntimeRenderWorker(
136
159
  return {
137
160
  ok: false,
138
161
  ms: Date.now() - start,
162
+ kind: "spawn",
139
163
  reason: err instanceof Error ? err.message : String(err),
140
164
  };
141
165
  }