@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
@@ -11,9 +11,10 @@
11
11
  import * as esbuild from 'esbuild';
12
12
  import { AgentWorkspace } from './workspace';
13
13
  import { getSoftWarnings } from './self-check';
14
+ import { restoreReactImport } from '../boilerplate/react-hooks.js';
14
15
  import { runTier0Checks } from '../check/index.js';
15
16
  import { runGatedAxisChecks, traceAxisChecksSkipped } from '../evaluation/axis-checks/dispatch.js';
16
- import type { AxisCheck, AxisCheckInput } from '../evaluation/types-public.js';
17
+ import type { AxisCheck, AxisCheckInput, EvalIssue } from '../evaluation/types-public.js';
17
18
  import type { Classification } from '../classifier/axes.js';
18
19
  import type { CanvasClass } from '../design-mode.js';
19
20
  import { PRIMITIVES_DOCUMENTATION } from '../validation/index.js';
@@ -24,6 +25,21 @@ import type {
24
25
  } from './types';
25
26
  import { DEFAULT_DESIGN_MODE, type DesignMode } from '../design-mode.js';
26
27
 
28
+ /**
29
+ * ggui#1876 — the auto-commit line's axis field: the F/W counts, then the check ids behind them, so a
30
+ * WARN the model saw at a turn is attributable in the log (the `[WARN]` text otherwise appears only at
31
+ * the final evaluation). `0F/1W[W:universal.tint_category_in_words]`; nothing appended at `0F/0W`.
32
+ * The `<n>F/<n>W` prefix is unchanged, so anything that reads the counts reads them as before. An
33
+ * issue with no `subcategory` (the axis checks always set one, as their id) is named by its category.
34
+ */
35
+ export function axisCountsWithIds(issues: readonly EvalIssue[]): string {
36
+ const idOf = (i: EvalIssue): string => i.subcategory ?? i.category;
37
+ const warns = issues.filter((i) => i.result === 'warn').map(idOf);
38
+ const fails = issues.filter((i) => i.result !== 'warn').map(idOf);
39
+ const groups = [...(fails.length > 0 ? [`F:${fails.join(',')}`] : []), ...(warns.length > 0 ? [`W:${warns.join(',')}`] : [])];
40
+ return `${fails.length}F/${warns.length}W${groups.length > 0 ? `[${groups.join(' ')}]` : ''}`;
41
+ }
42
+
27
43
  /**
28
44
  * Extract documentation for a single component from the full primitives docs.
29
45
  * Returns ~200-500 tokens instead of 43K.
@@ -106,7 +122,14 @@ async function autoCommit(
106
122
  // No post-compile formatting — the generated TSX is built as-is.
107
123
  // (Prettier auto-format was removed; it added a dependency + repair
108
124
  // churn without a quality gain.)
109
- const formatted = raw;
125
+ // ggui#1932: the boilerplate's DO NOT EDIT React import is code's to keep — restored here, before
126
+ // the build and tier-0, so a dropped hook never becomes a refusal the model spends a turn on.
127
+ const restore = restoreReactImport(raw);
128
+ if (restore.added.length > 0) {
129
+ workspace.write(restore.source);
130
+ console.log(`[coding-agent] react-import restored: ${restore.added.map((h) => `+${h}`).join(',')}`);
131
+ }
132
+ const formatted = restore.source;
110
133
 
111
134
  // Build with esbuild — keep all imports (react, @ggui-ai/design)
112
135
  // esbuild.transform() passes them through as-is, the runtime resolves them
@@ -197,7 +220,7 @@ async function autoCommit(
197
220
 
198
221
  const status = buildSuccess && selfCheckPassed ? 'PASS' : 'FAIL';
199
222
  console.log(
200
- `[coding-agent] auto-commit: ${status} | build=${buildMs}ms self-check=${selfCheckMs}ms git=${gitMs}ms total=${Date.now() - commitStart}ms | violations=${violations.length} | axis=${axisRun === undefined ? 'off' : `${axisIssues.length - axisWarnings.length}F/${axisWarnings.length}W`}`,
223
+ `[coding-agent] auto-commit: ${status} | build=${buildMs}ms self-check=${selfCheckMs}ms git=${gitMs}ms total=${Date.now() - commitStart}ms | violations=${violations.length} | axis=${axisRun === undefined ? 'off' : axisCountsWithIds(axisIssues)}`,
201
224
  );
202
225
  if (violations.length > 0) {
203
226
  for (const v of violations) {
@@ -40,7 +40,7 @@ import type {
40
40
  ModelRef,
41
41
  AppGenerationProfileEffort,
42
42
  } from "@ggui-ai/protocol";
43
- import { isGeneratorId, modelRefOfRoute } from "@ggui-ai/protocol";
43
+ import { DEFAULT_MODEL as PROTOCOL_DEFAULT_MODEL, isGeneratorId, modelRefOfRoute } from "@ggui-ai/protocol";
44
44
  import type { GadgetCatalogAdapter } from "@ggui-ai/gadgets";
45
45
  import type {
46
46
  GenerationMetadata,
@@ -85,7 +85,9 @@ import type { AgentConfig } from "./harness/llm-router.js";
85
85
 
86
86
  /** The slug for the OSS default seed generator. */
87
87
  const DEFAULT_TIER: GeneratorTier = "default";
88
- const DEFAULT_MODEL: ModelRef = "anthropic/claude-haiku-4-5";
88
+ // ggui#1794 — the open-source default is protocol's, one source: a default switch moves one constant
89
+ // (`@ggui-ai/protocol` `DEFAULT_MODEL`), never two. ggui#1743 moved it to Haiku 5.5 on benchmark's receipt.
90
+ const DEFAULT_MODEL: ModelRef = PROTOCOL_DEFAULT_MODEL;
89
91
 
90
92
  export interface CreateUiGeneratorOptions {
91
93
  /**
@@ -18,6 +18,8 @@
18
18
  // blueprint-reuse shape, and brand-bearing color via the closed
19
19
  // `--ggui-color-*` token manifest (no literals, no fallbacks).
20
20
 
21
+ import type { CanvasClass } from "@ggui-ai/design/rendering";
22
+
21
23
  export const DESIGN_MODES = ["constrained", "free"] as const;
22
24
 
23
25
  export type DesignMode = (typeof DESIGN_MODES)[number];
@@ -38,15 +40,8 @@ export const DEFAULT_DESIGN_MODE: DesignMode = "constrained";
38
40
  // breakpoints. `canvasForViewportWidth` is monotonic in width per
39
41
  // display mode (pinned by `design-mode.test.ts`).
40
42
 
41
- export const CANVAS_CLASSES = [
42
- "xs-chat-card",
43
- "mobile-fullscreen-small",
44
- "md",
45
- "lg",
46
- "xl",
47
- ] as const;
48
-
49
- export type CanvasClass = (typeof CANVAS_CLASSES)[number];
43
+ // The vocabulary itself lives in `@ggui-ai/design/rendering`, where the runtime reads it too.
44
+ export { CANVAS_CLASSES, type CanvasClass } from "@ggui-ai/design/rendering";
50
45
 
51
46
  export interface CanvasViewport {
52
47
  readonly width: number;
@@ -12,6 +12,7 @@ import {
12
12
  getEntityCollections,
13
13
  getMutatedEntityCollections,
14
14
  mkIssue,
15
+ seedsFromProp,
15
16
  } from "../helpers.js";
16
17
 
17
18
  function runStateSeededFromProps(input: AxisCheckInput): EvalIssue[] {
@@ -21,10 +22,7 @@ function runStateSeededFromProps(input: AxisCheckInput): EvalIssue[] {
21
22
  const entities = getMutatedEntityCollections(input.contract, all);
22
23
  const issues: EvalIssue[] = [];
23
24
  for (const e of entities) {
24
- const re = new RegExp(
25
- `useState(?:<[^>]*>)?\\s*\\([\\s\\S]{0,400}?props\\.${e.name}\\b`,
26
- );
27
- if (re.test(src)) continue;
25
+ if (seedsFromProp(src, e.name)) continue;
28
26
  issues.push(
29
27
  mkIssue(
30
28
  "state.merge.seeded_from_props",
@@ -57,7 +55,7 @@ function runNoHardcodedEntities(input: AxisCheckInput): EvalIssue[] {
57
55
  mkIssue(
58
56
  "state.merge.no_hardcoded_entities",
59
57
  `Hardcoded entity array literal (multiple objects with "${idField}") in the component — entity data should come from state/props.`,
60
- `Remove the literal array. Seed state from props.{entityProp} and merge updates via stream.`,
58
+ `Remove the literal array: this data comes from ${all.filter((e) => e.idField === idField).map((e) => `props.${e.name}`).join(" / ") || "props"}. Read it there (\`?? []\` for an absent value, never a literal default), seed state from it, and merge updates via stream.`,
61
59
  ),
62
60
  );
63
61
  }
@@ -12,6 +12,7 @@ import {
12
12
  getInitialValuePropNames,
13
13
  getSubmitActions,
14
14
  mkIssue,
15
+ seedsFromProp,
15
16
  } from "../helpers.js";
16
17
 
17
18
  function runSubmitHookPresent(input: AxisCheckInput): EvalIssue[] {
@@ -79,34 +80,6 @@ function runStateCoversPayload(input: AxisCheckInput): EvalIssue[] {
79
80
  return issues;
80
81
  }
81
82
 
82
- /**
83
- * Whether a `useState` initializer reads `props.<name>`: directly, or through a local bound from
84
- * it (`const initial = normalize(props.initialProfile)` then `useState(initial.name)`, or a
85
- * destructure `const { name } = props.initialProfile ?? {}` then `useState(name)`). The property
86
- * this check guards is "form state is seeded from the prop", not a spelling: ggui#1261's Exp 011
87
- * cell re-seeded correctly through a normalized local, was refused, and spent a turn restoring
88
- * the literal form.
89
- */
90
- function seedsFromProp(src: string, name: string): boolean {
91
- const propRef = `props\\??\\.${name}\\b`;
92
- const initializerReads = (ref: string): boolean =>
93
- new RegExp(`useState(?:<[^>]*>)?\\s*\\([\\s\\S]{0,400}?${ref}`).test(src);
94
- if (initializerReads(propRef)) return true;
95
- const locals = new Set<string>();
96
- for (const m of src.matchAll(new RegExp(`(?:const|let|var)\\s+(\\w+)\\s*(?::[^=]{1,80})?=\\s*[^;]{0,300}?${propRef}`, "g"))) {
97
- const id = m[1];
98
- if (id !== undefined) locals.add(id);
99
- }
100
- for (const m of src.matchAll(new RegExp(`(?:const|let|var)\\s*\\{([^}]{1,400})\\}\\s*=\\s*[^;]{0,300}?${propRef}`, "g"))) {
101
- for (const part of (m[1] ?? "").split(",")) {
102
- const id = part.split(":").pop()?.split("=")[0]?.trim();
103
- if (id !== undefined && /^\w+$/.test(id)) locals.add(id);
104
- }
105
- }
106
- for (const id of locals) if (initializerReads(`\\b${id}\\b`)) return true;
107
- return false;
108
- }
109
-
110
83
  function runInitialValuesSeeded(input: AxisCheckInput): EvalIssue[] {
111
84
  if (input.compiledCode === null) return [];
112
85
  const src = input.sourceCode;
@@ -0,0 +1,197 @@
1
+ // packages/ui-gen/src/evaluation/axis-checks/checks/tint.ts
2
+ //
3
+ // ggui#1846 — the categorical `tint` fill's usage rules, as WARNINGS. The kit
4
+ // gives `Box` / `Card` eight theme-tracked steps (`tint-1` … `tint-8`) for DATA
5
+ // categories. The rules (ggui-team-cdo's, taught in the system prompt) are a
6
+ // property of the rendered card, and from source each can only be read by a
7
+ // heuristic over names or JSX structure. A heuristic that FAILS loops a
8
+ // generation that is correct but trips it, so these warn: the model sees the
9
+ // repair line as feedback, nothing blocks, and the follow and false-fire rates
10
+ // are measured. An out-of-range step fails through the prop's own type instead.
11
+ //
12
+ // Not checked here: "the same category bound to two steps in one card". Reading
13
+ // that needs data flow, not the tag's text.
14
+
15
+ import type { EvalIssue } from "../../types-public.js";
16
+ import type { AxisCheck, AxisCheckInput } from "../types.js";
17
+ import { mkIssue } from "../helpers.js";
18
+
19
+ const ALL_RENDER_VALUES = [
20
+ "static", "list", "grid", "spatial", "timeline", "chart", "master-detail",
21
+ ] as const;
22
+
23
+ /** One tinted `Box` / `Card`: the tag, the `tint` value's source text, and the JSX between its open and close tags. */
24
+ export interface TintedElement {
25
+ readonly tag: "Box" | "Card";
26
+ readonly tint: string;
27
+ /** The JSX between the open and close tag; empty when self-closing. */
28
+ readonly inner: string;
29
+ /** For a self-closing element, its following siblings up to the next close tag (a swatch's label); else empty. */
30
+ readonly next: string;
31
+ }
32
+
33
+ interface OpenTag {
34
+ readonly attrs: string;
35
+ readonly end: number;
36
+ readonly selfClosing: boolean;
37
+ }
38
+
39
+ /**
40
+ * Read a JSX open tag whose `<Name` starts at `from`. Braces and string
41
+ * literals are skipped as units, so an `onClick={() => …}` arrow does not end
42
+ * the tag the way a `[^<>]` pattern would.
43
+ */
44
+ function scanOpenTag(src: string, from: number, nameLength: number): OpenTag | null {
45
+ let depth = 0;
46
+ let quote = "";
47
+ for (let i = from + 1 + nameLength; i < src.length; i++) {
48
+ const ch = src[i];
49
+ if (quote !== "") {
50
+ if (ch === "\\") i++;
51
+ else if (ch === quote) quote = "";
52
+ } else if (ch === '"' || ch === "'" || ch === "`") {
53
+ quote = ch;
54
+ } else if (ch === "{") {
55
+ depth++;
56
+ } else if (ch === "}") {
57
+ depth--;
58
+ } else if (ch === ">" && depth === 0) {
59
+ const attrs = src.slice(from + 1 + nameLength, i);
60
+ return { attrs, end: i + 1, selfClosing: attrs.trimEnd().endsWith("/") };
61
+ }
62
+ }
63
+ return null;
64
+ }
65
+
66
+ /** The value of a top-level `tint=` attribute, braces or quotes included; null when absent. */
67
+ function readTintAttr(attrs: string): string | null {
68
+ let depth = 0;
69
+ let quote = "";
70
+ for (let i = 0; i < attrs.length; i++) {
71
+ const ch = attrs[i];
72
+ if (quote !== "") {
73
+ if (ch === "\\") i++;
74
+ else if (ch === quote) quote = "";
75
+ continue;
76
+ }
77
+ if (ch === '"' || ch === "'" || ch === "`") quote = ch;
78
+ else if (ch === "{") depth++;
79
+ else if (ch === "}") depth--;
80
+ else if (depth === 0 && /\s/.test(attrs[i - 1] ?? " ")) {
81
+ const m = /^tint\s*=\s*/.exec(attrs.slice(i));
82
+ if (m === null) continue;
83
+ const v = i + m[0].length;
84
+ const open = attrs[v];
85
+ if (open === '"' || open === "'") {
86
+ const close = attrs.indexOf(open, v + 1);
87
+ return close < 0 ? null : attrs.slice(v, close + 1);
88
+ }
89
+ if (open !== "{") return null;
90
+ let d = 0;
91
+ for (let j = v; j < attrs.length; j++) {
92
+ if (attrs[j] === "{") d++;
93
+ else if (attrs[j] === "}" && --d === 0) return attrs.slice(v, j + 1);
94
+ }
95
+ return null;
96
+ }
97
+ }
98
+ return null;
99
+ }
100
+
101
+ /** Every tinted `Box` / `Card` in the source, with its inner JSX (empty when self-closing). */
102
+ export function findTintedElements(sourceCode: string): TintedElement[] {
103
+ const found: TintedElement[] = [];
104
+ for (const m of sourceCode.matchAll(/<(Box|Card)\b/g)) {
105
+ const tag = m[1] === "Card" ? "Card" : "Box";
106
+ const from = m.index ?? 0;
107
+ const open = scanOpenTag(sourceCode, from, tag.length);
108
+ if (open === null) continue;
109
+ const tint = readTintAttr(open.attrs);
110
+ if (tint === null) continue;
111
+ if (open.selfClosing) {
112
+ const close = sourceCode.indexOf("</", open.end);
113
+ found.push({ tag, tint, inner: "", next: sourceCode.slice(open.end, close < 0 ? sourceCode.length : close) });
114
+ continue;
115
+ }
116
+ // Walk to the matching close tag, counting nested opens of the same name.
117
+ const tagRx = new RegExp(`<(/?)${tag}\\b`, "g");
118
+ tagRx.lastIndex = open.end;
119
+ let depth = 1;
120
+ let end = sourceCode.length;
121
+ for (let t = tagRx.exec(sourceCode); t !== null; t = tagRx.exec(sourceCode)) {
122
+ if (t[1] === "/") {
123
+ if (--depth === 0) {
124
+ end = t.index;
125
+ break;
126
+ }
127
+ continue;
128
+ }
129
+ const nested = scanOpenTag(sourceCode, t.index, tag.length);
130
+ if (nested === null) break;
131
+ tagRx.lastIndex = nested.end;
132
+ if (!nested.selfClosing) depth++;
133
+ }
134
+ found.push({ tag, tint, inner: sourceCode.slice(open.end, end), next: "" });
135
+ }
136
+ return found;
137
+ }
138
+
139
+ const STATE_WORD_RX = /\b(status|state|isLive|live|draft|signal|error|success|warning|danger|isActive|active|selected|isSelected|online|offline|pending|loading|disabled)\b/i;
140
+
141
+ /** Text a reader sees inside the cell: a text primitive, a JSX expression child, or literal JSX text. */
142
+ function hasWords(inner: string): boolean {
143
+ if (/<(Text|Heading|Label|Badge|Caption|Link)\b/.test(inner)) return true;
144
+ if (/\{[^{}]+\}/.test(inner.replace(/\b[a-zA-Z]+=\{[^{}]*\}/g, ""))) return true;
145
+ return />\s*[^<>{}\s][^<>{}]*</.test(`>${inner}<`);
146
+ }
147
+
148
+ function runTintStateBound(input: AxisCheckInput): EvalIssue[] {
149
+ if (input.compiledCode === null) return [];
150
+ const bound = findTintedElements(input.sourceCode).filter((e) => e.tint.startsWith("{") && STATE_WORD_RX.test(e.tint));
151
+ if (bound.length === 0) return [];
152
+ return [
153
+ mkIssue(
154
+ "universal.tint_state_bound",
155
+ `\`tint\` reads as bound to a state (${bound.map((e) => `\`${e.tint}\``).join(", ")}) — tints are for data categories, never for state.`,
156
+ "A status takes live, draft or signal, not a tint. Keep `tint` for a data category (series, lane, kind, group), and show state with its own props.",
157
+ "warn",
158
+ ),
159
+ ];
160
+ }
161
+
162
+ function runTintCategoryInWords(input: AxisCheckInput): EvalIssue[] {
163
+ if (input.compiledCode === null) return [];
164
+ const tinted = findTintedElements(input.sourceCode);
165
+ if (tinted.length === 0) return [];
166
+ if (/\blegend\b/i.test(input.sourceCode)) return [];
167
+ if (tinted.every((e) => hasWords(e.inner) || hasWords(e.next))) return [];
168
+ return [
169
+ mkIssue(
170
+ "universal.tint_category_in_words",
171
+ "A tinted cell carries no words, and the card has no legend — colour is the only way to tell its category.",
172
+ "Give this cell a label, or add a legend to the card. The category is always also in words.",
173
+ "warn",
174
+ ),
175
+ ];
176
+ }
177
+
178
+ const MUTED_TONE_RX = /\btone\s*=\s*["'](subtle|muted)["']/;
179
+
180
+ function runTintMutedText(input: AxisCheckInput): EvalIssue[] {
181
+ if (input.compiledCode === null) return [];
182
+ if (!findTintedElements(input.sourceCode).some((e) => MUTED_TONE_RX.test(e.inner))) return [];
183
+ return [
184
+ mkIssue(
185
+ "universal.tint_muted_text",
186
+ 'Muted text (`tone="subtle"` / `tone="muted"`) sits on a tint — it can read under 4.5:1 there.',
187
+ "Text on a tint is the theme's ink only, one short line (a label, a symbol, a value). Put secondary or muted text beside the cell or in the legend.",
188
+ "warn",
189
+ ),
190
+ ];
191
+ }
192
+
193
+ export const TINT_CHECKS: readonly AxisCheck[] = [
194
+ { id: "universal.tint_state_bound", axis: "render", values: ALL_RENDER_VALUES, run: runTintStateBound },
195
+ { id: "universal.tint_category_in_words", axis: "render", values: ALL_RENDER_VALUES, run: runTintCategoryInWords },
196
+ { id: "universal.tint_muted_text", axis: "render", values: ALL_RENDER_VALUES, run: runTintMutedText },
197
+ ];
@@ -249,14 +249,128 @@ export function getInitialValuePropNames(contract?: DataContract): string[] {
249
249
  // Source scanning helpers
250
250
  // =============================================================================
251
251
 
252
+ /**
253
+ * The top-level property keys of the object literal whose `{` is at `open`, read by
254
+ * walking brackets and skipping strings, so a nested object or a `}` inside a string
255
+ * neither ends the literal early nor contributes keys. `undefined` when `open` is not a `{`.
256
+ */
257
+ function objectLiteralKeysAt(src: string, open: number): string[] | undefined {
258
+ if (src[open] !== "{") return undefined;
259
+ const keys: string[] = [];
260
+ let depth = 0;
261
+ let expectKey = true;
262
+ for (let i = open; i < src.length; i++) {
263
+ const c = src[i];
264
+ if (c === '"' || c === "'" || c === "`") {
265
+ // Skip the string; a quoted top-level key ("name": …) is a key too.
266
+ let j = i + 1;
267
+ while (j < src.length && src[j] !== c) j += src[j] === "\\" ? 2 : 1;
268
+ if (depth === 1 && expectKey && /^\s*:/.test(src.slice(j + 1))) keys.push(src.slice(i + 1, j));
269
+ i = j;
270
+ expectKey = false;
271
+ continue;
272
+ }
273
+ if (c === "{" || c === "[" || c === "(") {
274
+ depth++;
275
+ if (depth === 1) expectKey = true;
276
+ continue;
277
+ }
278
+ if (c === "}" || c === "]" || c === ")") {
279
+ depth--;
280
+ if (depth === 0) return keys;
281
+ continue;
282
+ }
283
+ if (depth !== 1) continue;
284
+ if (c === ",") {
285
+ expectKey = true;
286
+ continue;
287
+ }
288
+ if (expectKey && /[A-Za-z_$]/.test(c ?? "")) {
289
+ const m = /^([A-Za-z_$][\w$]*)\s*(:|,|\})/.exec(src.slice(i));
290
+ // `key: value`, or the shorthand `key,` / `key }`.
291
+ if (m !== null && m[1] !== undefined) keys.push(m[1]);
292
+ expectKey = false;
293
+ }
294
+ }
295
+ return keys;
296
+ }
297
+
298
+ /** The argument texts of the call whose `(` is at `open`, split on top-level commas. */
299
+ function callArgsAt(src: string, open: number): string[] {
300
+ const args: string[] = [];
301
+ let depth = 0;
302
+ let start = open + 1;
303
+ for (let i = open; i < src.length; i++) {
304
+ const c = src[i];
305
+ if (c === '"' || c === "'" || c === "`") {
306
+ let j = i + 1;
307
+ while (j < src.length && src[j] !== c) j += src[j] === "\\" ? 2 : 1;
308
+ i = j;
309
+ continue;
310
+ }
311
+ if (c === "(" || c === "{" || c === "[") depth++;
312
+ else if (c === ")" || c === "}" || c === "]") {
313
+ depth--;
314
+ if (depth === 0) {
315
+ args.push(src.slice(start, i));
316
+ return args;
317
+ }
318
+ } else if (c === "," && depth === 1) {
319
+ args.push(src.slice(start, i));
320
+ start = i + 1;
321
+ }
322
+ }
323
+ return args;
324
+ }
325
+
326
+ /**
327
+ * The keys an initial-state argument provides: an object literal (`{ … }`), a lazy
328
+ * initializer returning one (`() => ({ … })`), or an identifier bound to one in the same
329
+ * file (`const INITIAL: Form = { … }`, also behind a lazy `() => INITIAL`).
330
+ */
331
+ function initialStateKeys(src: string, argStart: number, argText: string): string[] {
332
+ const lead = argText.length - argText.trimStart().length;
333
+ let at = argStart + lead;
334
+ let text = argText.trim();
335
+ const lazy = /^\(\s*\)\s*=>\s*\(?\s*/.exec(text);
336
+ if (lazy !== null) {
337
+ at += lazy[0].length;
338
+ text = text.slice(lazy[0].length);
339
+ }
340
+ if (text.startsWith("{")) return objectLiteralKeysAt(src, at) ?? [];
341
+ const ident = /^([A-Za-z_$][\w$]*)\s*\)?\s*$/.exec(text);
342
+ if (ident === null || ident[1] === undefined) return [];
343
+ // One binding, or none: a name declared twice (shadowed, or re-declared in another scope) is
344
+ // ambiguous, and ambiguity reads as absence, so the check can never pass on the wrong binding.
345
+ const decls = [...src.matchAll(new RegExp(`(?:const|let|var)\\s+${ident[1]}\\s*(?::[^=]{1,200})?=\\s*`, "g"))];
346
+ const decl = decls.length === 1 ? decls[0] : undefined;
347
+ if (decl === undefined) return [];
348
+ return objectLiteralKeysAt(src, (decl.index ?? 0) + decl[0].length) ?? [];
349
+ }
350
+
252
351
  /**
253
352
  * Collect all key names that appear as state slots — either keys inside a
254
353
  * `useState({ ... })` object literal, or standalone state variables.
354
+ *
355
+ * ggui#1790 — also the initial-state shapes the inline-literal patterns cannot see, each
356
+ * idiomatic React: `useState(INITIAL)` with `const INITIAL = { … }` in the file, a lazy
357
+ * initializer `useState(() => ({ … }))`, and `useReducer(reducer, { … } | INITIAL)`. Missing
358
+ * them made `state.payload.covers_submit` report every payload key missing on a form that held
359
+ * them all, and the model, with nothing real to fix, spent the turn cap rewording a comment.
255
360
  */
256
361
  export function collectStateKeys(src: string): Set<string> {
257
362
  const keys = new Set<string>();
258
363
  const varRe = /const\s*\[\s*(\w+)\s*,\s*(\w+)\s*\]\s*=\s*useState/g;
259
364
  for (const m of src.matchAll(varRe)) keys.add(m[1]);
365
+ // ggui#1791 — a contextSpec slot is state too: the boilerplate binds each slot as
366
+ // `const [x, setX] = useGguiContext<T>('x')` under a DO-NOT-EDIT banner, so a form
367
+ // whose fields are context slots holds every payload key there and cannot move them
368
+ // into useState. Both the binding's name and the slot's name count.
369
+ const contextRe = /const\s*\[\s*(\w+)\s*,\s*\w+\s*\]\s*=\s*useGguiContext\s*(?:<[^>]*>)?\s*\(\s*['"`](\w+)['"`]/g;
370
+ for (const m of src.matchAll(contextRe)) {
371
+ keys.add(m[1]);
372
+ keys.add(m[2]);
373
+ }
260
374
  const objRe = /useState(?:<[^>]*>)?\s*\(\s*\{([\s\S]*?)\}\s*\)/g;
261
375
  for (const m of src.matchAll(objRe)) {
262
376
  const keyRe = /(?:^|,)\s*(\w+)\s*:/g;
@@ -267,6 +381,17 @@ export function collectStateKeys(src: string): Set<string> {
267
381
  const keyRe = /(?:^|,)\s*(\w+)\s*:/g;
268
382
  for (const km of m[1].matchAll(keyRe)) keys.add(km[1]);
269
383
  }
384
+ // ggui#1790 — the initial-state argument, resolved: useState's first, useReducer's second.
385
+ for (const m of src.matchAll(/\b(useState|useReducer)\s*(?:<[^>]*>)?\s*\(/g)) {
386
+ const open = (m.index ?? 0) + m[0].length - 1;
387
+ const args = callArgsAt(src, open);
388
+ const which = m[1] === "useReducer" ? 1 : 0;
389
+ const arg = args[which];
390
+ if (arg === undefined) continue;
391
+ let argStart = open + 1;
392
+ for (let k = 0; k < which; k++) argStart += (args[k]?.length ?? 0) + 1;
393
+ for (const key of initialStateKeys(src, argStart, arg)) keys.add(key);
394
+ }
270
395
  return keys;
271
396
  }
272
397
 
@@ -286,6 +411,127 @@ export function isMultiStepPrompt(prompt: string): boolean {
286
411
  // Issue factory
287
412
  // =============================================================================
288
413
 
414
+ /**
415
+ * Whether a `useState` initializer reads `props.<name>`: directly, or through a local bound from
416
+ * it (`const initial = normalize(props.initialProfile)` then `useState(initial.name)`, or a
417
+ * destructure `const { name } = props.initialProfile ?? {}` then `useState(name)`). The property
418
+ * this check guards is "form state is seeded from the prop", not a spelling: ggui#1261's Exp 011
419
+ * cell re-seeded correctly through a normalized local, was refused, and spent a turn restoring
420
+ * the literal form. ggui#1869: a nested generic (`useState<Array<Task>>(…)`), `props?.`,
421
+ * destructured props and destructured component parameters are all correct seeding too, and
422
+ * `state.merge.seeded_from_props` — a FAIL — refused them.
423
+ */
424
+ export function seedsFromProp(src: string, name: string): boolean {
425
+ const propRef = `props\\??\\.${name}\\b`;
426
+ const inits = useStateInitializers(src);
427
+ const anyInitReads = (rx: RegExp): boolean => inits.some((t) => rx.test(t));
428
+ if (anyInitReads(new RegExp(`\\b${propRef}`))) return true;
429
+ const locals = new Set<string>();
430
+ for (const m of src.matchAll(new RegExp(`(?:const|let|var)\\s+(\\w+)\\s*(?::[^=]{1,80})?=\\s*[^;]{0,300}?\\b${propRef}`, "g"))) {
431
+ const id = m[1];
432
+ if (id !== undefined) locals.add(id);
433
+ }
434
+ // `const { a, b } = props.<name> ?? {}` — every name bound reads the prop.
435
+ for (const m of src.matchAll(new RegExp(`(?:const|let|var)\\s*\\{([^}]{1,400})\\}\\s*=\\s*[^;]{0,300}?\\b${propRef}`, "g"))) {
436
+ for (const part of (m[1] ?? "").split(",")) {
437
+ const id = part.split(":").pop()?.split("=")[0]?.trim();
438
+ if (id !== undefined && /^\w+$/.test(id)) locals.add(id);
439
+ }
440
+ }
441
+ // `const { <name>: alias } = props` and `export default function Component({ <name>: alias }: Props)` — only the
442
+ // binding of <name>, and only the component's own parameter (a helper's destructured parameter is not its props).
443
+ const fromProps = /(?:const|let|var)\s*\{([^}]{1,400})\}\s*(?::[^=]{1,200})?=\s*props\b(?!\s*\??\.)|export\s+default\s+function\s*\w*\s*\(\s*\{([^}]{1,400})\}/g;
444
+ for (const m of src.matchAll(fromProps)) {
445
+ for (const part of (m[1] ?? m[2] ?? "").split(",")) {
446
+ const [key, alias] = part.split(":");
447
+ if (key?.split("=")[0]?.trim() !== name) continue;
448
+ const id = (alias ?? key).split("=")[0]?.trim();
449
+ if (id !== undefined && /^\w+$/.test(id)) locals.add(id);
450
+ }
451
+ }
452
+ for (const id of locals) if (anyInitReads(new RegExp(`\\b${id}\\b`))) return true;
453
+ return seedsContextSlotFromProp(src, [new RegExp(`\\b${propRef}`), ...[...locals].map((id) => new RegExp(`\\b${id}\\b`))]);
454
+ }
455
+
456
+ /**
457
+ * ggui#1869 — a generated context slot is seeded without any `useState`. The boilerplate
458
+ * declares each slot as `const [value, setValue] = useGguiContext<T>('key')`, the runtime owns
459
+ * its state, and the hook takes no initial value, so a form built on slots can only pre-fill in
460
+ * two ways, and both count:
461
+ * - an effect calls a slot setter with an argument that reads the prop
462
+ * (`useEffect(() => { if (p.name && !name) setName(p.name); }, [props.initialProfile])`);
463
+ * - a slot's value falls back to the prop (`const nameValue = name ?? props.initialProfile?.name`).
464
+ * A setter called from an event handler is not seeding (it runs on a click, not on mount), and a
465
+ * prop read that is merely near a slot is not one either.
466
+ */
467
+ function seedsContextSlotFromProp(src: string, readsProp: readonly RegExp[]): boolean {
468
+ const reads = (text: string): boolean => readsProp.some((rx) => rx.test(text));
469
+ const values: string[] = [];
470
+ const setters: string[] = [];
471
+ for (const m of src.matchAll(/(?:const|let|var)\s*\[\s*(\w+)\s*,\s*(\w+)\s*\]\s*=\s*useGguiContext\b/g)) {
472
+ if (m[1] !== undefined) values.push(m[1]);
473
+ if (m[2] !== undefined) setters.push(m[2]);
474
+ }
475
+ if (values.length === 0) return false;
476
+ for (const v of values) {
477
+ for (const m of src.matchAll(new RegExp(`(?<![.\\w$])${v}\\s*\\?\\?\\s*([^;\\n]{1,200})`, "g"))) {
478
+ if (reads(m[1] ?? "")) return true;
479
+ }
480
+ }
481
+ for (const body of callArguments(src, /\buse(?:Layout)?Effect\b/g)) {
482
+ for (const setter of setters) {
483
+ for (const arg of callArguments(body, new RegExp(`\\b${setter}\\b`, "g"))) if (reads(arg)) return true;
484
+ }
485
+ }
486
+ return false;
487
+ }
488
+
489
+ /**
490
+ * The argument text of every `useState(...)` call. A type argument is skipped with its angle
491
+ * brackets balanced (`useState<Record<string, Task[]>>(…)`; an arrow's `=>` inside it is not a
492
+ * close), and the argument runs to its matching paren, string literals skipped as units.
493
+ */
494
+ function useStateInitializers(src: string): string[] {
495
+ return callArguments(src, /\buseState\b/g);
496
+ }
497
+
498
+ /**
499
+ * The argument text of every call to a name `callee` matches (a global regex). A type argument is
500
+ * skipped with its angle brackets balanced, and the argument runs to its matching paren, string
501
+ * literals skipped as units. A match not followed by `(` is not a call and is skipped.
502
+ */
503
+ function callArguments(src: string, callee: RegExp): string[] {
504
+ const out: string[] = [];
505
+ for (const m of src.matchAll(callee)) {
506
+ let i = (m.index ?? 0) + m[0].length;
507
+ while (/\s/.test(src[i] ?? "")) i++;
508
+ if (src[i] === "<") {
509
+ let depth = 0;
510
+ for (; i < src.length; i++) {
511
+ if (src[i] === "<") depth++;
512
+ else if (src[i] === ">" && src[i - 1] !== "=" && --depth === 0) break;
513
+ }
514
+ i++;
515
+ while (/\s/.test(src[i] ?? "")) i++;
516
+ }
517
+ if (src[i] !== "(") continue;
518
+ const start = i + 1;
519
+ let depth = 0;
520
+ let quote = "";
521
+ for (; i < src.length; i++) {
522
+ const ch = src[i];
523
+ if (quote !== "") {
524
+ if (ch === "\\") i++;
525
+ else if (ch === quote) quote = "";
526
+ } else if (ch === '"' || ch === "'" || ch === "`") quote = ch;
527
+ else if (ch === "(") depth++;
528
+ else if (ch === ")" && --depth === 0) break;
529
+ }
530
+ out.push(src.slice(start, i));
531
+ }
532
+ return out;
533
+ }
534
+
289
535
  export function mkIssue(
290
536
  subcategory: string,
291
537
  description: string,
@@ -5,6 +5,7 @@
5
5
 
6
6
  import type { AxisCheck } from "./types.js";
7
7
  import { UNIVERSAL_CHECKS } from "./checks/universal.js";
8
+ import { TINT_CHECKS } from "./checks/tint.js";
8
9
  import { STATE_MERGE_CHECKS } from "./checks/state-merge.js";
9
10
  import { STATE_UI_AFFORDANCE_CHECKS } from "./checks/state-ui-affordance.js";
10
11
  import { REALTIME_CHECKS } from "./checks/realtime.js";
@@ -16,6 +17,7 @@ import { EXTRA_CHECKS } from "./extras.js";
16
17
 
17
18
  export const REGISTRY: readonly AxisCheck[] = [
18
19
  ...UNIVERSAL_CHECKS,
20
+ ...TINT_CHECKS,
19
21
  ...STATE_MERGE_CHECKS,
20
22
  ...STATE_UI_AFFORDANCE_CHECKS,
21
23
  ...REALTIME_CHECKS,