@ggui-ai/ui-gen 0.28.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 -19260
- 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 -21954
- 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 -1914
- 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 -1844
- package/dist/evaluation/axis-checks/registry.js.map +1 -1
- package/dist/evaluation/index.d.ts +49 -132
- package/dist/evaluation/index.js +302 -6466
- 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 -5126
- 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 +6 -5
- 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 +50 -11
- package/dist/index.js +57 -22015
- 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 +20 -16
- 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/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/design-mode.ts +4 -9
- package/src/evaluation/axis-checks/checks/state-merge.ts +2 -4
- 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 +121 -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/index.ts +3 -5
- package/src/evaluation/mcp-server.ts +0 -50
- package/src/evaluation/types-public.ts +6 -1
- package/src/evaluation/visual-evaluator.ts +161 -30
- 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/generate-task-runner.ts +1 -0
- package/src/harness/coding/run-coding-turn.ts +3 -0
- package/src/harness/coding/run-eval-round.ts +9 -0
- package/src/harness/llm-router.ts +50 -2
- package/src/harness/runtime.ts +17 -0
- package/src/index.ts +9 -0
- 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
|
@@ -0,0 +1,2052 @@
|
|
|
1
|
+
import { CANVAS_VIEWPORTS } from './chunk-3T2HMTON.js';
|
|
2
|
+
import { LUCIDE_ICON_NAMES, maxWidth } from '@ggui-ai/design';
|
|
3
|
+
import { listContractGadgets, HOOK_NAME_RE, STDLIB_GADGETS_PACKAGE } from '@ggui-ai/protocol';
|
|
4
|
+
|
|
5
|
+
var ID_FIELD_CANDIDATES = ["id", "uuid", "symbol", "key", "slug", "code"];
|
|
6
|
+
function getItemsProperties(p) {
|
|
7
|
+
return p.items?.properties ?? p.schema?.items?.properties;
|
|
8
|
+
}
|
|
9
|
+
function inferIdField(itemProps) {
|
|
10
|
+
if (!itemProps) return "id";
|
|
11
|
+
for (const cand of ID_FIELD_CANDIDATES) {
|
|
12
|
+
if (cand in itemProps) return cand;
|
|
13
|
+
}
|
|
14
|
+
for (const [k, v] of Object.entries(itemProps)) {
|
|
15
|
+
const vv = v;
|
|
16
|
+
if (vv?.type === "string" || vv?.schema?.type === "string") return k;
|
|
17
|
+
}
|
|
18
|
+
return "id";
|
|
19
|
+
}
|
|
20
|
+
function getRequiredPropNames(contract) {
|
|
21
|
+
const propsField = contract?.propsSpec;
|
|
22
|
+
const properties = propsField?.properties ?? {};
|
|
23
|
+
return Object.entries(properties).filter(([, p]) => p && typeof p === "object" && p.required === true).map(([name]) => name);
|
|
24
|
+
}
|
|
25
|
+
function getActionNames(contract) {
|
|
26
|
+
return Object.keys(contract?.actionSpec ?? {});
|
|
27
|
+
}
|
|
28
|
+
function getStreamEventNames(contract) {
|
|
29
|
+
return Object.keys(contract?.streamSpec ?? {});
|
|
30
|
+
}
|
|
31
|
+
function getGadgetNames(contract) {
|
|
32
|
+
if (!contract) return [];
|
|
33
|
+
return listContractGadgets(contract).filter((use) => HOOK_NAME_RE.test(use.name)).map(
|
|
34
|
+
(use) => use.name.length > 3 ? use.name.charAt(3).toLowerCase() + use.name.slice(4) : use.name
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
function getStdlibGadgetNames(contract) {
|
|
38
|
+
if (!contract) return [];
|
|
39
|
+
return listContractGadgets(contract).filter(
|
|
40
|
+
(use) => use.package === STDLIB_GADGETS_PACKAGE && HOOK_NAME_RE.test(use.name)
|
|
41
|
+
).map(
|
|
42
|
+
(use) => use.name.length > 3 ? use.name.charAt(3).toLowerCase() + use.name.slice(4) : use.name
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
function getEntityCollections(contract) {
|
|
46
|
+
const propsField = contract?.propsSpec;
|
|
47
|
+
const properties = propsField?.properties ?? {};
|
|
48
|
+
const entities = [];
|
|
49
|
+
for (const [name, p] of Object.entries(properties)) {
|
|
50
|
+
if (!p || typeof p !== "object") continue;
|
|
51
|
+
const pp = p;
|
|
52
|
+
const type = pp.type ?? pp.schema?.type;
|
|
53
|
+
const items = pp.items ?? pp.schema?.items;
|
|
54
|
+
if (type === "array" && items?.type === "object") {
|
|
55
|
+
entities.push({ name, idField: inferIdField(getItemsProperties(pp)) });
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return entities;
|
|
59
|
+
}
|
|
60
|
+
function singularize(name) {
|
|
61
|
+
if (name.endsWith("ies")) return name.slice(0, -3) + "y";
|
|
62
|
+
if (name.endsWith("ses")) return name.slice(0, -2);
|
|
63
|
+
if (name.endsWith("s") && !name.endsWith("ss")) return name.slice(0, -1);
|
|
64
|
+
return name;
|
|
65
|
+
}
|
|
66
|
+
function getMutatedEntityCollections(contract, allEntities) {
|
|
67
|
+
if (!contract) return allEntities;
|
|
68
|
+
const actionSpec = contract.actionSpec ?? {};
|
|
69
|
+
const streamSpec = contract.streamSpec ?? {};
|
|
70
|
+
const hasStreams = Object.keys(streamSpec).length > 0;
|
|
71
|
+
const referencedIdKeys = /* @__PURE__ */ new Set();
|
|
72
|
+
for (const action of Object.values(actionSpec)) {
|
|
73
|
+
const ex = action.example;
|
|
74
|
+
if (!ex || typeof ex !== "object" || Array.isArray(ex)) continue;
|
|
75
|
+
for (const key of Object.keys(ex)) {
|
|
76
|
+
if (key === "id" || key === "key" || key === "index") {
|
|
77
|
+
referencedIdKeys.add("id");
|
|
78
|
+
referencedIdKeys.add("key");
|
|
79
|
+
referencedIdKeys.add("index");
|
|
80
|
+
} else if (/Id$/.test(key)) {
|
|
81
|
+
referencedIdKeys.add(key.slice(0, -2).toLowerCase());
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
const mutated = allEntities.filter(
|
|
86
|
+
(e) => referencedIdKeys.has(singularize(e.name).toLowerCase())
|
|
87
|
+
);
|
|
88
|
+
if (mutated.length === 0 && hasStreams && allEntities.length > 0) {
|
|
89
|
+
return [allEntities[0]];
|
|
90
|
+
}
|
|
91
|
+
return mutated.length > 0 ? mutated : allEntities;
|
|
92
|
+
}
|
|
93
|
+
function countScalarKeys(example) {
|
|
94
|
+
if (!example || typeof example !== "object" || Array.isArray(example)) return [];
|
|
95
|
+
const keys = [];
|
|
96
|
+
for (const [k, v] of Object.entries(example)) {
|
|
97
|
+
const t = typeof v;
|
|
98
|
+
if (t === "string" || t === "number" || t === "boolean") keys.push(k);
|
|
99
|
+
}
|
|
100
|
+
return keys;
|
|
101
|
+
}
|
|
102
|
+
function getSubmitActions(contract) {
|
|
103
|
+
const actionSpec = contract?.actionSpec ?? {};
|
|
104
|
+
const result = [];
|
|
105
|
+
for (const [name, action] of Object.entries(actionSpec)) {
|
|
106
|
+
const ex = action.example;
|
|
107
|
+
const scalarKeys = countScalarKeys(ex);
|
|
108
|
+
if (scalarKeys.length < 3) continue;
|
|
109
|
+
const allKeys = ex && typeof ex === "object" && !Array.isArray(ex) ? Object.keys(ex) : scalarKeys;
|
|
110
|
+
result.push({ name, payloadKeys: allKeys });
|
|
111
|
+
}
|
|
112
|
+
return result;
|
|
113
|
+
}
|
|
114
|
+
function getArrStrProps(contract) {
|
|
115
|
+
const propsField = contract?.propsSpec;
|
|
116
|
+
const properties = propsField?.properties ?? {};
|
|
117
|
+
const names = [];
|
|
118
|
+
for (const [name, p] of Object.entries(properties)) {
|
|
119
|
+
if (!p || typeof p !== "object") continue;
|
|
120
|
+
const pp = p;
|
|
121
|
+
const type = pp.type ?? pp.schema?.type;
|
|
122
|
+
const items = pp.items ?? pp.schema?.items;
|
|
123
|
+
if (type === "array" && items?.type === "string") names.push(name);
|
|
124
|
+
}
|
|
125
|
+
return names;
|
|
126
|
+
}
|
|
127
|
+
function getInitialValuePropNames(contract) {
|
|
128
|
+
const propsField = contract?.propsSpec;
|
|
129
|
+
const properties = propsField?.properties ?? {};
|
|
130
|
+
const names = [];
|
|
131
|
+
for (const [name, p] of Object.entries(properties)) {
|
|
132
|
+
if (!p || typeof p !== "object") continue;
|
|
133
|
+
const pp = p;
|
|
134
|
+
const type = pp.type ?? pp.schema?.type;
|
|
135
|
+
if (type === "object" && /^initial/i.test(name)) names.push(name);
|
|
136
|
+
}
|
|
137
|
+
return names;
|
|
138
|
+
}
|
|
139
|
+
function objectLiteralKeysAt(src, open) {
|
|
140
|
+
if (src[open] !== "{") return void 0;
|
|
141
|
+
const keys = [];
|
|
142
|
+
let depth = 0;
|
|
143
|
+
let expectKey = true;
|
|
144
|
+
for (let i = open; i < src.length; i++) {
|
|
145
|
+
const c = src[i];
|
|
146
|
+
if (c === '"' || c === "'" || c === "`") {
|
|
147
|
+
let j = i + 1;
|
|
148
|
+
while (j < src.length && src[j] !== c) j += src[j] === "\\" ? 2 : 1;
|
|
149
|
+
if (depth === 1 && expectKey && /^\s*:/.test(src.slice(j + 1))) keys.push(src.slice(i + 1, j));
|
|
150
|
+
i = j;
|
|
151
|
+
expectKey = false;
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
if (c === "{" || c === "[" || c === "(") {
|
|
155
|
+
depth++;
|
|
156
|
+
if (depth === 1) expectKey = true;
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
if (c === "}" || c === "]" || c === ")") {
|
|
160
|
+
depth--;
|
|
161
|
+
if (depth === 0) return keys;
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
if (depth !== 1) continue;
|
|
165
|
+
if (c === ",") {
|
|
166
|
+
expectKey = true;
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
if (expectKey && /[A-Za-z_$]/.test(c ?? "")) {
|
|
170
|
+
const m = /^([A-Za-z_$][\w$]*)\s*(:|,|\})/.exec(src.slice(i));
|
|
171
|
+
if (m !== null && m[1] !== void 0) keys.push(m[1]);
|
|
172
|
+
expectKey = false;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return keys;
|
|
176
|
+
}
|
|
177
|
+
function callArgsAt(src, open) {
|
|
178
|
+
const args = [];
|
|
179
|
+
let depth = 0;
|
|
180
|
+
let start = open + 1;
|
|
181
|
+
for (let i = open; i < src.length; i++) {
|
|
182
|
+
const c = src[i];
|
|
183
|
+
if (c === '"' || c === "'" || c === "`") {
|
|
184
|
+
let j = i + 1;
|
|
185
|
+
while (j < src.length && src[j] !== c) j += src[j] === "\\" ? 2 : 1;
|
|
186
|
+
i = j;
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
if (c === "(" || c === "{" || c === "[") depth++;
|
|
190
|
+
else if (c === ")" || c === "}" || c === "]") {
|
|
191
|
+
depth--;
|
|
192
|
+
if (depth === 0) {
|
|
193
|
+
args.push(src.slice(start, i));
|
|
194
|
+
return args;
|
|
195
|
+
}
|
|
196
|
+
} else if (c === "," && depth === 1) {
|
|
197
|
+
args.push(src.slice(start, i));
|
|
198
|
+
start = i + 1;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return args;
|
|
202
|
+
}
|
|
203
|
+
function initialStateKeys(src, argStart, argText) {
|
|
204
|
+
const lead = argText.length - argText.trimStart().length;
|
|
205
|
+
let at = argStart + lead;
|
|
206
|
+
let text = argText.trim();
|
|
207
|
+
const lazy = /^\(\s*\)\s*=>\s*\(?\s*/.exec(text);
|
|
208
|
+
if (lazy !== null) {
|
|
209
|
+
at += lazy[0].length;
|
|
210
|
+
text = text.slice(lazy[0].length);
|
|
211
|
+
}
|
|
212
|
+
if (text.startsWith("{")) return objectLiteralKeysAt(src, at) ?? [];
|
|
213
|
+
const ident = /^([A-Za-z_$][\w$]*)\s*\)?\s*$/.exec(text);
|
|
214
|
+
if (ident === null || ident[1] === void 0) return [];
|
|
215
|
+
const decls = [...src.matchAll(new RegExp(`(?:const|let|var)\\s+${ident[1]}\\s*(?::[^=]{1,200})?=\\s*`, "g"))];
|
|
216
|
+
const decl = decls.length === 1 ? decls[0] : void 0;
|
|
217
|
+
if (decl === void 0) return [];
|
|
218
|
+
return objectLiteralKeysAt(src, (decl.index ?? 0) + decl[0].length) ?? [];
|
|
219
|
+
}
|
|
220
|
+
function collectStateKeys(src) {
|
|
221
|
+
const keys = /* @__PURE__ */ new Set();
|
|
222
|
+
const varRe = /const\s*\[\s*(\w+)\s*,\s*(\w+)\s*\]\s*=\s*useState/g;
|
|
223
|
+
for (const m of src.matchAll(varRe)) keys.add(m[1]);
|
|
224
|
+
const contextRe = /const\s*\[\s*(\w+)\s*,\s*\w+\s*\]\s*=\s*useGguiContext\s*(?:<[^>]*>)?\s*\(\s*['"`](\w+)['"`]/g;
|
|
225
|
+
for (const m of src.matchAll(contextRe)) {
|
|
226
|
+
keys.add(m[1]);
|
|
227
|
+
keys.add(m[2]);
|
|
228
|
+
}
|
|
229
|
+
const objRe = /useState(?:<[^>]*>)?\s*\(\s*\{([\s\S]*?)\}\s*\)/g;
|
|
230
|
+
for (const m of src.matchAll(objRe)) {
|
|
231
|
+
const keyRe = /(?:^|,)\s*(\w+)\s*:/g;
|
|
232
|
+
for (const km of m[1].matchAll(keyRe)) keys.add(km[1]);
|
|
233
|
+
}
|
|
234
|
+
const defaultObjRe = /useState(?:<[^>]*>)?\s*\([^)]*\|\|\s*\{([^}]*)\}/g;
|
|
235
|
+
for (const m of src.matchAll(defaultObjRe)) {
|
|
236
|
+
const keyRe = /(?:^|,)\s*(\w+)\s*:/g;
|
|
237
|
+
for (const km of m[1].matchAll(keyRe)) keys.add(km[1]);
|
|
238
|
+
}
|
|
239
|
+
for (const m of src.matchAll(/\b(useState|useReducer)\s*(?:<[^>]*>)?\s*\(/g)) {
|
|
240
|
+
const open = (m.index ?? 0) + m[0].length - 1;
|
|
241
|
+
const args = callArgsAt(src, open);
|
|
242
|
+
const which = m[1] === "useReducer" ? 1 : 0;
|
|
243
|
+
const arg = args[which];
|
|
244
|
+
if (arg === void 0) continue;
|
|
245
|
+
let argStart = open + 1;
|
|
246
|
+
for (let k = 0; k < which; k++) argStart += (args[k]?.length ?? 0) + 1;
|
|
247
|
+
for (const key of initialStateKeys(src, argStart, arg)) keys.add(key);
|
|
248
|
+
}
|
|
249
|
+
return keys;
|
|
250
|
+
}
|
|
251
|
+
function seedsFromProp(src, name) {
|
|
252
|
+
const propRef = `props\\??\\.${name}\\b`;
|
|
253
|
+
const inits = useStateInitializers(src);
|
|
254
|
+
const anyInitReads = (rx) => inits.some((t) => rx.test(t));
|
|
255
|
+
if (anyInitReads(new RegExp(`\\b${propRef}`))) return true;
|
|
256
|
+
const locals = /* @__PURE__ */ new Set();
|
|
257
|
+
for (const m of src.matchAll(new RegExp(`(?:const|let|var)\\s+(\\w+)\\s*(?::[^=]{1,80})?=\\s*[^;]{0,300}?\\b${propRef}`, "g"))) {
|
|
258
|
+
const id = m[1];
|
|
259
|
+
if (id !== void 0) locals.add(id);
|
|
260
|
+
}
|
|
261
|
+
for (const m of src.matchAll(new RegExp(`(?:const|let|var)\\s*\\{([^}]{1,400})\\}\\s*=\\s*[^;]{0,300}?\\b${propRef}`, "g"))) {
|
|
262
|
+
for (const part of (m[1] ?? "").split(",")) {
|
|
263
|
+
const id = part.split(":").pop()?.split("=")[0]?.trim();
|
|
264
|
+
if (id !== void 0 && /^\w+$/.test(id)) locals.add(id);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
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;
|
|
268
|
+
for (const m of src.matchAll(fromProps)) {
|
|
269
|
+
for (const part of (m[1] ?? m[2] ?? "").split(",")) {
|
|
270
|
+
const [key, alias] = part.split(":");
|
|
271
|
+
if (key?.split("=")[0]?.trim() !== name) continue;
|
|
272
|
+
const id = (alias ?? key).split("=")[0]?.trim();
|
|
273
|
+
if (id !== void 0 && /^\w+$/.test(id)) locals.add(id);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
for (const id of locals) if (anyInitReads(new RegExp(`\\b${id}\\b`))) return true;
|
|
277
|
+
return seedsContextSlotFromProp(src, [new RegExp(`\\b${propRef}`), ...[...locals].map((id) => new RegExp(`\\b${id}\\b`))]);
|
|
278
|
+
}
|
|
279
|
+
function seedsContextSlotFromProp(src, readsProp) {
|
|
280
|
+
const reads = (text) => readsProp.some((rx) => rx.test(text));
|
|
281
|
+
const values = [];
|
|
282
|
+
const setters = [];
|
|
283
|
+
for (const m of src.matchAll(/(?:const|let|var)\s*\[\s*(\w+)\s*,\s*(\w+)\s*\]\s*=\s*useGguiContext\b/g)) {
|
|
284
|
+
if (m[1] !== void 0) values.push(m[1]);
|
|
285
|
+
if (m[2] !== void 0) setters.push(m[2]);
|
|
286
|
+
}
|
|
287
|
+
if (values.length === 0) return false;
|
|
288
|
+
for (const v of values) {
|
|
289
|
+
for (const m of src.matchAll(new RegExp(`(?<![.\\w$])${v}\\s*\\?\\?\\s*([^;\\n]{1,200})`, "g"))) {
|
|
290
|
+
if (reads(m[1] ?? "")) return true;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
for (const body of callArguments(src, /\buse(?:Layout)?Effect\b/g)) {
|
|
294
|
+
for (const setter of setters) {
|
|
295
|
+
for (const arg of callArguments(body, new RegExp(`\\b${setter}\\b`, "g"))) if (reads(arg)) return true;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
return false;
|
|
299
|
+
}
|
|
300
|
+
function useStateInitializers(src) {
|
|
301
|
+
return callArguments(src, /\buseState\b/g);
|
|
302
|
+
}
|
|
303
|
+
function callArguments(src, callee) {
|
|
304
|
+
const out = [];
|
|
305
|
+
for (const m of src.matchAll(callee)) {
|
|
306
|
+
let i = (m.index ?? 0) + m[0].length;
|
|
307
|
+
while (/\s/.test(src[i] ?? "")) i++;
|
|
308
|
+
if (src[i] === "<") {
|
|
309
|
+
let depth2 = 0;
|
|
310
|
+
for (; i < src.length; i++) {
|
|
311
|
+
if (src[i] === "<") depth2++;
|
|
312
|
+
else if (src[i] === ">" && src[i - 1] !== "=" && --depth2 === 0) break;
|
|
313
|
+
}
|
|
314
|
+
i++;
|
|
315
|
+
while (/\s/.test(src[i] ?? "")) i++;
|
|
316
|
+
}
|
|
317
|
+
if (src[i] !== "(") continue;
|
|
318
|
+
const start = i + 1;
|
|
319
|
+
let depth = 0;
|
|
320
|
+
let quote = "";
|
|
321
|
+
for (; i < src.length; i++) {
|
|
322
|
+
const ch = src[i];
|
|
323
|
+
if (quote !== "") {
|
|
324
|
+
if (ch === "\\") i++;
|
|
325
|
+
else if (ch === quote) quote = "";
|
|
326
|
+
} else if (ch === '"' || ch === "'" || ch === "`") quote = ch;
|
|
327
|
+
else if (ch === "(") depth++;
|
|
328
|
+
else if (ch === ")" && --depth === 0) break;
|
|
329
|
+
}
|
|
330
|
+
out.push(src.slice(start, i));
|
|
331
|
+
}
|
|
332
|
+
return out;
|
|
333
|
+
}
|
|
334
|
+
function mkIssue(subcategory, description, fix, result = "fail") {
|
|
335
|
+
return { tier: 0, result, category: "mode", priority: "P0", subcategory, description, fix };
|
|
336
|
+
}
|
|
337
|
+
function cap(s) {
|
|
338
|
+
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
339
|
+
}
|
|
340
|
+
function axisCheckTraceEnabled() {
|
|
341
|
+
return process.env["GGUI_AXIS_CHECK_TRACE"] === "1";
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// src/evaluation/axis-checks/checks/universal.ts
|
|
345
|
+
var ALL_RENDER_VALUES = [
|
|
346
|
+
"static",
|
|
347
|
+
"list",
|
|
348
|
+
"grid",
|
|
349
|
+
"spatial",
|
|
350
|
+
"timeline",
|
|
351
|
+
"chart",
|
|
352
|
+
"master-detail"
|
|
353
|
+
];
|
|
354
|
+
function runPropCoverage(input) {
|
|
355
|
+
if (input.compiledCode === null) return [];
|
|
356
|
+
const src = input.sourceCode;
|
|
357
|
+
const requiredProps = getRequiredPropNames(input.contract);
|
|
358
|
+
const issues = [];
|
|
359
|
+
for (const name of requiredProps) {
|
|
360
|
+
const dotAccess = new RegExp(`\\bprops\\.${name}\\b`);
|
|
361
|
+
const bracketAccess = new RegExp(`\\bprops\\[['"\`]${name}['"\`]\\]`);
|
|
362
|
+
const destructured = new RegExp(
|
|
363
|
+
`props[^;]{0,200}\\{[^}]*\\b${name}\\b[^}]*\\}|\\{[^}]*\\b${name}\\b[^}]*\\}[^;]{0,10}=\\s*props`
|
|
364
|
+
);
|
|
365
|
+
if (dotAccess.test(src) || bracketAccess.test(src) || destructured.test(src))
|
|
366
|
+
continue;
|
|
367
|
+
issues.push(
|
|
368
|
+
mkIssue(
|
|
369
|
+
"universal.prop_coverage",
|
|
370
|
+
`Required prop "${name}" is not referenced anywhere in the component.`,
|
|
371
|
+
`Render props.${name} \u2014 the data contract marks it required.`
|
|
372
|
+
)
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
return issues;
|
|
376
|
+
}
|
|
377
|
+
function runNoPropMirror(input) {
|
|
378
|
+
if (input.compiledCode === null) return [];
|
|
379
|
+
const src = input.sourceCode;
|
|
380
|
+
const issues = [];
|
|
381
|
+
const re = /const\s*\[\s*(\w+)\s*,\s*(\w+)\s*\]\s*=\s*useState(?:<[^>]*>)?\s*\(\s*props\??\.(\w+)/g;
|
|
382
|
+
for (const m of src.matchAll(re)) {
|
|
383
|
+
const [full, stateVar, setter, propName] = m;
|
|
384
|
+
const idx = m.index ?? 0;
|
|
385
|
+
const after = src.slice(idx + full.length);
|
|
386
|
+
if (new RegExp(`\\b${setter}\\s*\\(`).test(after)) continue;
|
|
387
|
+
issues.push(
|
|
388
|
+
mkIssue(
|
|
389
|
+
"universal.no_prop_mirror",
|
|
390
|
+
`useState(props.${propName}) for "${stateVar}" has no "${setter}" call \u2014 this mirrors a prop without mutation.`,
|
|
391
|
+
`Read props.${propName} directly in the render; remove the useState for ${stateVar}.`,
|
|
392
|
+
"warn"
|
|
393
|
+
)
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
return issues;
|
|
397
|
+
}
|
|
398
|
+
function runNoPhantomUseState(input) {
|
|
399
|
+
if (input.compiledCode === null) return [];
|
|
400
|
+
const src = input.sourceCode;
|
|
401
|
+
const issues = [];
|
|
402
|
+
const re = /const\s*\[\s*(\w+)\s*,\s*(\w+)\s*\]\s*=\s*useState/g;
|
|
403
|
+
for (const m of src.matchAll(re)) {
|
|
404
|
+
const [full, stateVar, setter] = m;
|
|
405
|
+
const idx = m.index ?? 0;
|
|
406
|
+
const after = src.slice(idx + full.length);
|
|
407
|
+
const stateUsed = new RegExp(`\\b${stateVar}\\b`).test(after);
|
|
408
|
+
const setterUsed = new RegExp(`\\b${setter}\\b`).test(after);
|
|
409
|
+
if (stateUsed || setterUsed) continue;
|
|
410
|
+
issues.push(
|
|
411
|
+
mkIssue(
|
|
412
|
+
"universal.no_phantom_useState",
|
|
413
|
+
`useState for "${stateVar}" is declared but neither "${stateVar}" nor "${setter}" is referenced.`,
|
|
414
|
+
`Remove the useState for ${stateVar} \u2014 it is dead state.`,
|
|
415
|
+
"warn"
|
|
416
|
+
)
|
|
417
|
+
);
|
|
418
|
+
}
|
|
419
|
+
return issues;
|
|
420
|
+
}
|
|
421
|
+
function runPropSeedNoResync(input) {
|
|
422
|
+
if (input.compiledCode === null) return [];
|
|
423
|
+
const src = input.sourceCode;
|
|
424
|
+
const issues = [];
|
|
425
|
+
const re = /const\s*\[\s*(\w+)\s*,\s*(\w+)\s*\]\s*=\s*useState(?:<[^>]*>)?\s*\(\s*props\??\.(\w+)/g;
|
|
426
|
+
for (const m of src.matchAll(re)) {
|
|
427
|
+
const [full, stateVar, setter, propName] = m;
|
|
428
|
+
const idx = m.index ?? 0;
|
|
429
|
+
const after = src.slice(idx + full.length);
|
|
430
|
+
if (!new RegExp(`\\b${setter}\\s*\\(`).test(after)) continue;
|
|
431
|
+
const resync = new RegExp(
|
|
432
|
+
`\\[[^\\[\\]]*props\\??\\.${propName}\\b[^\\[\\]]*\\]`
|
|
433
|
+
);
|
|
434
|
+
if (resync.test(src)) continue;
|
|
435
|
+
issues.push(
|
|
436
|
+
mkIssue(
|
|
437
|
+
"universal.prop_seed_no_resync",
|
|
438
|
+
`useState(props.${propName}) seeds "${stateVar}" and mutates it, but no effect depends on [props.${propName}] \u2014 a ggui_amend repaint pushes new props that never reach this display (#480).`,
|
|
439
|
+
`Add \`useEffect(() => ${setter}(props.${propName}), [props.${propName}]);\` so agent repaints re-seed the state, or derive the display directly from props.${propName}.`,
|
|
440
|
+
"warn"
|
|
441
|
+
)
|
|
442
|
+
);
|
|
443
|
+
}
|
|
444
|
+
return issues;
|
|
445
|
+
}
|
|
446
|
+
var KNOWN_ICON_KEYS = new Set(
|
|
447
|
+
LUCIDE_ICON_NAMES.map((n) => n.replace(/[-_]/g, "").toLowerCase())
|
|
448
|
+
);
|
|
449
|
+
function isKnownIconName(name) {
|
|
450
|
+
return KNOWN_ICON_KEYS.has(name.replace(/[-_]/g, "").toLowerCase());
|
|
451
|
+
}
|
|
452
|
+
var ICON_NAME_LITERAL_RX = /<Icon\b[^>]*?\bname\s*=\s*(?:"([^"]*)"|'([^']*)'|\{\s*"([^"]*)"\s*\}|\{\s*'([^']*)'\s*\})/g;
|
|
453
|
+
function findUnknownIconNames(sourceCode) {
|
|
454
|
+
const unknown = /* @__PURE__ */ new Set();
|
|
455
|
+
for (const m of sourceCode.matchAll(ICON_NAME_LITERAL_RX)) {
|
|
456
|
+
const name = m[1] ?? m[2] ?? m[3] ?? m[4] ?? "";
|
|
457
|
+
if (name.length === 0) continue;
|
|
458
|
+
if (/[^\x20-\x7e]/.test(name)) continue;
|
|
459
|
+
if (!isKnownIconName(name)) unknown.add(name);
|
|
460
|
+
}
|
|
461
|
+
return [...unknown];
|
|
462
|
+
}
|
|
463
|
+
function runIconNameKnown(input) {
|
|
464
|
+
if (input.compiledCode === null) return [];
|
|
465
|
+
const unknown = findUnknownIconNames(input.sourceCode);
|
|
466
|
+
if (unknown.length === 0) return [];
|
|
467
|
+
const list = unknown.map((n) => `"${n}"`).join(", ");
|
|
468
|
+
return [
|
|
469
|
+
mkIssue(
|
|
470
|
+
"universal.icon_name_known",
|
|
471
|
+
`<Icon name=\u2026> uses ${unknown.length} name(s) outside the design system's Lucide subset: ${list} \u2014 an unknown name renders an empty box.`,
|
|
472
|
+
"Call get_available_icons and pick a listed name (kebab-case), or use an emoji directly."
|
|
473
|
+
)
|
|
474
|
+
];
|
|
475
|
+
}
|
|
476
|
+
var LADDER_STOP_AS_TEXT_RX = /(?<![\w-])color\s*(?:=\s*\{?\s*|:\s*)["'`]?\s*var\(--ggui-color-primary-(\d{2,3})\s*[,)]/g;
|
|
477
|
+
var TEXT_INK_STOP_MIN = 300;
|
|
478
|
+
var TEXT_INK_STOP_MAX = 600;
|
|
479
|
+
function findLadderStopsAsText(sourceCode) {
|
|
480
|
+
const found = /* @__PURE__ */ new Set();
|
|
481
|
+
for (const m of sourceCode.matchAll(LADDER_STOP_AS_TEXT_RX)) {
|
|
482
|
+
const digits = m[1] ?? "";
|
|
483
|
+
const stop = Number(digits);
|
|
484
|
+
if (stop >= TEXT_INK_STOP_MIN && stop <= TEXT_INK_STOP_MAX) found.add(`primary-${digits}`);
|
|
485
|
+
}
|
|
486
|
+
return [...found];
|
|
487
|
+
}
|
|
488
|
+
function runAccentTextInk(input) {
|
|
489
|
+
if (input.compiledCode === null) return [];
|
|
490
|
+
const stops = findLadderStopsAsText(input.sourceCode);
|
|
491
|
+
if (stops.length === 0) return [];
|
|
492
|
+
const list = stops.map((s) => `\`${s}\``).join(", ");
|
|
493
|
+
return [
|
|
494
|
+
mkIssue(
|
|
495
|
+
"universal.accent_text_ink",
|
|
496
|
+
`Text is painted with a brand-ladder fill stop as its color (${list}) \u2014 on a low-saturation brand these stops are mid-grey on the surface and the text fails readability.`,
|
|
497
|
+
'Accent text (eyebrows, taglines, links, labels, inline arrows) takes var(--ggui-color-link) or <Text tone="emphasized">; body text takes var(--ggui-color-onContainer); keep primary-* stops on fills and borders.'
|
|
498
|
+
)
|
|
499
|
+
];
|
|
500
|
+
}
|
|
501
|
+
var SCOPED_SURFACE_TAG_RX = /<(Card|Box)\b([^<>]*?)surface\s*=\s*["'](inverted|hero)["']([^<>]*?)>/g;
|
|
502
|
+
var STYLE_BACKGROUND_RX = /\bstyle\s*=\s*\{\{[^}]*\b(background|backgroundColor|backgroundImage)\s*:/;
|
|
503
|
+
function findScopedSurfaceBackgrounds(sourceCode) {
|
|
504
|
+
const hits = [];
|
|
505
|
+
for (const m of sourceCode.matchAll(SCOPED_SURFACE_TAG_RX)) {
|
|
506
|
+
const attrs = `${m[2] ?? ""} ${m[4] ?? ""}`;
|
|
507
|
+
if (STYLE_BACKGROUND_RX.test(attrs)) hits.push([m[1] ?? "", m[3] ?? ""]);
|
|
508
|
+
}
|
|
509
|
+
return hits;
|
|
510
|
+
}
|
|
511
|
+
function runScopedSurfaceOwnsGround(input) {
|
|
512
|
+
if (input.compiledCode === null) return [];
|
|
513
|
+
if (input.designMode === "free") return [];
|
|
514
|
+
const hits = findScopedSurfaceBackgrounds(input.sourceCode);
|
|
515
|
+
if (hits.length === 0) return [];
|
|
516
|
+
const [tag, surface] = hits[0];
|
|
517
|
+
return [
|
|
518
|
+
mkIssue(
|
|
519
|
+
"universal.scoped_surface_owns_ground",
|
|
520
|
+
`<${tag} surface="${surface}"> sets its own background in style \u2014 a scoped surface owns its ground; repainting it puts the scope's swapped inks over the wrong ground (ink on ink).`,
|
|
521
|
+
`Remove the background from that ${tag}'s style \u2014 surface="${surface}" paints the ground; pick another surface if a different ground is wanted.`
|
|
522
|
+
)
|
|
523
|
+
];
|
|
524
|
+
}
|
|
525
|
+
var CAPS_TEXT_RX = /<Text\b(?=[^>]*\bcaps\b)[^>]*>([\s\S]*?)<\/Text>/g;
|
|
526
|
+
function findInventedCapsLabels(sourceCode) {
|
|
527
|
+
const out = [];
|
|
528
|
+
for (const m of sourceCode.matchAll(CAPS_TEXT_RX)) {
|
|
529
|
+
const inner = (m[1] ?? "").trim();
|
|
530
|
+
if (inner.length === 0) continue;
|
|
531
|
+
if (inner.includes("{")) continue;
|
|
532
|
+
out.push(inner.replace(/\s+/g, " ").slice(0, 60));
|
|
533
|
+
}
|
|
534
|
+
return out;
|
|
535
|
+
}
|
|
536
|
+
function runCapsLabelInvented(input) {
|
|
537
|
+
if (input.compiledCode === null) return [];
|
|
538
|
+
const invented = findInventedCapsLabels(input.sourceCode);
|
|
539
|
+
if (invented.length === 0) return [];
|
|
540
|
+
const list = invented.map((t) => `"${t}"`).join(", ");
|
|
541
|
+
return [
|
|
542
|
+
mkIssue(
|
|
543
|
+
"universal.caps_label_invented",
|
|
544
|
+
`${invented.length} caps label(s) carry invented copy the props do not supply: ${list} \u2014 an eyebrow, kicker or overline the agent cannot change is chatter on every render.`,
|
|
545
|
+
"Remove the label, or bind its text to a prop (`<Text caps>{props.section}</Text>`); copy comes from the contract, never from the component."
|
|
546
|
+
)
|
|
547
|
+
];
|
|
548
|
+
}
|
|
549
|
+
var VIEWPORT_UNIT = String.raw`(?:calc\(\s*)?100(?:vh|dvh|svh|lvh)\b(?:[^)"'\x60;\n]*\))?`;
|
|
550
|
+
var VIEWPORT_STYLE_OBJECT_RX = new RegExp(
|
|
551
|
+
String.raw`\b(?:minHeight|height|minBlockSize|blockSize)\s*:\s*["'\x60]\s*${VIEWPORT_UNIT}["'\x60]?`,
|
|
552
|
+
"g"
|
|
553
|
+
);
|
|
554
|
+
var VIEWPORT_CSS_RX = new RegExp(
|
|
555
|
+
String.raw`(?<![\w-])(?:min-)?(?:height|block-size)\s*:\s*${VIEWPORT_UNIT}`,
|
|
556
|
+
"g"
|
|
557
|
+
);
|
|
558
|
+
var VIEWPORT_CLASS_RX = /(?<![\w-])(?:min-)?h-(?:screen|dvh|svh|lvh)\b/g;
|
|
559
|
+
function findViewportSizing(sourceCode) {
|
|
560
|
+
const hits = [];
|
|
561
|
+
for (const rx of [VIEWPORT_STYLE_OBJECT_RX, VIEWPORT_CSS_RX, VIEWPORT_CLASS_RX]) {
|
|
562
|
+
for (const m of sourceCode.matchAll(rx)) {
|
|
563
|
+
hits.push({ at: m.index ?? 0, text: m[0].replace(/\s+/g, " ").trim().slice(0, 60) });
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
return hits.sort((a, b) => a.at - b.at).map((h) => h.text);
|
|
567
|
+
}
|
|
568
|
+
function runViewportSized(input) {
|
|
569
|
+
if (input.compiledCode === null) return [];
|
|
570
|
+
const sized = findViewportSizing(input.sourceCode);
|
|
571
|
+
if (sized.length === 0) return [];
|
|
572
|
+
const list = sized.map((t) => "`" + t + "`").join(", ");
|
|
573
|
+
return [
|
|
574
|
+
mkIssue(
|
|
575
|
+
"universal.viewport_sized",
|
|
576
|
+
`${sized.length} element(s) are sized to the viewport: ${list} \u2014 the frame owns the height; a content-sized frame is measured from the content, so a viewport-sized root never shrinks and grows the frame on every re-measure.`,
|
|
577
|
+
"Remove the viewport height \u2014 size to content, never to the viewport. Under fullscreen the frame stretches your root already (centre inside it with `display: flex; flex-direction: column; justify-content: center`); to fill a parent that has a height use `height: 100%` or `flex: 1`."
|
|
578
|
+
)
|
|
579
|
+
];
|
|
580
|
+
}
|
|
581
|
+
var TERMINAL_WORDS = /* @__PURE__ */ new Set([
|
|
582
|
+
"submit",
|
|
583
|
+
"confirm",
|
|
584
|
+
"approve",
|
|
585
|
+
"schedule",
|
|
586
|
+
"reserve",
|
|
587
|
+
"book",
|
|
588
|
+
"pay",
|
|
589
|
+
"checkout",
|
|
590
|
+
"purchase",
|
|
591
|
+
"order"
|
|
592
|
+
]);
|
|
593
|
+
var TERMINAL_LABEL_RX = /\b(submit|confirm|approve|schedule|reserve|book|pay|checkout|purchase|place\s+order)\b/i;
|
|
594
|
+
var GUARD_RX = /\b(disabled|aria-disabled)\s*=/;
|
|
595
|
+
function nameWords(name) {
|
|
596
|
+
return name.split(/(?=[A-Z])|[_\-\s]+/).map((w) => w.toLowerCase()).filter((w) => w.length > 0);
|
|
597
|
+
}
|
|
598
|
+
function findUnguardedTerminalControls(sourceCode) {
|
|
599
|
+
if (GUARD_RX.test(sourceCode)) return [];
|
|
600
|
+
const names = /* @__PURE__ */ new Set();
|
|
601
|
+
for (const m of sourceCode.matchAll(/useAction(?:<[^>]*>)?\(\s*['"`]([^'"`]+)['"`]/g)) {
|
|
602
|
+
const name = m[1] ?? "";
|
|
603
|
+
if (nameWords(name).some((w) => TERMINAL_WORDS.has(w))) names.add(name);
|
|
604
|
+
}
|
|
605
|
+
for (const m of sourceCode.matchAll(/>([^<>{}]{1,60})<\/Button>/g)) {
|
|
606
|
+
const label = (m[1] ?? "").replace(/\s+/g, " ").trim();
|
|
607
|
+
if (label.length > 0 && TERMINAL_LABEL_RX.test(label)) names.add(label.slice(0, 40));
|
|
608
|
+
}
|
|
609
|
+
return [...names];
|
|
610
|
+
}
|
|
611
|
+
function runTerminalActionUnguarded(input) {
|
|
612
|
+
if (input.compiledCode === null) return [];
|
|
613
|
+
const found = findUnguardedTerminalControls(input.sourceCode);
|
|
614
|
+
if (found.length === 0) return [];
|
|
615
|
+
const list = found.map((t) => `"${t}"`).join(", ");
|
|
616
|
+
return [
|
|
617
|
+
mkIssue(
|
|
618
|
+
"universal.terminal_action_unguarded",
|
|
619
|
+
`${list} reads as an action the user means once, and nothing in this component disables the control after it fires \u2014 a double-click sends it twice.`,
|
|
620
|
+
'Hold the fired state (`const [submitted, setSubmitted] = useState(false)`), set it in the handler, and pass `disabled={submitted}` to that control with a word that says so ("Submitted"). If the action IS meant to repeat \u2014 sending a message, adding an item \u2014 leave it armed and ignore this.',
|
|
621
|
+
"warn"
|
|
622
|
+
)
|
|
623
|
+
];
|
|
624
|
+
}
|
|
625
|
+
function rootOpeningTag(sourceCode) {
|
|
626
|
+
const start = sourceCode.indexOf("export default function");
|
|
627
|
+
const body = start >= 0 ? sourceCode.slice(start) : sourceCode;
|
|
628
|
+
const ret = /\breturn\s*\(?\s*</.exec(body);
|
|
629
|
+
if (ret === null) return null;
|
|
630
|
+
const at = ret.index + ret[0].length - 1;
|
|
631
|
+
const nameMatch = /^<([A-Za-z][\w.]*)/.exec(body.slice(at));
|
|
632
|
+
if (nameMatch === null) return null;
|
|
633
|
+
let j = at + nameMatch[0].length;
|
|
634
|
+
let depth = 0;
|
|
635
|
+
let quote = null;
|
|
636
|
+
for (; j < body.length; j++) {
|
|
637
|
+
const ch = body[j];
|
|
638
|
+
if (quote !== null) {
|
|
639
|
+
if (ch === quote) quote = null;
|
|
640
|
+
continue;
|
|
641
|
+
}
|
|
642
|
+
if (ch === '"' || ch === "'" || ch === "`") quote = ch;
|
|
643
|
+
else if (ch === "{") depth++;
|
|
644
|
+
else if (ch === "}") depth--;
|
|
645
|
+
else if (ch === ">" && depth === 0) break;
|
|
646
|
+
}
|
|
647
|
+
return { name: nameMatch[1], attrs: body.slice(at + nameMatch[0].length, j) };
|
|
648
|
+
}
|
|
649
|
+
var CONTAINER_PRESETS = maxWidth;
|
|
650
|
+
var NOT_A_CAP = /* @__PURE__ */ new Set(["full", "none", "100%", "unset", "initial", "inherit"]);
|
|
651
|
+
function capPx(value) {
|
|
652
|
+
const v = value.trim();
|
|
653
|
+
if (NOT_A_CAP.has(v)) return void 0;
|
|
654
|
+
const resolved = CONTAINER_PRESETS[v] ?? v;
|
|
655
|
+
if (NOT_A_CAP.has(resolved)) return void 0;
|
|
656
|
+
const px = /^(\d+(?:\.\d+)?)px$/.exec(resolved);
|
|
657
|
+
return px === null ? null : Number(px[1]);
|
|
658
|
+
}
|
|
659
|
+
function findRootWidthCap(sourceCode) {
|
|
660
|
+
const root = rootOpeningTag(sourceCode);
|
|
661
|
+
if (root === null) return null;
|
|
662
|
+
const { name, attrs } = root;
|
|
663
|
+
const prop = /\bmaxWidth\s*=\s*(?:"([^"]*)"|'([^']*)'|\{\s*(?:"([^"]*)"|'([^']*)'|(\d+(?:\.\d+)?))\s*\})/.exec(attrs);
|
|
664
|
+
if (prop !== null) {
|
|
665
|
+
const raw = prop[1] ?? prop[2] ?? prop[3] ?? prop[4] ?? `${prop[5]}px`;
|
|
666
|
+
const px = capPx(raw);
|
|
667
|
+
return px === void 0 ? null : { element: name, cap: `maxWidth="${raw}"`, px };
|
|
668
|
+
}
|
|
669
|
+
const style = /\bmaxWidth\s*:\s*(?:"([^"]*)"|'([^']*)'|`([^`]*)`|(\d+(?:\.\d+)?))/.exec(attrs);
|
|
670
|
+
if (style !== null) {
|
|
671
|
+
const raw = style[1] ?? style[2] ?? style[3] ?? `${style[4]}px`;
|
|
672
|
+
const px = capPx(raw);
|
|
673
|
+
return px === void 0 ? null : { element: name, cap: `maxWidth: '${raw}'`, px };
|
|
674
|
+
}
|
|
675
|
+
const cls = /(?<![\w-])max-w-([\w[\]%.-]+)/.exec(attrs);
|
|
676
|
+
if (cls !== null) {
|
|
677
|
+
if (cls[1] === "full" || cls[1] === "none") return null;
|
|
678
|
+
return { element: name, cap: `max-w-${cls[1]}`, px: null };
|
|
679
|
+
}
|
|
680
|
+
if (name === "Container") return { element: name, cap: "lg (Container default)", px: capPx("lg") ?? null };
|
|
681
|
+
return null;
|
|
682
|
+
}
|
|
683
|
+
var WIDE_CANVASES = /* @__PURE__ */ new Set(["md", "lg", "xl"]);
|
|
684
|
+
function runRootWidthCap(input) {
|
|
685
|
+
if (input.compiledCode === null) return [];
|
|
686
|
+
if (input.canvas === void 0 || !WIDE_CANVASES.has(input.canvas)) return [];
|
|
687
|
+
const cap2 = findRootWidthCap(input.sourceCode);
|
|
688
|
+
if (cap2 === null) return [];
|
|
689
|
+
const width = CANVAS_VIEWPORTS[input.canvas].width;
|
|
690
|
+
if (cap2.px !== null && cap2.px >= width) return [];
|
|
691
|
+
const sized = cap2.px !== null ? `${cap2.px}px on a ${width}px canvas` : `on a ${width}px canvas`;
|
|
692
|
+
return [
|
|
693
|
+
mkIssue(
|
|
694
|
+
"universal.root_width_cap",
|
|
695
|
+
`The outermost element <${cap2.element}> carries a width cap (${cap2.cap}, ${sized}): on this canvas the card becomes a narrow strip with the frame's ground on either side. The frame owns the width; a cap belongs INSIDE the root, on the column that holds text.`,
|
|
696
|
+
'Let the root fill the frame \u2014 drop the cap on the element you return at the top (or `maxWidth="full"`) \u2014 and put the reading measure on an inner column: `<Box padding="lg"><Container maxWidth="sm">\u2026</Container></Box>`.',
|
|
697
|
+
"warn"
|
|
698
|
+
)
|
|
699
|
+
];
|
|
700
|
+
}
|
|
701
|
+
var LABEL_SPECIAL_CHAR = /[^a-z0-9\s]/i;
|
|
702
|
+
var normText = (s) => s.toLowerCase().replace(/\s+/g, " ").trim();
|
|
703
|
+
var alnumText = (s) => s.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
704
|
+
function visibleText(sourceCode) {
|
|
705
|
+
const jsx = (sourceCode.match(/>([^<>]*)</g) ?? []).map((m2) => m2.slice(1, -1).replace(/\{[^}]*\}/g, " ")).join(" ");
|
|
706
|
+
let attrs = "";
|
|
707
|
+
const attrRx = /\b(?:aria-label|placeholder|title|alt)\s*=\s*(?:"([^"]*)"|'([^']*)')/gi;
|
|
708
|
+
let m;
|
|
709
|
+
while ((m = attrRx.exec(sourceCode)) !== null) attrs += " " + (m[1] ?? m[2] ?? "");
|
|
710
|
+
return jsx + " " + attrs;
|
|
711
|
+
}
|
|
712
|
+
function findDroppedActionLabels(sourceCode, contract) {
|
|
713
|
+
const actionSpec = contract?.actionSpec;
|
|
714
|
+
if (actionSpec === void 0) return [];
|
|
715
|
+
if (/\.label\b/.test(sourceCode)) return [];
|
|
716
|
+
const vis = visibleText(sourceCode);
|
|
717
|
+
const normVis = normText(vis);
|
|
718
|
+
const alnumVis = alnumText(vis);
|
|
719
|
+
const out = [];
|
|
720
|
+
for (const [name, entry] of Object.entries(actionSpec)) {
|
|
721
|
+
const label = entry.label;
|
|
722
|
+
if (typeof label !== "string" || label.length === 0 || label === name) continue;
|
|
723
|
+
if (!LABEL_SPECIAL_CHAR.test(label)) continue;
|
|
724
|
+
if (alnumText(label).length < 3) continue;
|
|
725
|
+
if (normVis.includes(normText(label))) continue;
|
|
726
|
+
if (alnumVis.includes(alnumText(label))) out.push({ name, label });
|
|
727
|
+
}
|
|
728
|
+
return out;
|
|
729
|
+
}
|
|
730
|
+
function runActionLabelDropped(input) {
|
|
731
|
+
if (input.compiledCode === null) return [];
|
|
732
|
+
return findDroppedActionLabels(input.sourceCode, input.contract).map(
|
|
733
|
+
({ name, label }) => mkIssue(
|
|
734
|
+
"universal.action_label_dropped",
|
|
735
|
+
`The action "${name}" declares label "${label}", but the rendered control appears to drop its "${label.replace(/[a-z0-9\s]/gi, "").trim()}": the label's words are in the generated source, the full label is not \u2014 a transcription drop (ggui#1190).`,
|
|
736
|
+
`Render the label EXACTLY as the contract states it \u2014 button text should be the verbatim \`actionSpec.${name}.label\`, or render it from data (\`{x.label}\`), never a re-typed copy that drops "&", "/", "+", etc.`,
|
|
737
|
+
"warn"
|
|
738
|
+
)
|
|
739
|
+
);
|
|
740
|
+
}
|
|
741
|
+
var MIN_ECHO_CHARS = 12;
|
|
742
|
+
function collectStrings(value, out) {
|
|
743
|
+
if (typeof value === "string") {
|
|
744
|
+
out.push(value);
|
|
745
|
+
} else if (Array.isArray(value)) {
|
|
746
|
+
for (const v of value) collectStrings(v, out);
|
|
747
|
+
} else if (value !== null && typeof value === "object") {
|
|
748
|
+
for (const v of Object.values(value)) collectStrings(v, out);
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
function fixtureOnlyStrings(input) {
|
|
752
|
+
if (input.fixtureProps === void 0) return [];
|
|
753
|
+
const values = [];
|
|
754
|
+
collectStrings(input.fixtureProps, values);
|
|
755
|
+
const stated = `${input.originalPrompt}
|
|
756
|
+
${input.contract === void 0 ? "" : JSON.stringify(input.contract)}`;
|
|
757
|
+
const seen = /* @__PURE__ */ new Set();
|
|
758
|
+
const out = [];
|
|
759
|
+
for (const v of values) {
|
|
760
|
+
const t = v.trim();
|
|
761
|
+
if (t.length < MIN_ECHO_CHARS || !t.includes(" ") || seen.has(t)) continue;
|
|
762
|
+
seen.add(t);
|
|
763
|
+
if (stated.includes(t)) continue;
|
|
764
|
+
out.push(t);
|
|
765
|
+
}
|
|
766
|
+
return out;
|
|
767
|
+
}
|
|
768
|
+
function runFixtureDefaultEcho(input) {
|
|
769
|
+
if (input.compiledCode === null) return [];
|
|
770
|
+
const src = input.sourceCode;
|
|
771
|
+
const issues = [];
|
|
772
|
+
for (const value of fixtureOnlyStrings(input)) {
|
|
773
|
+
if (!src.includes(value)) continue;
|
|
774
|
+
issues.push(
|
|
775
|
+
mkIssue(
|
|
776
|
+
"universal.fixture_default_echo",
|
|
777
|
+
`A literal in the source equals a fixture value the request never stated: "${value.length > 60 ? `${value.slice(0, 57)}\u2026` : value}". The fixture is judging data; a visitor sees this copy whenever the agent leaves the prop out.`,
|
|
778
|
+
"Take that copy from the prop it renders and drop the literal \u2014 no default, fallback or placeholder that repeats a fixture value. A default may only carry text the request itself states.",
|
|
779
|
+
"warn"
|
|
780
|
+
)
|
|
781
|
+
);
|
|
782
|
+
}
|
|
783
|
+
return issues;
|
|
784
|
+
}
|
|
785
|
+
var FORM_OPEN_RX = /<form\b[^<>]*\bonSubmit\s*=[^<>]*>/g;
|
|
786
|
+
var CONTROL_TAG_RX = /<(?:Button|button|input)\b[^<>]*>/g;
|
|
787
|
+
var SUBMIT_TYPE_RX = /\btype\s*=\s*(?:["']submit["']|\{\s*["']submit["']\s*\})/;
|
|
788
|
+
function submitForms(sourceCode) {
|
|
789
|
+
const out = [];
|
|
790
|
+
for (const m of sourceCode.matchAll(FORM_OPEN_RX)) {
|
|
791
|
+
const start = m.index ?? 0;
|
|
792
|
+
const end = sourceCode.indexOf("</form>", start + m[0].length);
|
|
793
|
+
out.push(end === -1 ? sourceCode.slice(start) : sourceCode.slice(start, end + "</form>".length));
|
|
794
|
+
}
|
|
795
|
+
return out;
|
|
796
|
+
}
|
|
797
|
+
function runFormSubmitControl(input) {
|
|
798
|
+
if (input.compiledCode === null) return [];
|
|
799
|
+
const issues = [];
|
|
800
|
+
for (const form of submitForms(input.sourceCode)) {
|
|
801
|
+
const controls = [...form.matchAll(CONTROL_TAG_RX)].map((m) => m[0]);
|
|
802
|
+
if (controls.length === 0) continue;
|
|
803
|
+
if (controls.some((tag) => SUBMIT_TYPE_RX.test(tag))) continue;
|
|
804
|
+
const named = controls.find((tag) => /<Button\b/.test(tag)) ?? controls[0];
|
|
805
|
+
issues.push(
|
|
806
|
+
mkIssue(
|
|
807
|
+
"universal.form_submit_control",
|
|
808
|
+
`A <form onSubmit> renders ${controls.length} control(s) and none submits it: the design Button defaults to type="button", so clicking ${named.length > 60 ? `${named.slice(0, 57)}\u2026` : named} does nothing \u2014 only Enter in a field submits.`,
|
|
809
|
+
'Give the control that submits this form type="submit" \u2014 a Button inside a form defaults to type="button" and never submits on click; keep type="button" on the others.',
|
|
810
|
+
"fail"
|
|
811
|
+
)
|
|
812
|
+
);
|
|
813
|
+
}
|
|
814
|
+
return issues;
|
|
815
|
+
}
|
|
816
|
+
var UNIVERSAL_CHECKS = [
|
|
817
|
+
{
|
|
818
|
+
id: "universal.icon_name_known",
|
|
819
|
+
axis: "render",
|
|
820
|
+
values: ALL_RENDER_VALUES,
|
|
821
|
+
run: runIconNameKnown
|
|
822
|
+
},
|
|
823
|
+
{
|
|
824
|
+
id: "universal.caps_label_invented",
|
|
825
|
+
axis: "render",
|
|
826
|
+
values: ALL_RENDER_VALUES,
|
|
827
|
+
run: runCapsLabelInvented
|
|
828
|
+
},
|
|
829
|
+
{
|
|
830
|
+
id: "universal.viewport_sized",
|
|
831
|
+
axis: "render",
|
|
832
|
+
values: ALL_RENDER_VALUES,
|
|
833
|
+
run: runViewportSized
|
|
834
|
+
},
|
|
835
|
+
{
|
|
836
|
+
id: "universal.root_width_cap",
|
|
837
|
+
axis: "render",
|
|
838
|
+
values: ALL_RENDER_VALUES,
|
|
839
|
+
run: runRootWidthCap
|
|
840
|
+
},
|
|
841
|
+
{
|
|
842
|
+
id: "universal.action_label_dropped",
|
|
843
|
+
axis: "render",
|
|
844
|
+
values: ALL_RENDER_VALUES,
|
|
845
|
+
run: runActionLabelDropped
|
|
846
|
+
},
|
|
847
|
+
{
|
|
848
|
+
id: "universal.terminal_action_unguarded",
|
|
849
|
+
axis: "render",
|
|
850
|
+
values: ALL_RENDER_VALUES,
|
|
851
|
+
run: runTerminalActionUnguarded
|
|
852
|
+
},
|
|
853
|
+
{
|
|
854
|
+
id: "universal.form_submit_control",
|
|
855
|
+
axis: "render",
|
|
856
|
+
values: ALL_RENDER_VALUES,
|
|
857
|
+
run: runFormSubmitControl
|
|
858
|
+
},
|
|
859
|
+
{
|
|
860
|
+
id: "universal.fixture_default_echo",
|
|
861
|
+
axis: "render",
|
|
862
|
+
values: ALL_RENDER_VALUES,
|
|
863
|
+
run: runFixtureDefaultEcho
|
|
864
|
+
},
|
|
865
|
+
{
|
|
866
|
+
id: "universal.accent_text_ink",
|
|
867
|
+
axis: "render",
|
|
868
|
+
values: ALL_RENDER_VALUES,
|
|
869
|
+
run: runAccentTextInk
|
|
870
|
+
},
|
|
871
|
+
{
|
|
872
|
+
id: "universal.scoped_surface_owns_ground",
|
|
873
|
+
axis: "render",
|
|
874
|
+
values: ALL_RENDER_VALUES,
|
|
875
|
+
run: runScopedSurfaceOwnsGround
|
|
876
|
+
},
|
|
877
|
+
{
|
|
878
|
+
id: "universal.prop_coverage",
|
|
879
|
+
axis: "render",
|
|
880
|
+
values: ALL_RENDER_VALUES,
|
|
881
|
+
run: runPropCoverage
|
|
882
|
+
},
|
|
883
|
+
{
|
|
884
|
+
id: "universal.no_prop_mirror",
|
|
885
|
+
axis: "render",
|
|
886
|
+
values: ALL_RENDER_VALUES,
|
|
887
|
+
run: runNoPropMirror
|
|
888
|
+
},
|
|
889
|
+
{
|
|
890
|
+
id: "universal.no_phantom_useState",
|
|
891
|
+
axis: "render",
|
|
892
|
+
values: ALL_RENDER_VALUES,
|
|
893
|
+
run: runNoPhantomUseState
|
|
894
|
+
},
|
|
895
|
+
{
|
|
896
|
+
id: "universal.prop_seed_no_resync",
|
|
897
|
+
axis: "render",
|
|
898
|
+
values: ALL_RENDER_VALUES,
|
|
899
|
+
run: runPropSeedNoResync
|
|
900
|
+
}
|
|
901
|
+
];
|
|
902
|
+
|
|
903
|
+
// src/evaluation/axis-checks/checks/tint.ts
|
|
904
|
+
var ALL_RENDER_VALUES2 = [
|
|
905
|
+
"static",
|
|
906
|
+
"list",
|
|
907
|
+
"grid",
|
|
908
|
+
"spatial",
|
|
909
|
+
"timeline",
|
|
910
|
+
"chart",
|
|
911
|
+
"master-detail"
|
|
912
|
+
];
|
|
913
|
+
function scanOpenTag(src, from, nameLength) {
|
|
914
|
+
let depth = 0;
|
|
915
|
+
let quote = "";
|
|
916
|
+
for (let i = from + 1 + nameLength; i < src.length; i++) {
|
|
917
|
+
const ch = src[i];
|
|
918
|
+
if (quote !== "") {
|
|
919
|
+
if (ch === "\\") i++;
|
|
920
|
+
else if (ch === quote) quote = "";
|
|
921
|
+
} else if (ch === '"' || ch === "'" || ch === "`") {
|
|
922
|
+
quote = ch;
|
|
923
|
+
} else if (ch === "{") {
|
|
924
|
+
depth++;
|
|
925
|
+
} else if (ch === "}") {
|
|
926
|
+
depth--;
|
|
927
|
+
} else if (ch === ">" && depth === 0) {
|
|
928
|
+
const attrs = src.slice(from + 1 + nameLength, i);
|
|
929
|
+
return { attrs, end: i + 1, selfClosing: attrs.trimEnd().endsWith("/") };
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
return null;
|
|
933
|
+
}
|
|
934
|
+
function readTintAttr(attrs) {
|
|
935
|
+
let depth = 0;
|
|
936
|
+
let quote = "";
|
|
937
|
+
for (let i = 0; i < attrs.length; i++) {
|
|
938
|
+
const ch = attrs[i];
|
|
939
|
+
if (quote !== "") {
|
|
940
|
+
if (ch === "\\") i++;
|
|
941
|
+
else if (ch === quote) quote = "";
|
|
942
|
+
continue;
|
|
943
|
+
}
|
|
944
|
+
if (ch === '"' || ch === "'" || ch === "`") quote = ch;
|
|
945
|
+
else if (ch === "{") depth++;
|
|
946
|
+
else if (ch === "}") depth--;
|
|
947
|
+
else if (depth === 0 && /\s/.test(attrs[i - 1] ?? " ")) {
|
|
948
|
+
const m = /^tint\s*=\s*/.exec(attrs.slice(i));
|
|
949
|
+
if (m === null) continue;
|
|
950
|
+
const v = i + m[0].length;
|
|
951
|
+
const open = attrs[v];
|
|
952
|
+
if (open === '"' || open === "'") {
|
|
953
|
+
const close = attrs.indexOf(open, v + 1);
|
|
954
|
+
return close < 0 ? null : attrs.slice(v, close + 1);
|
|
955
|
+
}
|
|
956
|
+
if (open !== "{") return null;
|
|
957
|
+
let d = 0;
|
|
958
|
+
for (let j = v; j < attrs.length; j++) {
|
|
959
|
+
if (attrs[j] === "{") d++;
|
|
960
|
+
else if (attrs[j] === "}" && --d === 0) return attrs.slice(v, j + 1);
|
|
961
|
+
}
|
|
962
|
+
return null;
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
return null;
|
|
966
|
+
}
|
|
967
|
+
function findTintedElements(sourceCode) {
|
|
968
|
+
const found = [];
|
|
969
|
+
for (const m of sourceCode.matchAll(/<(Box|Card)\b/g)) {
|
|
970
|
+
const tag = m[1] === "Card" ? "Card" : "Box";
|
|
971
|
+
const from = m.index ?? 0;
|
|
972
|
+
const open = scanOpenTag(sourceCode, from, tag.length);
|
|
973
|
+
if (open === null) continue;
|
|
974
|
+
const tint = readTintAttr(open.attrs);
|
|
975
|
+
if (tint === null) continue;
|
|
976
|
+
if (open.selfClosing) {
|
|
977
|
+
const close = sourceCode.indexOf("</", open.end);
|
|
978
|
+
found.push({ tag, tint, inner: "", next: sourceCode.slice(open.end, close < 0 ? sourceCode.length : close) });
|
|
979
|
+
continue;
|
|
980
|
+
}
|
|
981
|
+
const tagRx = new RegExp(`<(/?)${tag}\\b`, "g");
|
|
982
|
+
tagRx.lastIndex = open.end;
|
|
983
|
+
let depth = 1;
|
|
984
|
+
let end = sourceCode.length;
|
|
985
|
+
for (let t = tagRx.exec(sourceCode); t !== null; t = tagRx.exec(sourceCode)) {
|
|
986
|
+
if (t[1] === "/") {
|
|
987
|
+
if (--depth === 0) {
|
|
988
|
+
end = t.index;
|
|
989
|
+
break;
|
|
990
|
+
}
|
|
991
|
+
continue;
|
|
992
|
+
}
|
|
993
|
+
const nested = scanOpenTag(sourceCode, t.index, tag.length);
|
|
994
|
+
if (nested === null) break;
|
|
995
|
+
tagRx.lastIndex = nested.end;
|
|
996
|
+
if (!nested.selfClosing) depth++;
|
|
997
|
+
}
|
|
998
|
+
found.push({ tag, tint, inner: sourceCode.slice(open.end, end), next: "" });
|
|
999
|
+
}
|
|
1000
|
+
return found;
|
|
1001
|
+
}
|
|
1002
|
+
var 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;
|
|
1003
|
+
function hasWords(inner) {
|
|
1004
|
+
if (/<(Text|Heading|Label|Badge|Caption|Link)\b/.test(inner)) return true;
|
|
1005
|
+
if (/\{[^{}]+\}/.test(inner.replace(/\b[a-zA-Z]+=\{[^{}]*\}/g, ""))) return true;
|
|
1006
|
+
return />\s*[^<>{}\s][^<>{}]*</.test(`>${inner}<`);
|
|
1007
|
+
}
|
|
1008
|
+
function runTintStateBound(input) {
|
|
1009
|
+
if (input.compiledCode === null) return [];
|
|
1010
|
+
const bound = findTintedElements(input.sourceCode).filter((e) => e.tint.startsWith("{") && STATE_WORD_RX.test(e.tint));
|
|
1011
|
+
if (bound.length === 0) return [];
|
|
1012
|
+
return [
|
|
1013
|
+
mkIssue(
|
|
1014
|
+
"universal.tint_state_bound",
|
|
1015
|
+
`\`tint\` reads as bound to a state (${bound.map((e) => `\`${e.tint}\``).join(", ")}) \u2014 tints are for data categories, never for state.`,
|
|
1016
|
+
"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.",
|
|
1017
|
+
"warn"
|
|
1018
|
+
)
|
|
1019
|
+
];
|
|
1020
|
+
}
|
|
1021
|
+
function runTintCategoryInWords(input) {
|
|
1022
|
+
if (input.compiledCode === null) return [];
|
|
1023
|
+
const tinted = findTintedElements(input.sourceCode);
|
|
1024
|
+
if (tinted.length === 0) return [];
|
|
1025
|
+
if (/\blegend\b/i.test(input.sourceCode)) return [];
|
|
1026
|
+
if (tinted.every((e) => hasWords(e.inner) || hasWords(e.next))) return [];
|
|
1027
|
+
return [
|
|
1028
|
+
mkIssue(
|
|
1029
|
+
"universal.tint_category_in_words",
|
|
1030
|
+
"A tinted cell carries no words, and the card has no legend \u2014 colour is the only way to tell its category.",
|
|
1031
|
+
"Give this cell a label, or add a legend to the card. The category is always also in words.",
|
|
1032
|
+
"warn"
|
|
1033
|
+
)
|
|
1034
|
+
];
|
|
1035
|
+
}
|
|
1036
|
+
var MUTED_TONE_RX = /\btone\s*=\s*["'](subtle|muted)["']/;
|
|
1037
|
+
function runTintMutedText(input) {
|
|
1038
|
+
if (input.compiledCode === null) return [];
|
|
1039
|
+
if (!findTintedElements(input.sourceCode).some((e) => MUTED_TONE_RX.test(e.inner))) return [];
|
|
1040
|
+
return [
|
|
1041
|
+
mkIssue(
|
|
1042
|
+
"universal.tint_muted_text",
|
|
1043
|
+
'Muted text (`tone="subtle"` / `tone="muted"`) sits on a tint \u2014 it can read under 4.5:1 there.',
|
|
1044
|
+
"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.",
|
|
1045
|
+
"warn"
|
|
1046
|
+
)
|
|
1047
|
+
];
|
|
1048
|
+
}
|
|
1049
|
+
var TINT_CHECKS = [
|
|
1050
|
+
{ id: "universal.tint_state_bound", axis: "render", values: ALL_RENDER_VALUES2, run: runTintStateBound },
|
|
1051
|
+
{ id: "universal.tint_category_in_words", axis: "render", values: ALL_RENDER_VALUES2, run: runTintCategoryInWords },
|
|
1052
|
+
{ id: "universal.tint_muted_text", axis: "render", values: ALL_RENDER_VALUES2, run: runTintMutedText }
|
|
1053
|
+
];
|
|
1054
|
+
|
|
1055
|
+
// src/evaluation/axis-checks/checks/state-merge.ts
|
|
1056
|
+
function runStateSeededFromProps(input) {
|
|
1057
|
+
if (input.compiledCode === null) return [];
|
|
1058
|
+
const src = input.sourceCode;
|
|
1059
|
+
const all = getEntityCollections(input.contract);
|
|
1060
|
+
const entities = getMutatedEntityCollections(input.contract, all);
|
|
1061
|
+
const issues = [];
|
|
1062
|
+
for (const e of entities) {
|
|
1063
|
+
if (seedsFromProp(src, e.name)) continue;
|
|
1064
|
+
issues.push(
|
|
1065
|
+
mkIssue(
|
|
1066
|
+
"state.merge.seeded_from_props",
|
|
1067
|
+
`Entity collection "${e.name}" is not seeded from props \u2014 no useState initializer reads props.${e.name}.`,
|
|
1068
|
+
`Add \`const [${e.name}, set${cap(e.name)}] = useState(props.${e.name});\` paired with \`useEffect(() => set${cap(e.name)}(props.${e.name}), [props.${e.name}]);\` so stream/action updates merge into live state AND agent repaints (ggui_amend) re-seed it (#480).`
|
|
1069
|
+
)
|
|
1070
|
+
);
|
|
1071
|
+
}
|
|
1072
|
+
return issues;
|
|
1073
|
+
}
|
|
1074
|
+
function runNoHardcodedEntities(input) {
|
|
1075
|
+
if (input.compiledCode === null) return [];
|
|
1076
|
+
const src = input.sourceCode;
|
|
1077
|
+
const all = getEntityCollections(input.contract);
|
|
1078
|
+
const entities = getMutatedEntityCollections(input.contract, all);
|
|
1079
|
+
if (entities.length === 0) return [];
|
|
1080
|
+
const uncommented = src.replace(/\/\*[\s\S]*?\*\//g, "").replace(/\/\/[^\n]*/g, "");
|
|
1081
|
+
const issues = [];
|
|
1082
|
+
const idFields = [...new Set(entities.map((e) => e.idField))];
|
|
1083
|
+
for (const idField of idFields) {
|
|
1084
|
+
const re = new RegExp(
|
|
1085
|
+
`\\[\\s*\\{[^}]*\\b${idField}\\s*:[^}]*\\}\\s*,\\s*\\{[^}]*\\b${idField}\\s*:`,
|
|
1086
|
+
"g"
|
|
1087
|
+
);
|
|
1088
|
+
if (!re.test(uncommented)) continue;
|
|
1089
|
+
issues.push(
|
|
1090
|
+
mkIssue(
|
|
1091
|
+
"state.merge.no_hardcoded_entities",
|
|
1092
|
+
`Hardcoded entity array literal (multiple objects with "${idField}") in the component \u2014 entity data should come from state/props.`,
|
|
1093
|
+
`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.`
|
|
1094
|
+
)
|
|
1095
|
+
);
|
|
1096
|
+
}
|
|
1097
|
+
return issues;
|
|
1098
|
+
}
|
|
1099
|
+
function runDerivedViewMemoized(input) {
|
|
1100
|
+
if (input.compiledCode === null) return [];
|
|
1101
|
+
const src = input.sourceCode;
|
|
1102
|
+
const all = getEntityCollections(input.contract);
|
|
1103
|
+
const entities = getMutatedEntityCollections(input.contract, all);
|
|
1104
|
+
if (entities.length === 0) return [];
|
|
1105
|
+
const returnIdx = src.indexOf("return (");
|
|
1106
|
+
if (returnIdx < 0) return [];
|
|
1107
|
+
const renderBody = src.slice(returnIdx);
|
|
1108
|
+
const issues = [];
|
|
1109
|
+
for (const e of entities) {
|
|
1110
|
+
const re = new RegExp(`\\b${e.name}\\s*\\.(filter|reduce|sort)\\s*\\(`);
|
|
1111
|
+
if (re.test(renderBody)) {
|
|
1112
|
+
issues.push(
|
|
1113
|
+
mkIssue(
|
|
1114
|
+
"state.merge.derived_view_memoized",
|
|
1115
|
+
`Derived view (${e.name}.filter/reduce/sort) computed inside the render body \u2014 should be wrapped in useMemo.`,
|
|
1116
|
+
`Extract to \`const ${e.name}Filtered = useMemo(() => ${e.name}.filter(...), [${e.name}, ...]);\` before the return.`,
|
|
1117
|
+
"warn"
|
|
1118
|
+
)
|
|
1119
|
+
);
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
return issues;
|
|
1123
|
+
}
|
|
1124
|
+
function runMapKeyIsId(input) {
|
|
1125
|
+
if (input.compiledCode === null) return [];
|
|
1126
|
+
const src = input.sourceCode;
|
|
1127
|
+
const INDEX_NAMES = /* @__PURE__ */ new Set(["index", "idx", "i", "j", "k", "n", "ix"]);
|
|
1128
|
+
const re = /key\s*=\s*\{\s*(\w+)\s*\}/g;
|
|
1129
|
+
const issues = [];
|
|
1130
|
+
for (const m of src.matchAll(re)) {
|
|
1131
|
+
const key = m[1];
|
|
1132
|
+
if (!INDEX_NAMES.has(key)) continue;
|
|
1133
|
+
issues.push(
|
|
1134
|
+
mkIssue(
|
|
1135
|
+
"render.map_key_is_id",
|
|
1136
|
+
`Array key uses index variable "${key}" \u2014 reorders and stream merges will break React reconciliation.`,
|
|
1137
|
+
`Replace key={${key}} with key={item.id} \u2014 or, when the items carry no id, with the field that names each one (item.label / item.symbol / item.name).`
|
|
1138
|
+
)
|
|
1139
|
+
);
|
|
1140
|
+
}
|
|
1141
|
+
return issues;
|
|
1142
|
+
}
|
|
1143
|
+
var STATE_MERGE_CHECKS = [
|
|
1144
|
+
{
|
|
1145
|
+
id: "state.merge.seeded_from_props",
|
|
1146
|
+
axis: "state",
|
|
1147
|
+
values: ["merge"],
|
|
1148
|
+
run: runStateSeededFromProps
|
|
1149
|
+
},
|
|
1150
|
+
{
|
|
1151
|
+
id: "state.merge.no_hardcoded_entities",
|
|
1152
|
+
axis: "state",
|
|
1153
|
+
values: ["merge"],
|
|
1154
|
+
run: runNoHardcodedEntities
|
|
1155
|
+
},
|
|
1156
|
+
{
|
|
1157
|
+
id: "state.merge.derived_view_memoized",
|
|
1158
|
+
axis: "state",
|
|
1159
|
+
values: ["merge"],
|
|
1160
|
+
run: runDerivedViewMemoized
|
|
1161
|
+
},
|
|
1162
|
+
// Map-key check: gated on any iterating render. state=merge always
|
|
1163
|
+
// implies iteration, so the render gate already covers it.
|
|
1164
|
+
{
|
|
1165
|
+
id: "render.map_key_is_id",
|
|
1166
|
+
axis: "render",
|
|
1167
|
+
values: ["list", "grid", "timeline", "master-detail"],
|
|
1168
|
+
run: runMapKeyIsId
|
|
1169
|
+
}
|
|
1170
|
+
];
|
|
1171
|
+
|
|
1172
|
+
// src/evaluation/axis-checks/checks/state-ui-affordance.ts
|
|
1173
|
+
var STATE_ARIA_OR_ROLE = /aria-(checked|pressed|selected|expanded|current)\s*=|role\s*=/;
|
|
1174
|
+
var STATE_PRIMITIVE_IN_SUBTREE = /<(Checkbox|Toggle|Tabs|Accordion)[\s/>]/;
|
|
1175
|
+
var SEMANTIC_PRIMITIVES = /* @__PURE__ */ new Set(["Button", "Checkbox", "Toggle", "Link", "MenuItem"]);
|
|
1176
|
+
function collectSetters(src) {
|
|
1177
|
+
const setters = /* @__PURE__ */ new Set();
|
|
1178
|
+
for (const m of src.matchAll(/const\s*\[\s*\w+\s*,\s*(set\w+)\s*\]\s*=\s*useState/g)) {
|
|
1179
|
+
setters.add(m[1]);
|
|
1180
|
+
}
|
|
1181
|
+
return setters;
|
|
1182
|
+
}
|
|
1183
|
+
function collectHandlers(src, setters) {
|
|
1184
|
+
const handlers = /* @__PURE__ */ new Set();
|
|
1185
|
+
for (const m of src.matchAll(/const\s+(\w+)\s*=\s*(?:\([^)]*\)|\w+)\s*=>\s*\{?/g)) {
|
|
1186
|
+
const body = src.slice(m.index + m[0].length, m.index + m[0].length + 400);
|
|
1187
|
+
for (const s of setters) {
|
|
1188
|
+
if (body.includes(s)) {
|
|
1189
|
+
handlers.add(m[1]);
|
|
1190
|
+
break;
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
return handlers;
|
|
1195
|
+
}
|
|
1196
|
+
function attrBlock(src, start) {
|
|
1197
|
+
let depth = 0;
|
|
1198
|
+
for (let i = start; i < src.length; i++) {
|
|
1199
|
+
const c = src[i];
|
|
1200
|
+
if (c === "{") depth++;
|
|
1201
|
+
else if (c === "}") depth--;
|
|
1202
|
+
else if (c === ">" && depth === 0) return { block: src.slice(start, i), end: i };
|
|
1203
|
+
}
|
|
1204
|
+
return { block: src.slice(start, start + 900), end: start + 900 };
|
|
1205
|
+
}
|
|
1206
|
+
function onClickValue(block) {
|
|
1207
|
+
const m = block.match(/onClick\s*=\s*\{/);
|
|
1208
|
+
if (!m || m.index === void 0) return null;
|
|
1209
|
+
const open = m.index + m[0].length - 1;
|
|
1210
|
+
let depth = 0;
|
|
1211
|
+
for (let i = open; i < block.length; i++) {
|
|
1212
|
+
if (block[i] === "{") depth++;
|
|
1213
|
+
else if (block[i] === "}") {
|
|
1214
|
+
depth--;
|
|
1215
|
+
if (depth === 0) return block.slice(open + 1, i);
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
return block.slice(open + 1);
|
|
1219
|
+
}
|
|
1220
|
+
function subtree(src, pos, tag) {
|
|
1221
|
+
const close = src.indexOf(`</${tag}>`, pos);
|
|
1222
|
+
return src.slice(pos, close === -1 ? pos + 700 : close);
|
|
1223
|
+
}
|
|
1224
|
+
function runStateAriaPresent(input) {
|
|
1225
|
+
if (input.compiledCode === null) return [];
|
|
1226
|
+
const src = input.sourceCode;
|
|
1227
|
+
const setters = collectSetters(src);
|
|
1228
|
+
const handlers = collectHandlers(src, setters);
|
|
1229
|
+
const issues = [];
|
|
1230
|
+
const flaggedLines = /* @__PURE__ */ new Set();
|
|
1231
|
+
const localComponents = /* @__PURE__ */ new Set();
|
|
1232
|
+
for (const m of src.matchAll(/(?:function\s+([A-Z]\w*)\s*\(|const\s+([A-Z]\w*)\s*=)/g)) {
|
|
1233
|
+
localComponents.add(m[1] ?? m[2]);
|
|
1234
|
+
}
|
|
1235
|
+
for (const m of src.matchAll(/<([A-Za-z][A-Za-z0-9]*)\b/g)) {
|
|
1236
|
+
const tag = m[1];
|
|
1237
|
+
if (m.index === void 0) continue;
|
|
1238
|
+
if (localComponents.has(tag)) continue;
|
|
1239
|
+
const { block, end } = attrBlock(src, m.index + m[0].length);
|
|
1240
|
+
const clickValue = onClickValue(block);
|
|
1241
|
+
if (clickValue === null) continue;
|
|
1242
|
+
const mutatesAffordance = [...setters].some((s) => clickValue.includes(s)) || [...handlers].some((h) => new RegExp(`\\b${h}\\b`).test(clickValue)) || // Extracted-subcomponent shape: the toggle handler arrives as an
|
|
1243
|
+
// `on[A-Z]…` callback prop and is invoked from onClick — inside a
|
|
1244
|
+
// ui-affordance component that IS the affordance mutation.
|
|
1245
|
+
/\bon[A-Z]\w*\b/.test(clickValue);
|
|
1246
|
+
if (!mutatesAffordance) continue;
|
|
1247
|
+
if (SEMANTIC_PRIMITIVES.has(tag)) continue;
|
|
1248
|
+
if (STATE_ARIA_OR_ROLE.test(block)) continue;
|
|
1249
|
+
if (STATE_PRIMITIVE_IN_SUBTREE.test(subtree(src, end, tag))) continue;
|
|
1250
|
+
const line = src.slice(0, m.index).split("\n").length;
|
|
1251
|
+
if (flaggedLines.has(line)) continue;
|
|
1252
|
+
flaggedLines.add(line);
|
|
1253
|
+
issues.push(
|
|
1254
|
+
mkIssue(
|
|
1255
|
+
"state.ui_affordance.state_aria_present",
|
|
1256
|
+
`Line ${line}: <${tag}> toggles the UI affordance from its onClick but exposes no state ARIA \u2014 its checked/selected/expanded state is invisible to assistive technology (styling is the only carrier).`,
|
|
1257
|
+
`Add the matching state attribute to the SAME element that carries the onClick: \`role="checkbox"\` + \`aria-checked={\u2026}\` for a toggle row, \`aria-pressed\` for a toggle button, \`aria-selected\` for a selection, \`aria-expanded\` for an open/close affordance \u2014 or replace the hand-rolled control with the state-carrying primitive (Checkbox / Toggle / Tabs / Accordion). \`as={Clickable}\` adds button + keyboard semantics but NOT state.`
|
|
1258
|
+
)
|
|
1259
|
+
);
|
|
1260
|
+
}
|
|
1261
|
+
return issues;
|
|
1262
|
+
}
|
|
1263
|
+
var STATE_UI_AFFORDANCE_CHECKS = [
|
|
1264
|
+
{
|
|
1265
|
+
id: "state.ui_affordance.state_aria_present",
|
|
1266
|
+
axis: "state",
|
|
1267
|
+
values: ["ui-affordance"],
|
|
1268
|
+
run: runStateAriaPresent
|
|
1269
|
+
}
|
|
1270
|
+
];
|
|
1271
|
+
|
|
1272
|
+
// src/evaluation/axis-checks/checks/realtime.ts
|
|
1273
|
+
var REALTIME_ACTIVE = ["merge", "append", "status", "presence", "mixed"];
|
|
1274
|
+
function runStreamHandlerPerEvent(input) {
|
|
1275
|
+
if (input.compiledCode === null) return [];
|
|
1276
|
+
const src = input.sourceCode;
|
|
1277
|
+
const eventNames = getStreamEventNames(input.contract);
|
|
1278
|
+
const issues = [];
|
|
1279
|
+
for (const name of eventNames) {
|
|
1280
|
+
const re = new RegExp(`useStream(?:<[^>]*>)?\\s*\\(\\s*['"\`]${name}['"\`]`);
|
|
1281
|
+
if (re.test(src)) continue;
|
|
1282
|
+
issues.push(
|
|
1283
|
+
mkIssue(
|
|
1284
|
+
"realtime.stream_handler_per_event",
|
|
1285
|
+
`Stream event "${name}" declared in the contract has no useStream('${name}') call.`,
|
|
1286
|
+
`Add \`const ${name} = useStream<...>('${name}');\` and handle ${name}.latest in a useEffect.`
|
|
1287
|
+
)
|
|
1288
|
+
);
|
|
1289
|
+
}
|
|
1290
|
+
return issues;
|
|
1291
|
+
}
|
|
1292
|
+
function runStreamMergesById(input) {
|
|
1293
|
+
if (input.compiledCode === null) return [];
|
|
1294
|
+
const src = input.sourceCode;
|
|
1295
|
+
const eventNames = getStreamEventNames(input.contract);
|
|
1296
|
+
const all = getEntityCollections(input.contract);
|
|
1297
|
+
const entities = getMutatedEntityCollections(input.contract, all);
|
|
1298
|
+
if (eventNames.length === 0 || entities.length === 0) return [];
|
|
1299
|
+
const issues = [];
|
|
1300
|
+
const idFields = new Set(entities.map((e) => e.idField));
|
|
1301
|
+
for (const idField of idFields) {
|
|
1302
|
+
const re = new RegExp(`\\.${idField}\\b`);
|
|
1303
|
+
if (re.test(src)) continue;
|
|
1304
|
+
issues.push(
|
|
1305
|
+
mkIssue(
|
|
1306
|
+
"realtime.merge.stream_merges_by_id",
|
|
1307
|
+
`Entity id field "${idField}" is never referenced in the source \u2014 stream merge likely does not key by id.`,
|
|
1308
|
+
`In the stream handler, merge by id: setItems(prev => prev.map(x => x.${idField} === update.${idField} ? {...x, ...update} : x)).`,
|
|
1309
|
+
"warn"
|
|
1310
|
+
)
|
|
1311
|
+
);
|
|
1312
|
+
}
|
|
1313
|
+
return issues;
|
|
1314
|
+
}
|
|
1315
|
+
var REALTIME_CHECKS = [
|
|
1316
|
+
{
|
|
1317
|
+
id: "realtime.stream_handler_per_event",
|
|
1318
|
+
axis: "realtime",
|
|
1319
|
+
values: REALTIME_ACTIVE,
|
|
1320
|
+
run: runStreamHandlerPerEvent
|
|
1321
|
+
},
|
|
1322
|
+
{
|
|
1323
|
+
id: "realtime.merge.stream_merges_by_id",
|
|
1324
|
+
axis: "realtime",
|
|
1325
|
+
values: ["merge", "mixed"],
|
|
1326
|
+
run: runStreamMergesById
|
|
1327
|
+
}
|
|
1328
|
+
];
|
|
1329
|
+
|
|
1330
|
+
// src/evaluation/axis-checks/checks/writes.ts
|
|
1331
|
+
var ACTIVE_WRITES = [
|
|
1332
|
+
"commit",
|
|
1333
|
+
"multi-commit",
|
|
1334
|
+
"per-item",
|
|
1335
|
+
"submit",
|
|
1336
|
+
"compose"
|
|
1337
|
+
];
|
|
1338
|
+
function runActionHookWired(input) {
|
|
1339
|
+
if (input.compiledCode === null) return [];
|
|
1340
|
+
const src = input.sourceCode;
|
|
1341
|
+
const actionNames = getActionNames(input.contract);
|
|
1342
|
+
const issues = [];
|
|
1343
|
+
for (const name of actionNames) {
|
|
1344
|
+
const re = new RegExp(`useAction(?:<[^>]*>)?\\s*\\(\\s*['"\`]${name}['"\`]`);
|
|
1345
|
+
if (re.test(src)) continue;
|
|
1346
|
+
issues.push(
|
|
1347
|
+
mkIssue(
|
|
1348
|
+
"writes.action_hook_wired",
|
|
1349
|
+
`Contract action "${name}" has no useAction('${name}') call.`,
|
|
1350
|
+
`Add \`const ${name} = useAction<...>('${name}');\` and wire it to the relevant control.`
|
|
1351
|
+
)
|
|
1352
|
+
);
|
|
1353
|
+
}
|
|
1354
|
+
return issues;
|
|
1355
|
+
}
|
|
1356
|
+
function runActionHandlerAttached(input) {
|
|
1357
|
+
if (input.compiledCode === null) return [];
|
|
1358
|
+
const src = input.sourceCode;
|
|
1359
|
+
const actionNames = getActionNames(input.contract);
|
|
1360
|
+
const issues = [];
|
|
1361
|
+
for (const name of actionNames) {
|
|
1362
|
+
const declRe = new RegExp(
|
|
1363
|
+
`const\\s+(\\w+)\\s*=\\s*useAction(?:<[^>]*>)?\\s*\\(\\s*['"\`]${name}['"\`]`
|
|
1364
|
+
);
|
|
1365
|
+
const m = src.match(declRe);
|
|
1366
|
+
if (!m) continue;
|
|
1367
|
+
const constName = m[1];
|
|
1368
|
+
const rest = src.replace(m[0], "");
|
|
1369
|
+
if (new RegExp(`\\b${constName}\\s*\\(`).test(rest)) continue;
|
|
1370
|
+
issues.push(
|
|
1371
|
+
mkIssue(
|
|
1372
|
+
"writes.action_handler_attached",
|
|
1373
|
+
`useAction result "${constName}" (for action "${name}") is declared but never invoked.`,
|
|
1374
|
+
`Call ${constName}({...}) from an interactive element (e.g., <Button onClick={() => ${constName}(payload)}>).`
|
|
1375
|
+
)
|
|
1376
|
+
);
|
|
1377
|
+
}
|
|
1378
|
+
return issues;
|
|
1379
|
+
}
|
|
1380
|
+
function runSubmitDisabledPath(input) {
|
|
1381
|
+
if (input.compiledCode === null) return [];
|
|
1382
|
+
const src = input.sourceCode;
|
|
1383
|
+
const submits = getSubmitActions(input.contract);
|
|
1384
|
+
if (submits.length === 0) return [];
|
|
1385
|
+
if (/disabled\s*=\s*\{/.test(src)) return [];
|
|
1386
|
+
return [
|
|
1387
|
+
mkIssue(
|
|
1388
|
+
"writes.submit.disabled_path",
|
|
1389
|
+
`Form has no \`disabled={...}\` expression anywhere \u2014 submit is likely unconditional.`,
|
|
1390
|
+
`Gate submit on validation: e.g. \`<Button disabled={!isValid} onClick={handleSubmit}>\`.`,
|
|
1391
|
+
"warn"
|
|
1392
|
+
)
|
|
1393
|
+
];
|
|
1394
|
+
}
|
|
1395
|
+
var WRITES_CHECKS = [
|
|
1396
|
+
{
|
|
1397
|
+
id: "writes.action_hook_wired",
|
|
1398
|
+
axis: "writes",
|
|
1399
|
+
values: ACTIVE_WRITES,
|
|
1400
|
+
run: runActionHookWired
|
|
1401
|
+
},
|
|
1402
|
+
{
|
|
1403
|
+
id: "writes.action_handler_attached",
|
|
1404
|
+
axis: "writes",
|
|
1405
|
+
values: ACTIVE_WRITES,
|
|
1406
|
+
run: runActionHandlerAttached
|
|
1407
|
+
},
|
|
1408
|
+
{
|
|
1409
|
+
id: "writes.submit.disabled_path",
|
|
1410
|
+
axis: "writes",
|
|
1411
|
+
values: ["submit"],
|
|
1412
|
+
run: runSubmitDisabledPath
|
|
1413
|
+
}
|
|
1414
|
+
];
|
|
1415
|
+
|
|
1416
|
+
// src/evaluation/axis-checks/checks/state-payload.ts
|
|
1417
|
+
function runSubmitHookPresent(input) {
|
|
1418
|
+
if (input.compiledCode === null) return [];
|
|
1419
|
+
const src = input.sourceCode;
|
|
1420
|
+
const submits = getSubmitActions(input.contract);
|
|
1421
|
+
const issues = [];
|
|
1422
|
+
for (const s of submits) {
|
|
1423
|
+
const re = new RegExp(`useAction(?:<[^>]*>)?\\s*\\(\\s*['"\`]${s.name}['"\`]`);
|
|
1424
|
+
if (re.test(src)) continue;
|
|
1425
|
+
issues.push(
|
|
1426
|
+
mkIssue(
|
|
1427
|
+
"writes.submit.hook_present",
|
|
1428
|
+
`Submit action "${s.name}" has no useAction('${s.name}') call.`,
|
|
1429
|
+
`Add \`const ${s.name} = useAction<...>('${s.name}');\` and invoke it from the submit button.`
|
|
1430
|
+
)
|
|
1431
|
+
);
|
|
1432
|
+
}
|
|
1433
|
+
return issues;
|
|
1434
|
+
}
|
|
1435
|
+
function runSubmitHandlerAttached(input) {
|
|
1436
|
+
if (input.compiledCode === null) return [];
|
|
1437
|
+
const src = input.sourceCode;
|
|
1438
|
+
const submits = getSubmitActions(input.contract);
|
|
1439
|
+
const issues = [];
|
|
1440
|
+
for (const s of submits) {
|
|
1441
|
+
const declRe = new RegExp(
|
|
1442
|
+
`const\\s+(\\w+)\\s*=\\s*useAction(?:<[^>]*>)?\\s*\\(\\s*['"\`]${s.name}['"\`]`
|
|
1443
|
+
);
|
|
1444
|
+
const m = src.match(declRe);
|
|
1445
|
+
if (!m) continue;
|
|
1446
|
+
const constName = m[1];
|
|
1447
|
+
const rest = src.replace(m[0], "");
|
|
1448
|
+
if (new RegExp(`\\b${constName}\\s*\\(`).test(rest)) continue;
|
|
1449
|
+
issues.push(
|
|
1450
|
+
mkIssue(
|
|
1451
|
+
"writes.submit.handler_attached",
|
|
1452
|
+
`useAction result "${constName}" (for submit "${s.name}") is declared but never invoked.`,
|
|
1453
|
+
`Call ${constName}(payload) from the submit button's onClick handler, after validation.`
|
|
1454
|
+
)
|
|
1455
|
+
);
|
|
1456
|
+
}
|
|
1457
|
+
return issues;
|
|
1458
|
+
}
|
|
1459
|
+
function runStateCoversPayload(input) {
|
|
1460
|
+
if (input.compiledCode === null) return [];
|
|
1461
|
+
const src = input.sourceCode;
|
|
1462
|
+
const submits = getSubmitActions(input.contract);
|
|
1463
|
+
if (submits.length === 0) return [];
|
|
1464
|
+
const stateKeys = collectStateKeys(src);
|
|
1465
|
+
const issues = [];
|
|
1466
|
+
for (const s of submits) {
|
|
1467
|
+
const missing = s.payloadKeys.filter((k) => !stateKeys.has(k));
|
|
1468
|
+
if (missing.length === 0) continue;
|
|
1469
|
+
issues.push(
|
|
1470
|
+
mkIssue(
|
|
1471
|
+
"state.payload.covers_submit",
|
|
1472
|
+
`Submit action "${s.name}" expects payload keys [${s.payloadKeys.join(", ")}] but state does not cover: ${missing.join(", ")}.`,
|
|
1473
|
+
`Add a state slot for each missing key so the final payload can be assembled.`
|
|
1474
|
+
)
|
|
1475
|
+
);
|
|
1476
|
+
}
|
|
1477
|
+
return issues;
|
|
1478
|
+
}
|
|
1479
|
+
function runInitialValuesSeeded(input) {
|
|
1480
|
+
if (input.compiledCode === null) return [];
|
|
1481
|
+
const src = input.sourceCode;
|
|
1482
|
+
const initialProps = getInitialValuePropNames(input.contract);
|
|
1483
|
+
const issues = [];
|
|
1484
|
+
for (const name of initialProps) {
|
|
1485
|
+
if (seedsFromProp(src, name)) continue;
|
|
1486
|
+
issues.push(
|
|
1487
|
+
mkIssue(
|
|
1488
|
+
"state.payload.initial_values_seeded",
|
|
1489
|
+
`Prop "${name}" (pre-filled initial values) is never read in a useState initializer.`,
|
|
1490
|
+
`Seed form state from props.${name} so edit mode pre-fills.`
|
|
1491
|
+
)
|
|
1492
|
+
);
|
|
1493
|
+
}
|
|
1494
|
+
return issues;
|
|
1495
|
+
}
|
|
1496
|
+
function runOptionListsConsumed(input) {
|
|
1497
|
+
if (input.compiledCode === null) return [];
|
|
1498
|
+
const src = input.sourceCode;
|
|
1499
|
+
const arrStrNames = getArrStrProps(input.contract);
|
|
1500
|
+
const hasAnyMap = /\.map\s*\(/.test(src);
|
|
1501
|
+
const issues = [];
|
|
1502
|
+
for (const name of arrStrNames) {
|
|
1503
|
+
const referenced = new RegExp(`\\bprops\\.${name}\\b`).test(src);
|
|
1504
|
+
if (referenced && hasAnyMap) continue;
|
|
1505
|
+
const reason = !referenced ? `Option list prop "${name}" (arr<str>) is never referenced \u2014 users cannot see the options.` : `Option list prop "${name}" (arr<str>) is referenced but the component has no .map() \u2014 options are not rendered as choices.`;
|
|
1506
|
+
issues.push(
|
|
1507
|
+
mkIssue(
|
|
1508
|
+
"state.payload.option_lists_consumed",
|
|
1509
|
+
reason,
|
|
1510
|
+
`Render options with \`props.${name}.map(option => <RadioOption value={option} ... />)\`.`
|
|
1511
|
+
)
|
|
1512
|
+
);
|
|
1513
|
+
}
|
|
1514
|
+
return issues;
|
|
1515
|
+
}
|
|
1516
|
+
function runNoOrphanPayloadKey(input) {
|
|
1517
|
+
if (input.compiledCode === null) return [];
|
|
1518
|
+
const src = input.sourceCode;
|
|
1519
|
+
const submits = getSubmitActions(input.contract);
|
|
1520
|
+
if (submits.length === 0) return [];
|
|
1521
|
+
const body = src.replace(/interface\s+Action\w+[\s\S]*?\n}/g, "").replace(/type\s+Action\w+[\s\S]*?\n;/g, "");
|
|
1522
|
+
const issues = [];
|
|
1523
|
+
for (const s of submits) {
|
|
1524
|
+
const orphans = [];
|
|
1525
|
+
for (const key of s.payloadKeys) {
|
|
1526
|
+
const re = new RegExp(`\\b${key}\\b`);
|
|
1527
|
+
if (!re.test(body)) orphans.push(key);
|
|
1528
|
+
}
|
|
1529
|
+
if (orphans.length === 0) continue;
|
|
1530
|
+
issues.push(
|
|
1531
|
+
mkIssue(
|
|
1532
|
+
"state.payload.no_orphan_key",
|
|
1533
|
+
`Submit payload keys [${orphans.join(", ")}] never appear in the component body \u2014 missing from the submitted payload.`,
|
|
1534
|
+
`Add UI and state for these keys, or remove them from the ActionEntry if not needed.`,
|
|
1535
|
+
"warn"
|
|
1536
|
+
)
|
|
1537
|
+
);
|
|
1538
|
+
}
|
|
1539
|
+
return issues;
|
|
1540
|
+
}
|
|
1541
|
+
var STATE_PAYLOAD_CHECKS = [
|
|
1542
|
+
{
|
|
1543
|
+
id: "writes.submit.hook_present",
|
|
1544
|
+
axis: "writes",
|
|
1545
|
+
values: ["submit"],
|
|
1546
|
+
run: runSubmitHookPresent
|
|
1547
|
+
},
|
|
1548
|
+
{
|
|
1549
|
+
id: "writes.submit.handler_attached",
|
|
1550
|
+
axis: "writes",
|
|
1551
|
+
values: ["submit"],
|
|
1552
|
+
run: runSubmitHandlerAttached
|
|
1553
|
+
},
|
|
1554
|
+
{
|
|
1555
|
+
id: "state.payload.covers_submit",
|
|
1556
|
+
axis: "state",
|
|
1557
|
+
values: ["payload"],
|
|
1558
|
+
run: runStateCoversPayload
|
|
1559
|
+
},
|
|
1560
|
+
{
|
|
1561
|
+
id: "state.payload.initial_values_seeded",
|
|
1562
|
+
axis: "state",
|
|
1563
|
+
values: ["payload", "draft"],
|
|
1564
|
+
run: runInitialValuesSeeded
|
|
1565
|
+
},
|
|
1566
|
+
{
|
|
1567
|
+
id: "state.payload.option_lists_consumed",
|
|
1568
|
+
axis: "state",
|
|
1569
|
+
values: ["payload"],
|
|
1570
|
+
run: runOptionListsConsumed
|
|
1571
|
+
},
|
|
1572
|
+
{
|
|
1573
|
+
id: "state.payload.no_orphan_key",
|
|
1574
|
+
axis: "state",
|
|
1575
|
+
values: ["payload"],
|
|
1576
|
+
run: runNoOrphanPayloadKey
|
|
1577
|
+
}
|
|
1578
|
+
];
|
|
1579
|
+
|
|
1580
|
+
// src/evaluation/axis-checks/checks/tooling.ts
|
|
1581
|
+
var CLIENT_PRESENT = ["client", "both"];
|
|
1582
|
+
var ALL_TOOLING_VALUES = ["none", "wired", "client", "both"];
|
|
1583
|
+
var REALTIME_ACTIVE2 = [
|
|
1584
|
+
"merge",
|
|
1585
|
+
"append",
|
|
1586
|
+
"status",
|
|
1587
|
+
"presence",
|
|
1588
|
+
"mixed"
|
|
1589
|
+
];
|
|
1590
|
+
function runGadgetHookCalled(input) {
|
|
1591
|
+
if (input.compiledCode === null) return [];
|
|
1592
|
+
const src = input.sourceCode;
|
|
1593
|
+
const names = getGadgetNames(input.contract);
|
|
1594
|
+
const issues = [];
|
|
1595
|
+
for (const name of names) {
|
|
1596
|
+
const re = new RegExp(`const\\s+${name}\\s*=`);
|
|
1597
|
+
if (re.test(src)) continue;
|
|
1598
|
+
issues.push(
|
|
1599
|
+
mkIssue(
|
|
1600
|
+
"tooling.clientCapability.hook_called",
|
|
1601
|
+
`Contract clientCapability "${name}" has no \`const ${name} = \u2026()\` hook call.`,
|
|
1602
|
+
`Import the declared hook (default package: @ggui-ai/gadgets) and bind its return value to \`const ${name}\` at the top of the component; surface \`.value\` / \`.status\` in JSX.`
|
|
1603
|
+
)
|
|
1604
|
+
);
|
|
1605
|
+
}
|
|
1606
|
+
return issues;
|
|
1607
|
+
}
|
|
1608
|
+
function runClientCapabilityStartCalled(input) {
|
|
1609
|
+
if (input.compiledCode === null) return [];
|
|
1610
|
+
const src = input.sourceCode;
|
|
1611
|
+
const names = getStdlibGadgetNames(input.contract);
|
|
1612
|
+
const issues = [];
|
|
1613
|
+
for (const name of names) {
|
|
1614
|
+
const bindingRe = new RegExp(`const\\s+${name}\\s*=`);
|
|
1615
|
+
if (!bindingRe.test(src)) continue;
|
|
1616
|
+
const startRe = new RegExp(`\\b${name}\\s*\\.\\s*start\\s*\\(`);
|
|
1617
|
+
if (startRe.test(src)) continue;
|
|
1618
|
+
issues.push(
|
|
1619
|
+
mkIssue(
|
|
1620
|
+
"tooling.clientCapability.start_called",
|
|
1621
|
+
`clientCapability "${name}" is bound but \`${name}.start(\u2026)\` is never invoked \u2014 the capability stays in 'idle' and the feature won't fire.`,
|
|
1622
|
+
`Wire \`${name}.start({...})\` to a UI control (Button onClick, effect, etc.). Read \`.status\` to gate the UI between 'idle' / 'prompting' / 'active' / 'completed' / 'denied' / 'error'.`
|
|
1623
|
+
)
|
|
1624
|
+
);
|
|
1625
|
+
}
|
|
1626
|
+
return issues;
|
|
1627
|
+
}
|
|
1628
|
+
function collectStreamSourceTools(contract) {
|
|
1629
|
+
const result = /* @__PURE__ */ new Map();
|
|
1630
|
+
const streamSpec = contract?.streamSpec;
|
|
1631
|
+
if (!streamSpec || typeof streamSpec !== "object") return result;
|
|
1632
|
+
for (const [channelName, entryRaw] of Object.entries(streamSpec)) {
|
|
1633
|
+
const entry = entryRaw;
|
|
1634
|
+
if (!entry || typeof entry !== "object") continue;
|
|
1635
|
+
const tool = entry.source?.tool;
|
|
1636
|
+
if (typeof tool !== "string" || tool.length === 0) continue;
|
|
1637
|
+
result.set(channelName, tool);
|
|
1638
|
+
}
|
|
1639
|
+
return result;
|
|
1640
|
+
}
|
|
1641
|
+
function runStreamSourceNoDirectCall(input) {
|
|
1642
|
+
if (input.compiledCode === null) return [];
|
|
1643
|
+
const src = input.sourceCode;
|
|
1644
|
+
const map = collectStreamSourceTools(input.contract);
|
|
1645
|
+
if (map.size === 0) return [];
|
|
1646
|
+
const issues = [];
|
|
1647
|
+
for (const [channelName, toolName] of map) {
|
|
1648
|
+
const callRe = new RegExp(`\\b${toolName}\\s*\\(`);
|
|
1649
|
+
if (!callRe.test(src)) continue;
|
|
1650
|
+
issues.push(
|
|
1651
|
+
mkIssue(
|
|
1652
|
+
"realtime.stream_source.no_direct_call",
|
|
1653
|
+
`Source code calls \`${toolName}(...)\` directly but the contract declares it as the source tool for streamSpec.${channelName}. Source tools are agent-side / runtime-polled; the component MUST NOT invoke them.`,
|
|
1654
|
+
`Subscribe to the channel via \`const ${channelName} = useStream('${channelName}');\` and read \`${channelName}.latest\` / \`${channelName}.all\` \u2014 the runtime polls or subscribes to '${toolName}' on the component's behalf.`
|
|
1655
|
+
)
|
|
1656
|
+
);
|
|
1657
|
+
}
|
|
1658
|
+
return issues;
|
|
1659
|
+
}
|
|
1660
|
+
var RETIRED_IDENTIFIERS = [
|
|
1661
|
+
{
|
|
1662
|
+
pattern: /\buseWiredTool\b/,
|
|
1663
|
+
id: "useWiredTool",
|
|
1664
|
+
label: "`useWiredTool(...)` hook",
|
|
1665
|
+
replacement: "Use `useAction(name)` for user gestures the agent should react to. `agentCapabilities.tools` entries are agent-side catalog declarations \u2014 the component never calls them directly."
|
|
1666
|
+
},
|
|
1667
|
+
{
|
|
1668
|
+
pattern: /\buseAgentTool\b/,
|
|
1669
|
+
id: "useAgentTool",
|
|
1670
|
+
label: "`useAgentTool(...)` hook",
|
|
1671
|
+
replacement: "Agent-side tools are NEVER imported as component hooks. The contract declares them under `agentCapabilities.tools` for cross-ref (actionSpec.nextStep / streamSpec.source.tool); the component reacts via `useAction` / `useStream`."
|
|
1672
|
+
},
|
|
1673
|
+
{
|
|
1674
|
+
pattern: /\bcallWiredTool\b/,
|
|
1675
|
+
id: "callWiredTool",
|
|
1676
|
+
label: "`callWiredTool(...)` call",
|
|
1677
|
+
replacement: "Component-side direct invocation of agent-side tools is retired. Fire a UI gesture via `useAction(name)`; the agent reacts on its next turn (`nextStep` hint optional)."
|
|
1678
|
+
},
|
|
1679
|
+
{
|
|
1680
|
+
pattern: /\buseClientTool\b/,
|
|
1681
|
+
id: "useClientTool",
|
|
1682
|
+
label: "`useClientTool(name, handler)` hook",
|
|
1683
|
+
replacement: "Use a gadget hook from `@ggui-ai/gadgets` (e.g., `useGeolocation`, `useClipboardWrite`) and thread the result into a contextSpec slot or actionSpec payload."
|
|
1684
|
+
},
|
|
1685
|
+
{
|
|
1686
|
+
pattern: /\bdispatch\s*:\s*\{\s*kind\s*:/,
|
|
1687
|
+
id: "dispatch.kind",
|
|
1688
|
+
label: "`dispatch: { kind: '...' }` discriminated union",
|
|
1689
|
+
replacement: "ActionEntry.dispatch is retired. Use the flat `nextStep?: '<tool>'` field on the action entry instead."
|
|
1690
|
+
},
|
|
1691
|
+
{
|
|
1692
|
+
pattern: /\bintendedTool\b/,
|
|
1693
|
+
id: "intendedTool",
|
|
1694
|
+
label: "`intendedTool` field",
|
|
1695
|
+
replacement: "Use the flat `nextStep` field \u2014 the hint surface is one optional advisory string, not a nested discriminator."
|
|
1696
|
+
},
|
|
1697
|
+
{
|
|
1698
|
+
pattern: /\bmode\s*:\s*['"`]host-routed['"`]/,
|
|
1699
|
+
id: "mode.host-routed",
|
|
1700
|
+
label: "`mode: 'host-routed'`",
|
|
1701
|
+
replacement: "The `mode` field on action entries is retired. All actions are agent-routed; use `nextStep` for the optional tool hint."
|
|
1702
|
+
},
|
|
1703
|
+
{
|
|
1704
|
+
pattern: /\bbroadcast\s*:\s*\{/,
|
|
1705
|
+
id: "broadcast",
|
|
1706
|
+
label: "`broadcast: { \u2026 }` contract field",
|
|
1707
|
+
replacement: "Move the channel source declaration to `streamSpec[channel].source = { tool, args? }`."
|
|
1708
|
+
},
|
|
1709
|
+
{
|
|
1710
|
+
// Match `agentTools` only as a contract-shaped object key or
|
|
1711
|
+
// property access — not as a local variable name in unrelated
|
|
1712
|
+
// code. `\bagentTools\s*[:.]` catches `{ agentTools: {...} }` /
|
|
1713
|
+
// `contract.agentTools` while ignoring `const agentTools = ...`.
|
|
1714
|
+
pattern: /\bagentTools\s*[:.]/,
|
|
1715
|
+
id: "contract.agentTools",
|
|
1716
|
+
label: "`agentTools` top-level contract field",
|
|
1717
|
+
replacement: "The top-level `agentTools` field is retired. Declare agent-side tools under `agentCapabilities.tools` (catalog nested under a capabilities parent for symmetry with `clientCapabilities`)."
|
|
1718
|
+
},
|
|
1719
|
+
{
|
|
1720
|
+
pattern: /\bclientCapabilities\s*\.\s*capabilities\b/,
|
|
1721
|
+
id: "clientCapabilities.capabilities",
|
|
1722
|
+
label: "`clientCapabilities.capabilities` inner key",
|
|
1723
|
+
replacement: "The inner `capabilities` key is retired. Use `clientCapabilities.gadgets` \u2014 entries are library-hook declarations, not RPC capabilities."
|
|
1724
|
+
},
|
|
1725
|
+
{
|
|
1726
|
+
pattern: /['"`]@ggui-ai\/client-tools['"`]/,
|
|
1727
|
+
id: "package.@ggui-ai/client-tools",
|
|
1728
|
+
label: "`@ggui-ai/client-tools` package import",
|
|
1729
|
+
replacement: "The package was renamed to `@ggui-ai/gadgets`. Update the import string."
|
|
1730
|
+
},
|
|
1731
|
+
{
|
|
1732
|
+
pattern: /\bPushStory\b/,
|
|
1733
|
+
id: "PushStory",
|
|
1734
|
+
label: "`PushStory` type / `pushStorySchema` schema",
|
|
1735
|
+
replacement: "`PushStory` was retired when the handshake input was flattened. The current wire is `ggui_handshake({intent, blueprintDraft: {contract, variance?, generator?}})` + `ggui_render({handshakeId, props, override?: {contract?, variance?}})` (omit `override` to accept the proposal as-is)."
|
|
1736
|
+
},
|
|
1737
|
+
{
|
|
1738
|
+
pattern: /\bpushStorySchema\b/,
|
|
1739
|
+
id: "pushStorySchema",
|
|
1740
|
+
label: "`pushStorySchema` zod schema",
|
|
1741
|
+
replacement: "`pushStorySchema` was retired alongside `PushStory`. Current schemas: `handshakeInputSchema` + `renderInputSchema` (with the optional `override` re-aim) in `@ggui-ai/protocol`."
|
|
1742
|
+
},
|
|
1743
|
+
{
|
|
1744
|
+
pattern: /\bstory\s*\.\s*adapters\b/,
|
|
1745
|
+
id: "story.adapters",
|
|
1746
|
+
label: "`story.adapters` field access",
|
|
1747
|
+
replacement: "The story.adapters gate was retired alongside `PushStory`. Per-app permission gates flow through `clientCapabilities.gadgets[*].permission` (Permissions-Policy derivation)."
|
|
1748
|
+
},
|
|
1749
|
+
{
|
|
1750
|
+
pattern: /\bdeclaredAdapters\b/,
|
|
1751
|
+
id: "declaredAdapters",
|
|
1752
|
+
label: "`declaredAdapters` field / runtime gate",
|
|
1753
|
+
replacement: "App-level `declaredAdapters` was retired. Per-app permission gates derive from `clientCapabilities.gadgets[*].permission` instead."
|
|
1754
|
+
},
|
|
1755
|
+
{
|
|
1756
|
+
pattern: /\bassertAdaptersDeclared\b/,
|
|
1757
|
+
id: "assertAdaptersDeclared",
|
|
1758
|
+
label: "`assertAdaptersDeclared(...)` runtime call",
|
|
1759
|
+
replacement: "The runtime adapter-gate function is retired. Permissions-Policy is derived per-contract at render commit time and threaded through the bootstrap projection."
|
|
1760
|
+
},
|
|
1761
|
+
{
|
|
1762
|
+
pattern: /\bHandshakeStoredStory\b/,
|
|
1763
|
+
id: "HandshakeStoredStory",
|
|
1764
|
+
label: "`HandshakeStoredStory` storage type",
|
|
1765
|
+
replacement: "The OSS handler's stored type is now `HandshakeStoredInput` with the MVB-5 `{intent, blueprintDraft, forceCreate?}` shape."
|
|
1766
|
+
},
|
|
1767
|
+
{
|
|
1768
|
+
pattern: /\brecord\s*\.\s*story\b/,
|
|
1769
|
+
id: "record.story",
|
|
1770
|
+
label: "`record.story.*` access on handshake storage",
|
|
1771
|
+
replacement: "Handshake storage was flattened. Read `record.input.*` (intent / blueprintDraft) \u2014 the nested `story` wrapper is gone. MVB-5 also adds `record.suggestion` + `record.effectiveContract`."
|
|
1772
|
+
}
|
|
1773
|
+
];
|
|
1774
|
+
function runNoRetiredIdentifiers(input) {
|
|
1775
|
+
const src = input.sourceCode;
|
|
1776
|
+
const issues = [];
|
|
1777
|
+
for (const rule of RETIRED_IDENTIFIERS) {
|
|
1778
|
+
if (!rule.pattern.test(src)) continue;
|
|
1779
|
+
issues.push(
|
|
1780
|
+
mkIssue(
|
|
1781
|
+
`universal.no_retired_identifiers.${rule.id}`,
|
|
1782
|
+
`Source contains ${rule.label} \u2014 retired from the contract surface.`,
|
|
1783
|
+
rule.replacement
|
|
1784
|
+
)
|
|
1785
|
+
);
|
|
1786
|
+
}
|
|
1787
|
+
return issues;
|
|
1788
|
+
}
|
|
1789
|
+
var TOOLING_CHECKS = [
|
|
1790
|
+
{
|
|
1791
|
+
id: "tooling.clientCapability.hook_called",
|
|
1792
|
+
axis: "tooling",
|
|
1793
|
+
values: CLIENT_PRESENT,
|
|
1794
|
+
run: runGadgetHookCalled
|
|
1795
|
+
},
|
|
1796
|
+
{
|
|
1797
|
+
id: "tooling.clientCapability.start_called",
|
|
1798
|
+
axis: "tooling",
|
|
1799
|
+
values: CLIENT_PRESENT,
|
|
1800
|
+
run: runClientCapabilityStartCalled
|
|
1801
|
+
},
|
|
1802
|
+
{
|
|
1803
|
+
// Stream-source direct-call check. Gated on realtime axis (rather
|
|
1804
|
+
// than tooling) since stream sources are a realtime concern — but
|
|
1805
|
+
// logically lives in tooling.ts alongside the other tool-reference
|
|
1806
|
+
// checks since the issue class is about referenced agentTools.
|
|
1807
|
+
id: "realtime.stream_source.no_direct_call",
|
|
1808
|
+
axis: "realtime",
|
|
1809
|
+
values: REALTIME_ACTIVE2,
|
|
1810
|
+
run: runStreamSourceNoDirectCall
|
|
1811
|
+
},
|
|
1812
|
+
{
|
|
1813
|
+
// Universal — fires on every tooling-axis value. The check itself
|
|
1814
|
+
// is contract-agnostic; the axis gate is "every contract" via the
|
|
1815
|
+
// full ALL_TOOLING_VALUES list (rather than the universal-check
|
|
1816
|
+
// module's "render" gate convention) so the anti-pattern stays
|
|
1817
|
+
// co-located with the other tooling-related rules.
|
|
1818
|
+
id: "universal.no_retired_identifiers",
|
|
1819
|
+
axis: "tooling",
|
|
1820
|
+
values: ALL_TOOLING_VALUES,
|
|
1821
|
+
run: runNoRetiredIdentifiers
|
|
1822
|
+
}
|
|
1823
|
+
];
|
|
1824
|
+
|
|
1825
|
+
// src/evaluation/axis-checks/checks/grid.ts
|
|
1826
|
+
var BOARD_PROP_RX = /^(columns?|lanes?|stages?|lists?|buckets?|swimlanes?)$/i;
|
|
1827
|
+
var BOARD_PROMPT_RX = /\b(kanban|board)\b/i;
|
|
1828
|
+
var BOARD_NAMES = ["columns", "lanes", "stages", "lists", "buckets", "swimlanes"];
|
|
1829
|
+
var LAYOUT_TAG_RX = /<(\/?)(Grid|Row|Stack|Card|Box|Container|ScrollArea|div|section)\b([^<>]*?)>/g;
|
|
1830
|
+
var SIDE_BY_SIDE = /* @__PURE__ */ new Set(["Grid", "Row"]);
|
|
1831
|
+
function boardPropNames(contract) {
|
|
1832
|
+
const propsField = contract?.propsSpec;
|
|
1833
|
+
const properties = propsField?.properties ?? {};
|
|
1834
|
+
return Object.entries(properties).filter(([name, p]) => BOARD_PROP_RX.test(name) && (p?.type === "array" || p?.schema?.type === "array")).map(([name]) => name);
|
|
1835
|
+
}
|
|
1836
|
+
function jsxMapPositions(sourceCode, name) {
|
|
1837
|
+
const rx = new RegExp(
|
|
1838
|
+
`\\b${name}(?:\\?\\.)?\\.map\\(\\s*\\(?\\s*\\w+(?:\\s*,\\s*\\w+)?\\s*\\)?\\s*=>\\s*(?:\\(?\\s*<|\\{[^{}]{0,600}?\\breturn\\s*\\(?\\s*<)`,
|
|
1839
|
+
"g"
|
|
1840
|
+
);
|
|
1841
|
+
return [...sourceCode.matchAll(rx)].map((m) => m.index ?? 0);
|
|
1842
|
+
}
|
|
1843
|
+
function enclosingLayoutTag(sourceCode, pos) {
|
|
1844
|
+
const stack = [];
|
|
1845
|
+
for (const m of sourceCode.slice(0, pos).matchAll(LAYOUT_TAG_RX)) {
|
|
1846
|
+
const closing = m[1] === "/";
|
|
1847
|
+
const name = m[2] ?? "";
|
|
1848
|
+
const attrs = m[3] ?? "";
|
|
1849
|
+
if (closing) {
|
|
1850
|
+
if (stack[stack.length - 1] === name) stack.pop();
|
|
1851
|
+
continue;
|
|
1852
|
+
}
|
|
1853
|
+
if (attrs.trimEnd().endsWith("/")) continue;
|
|
1854
|
+
stack.push(name);
|
|
1855
|
+
}
|
|
1856
|
+
return stack[stack.length - 1];
|
|
1857
|
+
}
|
|
1858
|
+
function describeBoardRead(input) {
|
|
1859
|
+
const declared = boardPropNames(input.contract);
|
|
1860
|
+
const names = declared.length > 0 ? declared : BOARD_PROMPT_RX.test(input.originalPrompt) ? [...BOARD_NAMES] : BOARD_NAMES.filter((n) => jsxMapPositions(input.sourceCode, n).length > 0);
|
|
1861
|
+
const perName = names.map((name) => {
|
|
1862
|
+
const positions = jsxMapPositions(input.sourceCode, name);
|
|
1863
|
+
return { name, maps: positions.length, enclosing: positions.map((pos) => enclosingLayoutTag(input.sourceCode, pos) ?? "nothing") };
|
|
1864
|
+
});
|
|
1865
|
+
return { declared, names, perName };
|
|
1866
|
+
}
|
|
1867
|
+
function findStackedBoardColumns(input) {
|
|
1868
|
+
const stacked = [];
|
|
1869
|
+
for (const { name, enclosing } of describeBoardRead(input).perName) {
|
|
1870
|
+
for (const tag of enclosing) if (!SIDE_BY_SIDE.has(tag)) stacked.push([name, tag]);
|
|
1871
|
+
}
|
|
1872
|
+
return stacked;
|
|
1873
|
+
}
|
|
1874
|
+
function runBoardColumnsSideBySide(input) {
|
|
1875
|
+
if (input.compiledCode === null) return [];
|
|
1876
|
+
const standDown = input.designMode === "free";
|
|
1877
|
+
const stacked = standDown ? [] : findStackedBoardColumns(input);
|
|
1878
|
+
if (axisCheckTraceEnabled()) {
|
|
1879
|
+
const line = {
|
|
1880
|
+
...describeBoardRead(input),
|
|
1881
|
+
designMode: input.designMode ?? "constrained",
|
|
1882
|
+
standDown,
|
|
1883
|
+
result: standDown ? "stand-down" : stacked.length > 0 ? "fail" : "pass"
|
|
1884
|
+
};
|
|
1885
|
+
console.log(JSON.stringify({ boardColumnsTrace: line }));
|
|
1886
|
+
}
|
|
1887
|
+
if (stacked.length === 0) return [];
|
|
1888
|
+
const [name, tag] = stacked[0];
|
|
1889
|
+
return [
|
|
1890
|
+
mkIssue(
|
|
1891
|
+
"grid.board_columns_side_by_side",
|
|
1892
|
+
`The board's \`${name}\` are composed under <${tag}> \u2014 a vertical stack of column sections; at md and wider the columns must sit side by side.`,
|
|
1893
|
+
`Wrap the ${name} map in <Grid columns={{ base: 1, md: ${name}.length }} gap="md"> (each column its own <Stack> of cards); never a <Stack> of column sections.`
|
|
1894
|
+
)
|
|
1895
|
+
];
|
|
1896
|
+
}
|
|
1897
|
+
var GRID_CHECKS = [
|
|
1898
|
+
{
|
|
1899
|
+
id: "grid.board_columns_side_by_side",
|
|
1900
|
+
axis: "render",
|
|
1901
|
+
values: ["grid"],
|
|
1902
|
+
run: runBoardColumnsSideBySide
|
|
1903
|
+
}
|
|
1904
|
+
];
|
|
1905
|
+
|
|
1906
|
+
// src/evaluation/axis-checks/extras.ts
|
|
1907
|
+
function mkIssue2(subcategory, description, fix, result = "warn") {
|
|
1908
|
+
return { tier: 0, result, category: "mode", subcategory, description, fix };
|
|
1909
|
+
}
|
|
1910
|
+
var dragTriggerWired = {
|
|
1911
|
+
id: "writeTrigger.drag.handlers_wired",
|
|
1912
|
+
axis: "writeTrigger",
|
|
1913
|
+
values: ["drag"],
|
|
1914
|
+
run(input) {
|
|
1915
|
+
if (input.compiledCode === null) return [];
|
|
1916
|
+
const src = input.sourceCode;
|
|
1917
|
+
const hasStart = /onDragStart\s*=/.test(src);
|
|
1918
|
+
const hasDrop = /onDrop\s*=/.test(src);
|
|
1919
|
+
if (hasStart && hasDrop) return [];
|
|
1920
|
+
return [
|
|
1921
|
+
mkIssue2(
|
|
1922
|
+
"writeTrigger.drag.handlers_wired",
|
|
1923
|
+
`Classified as writeTrigger=drag but component lacks ${!hasStart ? "onDragStart" : ""}${!hasStart && !hasDrop ? " + " : ""}${!hasDrop ? "onDrop" : ""} handlers.`,
|
|
1924
|
+
"Attach onDragStart to draggable items and onDrop (with onDragOver preventDefault) to drop zones.",
|
|
1925
|
+
"fail"
|
|
1926
|
+
)
|
|
1927
|
+
];
|
|
1928
|
+
}
|
|
1929
|
+
};
|
|
1930
|
+
var swipeTriggerWired = {
|
|
1931
|
+
id: "writeTrigger.swipe.handlers_wired",
|
|
1932
|
+
axis: "writeTrigger",
|
|
1933
|
+
values: ["swipe"],
|
|
1934
|
+
run(input) {
|
|
1935
|
+
if (input.compiledCode === null) return [];
|
|
1936
|
+
const src = input.sourceCode;
|
|
1937
|
+
const hasTouch = /onTouchStart\s*=/.test(src) && /onTouchEnd\s*=/.test(src);
|
|
1938
|
+
if (!hasTouch) {
|
|
1939
|
+
return [
|
|
1940
|
+
mkIssue2(
|
|
1941
|
+
"writeTrigger.swipe.handlers_wired",
|
|
1942
|
+
"Classified as writeTrigger=swipe but component lacks onTouchStart + onTouchEnd handlers.",
|
|
1943
|
+
"Wire onTouchStart to record the start X/Y, onTouchEnd to classify direction and fire the action.",
|
|
1944
|
+
"fail"
|
|
1945
|
+
)
|
|
1946
|
+
];
|
|
1947
|
+
}
|
|
1948
|
+
return [];
|
|
1949
|
+
}
|
|
1950
|
+
};
|
|
1951
|
+
var composeCrossEntity = {
|
|
1952
|
+
id: "writes.compose.cross_entity_ids",
|
|
1953
|
+
axis: "writes",
|
|
1954
|
+
values: ["compose"],
|
|
1955
|
+
run(input) {
|
|
1956
|
+
if (input.compiledCode === null) return [];
|
|
1957
|
+
const src = input.sourceCode;
|
|
1958
|
+
const re = /\{\s*[^}]*?\b(\w*Id|id)\b[^}]*?,\s*[^}]*?\b(\w*Id|id)\b[^}]*?\}/s;
|
|
1959
|
+
if (re.test(src)) return [];
|
|
1960
|
+
return [
|
|
1961
|
+
mkIssue2(
|
|
1962
|
+
"writes.compose.cross_entity_ids",
|
|
1963
|
+
"Classified as writes=compose but no action invocation passes two id-bearing keys together.",
|
|
1964
|
+
"The compose action must receive both entity ids in one payload, e.g. `schedule({ eventId, calendarId })`.",
|
|
1965
|
+
"warn"
|
|
1966
|
+
)
|
|
1967
|
+
];
|
|
1968
|
+
}
|
|
1969
|
+
};
|
|
1970
|
+
var multiStepHasState = {
|
|
1971
|
+
id: "layout.multi_step.state_present",
|
|
1972
|
+
axis: "layout",
|
|
1973
|
+
values: ["multi-step"],
|
|
1974
|
+
run(input) {
|
|
1975
|
+
if (input.compiledCode === null) return [];
|
|
1976
|
+
const src = input.sourceCode;
|
|
1977
|
+
const hasIntStep = /useState(?:<number>)?\s*\(\s*[0-9]+\s*\)/.test(src);
|
|
1978
|
+
if (hasIntStep) return [];
|
|
1979
|
+
return [
|
|
1980
|
+
mkIssue2(
|
|
1981
|
+
"layout.multi_step.state_present",
|
|
1982
|
+
"Classified as layout=multi-step but no integer-typed useState tracks the current step.",
|
|
1983
|
+
"Add `const [step, setStep] = useState(0);` and branch rendering on it.",
|
|
1984
|
+
"fail"
|
|
1985
|
+
)
|
|
1986
|
+
];
|
|
1987
|
+
}
|
|
1988
|
+
};
|
|
1989
|
+
var stepperAdopted = {
|
|
1990
|
+
id: "layout.multi_step.stepper_adopted",
|
|
1991
|
+
axis: "layout",
|
|
1992
|
+
values: ["multi-step"],
|
|
1993
|
+
run(input) {
|
|
1994
|
+
if (input.compiledCode === null) return [];
|
|
1995
|
+
if (input.designMode === "free") return [];
|
|
1996
|
+
if (/<Stepper[\s/>]/.test(input.sourceCode)) return [];
|
|
1997
|
+
return [
|
|
1998
|
+
mkIssue2(
|
|
1999
|
+
"layout.multi_step.stepper_adopted",
|
|
2000
|
+
"Classified as layout=multi-step but the design system's Stepper composite is never rendered.",
|
|
2001
|
+
"Render <Stepper steps={STEPS} current={step} /> above the step body \u2014 STEPS is a top-level const array of step labels and `step` is your own useState index. Stepper is display-only: it never stores or advances the step itself.",
|
|
2002
|
+
"fail"
|
|
2003
|
+
)
|
|
2004
|
+
];
|
|
2005
|
+
}
|
|
2006
|
+
};
|
|
2007
|
+
var mixedStreamsHaveHandlers = {
|
|
2008
|
+
id: "realtime.mixed.handlers_per_event",
|
|
2009
|
+
axis: "realtime",
|
|
2010
|
+
values: ["mixed"],
|
|
2011
|
+
run(input) {
|
|
2012
|
+
if (input.compiledCode === null) return [];
|
|
2013
|
+
const src = input.sourceCode;
|
|
2014
|
+
const matches = src.match(/useStream\s*(?:<[^>]*>)?\s*\(/g);
|
|
2015
|
+
const count = matches?.length ?? 0;
|
|
2016
|
+
if (count >= 2) return [];
|
|
2017
|
+
return [
|
|
2018
|
+
mkIssue2(
|
|
2019
|
+
"realtime.mixed.handlers_per_event",
|
|
2020
|
+
`Classified as realtime=mixed but only ${count} useStream call(s) found \u2014 mixed streams need one handler per event.`,
|
|
2021
|
+
"Add a separate `useStream('eventName')` for each event in the contract.",
|
|
2022
|
+
"fail"
|
|
2023
|
+
)
|
|
2024
|
+
];
|
|
2025
|
+
}
|
|
2026
|
+
};
|
|
2027
|
+
var EXTRA_CHECKS = [
|
|
2028
|
+
dragTriggerWired,
|
|
2029
|
+
swipeTriggerWired,
|
|
2030
|
+
composeCrossEntity,
|
|
2031
|
+
multiStepHasState,
|
|
2032
|
+
stepperAdopted,
|
|
2033
|
+
mixedStreamsHaveHandlers
|
|
2034
|
+
];
|
|
2035
|
+
|
|
2036
|
+
// src/evaluation/axis-checks/registry.ts
|
|
2037
|
+
var REGISTRY = [
|
|
2038
|
+
...UNIVERSAL_CHECKS,
|
|
2039
|
+
...TINT_CHECKS,
|
|
2040
|
+
...STATE_MERGE_CHECKS,
|
|
2041
|
+
...STATE_UI_AFFORDANCE_CHECKS,
|
|
2042
|
+
...REALTIME_CHECKS,
|
|
2043
|
+
...WRITES_CHECKS,
|
|
2044
|
+
...STATE_PAYLOAD_CHECKS,
|
|
2045
|
+
...TOOLING_CHECKS,
|
|
2046
|
+
...GRID_CHECKS,
|
|
2047
|
+
...EXTRA_CHECKS
|
|
2048
|
+
];
|
|
2049
|
+
|
|
2050
|
+
export { EXTRA_CHECKS, REGISTRY, axisCheckTraceEnabled };
|
|
2051
|
+
//# sourceMappingURL=chunk-GYINWMPR.js.map
|
|
2052
|
+
//# sourceMappingURL=chunk-GYINWMPR.js.map
|