@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.
- package/dist/{adapter-h4_V69LI.d.ts → adapter-DLQ-5COm.d.ts} +1 -1
- package/dist/adapters/base.js +2 -27
- package/dist/adapters/base.js.map +1 -1
- package/dist/adapters/generation-dispatch.d.ts +14 -6
- package/dist/adapters/generation-dispatch.js +40 -19095
- package/dist/adapters/generation-dispatch.js.map +1 -1
- package/dist/adapters/index.d.ts +1 -1
- package/dist/adapters/index.js +25 -3561
- package/dist/adapters/index.js.map +1 -1
- package/dist/adapters/types.js +2 -9
- package/dist/adapters/types.js.map +1 -1
- package/dist/advanced/index.d.ts +13 -12
- package/dist/advanced/index.js +63 -21789
- package/dist/advanced/index.js.map +1 -1
- package/dist/{bank-Ctdp3FUA.d.ts → bank-LWaiAtaA.d.ts} +1 -1
- package/dist/blueprint-validator.js +6 -2301
- package/dist/blueprint-validator.js.map +1 -1
- package/dist/boilerplate.d.ts +3 -2
- package/dist/boilerplate.js +6 -1867
- package/dist/boilerplate.js.map +1 -1
- package/dist/check/index.d.ts +3 -2
- package/dist/check/index.js +6 -1923
- package/dist/check/index.js.map +1 -1
- package/dist/chunk-33B3BAWS.js +24 -0
- package/dist/chunk-33B3BAWS.js.map +1 -0
- package/dist/chunk-3HSGA6NI.js +591 -0
- package/dist/chunk-3HSGA6NI.js.map +1 -0
- package/dist/chunk-3NNNGNTO.js +1707 -0
- package/dist/chunk-3NNNGNTO.js.map +1 -0
- package/dist/chunk-3T2HMTON.js +79 -0
- package/dist/chunk-3T2HMTON.js.map +1 -0
- package/dist/chunk-3ZSSDV3L.js +123 -0
- package/dist/chunk-3ZSSDV3L.js.map +1 -0
- package/dist/chunk-4MBR5GA3.js +99 -0
- package/dist/chunk-4MBR5GA3.js.map +1 -0
- package/dist/chunk-56NJFD55.js +337 -0
- package/dist/chunk-56NJFD55.js.map +1 -0
- package/dist/chunk-5AGTY555.js +26 -0
- package/dist/chunk-5AGTY555.js.map +1 -0
- package/dist/chunk-5EJM7EGY.js +115 -0
- package/dist/chunk-5EJM7EGY.js.map +1 -0
- package/dist/chunk-5KXCQLLR.js +529 -0
- package/dist/chunk-5KXCQLLR.js.map +1 -0
- package/dist/chunk-5VBXHVEH.js +171 -0
- package/dist/chunk-5VBXHVEH.js.map +1 -0
- package/dist/chunk-6KDDEING.js +515 -0
- package/dist/chunk-6KDDEING.js.map +1 -0
- package/dist/chunk-6VDRWAP6.js +123 -0
- package/dist/chunk-6VDRWAP6.js.map +1 -0
- package/dist/chunk-732IBFEJ.js +2400 -0
- package/dist/chunk-732IBFEJ.js.map +1 -0
- package/dist/chunk-73AURS3I.js +46 -0
- package/dist/chunk-73AURS3I.js.map +1 -0
- package/dist/chunk-77H5SKLX.js +8 -0
- package/dist/chunk-77H5SKLX.js.map +1 -0
- package/dist/chunk-7QE3AHLE.js +35 -0
- package/dist/chunk-7QE3AHLE.js.map +1 -0
- package/dist/chunk-A375NNWN.js +11 -0
- package/dist/chunk-A375NNWN.js.map +1 -0
- package/dist/chunk-AOU33EFS.js +140 -0
- package/dist/chunk-AOU33EFS.js.map +1 -0
- package/dist/chunk-BA4QYA6E.js +461 -0
- package/dist/chunk-BA4QYA6E.js.map +1 -0
- package/dist/chunk-DBP764GL.js +3 -0
- package/dist/chunk-DBP764GL.js.map +1 -0
- package/dist/chunk-DUU3ZJDQ.js +41 -0
- package/dist/chunk-DUU3ZJDQ.js.map +1 -0
- package/dist/chunk-DXKRBXNU.js +30 -0
- package/dist/chunk-DXKRBXNU.js.map +1 -0
- package/dist/chunk-FB7OEEAH.js +1467 -0
- package/dist/chunk-FB7OEEAH.js.map +1 -0
- package/dist/chunk-FLRNMZV2.js +18 -0
- package/dist/chunk-FLRNMZV2.js.map +1 -0
- package/dist/chunk-FWJIMKFZ.js +2032 -0
- package/dist/chunk-FWJIMKFZ.js.map +1 -0
- package/dist/chunk-FXRHWYIQ.js +163 -0
- package/dist/chunk-FXRHWYIQ.js.map +1 -0
- package/dist/chunk-G3PMV62Z.js +33 -0
- package/dist/chunk-G3PMV62Z.js.map +1 -0
- package/dist/chunk-GAJI7Y3P.js +13 -0
- package/dist/chunk-GAJI7Y3P.js.map +1 -0
- package/dist/chunk-GYINWMPR.js +2052 -0
- package/dist/chunk-GYINWMPR.js.map +1 -0
- package/dist/chunk-HD6ABIK7.js +484 -0
- package/dist/chunk-HD6ABIK7.js.map +1 -0
- package/dist/chunk-HDQVHYO6.js +100 -0
- package/dist/chunk-HDQVHYO6.js.map +1 -0
- package/dist/chunk-IHUT3SIW.js +7 -0
- package/dist/chunk-IHUT3SIW.js.map +1 -0
- package/dist/chunk-JDH5EGNZ.js +126 -0
- package/dist/chunk-JDH5EGNZ.js.map +1 -0
- package/dist/chunk-JMD4RGSV.js +37 -0
- package/dist/chunk-JMD4RGSV.js.map +1 -0
- package/dist/chunk-KJWGZL5B.js +33 -0
- package/dist/chunk-KJWGZL5B.js.map +1 -0
- package/dist/chunk-MKMDQFZ7.js +86 -0
- package/dist/chunk-MKMDQFZ7.js.map +1 -0
- package/dist/chunk-NF6JUZWF.js +85 -0
- package/dist/chunk-NF6JUZWF.js.map +1 -0
- package/dist/chunk-O2YC6XDC.js +1933 -0
- package/dist/chunk-O2YC6XDC.js.map +1 -0
- package/dist/chunk-OVLH5ICV.js +29 -0
- package/dist/chunk-OVLH5ICV.js.map +1 -0
- package/dist/chunk-PBOYHGSD.js +106 -0
- package/dist/chunk-PBOYHGSD.js.map +1 -0
- package/dist/chunk-PJFH7QWA.js +81 -0
- package/dist/chunk-PJFH7QWA.js.map +1 -0
- package/dist/chunk-PUH4CXGZ.js +284 -0
- package/dist/chunk-PUH4CXGZ.js.map +1 -0
- package/dist/chunk-QLXXPGTV.js +18 -0
- package/dist/chunk-QLXXPGTV.js.map +1 -0
- package/dist/chunk-R5GIGPW2.js +2845 -0
- package/dist/chunk-R5GIGPW2.js.map +1 -0
- package/dist/chunk-RH4IUSTO.js +83 -0
- package/dist/chunk-RH4IUSTO.js.map +1 -0
- package/dist/chunk-S2GVXKKY.js +153 -0
- package/dist/chunk-S2GVXKKY.js.map +1 -0
- package/dist/chunk-TVHGKFJS.js +58 -0
- package/dist/chunk-TVHGKFJS.js.map +1 -0
- package/dist/chunk-U7GALWLG.js +66 -0
- package/dist/chunk-U7GALWLG.js.map +1 -0
- package/dist/chunk-VTC263LT.js +256 -0
- package/dist/chunk-VTC263LT.js.map +1 -0
- package/dist/chunk-WEOXFEVE.js +739 -0
- package/dist/chunk-WEOXFEVE.js.map +1 -0
- package/dist/chunk-WYHP46LT.js +28 -0
- package/dist/chunk-WYHP46LT.js.map +1 -0
- package/dist/chunk-XGLO5LGG.js +274 -0
- package/dist/chunk-XGLO5LGG.js.map +1 -0
- package/dist/chunk-XRBCRKA2.js +29 -0
- package/dist/chunk-XRBCRKA2.js.map +1 -0
- package/dist/chunk-XW7ADFFV.js +340 -0
- package/dist/chunk-XW7ADFFV.js.map +1 -0
- package/dist/chunk-YIQ42IUX.js +34 -0
- package/dist/chunk-YIQ42IUX.js.map +1 -0
- package/dist/chunk-YJZTUY7B.js +101 -0
- package/dist/chunk-YJZTUY7B.js.map +1 -0
- package/dist/chunk-Z3CXME3K.js +107 -0
- package/dist/chunk-Z3CXME3K.js.map +1 -0
- package/dist/classifier/index.js +2 -527
- package/dist/classifier/index.js.map +1 -1
- package/dist/client-JBPCXU7W.js +4 -0
- package/dist/client-JBPCXU7W.js.map +1 -0
- package/dist/compose.js +3 -367
- package/dist/compose.js.map +1 -1
- package/dist/cost-tracker-HHGQ4FTW.js +67 -0
- package/dist/cost-tracker-HHGQ4FTW.js.map +1 -0
- package/dist/{create-ui-generator-BrZHF0cf.d.ts → create-ui-generator-BY-WeaPi.d.ts} +3 -3
- package/dist/{design-mode-DGWN_Gkm.d.ts → design-mode-XyOQmzE0.d.ts} +4 -3
- package/dist/{dispatch-B07TI30d.d.ts → dispatch-8LzTddKw.d.ts} +3 -2
- package/dist/dist-UALN6ZVB.js +543 -0
- package/dist/dist-UALN6ZVB.js.map +1 -0
- package/dist/dupe-break-X6K7QFIS.js +4 -0
- package/dist/dupe-break-X6K7QFIS.js.map +1 -0
- package/dist/evaluation/axis-checks/index.d.ts +5 -4
- package/dist/evaluation/axis-checks/index.js +6 -1818
- package/dist/evaluation/axis-checks/index.js.map +1 -1
- package/dist/evaluation/axis-checks/registry.d.ts +3 -2
- package/dist/evaluation/axis-checks/registry.js +3 -1748
- package/dist/evaluation/axis-checks/registry.js.map +1 -1
- package/dist/evaluation/index.d.ts +49 -132
- package/dist/evaluation/index.js +302 -6368
- package/dist/evaluation/index.js.map +1 -1
- package/dist/evaluation/types.d.ts +3 -2
- package/dist/evaluation/types.js +2 -5
- package/dist/evaluation/types.js.map +1 -1
- package/dist/fragments/index.js +2 -338
- package/dist/fragments/index.js.map +1 -1
- package/dist/{generate-tSO_shQ0.d.ts → generate-DQ4ci90s.d.ts} +1 -1
- package/dist/harness/check/runtime-render/index.d.ts +35 -15
- package/dist/harness/check/runtime-render/index.js +6 -2390
- package/dist/harness/check/runtime-render/index.js.map +1 -1
- package/dist/harness/check/runtime-render/render-check-worker.js +4 -1739
- package/dist/harness/check/runtime-render/render-check-worker.js.map +1 -1
- package/dist/harness/index.d.ts +3 -2
- package/dist/harness/index.js +23 -5124
- package/dist/harness/index.js.map +1 -1
- package/dist/harness/llm-trace-sink.js +2 -24
- package/dist/harness/llm-trace-sink.js.map +1 -1
- package/dist/harness/prompts.d.ts +2 -2
- package/dist/harness/prompts.js +8 -324
- package/dist/harness/prompts.js.map +1 -1
- package/dist/harness/result-types.d.ts +18 -6
- package/dist/harness/result-types.js +2 -0
- package/dist/harness/result-types.js.map +1 -1
- package/dist/harness/runtime.d.ts +22 -5
- package/dist/harness/runtime.js +10 -2313
- package/dist/harness/runtime.js.map +1 -1
- package/dist/harness/types-public.d.ts +3 -2
- package/dist/harness/types-public.js +16 -2954
- package/dist/harness/types-public.js.map +1 -1
- package/dist/harness/validator-trace-sink.d.ts +3 -2
- package/dist/harness/validator-trace-sink.js +2 -26
- package/dist/harness/validator-trace-sink.js.map +1 -1
- package/dist/hash.js +2 -39
- package/dist/hash.js.map +1 -1
- package/dist/hashline-5OKREIRM.js +4 -0
- package/dist/hashline-5OKREIRM.js.map +1 -0
- package/dist/index.d.ts +51 -12
- package/dist/index.js +57 -21850
- package/dist/index.js.map +1 -1
- package/dist/llm-evaluator-PN3WFCS4.js +653 -0
- package/dist/llm-evaluator-PN3WFCS4.js.map +1 -0
- package/dist/magic-string.es-2NZTC4GZ.js +1307 -0
- package/dist/magic-string.es-2NZTC4GZ.js.map +1 -0
- package/dist/patch.d.ts +3 -2
- package/dist/patch.js +2 -56
- package/dist/patch.js.map +1 -1
- package/dist/policy-VCYKGSAC.js +5 -0
- package/dist/policy-VCYKGSAC.js.map +1 -0
- package/dist/policy.js +2 -35
- package/dist/policy.js.map +1 -1
- package/dist/primitive-index-2OF6F4TZ.js +131 -0
- package/dist/primitive-index-2OF6F4TZ.js.map +1 -0
- package/dist/primitive-slice-WTMBLYFJ.js +235 -0
- package/dist/primitive-slice-WTMBLYFJ.js.map +1 -0
- package/dist/{prompts-BhNUuRoZ.d.ts → prompts-CLZGywMP.d.ts} +1 -1
- package/dist/provider-adapter-contract.js +10407 -14204
- package/dist/provider-adapter-contract.js.map +1 -1
- package/dist/provider-adapter.js +2 -44
- package/dist/provider-adapter.js.map +1 -1
- package/dist/providers/index.d.ts +89 -1
- package/dist/providers/index.js +115 -44
- package/dist/providers/index.js.map +1 -1
- package/dist/raw-C6KBH5I3.js +141 -0
- package/dist/raw-C6KBH5I3.js.map +1 -0
- package/dist/raw-FXSBXQTZ.js +7 -0
- package/dist/raw-FXSBXQTZ.js.map +1 -0
- package/dist/raw-MURDUPGK.js +8 -0
- package/dist/raw-MURDUPGK.js.map +1 -0
- package/dist/raw-T3GW3UVX.js +8 -0
- package/dist/raw-T3GW3UVX.js.map +1 -0
- package/dist/render-check-host-ZNIS6BJ5.js +5 -0
- package/dist/render-check-host-ZNIS6BJ5.js.map +1 -0
- package/dist/sdk-4JNSDNSR.js +6 -0
- package/dist/sdk-4JNSDNSR.js.map +1 -0
- package/dist/sdk-DIVJMHCX.js +7 -0
- package/dist/sdk-DIVJMHCX.js.map +1 -0
- package/dist/sdk-QRJOCRTS.js +7 -0
- package/dist/sdk-QRJOCRTS.js.map +1 -0
- package/dist/tag-balance-CTHCTRJ7.js +123 -0
- package/dist/tag-balance-CTHCTRJ7.js.map +1 -0
- package/dist/tools/get-primitives-ts.d.ts +1 -1
- package/dist/tools/get-primitives-ts.js +3 -1
- package/dist/tools/get-primitives-ts.js.map +1 -1
- package/dist/tools/render-check-worker.js +3 -40
- package/dist/tools/render-check-worker.js.map +1 -1
- package/dist/{types-public-h8IMnYuh.d.ts → types-public-hYlrerZh.d.ts} +35 -2
- package/dist/validation/index.d.ts +2 -2
- package/dist/validation/index.js +3 -498
- package/dist/validation/index.js.map +1 -1
- package/dist/{visual-evaluator-C2strxqD.d.ts → visual-evaluator-KskajxD5.d.ts} +14 -4
- package/dist/visual-evaluator-TGEXYQTP.js +12 -0
- package/dist/visual-evaluator-TGEXYQTP.js.map +1 -0
- package/dist/workflows.d.ts +3 -2
- package/dist/workflows.js +2 -151
- package/dist/workflows.js.map +1 -1
- package/package.json +21 -17
- package/src/adapters/claude/sdk.ts +4 -2
- package/src/adapters/claude/tool-bridge.ts +2 -1
- package/src/adapters/generation-dispatch.ts +9 -0
- package/src/adapters/openai/raw.ts +4 -0
- package/src/adapters/openai/sdk.ts +6 -1
- package/src/adapters/openai/tokens.ts +21 -7
- package/src/adapters/provider-router.ts +1 -0
- package/src/adapters/tool-bridge.ts +1 -1
- package/src/adapters/tools.ts +1 -1
- package/src/advanced/feedback.ts +1 -2
- package/src/advanced/generator.ts +4 -3
- package/src/boilerplate/free-design-prompt.ts +7 -0
- package/src/boilerplate/generate.ts +18 -5
- package/src/boilerplate/hard-sections.ts +8 -3
- package/src/boilerplate/react-hooks.ts +56 -0
- package/src/boilerplate/system-prompt.ts +12 -3
- package/src/check/run-tier0.ts +233 -8
- package/src/check/type-checker.ts +128 -16
- package/src/coding-agent/tools.ts +26 -3
- package/src/create-ui-generator.ts +4 -2
- package/src/design-mode.ts +4 -9
- package/src/evaluation/axis-checks/checks/state-merge.ts +3 -5
- package/src/evaluation/axis-checks/checks/state-payload.ts +1 -28
- package/src/evaluation/axis-checks/checks/tint.ts +197 -0
- package/src/evaluation/axis-checks/helpers.ts +246 -0
- package/src/evaluation/axis-checks/registry.ts +2 -0
- package/src/evaluation/criteria/__fixtures__/edge-v2-draw2.json +4629 -0
- package/src/evaluation/criteria/bank.ts +1 -1
- package/src/evaluation/criteria/edge-instruments.ts +14 -0
- package/src/evaluation/criteria/edge-probe-v2.ts +352 -0
- package/src/evaluation/criteria/edge-v2.ts +242 -0
- package/src/evaluation/criteria/resolve.ts +24 -5
- package/src/evaluation/evaluator.ts +2 -2
- package/src/evaluation/index.ts +3 -5
- package/src/evaluation/llm-evaluator.ts +2 -2
- package/src/evaluation/mcp-server.ts +0 -50
- package/src/evaluation/types-public.ts +6 -1
- package/src/evaluation/visual-evaluator.ts +162 -31
- package/src/harness/check/runtime-render/index.ts +28 -4
- package/src/harness/check/runtime-render/render-check.ts +20 -21
- package/src/harness/claude-agent-sdk-loader.ts +93 -0
- package/src/harness/claude-code-login-query.ts +1 -1
- package/src/harness/coding/assemble-result.ts +1 -0
- package/src/harness/coding/dupe-break.ts +70 -0
- package/src/harness/coding/generate-task-runner.ts +6 -1
- package/src/harness/coding/run-coding-turn.ts +59 -1
- package/src/harness/coding/run-eval-round.ts +9 -0
- package/src/harness/llm-router.ts +50 -2
- package/src/harness/result-types.ts +12 -0
- package/src/harness/runtime.ts +17 -0
- package/src/index.ts +10 -1
- package/src/providers/anthropic-tool-call.ts +263 -0
- package/src/providers/index.ts +13 -0
- package/src/tools/get-primitives-ts.ts +1 -1
- package/src/tools/render-check.ts +4 -4
- package/src/validation/primitives.ts +2 -1
- 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
|
-
|
|
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' :
|
|
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
|
-
|
|
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
|
/**
|
package/src/design-mode.ts
CHANGED
|
@@ -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
|
-
|
|
42
|
-
|
|
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
|
-
|
|
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
|
|
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,
|