@ggui-ai/ui-gen 0.27.0 → 0.29.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{adapter-h4_V69LI.d.ts → adapter-DLQ-5COm.d.ts} +1 -1
- package/dist/adapters/base.js +2 -27
- package/dist/adapters/base.js.map +1 -1
- package/dist/adapters/generation-dispatch.d.ts +14 -6
- package/dist/adapters/generation-dispatch.js +40 -19095
- package/dist/adapters/generation-dispatch.js.map +1 -1
- package/dist/adapters/index.d.ts +1 -1
- package/dist/adapters/index.js +25 -3561
- package/dist/adapters/index.js.map +1 -1
- package/dist/adapters/types.js +2 -9
- package/dist/adapters/types.js.map +1 -1
- package/dist/advanced/index.d.ts +13 -12
- package/dist/advanced/index.js +63 -21789
- package/dist/advanced/index.js.map +1 -1
- package/dist/{bank-Ctdp3FUA.d.ts → bank-LWaiAtaA.d.ts} +1 -1
- package/dist/blueprint-validator.js +6 -2301
- package/dist/blueprint-validator.js.map +1 -1
- package/dist/boilerplate.d.ts +3 -2
- package/dist/boilerplate.js +6 -1867
- package/dist/boilerplate.js.map +1 -1
- package/dist/check/index.d.ts +3 -2
- package/dist/check/index.js +6 -1923
- package/dist/check/index.js.map +1 -1
- package/dist/chunk-33B3BAWS.js +24 -0
- package/dist/chunk-33B3BAWS.js.map +1 -0
- package/dist/chunk-3HSGA6NI.js +591 -0
- package/dist/chunk-3HSGA6NI.js.map +1 -0
- package/dist/chunk-3NNNGNTO.js +1707 -0
- package/dist/chunk-3NNNGNTO.js.map +1 -0
- package/dist/chunk-3T2HMTON.js +79 -0
- package/dist/chunk-3T2HMTON.js.map +1 -0
- package/dist/chunk-3ZSSDV3L.js +123 -0
- package/dist/chunk-3ZSSDV3L.js.map +1 -0
- package/dist/chunk-4MBR5GA3.js +99 -0
- package/dist/chunk-4MBR5GA3.js.map +1 -0
- package/dist/chunk-56NJFD55.js +337 -0
- package/dist/chunk-56NJFD55.js.map +1 -0
- package/dist/chunk-5AGTY555.js +26 -0
- package/dist/chunk-5AGTY555.js.map +1 -0
- package/dist/chunk-5EJM7EGY.js +115 -0
- package/dist/chunk-5EJM7EGY.js.map +1 -0
- package/dist/chunk-5KXCQLLR.js +529 -0
- package/dist/chunk-5KXCQLLR.js.map +1 -0
- package/dist/chunk-5VBXHVEH.js +171 -0
- package/dist/chunk-5VBXHVEH.js.map +1 -0
- package/dist/chunk-6KDDEING.js +515 -0
- package/dist/chunk-6KDDEING.js.map +1 -0
- package/dist/chunk-6VDRWAP6.js +123 -0
- package/dist/chunk-6VDRWAP6.js.map +1 -0
- package/dist/chunk-732IBFEJ.js +2400 -0
- package/dist/chunk-732IBFEJ.js.map +1 -0
- package/dist/chunk-73AURS3I.js +46 -0
- package/dist/chunk-73AURS3I.js.map +1 -0
- package/dist/chunk-77H5SKLX.js +8 -0
- package/dist/chunk-77H5SKLX.js.map +1 -0
- package/dist/chunk-7QE3AHLE.js +35 -0
- package/dist/chunk-7QE3AHLE.js.map +1 -0
- package/dist/chunk-A375NNWN.js +11 -0
- package/dist/chunk-A375NNWN.js.map +1 -0
- package/dist/chunk-AOU33EFS.js +140 -0
- package/dist/chunk-AOU33EFS.js.map +1 -0
- package/dist/chunk-BA4QYA6E.js +461 -0
- package/dist/chunk-BA4QYA6E.js.map +1 -0
- package/dist/chunk-DBP764GL.js +3 -0
- package/dist/chunk-DBP764GL.js.map +1 -0
- package/dist/chunk-DUU3ZJDQ.js +41 -0
- package/dist/chunk-DUU3ZJDQ.js.map +1 -0
- package/dist/chunk-DXKRBXNU.js +30 -0
- package/dist/chunk-DXKRBXNU.js.map +1 -0
- package/dist/chunk-FB7OEEAH.js +1467 -0
- package/dist/chunk-FB7OEEAH.js.map +1 -0
- package/dist/chunk-FLRNMZV2.js +18 -0
- package/dist/chunk-FLRNMZV2.js.map +1 -0
- package/dist/chunk-FWJIMKFZ.js +2032 -0
- package/dist/chunk-FWJIMKFZ.js.map +1 -0
- package/dist/chunk-FXRHWYIQ.js +163 -0
- package/dist/chunk-FXRHWYIQ.js.map +1 -0
- package/dist/chunk-G3PMV62Z.js +33 -0
- package/dist/chunk-G3PMV62Z.js.map +1 -0
- package/dist/chunk-GAJI7Y3P.js +13 -0
- package/dist/chunk-GAJI7Y3P.js.map +1 -0
- package/dist/chunk-GYINWMPR.js +2052 -0
- package/dist/chunk-GYINWMPR.js.map +1 -0
- package/dist/chunk-HD6ABIK7.js +484 -0
- package/dist/chunk-HD6ABIK7.js.map +1 -0
- package/dist/chunk-HDQVHYO6.js +100 -0
- package/dist/chunk-HDQVHYO6.js.map +1 -0
- package/dist/chunk-IHUT3SIW.js +7 -0
- package/dist/chunk-IHUT3SIW.js.map +1 -0
- package/dist/chunk-JDH5EGNZ.js +126 -0
- package/dist/chunk-JDH5EGNZ.js.map +1 -0
- package/dist/chunk-JMD4RGSV.js +37 -0
- package/dist/chunk-JMD4RGSV.js.map +1 -0
- package/dist/chunk-KJWGZL5B.js +33 -0
- package/dist/chunk-KJWGZL5B.js.map +1 -0
- package/dist/chunk-MKMDQFZ7.js +86 -0
- package/dist/chunk-MKMDQFZ7.js.map +1 -0
- package/dist/chunk-NF6JUZWF.js +85 -0
- package/dist/chunk-NF6JUZWF.js.map +1 -0
- package/dist/chunk-O2YC6XDC.js +1933 -0
- package/dist/chunk-O2YC6XDC.js.map +1 -0
- package/dist/chunk-OVLH5ICV.js +29 -0
- package/dist/chunk-OVLH5ICV.js.map +1 -0
- package/dist/chunk-PBOYHGSD.js +106 -0
- package/dist/chunk-PBOYHGSD.js.map +1 -0
- package/dist/chunk-PJFH7QWA.js +81 -0
- package/dist/chunk-PJFH7QWA.js.map +1 -0
- package/dist/chunk-PUH4CXGZ.js +284 -0
- package/dist/chunk-PUH4CXGZ.js.map +1 -0
- package/dist/chunk-QLXXPGTV.js +18 -0
- package/dist/chunk-QLXXPGTV.js.map +1 -0
- package/dist/chunk-R5GIGPW2.js +2845 -0
- package/dist/chunk-R5GIGPW2.js.map +1 -0
- package/dist/chunk-RH4IUSTO.js +83 -0
- package/dist/chunk-RH4IUSTO.js.map +1 -0
- package/dist/chunk-S2GVXKKY.js +153 -0
- package/dist/chunk-S2GVXKKY.js.map +1 -0
- package/dist/chunk-TVHGKFJS.js +58 -0
- package/dist/chunk-TVHGKFJS.js.map +1 -0
- package/dist/chunk-U7GALWLG.js +66 -0
- package/dist/chunk-U7GALWLG.js.map +1 -0
- package/dist/chunk-VTC263LT.js +256 -0
- package/dist/chunk-VTC263LT.js.map +1 -0
- package/dist/chunk-WEOXFEVE.js +739 -0
- package/dist/chunk-WEOXFEVE.js.map +1 -0
- package/dist/chunk-WYHP46LT.js +28 -0
- package/dist/chunk-WYHP46LT.js.map +1 -0
- package/dist/chunk-XGLO5LGG.js +274 -0
- package/dist/chunk-XGLO5LGG.js.map +1 -0
- package/dist/chunk-XRBCRKA2.js +29 -0
- package/dist/chunk-XRBCRKA2.js.map +1 -0
- package/dist/chunk-XW7ADFFV.js +340 -0
- package/dist/chunk-XW7ADFFV.js.map +1 -0
- package/dist/chunk-YIQ42IUX.js +34 -0
- package/dist/chunk-YIQ42IUX.js.map +1 -0
- package/dist/chunk-YJZTUY7B.js +101 -0
- package/dist/chunk-YJZTUY7B.js.map +1 -0
- package/dist/chunk-Z3CXME3K.js +107 -0
- package/dist/chunk-Z3CXME3K.js.map +1 -0
- package/dist/classifier/index.js +2 -527
- package/dist/classifier/index.js.map +1 -1
- package/dist/client-JBPCXU7W.js +4 -0
- package/dist/client-JBPCXU7W.js.map +1 -0
- package/dist/compose.js +3 -367
- package/dist/compose.js.map +1 -1
- package/dist/cost-tracker-HHGQ4FTW.js +67 -0
- package/dist/cost-tracker-HHGQ4FTW.js.map +1 -0
- package/dist/{create-ui-generator-BrZHF0cf.d.ts → create-ui-generator-BY-WeaPi.d.ts} +3 -3
- package/dist/{design-mode-DGWN_Gkm.d.ts → design-mode-XyOQmzE0.d.ts} +4 -3
- package/dist/{dispatch-B07TI30d.d.ts → dispatch-8LzTddKw.d.ts} +3 -2
- package/dist/dist-UALN6ZVB.js +543 -0
- package/dist/dist-UALN6ZVB.js.map +1 -0
- package/dist/dupe-break-X6K7QFIS.js +4 -0
- package/dist/dupe-break-X6K7QFIS.js.map +1 -0
- package/dist/evaluation/axis-checks/index.d.ts +5 -4
- package/dist/evaluation/axis-checks/index.js +6 -1818
- package/dist/evaluation/axis-checks/index.js.map +1 -1
- package/dist/evaluation/axis-checks/registry.d.ts +3 -2
- package/dist/evaluation/axis-checks/registry.js +3 -1748
- package/dist/evaluation/axis-checks/registry.js.map +1 -1
- package/dist/evaluation/index.d.ts +49 -132
- package/dist/evaluation/index.js +302 -6368
- package/dist/evaluation/index.js.map +1 -1
- package/dist/evaluation/types.d.ts +3 -2
- package/dist/evaluation/types.js +2 -5
- package/dist/evaluation/types.js.map +1 -1
- package/dist/fragments/index.js +2 -338
- package/dist/fragments/index.js.map +1 -1
- package/dist/{generate-tSO_shQ0.d.ts → generate-DQ4ci90s.d.ts} +1 -1
- package/dist/harness/check/runtime-render/index.d.ts +35 -15
- package/dist/harness/check/runtime-render/index.js +6 -2390
- package/dist/harness/check/runtime-render/index.js.map +1 -1
- package/dist/harness/check/runtime-render/render-check-worker.js +4 -1739
- package/dist/harness/check/runtime-render/render-check-worker.js.map +1 -1
- package/dist/harness/index.d.ts +3 -2
- package/dist/harness/index.js +23 -5124
- package/dist/harness/index.js.map +1 -1
- package/dist/harness/llm-trace-sink.js +2 -24
- package/dist/harness/llm-trace-sink.js.map +1 -1
- package/dist/harness/prompts.d.ts +2 -2
- package/dist/harness/prompts.js +8 -324
- package/dist/harness/prompts.js.map +1 -1
- package/dist/harness/result-types.d.ts +18 -6
- package/dist/harness/result-types.js +2 -0
- package/dist/harness/result-types.js.map +1 -1
- package/dist/harness/runtime.d.ts +22 -5
- package/dist/harness/runtime.js +10 -2313
- package/dist/harness/runtime.js.map +1 -1
- package/dist/harness/types-public.d.ts +3 -2
- package/dist/harness/types-public.js +16 -2954
- package/dist/harness/types-public.js.map +1 -1
- package/dist/harness/validator-trace-sink.d.ts +3 -2
- package/dist/harness/validator-trace-sink.js +2 -26
- package/dist/harness/validator-trace-sink.js.map +1 -1
- package/dist/hash.js +2 -39
- package/dist/hash.js.map +1 -1
- package/dist/hashline-5OKREIRM.js +4 -0
- package/dist/hashline-5OKREIRM.js.map +1 -0
- package/dist/index.d.ts +51 -12
- package/dist/index.js +57 -21850
- package/dist/index.js.map +1 -1
- package/dist/llm-evaluator-PN3WFCS4.js +653 -0
- package/dist/llm-evaluator-PN3WFCS4.js.map +1 -0
- package/dist/magic-string.es-2NZTC4GZ.js +1307 -0
- package/dist/magic-string.es-2NZTC4GZ.js.map +1 -0
- package/dist/patch.d.ts +3 -2
- package/dist/patch.js +2 -56
- package/dist/patch.js.map +1 -1
- package/dist/policy-VCYKGSAC.js +5 -0
- package/dist/policy-VCYKGSAC.js.map +1 -0
- package/dist/policy.js +2 -35
- package/dist/policy.js.map +1 -1
- package/dist/primitive-index-2OF6F4TZ.js +131 -0
- package/dist/primitive-index-2OF6F4TZ.js.map +1 -0
- package/dist/primitive-slice-WTMBLYFJ.js +235 -0
- package/dist/primitive-slice-WTMBLYFJ.js.map +1 -0
- package/dist/{prompts-BhNUuRoZ.d.ts → prompts-CLZGywMP.d.ts} +1 -1
- package/dist/provider-adapter-contract.js +10407 -14204
- package/dist/provider-adapter-contract.js.map +1 -1
- package/dist/provider-adapter.js +2 -44
- package/dist/provider-adapter.js.map +1 -1
- package/dist/providers/index.d.ts +89 -1
- package/dist/providers/index.js +115 -44
- package/dist/providers/index.js.map +1 -1
- package/dist/raw-C6KBH5I3.js +141 -0
- package/dist/raw-C6KBH5I3.js.map +1 -0
- package/dist/raw-FXSBXQTZ.js +7 -0
- package/dist/raw-FXSBXQTZ.js.map +1 -0
- package/dist/raw-MURDUPGK.js +8 -0
- package/dist/raw-MURDUPGK.js.map +1 -0
- package/dist/raw-T3GW3UVX.js +8 -0
- package/dist/raw-T3GW3UVX.js.map +1 -0
- package/dist/render-check-host-ZNIS6BJ5.js +5 -0
- package/dist/render-check-host-ZNIS6BJ5.js.map +1 -0
- package/dist/sdk-4JNSDNSR.js +6 -0
- package/dist/sdk-4JNSDNSR.js.map +1 -0
- package/dist/sdk-DIVJMHCX.js +7 -0
- package/dist/sdk-DIVJMHCX.js.map +1 -0
- package/dist/sdk-QRJOCRTS.js +7 -0
- package/dist/sdk-QRJOCRTS.js.map +1 -0
- package/dist/tag-balance-CTHCTRJ7.js +123 -0
- package/dist/tag-balance-CTHCTRJ7.js.map +1 -0
- package/dist/tools/get-primitives-ts.d.ts +1 -1
- package/dist/tools/get-primitives-ts.js +3 -1
- package/dist/tools/get-primitives-ts.js.map +1 -1
- package/dist/tools/render-check-worker.js +3 -40
- package/dist/tools/render-check-worker.js.map +1 -1
- package/dist/{types-public-h8IMnYuh.d.ts → types-public-hYlrerZh.d.ts} +35 -2
- package/dist/validation/index.d.ts +2 -2
- package/dist/validation/index.js +3 -498
- package/dist/validation/index.js.map +1 -1
- package/dist/{visual-evaluator-C2strxqD.d.ts → visual-evaluator-KskajxD5.d.ts} +14 -4
- package/dist/visual-evaluator-TGEXYQTP.js +12 -0
- package/dist/visual-evaluator-TGEXYQTP.js.map +1 -0
- package/dist/workflows.d.ts +3 -2
- package/dist/workflows.js +2 -151
- package/dist/workflows.js.map +1 -1
- package/package.json +21 -17
- package/src/adapters/claude/sdk.ts +4 -2
- package/src/adapters/claude/tool-bridge.ts +2 -1
- package/src/adapters/generation-dispatch.ts +9 -0
- package/src/adapters/openai/raw.ts +4 -0
- package/src/adapters/openai/sdk.ts +6 -1
- package/src/adapters/openai/tokens.ts +21 -7
- package/src/adapters/provider-router.ts +1 -0
- package/src/adapters/tool-bridge.ts +1 -1
- package/src/adapters/tools.ts +1 -1
- package/src/advanced/feedback.ts +1 -2
- package/src/advanced/generator.ts +4 -3
- package/src/boilerplate/free-design-prompt.ts +7 -0
- package/src/boilerplate/generate.ts +18 -5
- package/src/boilerplate/hard-sections.ts +8 -3
- package/src/boilerplate/react-hooks.ts +56 -0
- package/src/boilerplate/system-prompt.ts +12 -3
- package/src/check/run-tier0.ts +233 -8
- package/src/check/type-checker.ts +128 -16
- package/src/coding-agent/tools.ts +26 -3
- package/src/create-ui-generator.ts +4 -2
- package/src/design-mode.ts +4 -9
- package/src/evaluation/axis-checks/checks/state-merge.ts +3 -5
- package/src/evaluation/axis-checks/checks/state-payload.ts +1 -28
- package/src/evaluation/axis-checks/checks/tint.ts +197 -0
- package/src/evaluation/axis-checks/helpers.ts +246 -0
- package/src/evaluation/axis-checks/registry.ts +2 -0
- package/src/evaluation/criteria/__fixtures__/edge-v2-draw2.json +4629 -0
- package/src/evaluation/criteria/bank.ts +1 -1
- package/src/evaluation/criteria/edge-instruments.ts +14 -0
- package/src/evaluation/criteria/edge-probe-v2.ts +352 -0
- package/src/evaluation/criteria/edge-v2.ts +242 -0
- package/src/evaluation/criteria/resolve.ts +24 -5
- package/src/evaluation/evaluator.ts +2 -2
- package/src/evaluation/index.ts +3 -5
- package/src/evaluation/llm-evaluator.ts +2 -2
- package/src/evaluation/mcp-server.ts +0 -50
- package/src/evaluation/types-public.ts +6 -1
- package/src/evaluation/visual-evaluator.ts +162 -31
- package/src/harness/check/runtime-render/index.ts +28 -4
- package/src/harness/check/runtime-render/render-check.ts +20 -21
- package/src/harness/claude-agent-sdk-loader.ts +93 -0
- package/src/harness/claude-code-login-query.ts +1 -1
- package/src/harness/coding/assemble-result.ts +1 -0
- package/src/harness/coding/dupe-break.ts +70 -0
- package/src/harness/coding/generate-task-runner.ts +6 -1
- package/src/harness/coding/run-coding-turn.ts +59 -1
- package/src/harness/coding/run-eval-round.ts +9 -0
- package/src/harness/llm-router.ts +50 -2
- package/src/harness/result-types.ts +12 -0
- package/src/harness/runtime.ts +17 -0
- package/src/index.ts +10 -1
- package/src/providers/anthropic-tool-call.ts +263 -0
- package/src/providers/index.ts +13 -0
- package/src/tools/get-primitives-ts.ts +1 -1
- package/src/tools/render-check.ts +4 -4
- package/src/validation/primitives.ts +2 -1
- package/src/evaluation/loop.ts +0 -218
package/dist/check/index.js
CHANGED
|
@@ -1,1925 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
import
|
|
3
|
-
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
import
|
|
7
|
-
import { listContractGadgets, HOOK_NAME_RE } from '@ggui-ai/protocol';
|
|
8
|
-
import { consumedTokenManifest } from '@ggui-ai/design/themes';
|
|
9
|
-
|
|
10
|
-
// src/check/extract-wire-calls.ts
|
|
11
|
-
var HOOK_TO_KIND = {
|
|
12
|
-
useAction: "action",
|
|
13
|
-
useStream: "stream",
|
|
14
|
-
// `useGguiContext('slot')` is the client→agent observable-state
|
|
15
|
-
// hook. Treated as a wire call site for tier-0 preservation +
|
|
16
|
-
// undeclared detection because the runtime registers one
|
|
17
|
-
// React.Context per declared contextSpec slot at boot — referencing
|
|
18
|
-
// an undeclared slot throws synchronously at first paint.
|
|
19
|
-
useGguiContext: "context"
|
|
20
|
-
// Pre-rename component-side tool hooks are retired. The contract's
|
|
21
|
-
// `agentCapabilities.tools` catalog is agent-side declaration only —
|
|
22
|
-
// no component hook surface. Cross-refs surface via
|
|
23
|
-
// `actionSpec[*].nextStep` (already covered by the `action` kind)
|
|
24
|
-
// and `streamSpec[*].source.tool` (covered by `stream`).
|
|
25
|
-
};
|
|
26
|
-
var ALL_WIRE_HOOKS = Object.keys(HOOK_TO_KIND);
|
|
27
|
-
function extractWireCallSites(code) {
|
|
28
|
-
const sf = ts4.createSourceFile(
|
|
29
|
-
"component.tsx",
|
|
30
|
-
code,
|
|
31
|
-
ts4.ScriptTarget.Latest,
|
|
32
|
-
/*setParentNodes*/
|
|
33
|
-
true,
|
|
34
|
-
ts4.ScriptKind.TSX
|
|
35
|
-
);
|
|
36
|
-
const sites = [];
|
|
37
|
-
function visit(node) {
|
|
38
|
-
if (ts4.isCallExpression(node)) {
|
|
39
|
-
const callee = node.expression;
|
|
40
|
-
if (ts4.isIdentifier(callee)) {
|
|
41
|
-
const kind = HOOK_TO_KIND[callee.text];
|
|
42
|
-
if (kind) {
|
|
43
|
-
const firstArg = node.arguments[0];
|
|
44
|
-
if (firstArg && ts4.isStringLiteral(firstArg)) {
|
|
45
|
-
sites.push({ kind, name: firstArg.text });
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
ts4.forEachChild(node, visit);
|
|
51
|
-
}
|
|
52
|
-
visit(sf);
|
|
53
|
-
return sites;
|
|
54
|
-
}
|
|
55
|
-
function extractWireImports(code) {
|
|
56
|
-
const sf = ts4.createSourceFile(
|
|
57
|
-
"component.tsx",
|
|
58
|
-
code,
|
|
59
|
-
ts4.ScriptTarget.Latest,
|
|
60
|
-
/*setParentNodes*/
|
|
61
|
-
true,
|
|
62
|
-
ts4.ScriptKind.TSX
|
|
63
|
-
);
|
|
64
|
-
const imported = /* @__PURE__ */ new Set();
|
|
65
|
-
for (const stmt of sf.statements) {
|
|
66
|
-
if (!ts4.isImportDeclaration(stmt)) continue;
|
|
67
|
-
const moduleSpecifier = stmt.moduleSpecifier;
|
|
68
|
-
if (!ts4.isStringLiteral(moduleSpecifier)) continue;
|
|
69
|
-
if (moduleSpecifier.text !== "@ggui-ai/wire") continue;
|
|
70
|
-
const clause = stmt.importClause;
|
|
71
|
-
if (!clause) continue;
|
|
72
|
-
const bindings = clause.namedBindings;
|
|
73
|
-
if (!bindings) continue;
|
|
74
|
-
if (!ts4.isNamedImports(bindings)) continue;
|
|
75
|
-
for (const el of bindings.elements) {
|
|
76
|
-
imported.add(el.name.text);
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
return imported;
|
|
80
|
-
}
|
|
81
|
-
function collectExpectedWires(contract) {
|
|
82
|
-
const expected = [];
|
|
83
|
-
const actionsMap = contract.actionSpec ?? {};
|
|
84
|
-
for (const name of Object.keys(actionsMap)) {
|
|
85
|
-
expected.push({ kind: "action", name });
|
|
86
|
-
}
|
|
87
|
-
const streamsMap = contract.streamSpec ?? {};
|
|
88
|
-
for (const name of Object.keys(streamsMap)) {
|
|
89
|
-
expected.push({ kind: "stream", name });
|
|
90
|
-
}
|
|
91
|
-
const contextMap = contract.contextSpec ?? {};
|
|
92
|
-
for (const name of Object.keys(contextMap)) {
|
|
93
|
-
expected.push({ kind: "context", name });
|
|
94
|
-
}
|
|
95
|
-
return expected;
|
|
96
|
-
}
|
|
97
|
-
function checkWirePreservation(code, contract) {
|
|
98
|
-
const expected = collectExpectedWires(contract);
|
|
99
|
-
const actual = extractWireCallSites(code);
|
|
100
|
-
const actualKeys = new Set(actual.map((s) => `${s.kind}:${s.name}`));
|
|
101
|
-
const expectedKeys = new Set(expected.map((s) => `${s.kind}:${s.name}`));
|
|
102
|
-
const missing = expected.filter((s) => !actualKeys.has(`${s.kind}:${s.name}`));
|
|
103
|
-
const extra = actual.filter((s) => !expectedKeys.has(`${s.kind}:${s.name}`));
|
|
104
|
-
return { missing, extra };
|
|
105
|
-
}
|
|
106
|
-
function checkWireImports(code) {
|
|
107
|
-
const used = extractWireCallSites(code);
|
|
108
|
-
const imports = extractWireImports(code);
|
|
109
|
-
const usedHookSet = new Set(
|
|
110
|
-
used.map(
|
|
111
|
-
(s) => (
|
|
112
|
-
// Reverse the kind → hook-name lookup. Closed set of 5 —
|
|
113
|
-
// hardcoded to stay cheap and explicit.
|
|
114
|
-
{
|
|
115
|
-
action: "useAction",
|
|
116
|
-
stream: "useStream",
|
|
117
|
-
context: "useGguiContext"
|
|
118
|
-
}[s.kind]
|
|
119
|
-
)
|
|
120
|
-
)
|
|
121
|
-
);
|
|
122
|
-
const missing = [];
|
|
123
|
-
for (const hook of ALL_WIRE_HOOKS) {
|
|
124
|
-
if (!usedHookSet.has(hook)) continue;
|
|
125
|
-
if (imports.has(hook)) continue;
|
|
126
|
-
missing.push({ hook, kind: HOOK_TO_KIND[hook] });
|
|
127
|
-
}
|
|
128
|
-
return { missing };
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// src/boilerplate/json-schema-ts.ts
|
|
132
|
-
function jsonSchemaTypeToTs(schema) {
|
|
133
|
-
const unionMembers = schema.oneOf ?? schema.anyOf;
|
|
134
|
-
if (unionMembers?.length) {
|
|
135
|
-
const types = [...new Set(unionMembers.map((s) => jsonSchemaTypeToTs(s)))];
|
|
136
|
-
return types.length === 1 ? types[0] : types.join(" | ");
|
|
137
|
-
}
|
|
138
|
-
if (schema.const !== void 0) {
|
|
139
|
-
return typeof schema.const === "string" ? `'${schema.const}'` : String(schema.const);
|
|
140
|
-
}
|
|
141
|
-
if (schema.enum?.length) {
|
|
142
|
-
return schema.enum.map((v) => typeof v === "string" ? `'${v}'` : String(v)).join(" | ");
|
|
143
|
-
}
|
|
144
|
-
if (Array.isArray(schema.type)) {
|
|
145
|
-
const { nullable: _nullable, ...rest } = schema;
|
|
146
|
-
const members = [...new Set(schema.type)].map(
|
|
147
|
-
(member) => jsonSchemaTypeToTs({ ...rest, type: member })
|
|
148
|
-
);
|
|
149
|
-
const unique = [...new Set(members)];
|
|
150
|
-
if (schema.nullable && !unique.includes("null")) unique.push("null");
|
|
151
|
-
return unique.join(" | ");
|
|
152
|
-
}
|
|
153
|
-
let result;
|
|
154
|
-
switch (schema.type) {
|
|
155
|
-
case "string":
|
|
156
|
-
result = "string";
|
|
157
|
-
break;
|
|
158
|
-
case "number":
|
|
159
|
-
case "integer":
|
|
160
|
-
result = "number";
|
|
161
|
-
break;
|
|
162
|
-
case "boolean":
|
|
163
|
-
result = "boolean";
|
|
164
|
-
break;
|
|
165
|
-
case "null":
|
|
166
|
-
return "null";
|
|
167
|
-
case "array": {
|
|
168
|
-
if (schema.items) {
|
|
169
|
-
const itemType = jsonSchemaTypeToTs(schema.items);
|
|
170
|
-
const itemsDeclareObject = Array.isArray(schema.items.type) ? schema.items.type.includes("object") : schema.items.type === "object";
|
|
171
|
-
result = itemsDeclareObject && schema.items.properties ? `Array<${itemType}>` : itemType.includes("|") ? `(${itemType})[]` : `${itemType}[]`;
|
|
172
|
-
} else {
|
|
173
|
-
result = "unknown[]";
|
|
174
|
-
}
|
|
175
|
-
break;
|
|
176
|
-
}
|
|
177
|
-
case "object": {
|
|
178
|
-
if (schema.properties) {
|
|
179
|
-
const required = schema.required ?? [];
|
|
180
|
-
const fields = Object.entries(schema.properties).map(([key, prop]) => {
|
|
181
|
-
const opt = !required.includes(key);
|
|
182
|
-
return `${key}${opt ? "?" : ""}: ${jsonSchemaTypeToTs(prop)}`;
|
|
183
|
-
}).join("; ");
|
|
184
|
-
result = `{ ${fields} }`;
|
|
185
|
-
} else if (schema.additionalProperties && typeof schema.additionalProperties === "object") {
|
|
186
|
-
result = `Record<string, ${jsonSchemaTypeToTs(schema.additionalProperties)}>`;
|
|
187
|
-
} else {
|
|
188
|
-
result = "Record<string, unknown>";
|
|
189
|
-
}
|
|
190
|
-
break;
|
|
191
|
-
}
|
|
192
|
-
default:
|
|
193
|
-
result = "unknown";
|
|
194
|
-
}
|
|
195
|
-
if (schema.nullable && result !== "unknown") {
|
|
196
|
-
return `${result} | null`;
|
|
197
|
-
}
|
|
198
|
-
return result;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
// src/check/contract-validation.ts
|
|
202
|
-
function extractPropsInterface(code) {
|
|
203
|
-
const sf = ts4.createSourceFile("component.tsx", code, ts4.ScriptTarget.Latest, true, ts4.ScriptKind.TSX);
|
|
204
|
-
const props = [];
|
|
205
|
-
function visitMembers(members) {
|
|
206
|
-
for (const member of members) {
|
|
207
|
-
if (ts4.isPropertySignature(member) && member.name) {
|
|
208
|
-
const name = member.name.getText(sf);
|
|
209
|
-
const type = member.type ? member.type.getText(sf) : "unknown";
|
|
210
|
-
const optional = !!member.questionToken;
|
|
211
|
-
props.push({ name, type, optional });
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
function visit(node) {
|
|
216
|
-
if (ts4.isInterfaceDeclaration(node) && node.name.text === "Props") {
|
|
217
|
-
visitMembers(node.members);
|
|
218
|
-
return;
|
|
219
|
-
}
|
|
220
|
-
if (ts4.isTypeAliasDeclaration(node) && node.name.text === "Props") {
|
|
221
|
-
if (ts4.isTypeLiteralNode(node.type)) {
|
|
222
|
-
visitMembers(node.type.members);
|
|
223
|
-
}
|
|
224
|
-
return;
|
|
225
|
-
}
|
|
226
|
-
ts4.forEachChild(node, visit);
|
|
227
|
-
}
|
|
228
|
-
visit(sf);
|
|
229
|
-
return props.length > 0 ? props : null;
|
|
230
|
-
}
|
|
231
|
-
function validatePropsAgainstSchema(code, spec) {
|
|
232
|
-
const issues = [];
|
|
233
|
-
const extracted = extractPropsInterface(code);
|
|
234
|
-
if (!extracted) {
|
|
235
|
-
return [];
|
|
236
|
-
}
|
|
237
|
-
const extractedMap = new Map(extracted.map((p) => [p.name, p]));
|
|
238
|
-
for (const [propName, entry] of Object.entries(spec.properties)) {
|
|
239
|
-
const extractedProp = extractedMap.get(propName);
|
|
240
|
-
if (!extractedProp) {
|
|
241
|
-
if (entry.required) {
|
|
242
|
-
issues.push({
|
|
243
|
-
severity: "error",
|
|
244
|
-
field: propName,
|
|
245
|
-
message: `Props interface is missing required field '${propName}' from the data contract`,
|
|
246
|
-
fix: `Add \`${propName}${entry.required ? "" : "?"}: ${jsonSchemaTypeToTs(entry.schema)}\` to your Props interface`
|
|
247
|
-
});
|
|
248
|
-
} else {
|
|
249
|
-
issues.push({
|
|
250
|
-
severity: "warning",
|
|
251
|
-
field: propName,
|
|
252
|
-
message: `Props interface is missing optional field '${propName}' from the data contract`,
|
|
253
|
-
fix: `Consider adding \`${propName}?: ${jsonSchemaTypeToTs(entry.schema)}\` to your Props interface`
|
|
254
|
-
});
|
|
255
|
-
}
|
|
256
|
-
continue;
|
|
257
|
-
}
|
|
258
|
-
if (extractedProp.type !== "unknown") {
|
|
259
|
-
const expectedTsType = jsonSchemaTypeToTs(entry.schema);
|
|
260
|
-
if (!isTypeCompatible(extractedProp.type, expectedTsType, entry.schema)) {
|
|
261
|
-
issues.push({
|
|
262
|
-
severity: "warning",
|
|
263
|
-
field: propName,
|
|
264
|
-
message: `Field '${propName}' has type '${extractedProp.type}' but contract expects '${expectedTsType}'`,
|
|
265
|
-
fix: `Change \`${propName}\` type to \`${expectedTsType}\``
|
|
266
|
-
});
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
return issues;
|
|
271
|
-
}
|
|
272
|
-
function isTypeCompatible(tsType, expectedTsType, schema) {
|
|
273
|
-
const normalized = tsType.replace(/\s/g, "").toLowerCase();
|
|
274
|
-
switch (schema.type) {
|
|
275
|
-
case "string":
|
|
276
|
-
return normalized.includes("string");
|
|
277
|
-
case "number":
|
|
278
|
-
case "integer":
|
|
279
|
-
return normalized.includes("number");
|
|
280
|
-
case "boolean":
|
|
281
|
-
return normalized.includes("boolean");
|
|
282
|
-
case "array":
|
|
283
|
-
return normalized.includes("[]") || normalized.includes("array");
|
|
284
|
-
case "object":
|
|
285
|
-
return !["string", "number", "boolean"].some((t) => normalized === t);
|
|
286
|
-
default:
|
|
287
|
-
return true;
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
function propsSpecToTypeScript(spec, indent = 2) {
|
|
291
|
-
const pad = " ".repeat(indent);
|
|
292
|
-
const lines = [];
|
|
293
|
-
for (const [propName, entry] of Object.entries(spec.properties)) {
|
|
294
|
-
if (entry.description) {
|
|
295
|
-
lines.push(`${pad}/** ${entry.description} */`);
|
|
296
|
-
}
|
|
297
|
-
const optional = !entry.required;
|
|
298
|
-
const tsType = jsonSchemaTypeToTs(entry.schema);
|
|
299
|
-
const defaultStr = entry.default !== void 0 ? ` // default: ${JSON.stringify(entry.default)}` : "";
|
|
300
|
-
lines.push(`${pad}${propName}${optional ? "?" : ""}: ${tsType};${defaultStr}`);
|
|
301
|
-
if (entry.example !== void 0) {
|
|
302
|
-
const exampleStr = JSON.stringify(entry.example, null, 2).split("\n").map((l, i) => i === 0 ? `${pad}// Example: ${l}` : `${pad}// ${l}`).join("\n");
|
|
303
|
-
lines.push(exampleStr);
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
return `{
|
|
307
|
-
${lines.join("\n")}
|
|
308
|
-
}`;
|
|
309
|
-
}
|
|
310
|
-
function validateStreamSpecConformance(code, spec) {
|
|
311
|
-
const issues = [];
|
|
312
|
-
const usesWireStream = code.includes("useStream");
|
|
313
|
-
if (!usesWireStream) {
|
|
314
|
-
issues.push({
|
|
315
|
-
severity: "error",
|
|
316
|
-
field: "__streamSpec__",
|
|
317
|
-
message: "Component does not handle stream events \u2014 use useStream() from the boilerplate wire hooks",
|
|
318
|
-
fix: "Use the useStream('eventName') hook from the boilerplate to receive real-time data from the agent"
|
|
319
|
-
});
|
|
320
|
-
return issues;
|
|
321
|
-
}
|
|
322
|
-
for (const [channelName, entry] of Object.entries(spec)) {
|
|
323
|
-
if (!code.includes(`'${channelName}'`) && !code.includes(`"${channelName}"`)) {
|
|
324
|
-
issues.push({
|
|
325
|
-
severity: "warning",
|
|
326
|
-
field: channelName,
|
|
327
|
-
message: `StreamSpec declares channel '${channelName}' but component doesn't reference it`,
|
|
328
|
-
fix: `Add useStream('${channelName}') to receive ${entry.description || channelName} events`
|
|
329
|
-
});
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
return issues;
|
|
333
|
-
}
|
|
334
|
-
function validateActionSpecConformance(code, spec) {
|
|
335
|
-
const issues = [];
|
|
336
|
-
for (const [actionId, entry] of Object.entries(spec)) {
|
|
337
|
-
const idReferenced = code.includes(`'${actionId}'`) || code.includes(`"${actionId}"`);
|
|
338
|
-
const labelReferenced = code.includes(entry.label);
|
|
339
|
-
const callbackName = `on${actionId.charAt(0).toUpperCase()}${actionId.slice(1)}`;
|
|
340
|
-
const callbackReferenced = code.includes(callbackName);
|
|
341
|
-
if (!idReferenced && !labelReferenced && !callbackReferenced) {
|
|
342
|
-
issues.push({
|
|
343
|
-
severity: "error",
|
|
344
|
-
field: actionId,
|
|
345
|
-
message: `ActionSpec declares action '${actionId}' ("${entry.label}") but component doesn't wire it`,
|
|
346
|
-
fix: `Use the useAction('${actionId}') hook from the boilerplate and wire it to a button or form submit`
|
|
347
|
-
});
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
return issues;
|
|
351
|
-
}
|
|
352
|
-
function validateAllContracts(code, contract) {
|
|
353
|
-
const issues = [];
|
|
354
|
-
if (contract.propsSpec) {
|
|
355
|
-
issues.push(...validatePropsAgainstSchema(code, contract.propsSpec));
|
|
356
|
-
}
|
|
357
|
-
if (contract.streamSpec) {
|
|
358
|
-
issues.push(...validateStreamSpecConformance(code, contract.streamSpec));
|
|
359
|
-
}
|
|
360
|
-
if (contract.actionSpec) {
|
|
361
|
-
issues.push(...validateActionSpecConformance(code, contract.actionSpec));
|
|
362
|
-
}
|
|
363
|
-
return issues;
|
|
364
|
-
}
|
|
365
|
-
function inferPropsSpecFromSampleData(data) {
|
|
366
|
-
const properties = {};
|
|
367
|
-
for (const [key, value] of Object.entries(data)) {
|
|
368
|
-
properties[key] = {
|
|
369
|
-
schema: inferJsonSchema(value),
|
|
370
|
-
required: true,
|
|
371
|
-
example: value
|
|
372
|
-
};
|
|
373
|
-
}
|
|
374
|
-
return { properties };
|
|
375
|
-
}
|
|
376
|
-
function inferJsonSchema(value) {
|
|
377
|
-
if (value === null || value === void 0) return { type: "null" };
|
|
378
|
-
if (typeof value === "string") return { type: "string" };
|
|
379
|
-
if (typeof value === "number") return { type: "number" };
|
|
380
|
-
if (typeof value === "boolean") return { type: "boolean" };
|
|
381
|
-
if (Array.isArray(value)) {
|
|
382
|
-
if (value.length === 0) return { type: "array" };
|
|
383
|
-
return {
|
|
384
|
-
type: "array",
|
|
385
|
-
items: inferJsonSchema(value[0])
|
|
386
|
-
};
|
|
387
|
-
}
|
|
388
|
-
if (typeof value === "object") {
|
|
389
|
-
const properties = {};
|
|
390
|
-
const required = [];
|
|
391
|
-
for (const [k, v] of Object.entries(value)) {
|
|
392
|
-
properties[k] = inferJsonSchema(v);
|
|
393
|
-
required.push(k);
|
|
394
|
-
}
|
|
395
|
-
return { type: "object", properties, required };
|
|
396
|
-
}
|
|
397
|
-
return { type: "string" };
|
|
398
|
-
}
|
|
399
|
-
var ALLOWED_IMPORT_BASES = [
|
|
400
|
-
"react",
|
|
401
|
-
"react-dom",
|
|
402
|
-
"@ggui-ai/design",
|
|
403
|
-
"@ggui-ai/wire",
|
|
404
|
-
"@ggui-ai/gadgets"
|
|
405
|
-
];
|
|
406
|
-
function isAllowedImport(specifier, gadgetPackages) {
|
|
407
|
-
if (FORBIDDEN_IMPORT_SPECIFIERS.includes(specifier)) return false;
|
|
408
|
-
for (const base of ALLOWED_IMPORT_BASES) {
|
|
409
|
-
if (specifier === base || specifier.startsWith(`${base}/`)) return true;
|
|
410
|
-
}
|
|
411
|
-
return gadgetPackages?.has(specifier) ?? false;
|
|
412
|
-
}
|
|
413
|
-
function describeAllowedImports() {
|
|
414
|
-
return "react, @ggui-ai/design, @ggui-ai/wire, @ggui-ai/gadgets, or a gadget package declared on the contract \u2014 never @ggui-ai/wire/internal (the writer side of the wire; components read useRender().isConnected)";
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
// src/check/type-checker.ts
|
|
418
|
-
var BLOCKING_CODES = /* @__PURE__ */ new Set([
|
|
419
|
-
2304,
|
|
420
|
-
// Cannot find name
|
|
421
|
-
// 2307 (Cannot find module) is NOT blocking — Lambda bundles code without
|
|
422
|
-
// type declarations, so the VFS can't resolve react/@ggui-ai/design.
|
|
423
|
-
// Forbidden imports are caught by runSelfChecks regex instead.
|
|
424
|
-
2305,
|
|
425
|
-
// Module has no exported member
|
|
426
|
-
2322,
|
|
427
|
-
// Type not assignable
|
|
428
|
-
2339,
|
|
429
|
-
// Property does not exist on type
|
|
430
|
-
2741,
|
|
431
|
-
// Missing required property
|
|
432
|
-
2769,
|
|
433
|
-
// No overload matches this call
|
|
434
|
-
17004,
|
|
435
|
-
// Cannot use JSX unless '--jsx' flag
|
|
436
|
-
18047,
|
|
437
|
-
// 'X' is possibly 'null' — causes runtime crash
|
|
438
|
-
18048
|
|
439
|
-
// 'X' is possibly 'undefined' — causes runtime crash
|
|
440
|
-
]);
|
|
441
|
-
function generateFix(code, message, sourceLine) {
|
|
442
|
-
const sourceHint = sourceLine && sourceLine.length > 0 && sourceLine.length <= 140 ? ` Offending line: \`${sourceLine}\`` : "";
|
|
443
|
-
if ((code === 2322 || code === 2339) && message && /onClick|onDoubleClick|onMouseEnter|onMouseLeave|onPress/.test(message)) {
|
|
444
|
-
return `This structural primitive has no event handlers of its own \u2014 add the trait as a PROP: as={Clickable} (then onClick works), imported from @ggui-ai/design. Do NOT wrap it in <Clickable>\u2026</Clickable>; as is a prop, not a wrapper element.${sourceHint}`;
|
|
445
|
-
}
|
|
446
|
-
if (code === 2339 && message && /Property '_[a-zA-Z]/.test(message)) {
|
|
447
|
-
return `You renamed a prop with a leading underscore to silence \`no-unused-vars\`, but the prop on \`Props\` doesn't have that prefix. Restore the original name; better, don't destructure props you won't render \u2014 access them via \`props.fieldName\` only when needed.${sourceHint}`;
|
|
448
|
-
}
|
|
449
|
-
if (code === 2304 && message && !message.includes("module") && !message.includes("import")) {
|
|
450
|
-
return `This name is not defined in scope. Either you destructured props (use \`props.fieldName\` directly instead) OR you removed a helper declaration in this patch but kept a JSX/expression reference to it. Read your full patch and either restore the declaration or remove the reference.${sourceHint}`;
|
|
451
|
-
}
|
|
452
|
-
if ((code === 2322 || code === 2769) && message && /unknown.*ReactNode|ReactNode.*unknown/.test(message)) {
|
|
453
|
-
return `This value has type 'unknown' and cannot be rendered in JSX. Cast it: String(value) or add a type annotation.${sourceHint}`;
|
|
454
|
-
}
|
|
455
|
-
switch (code) {
|
|
456
|
-
case 2307:
|
|
457
|
-
return `Only these imports are allowed: ${describeAllowedImports()}${sourceHint}`;
|
|
458
|
-
case 2322:
|
|
459
|
-
case 2769:
|
|
460
|
-
return `Type mismatch on this expression. Check the offending line below \u2014 the prop name in JSX is what TypeScript is rejecting; the message tells you the expected type.${sourceHint}`;
|
|
461
|
-
case 2339:
|
|
462
|
-
return `This prop doesn't exist on this component \u2014 check the available props on the component's interface (visible at the top of the file, or in the design-system reference).${sourceHint}`;
|
|
463
|
-
case 2305:
|
|
464
|
-
return `This name is not defined. Check your imports and variable declarations.${sourceHint}`;
|
|
465
|
-
case 2741:
|
|
466
|
-
return `A required prop is missing. Check the component's Props interface.${sourceHint}`;
|
|
467
|
-
case 18047:
|
|
468
|
-
case 18048:
|
|
469
|
-
return `This value might be null/undefined. Every dereference on the same nullable still needs \`?.\` \u2014 \`x?.foo && x?.bar\`, NOT \`x?.foo && x.bar\`. Or hoist: \`const v = x; if (!v) return null;\` then access \`v.foo\`/\`v.bar\` unguarded.${sourceHint}`;
|
|
470
|
-
default:
|
|
471
|
-
return `Review the TypeScript error and fix the type issue.${sourceHint}`;
|
|
472
|
-
}
|
|
473
|
-
}
|
|
474
|
-
var vfsCache = null;
|
|
475
|
-
function parseAndStore(vfs, virtualPath, content) {
|
|
476
|
-
const sourceFile = ts4.createSourceFile(
|
|
477
|
-
virtualPath,
|
|
478
|
-
content,
|
|
479
|
-
ts4.ScriptTarget.ES2020,
|
|
480
|
-
true,
|
|
481
|
-
virtualPath.endsWith(".tsx") ? ts4.ScriptKind.TSX : ts4.ScriptKind.TS
|
|
482
|
-
);
|
|
483
|
-
vfs.set(virtualPath, { content, sourceFile });
|
|
484
|
-
}
|
|
485
|
-
function walkDir(dir, filter) {
|
|
486
|
-
const results = [];
|
|
487
|
-
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
488
|
-
const full = path.join(dir, entry.name);
|
|
489
|
-
if (entry.isDirectory()) {
|
|
490
|
-
results.push(...walkDir(full, filter));
|
|
491
|
-
} else if (filter(full)) {
|
|
492
|
-
results.push(full);
|
|
493
|
-
}
|
|
494
|
-
}
|
|
495
|
-
return results;
|
|
496
|
-
}
|
|
497
|
-
function findReactTypesDir() {
|
|
498
|
-
try {
|
|
499
|
-
const indexDts = createRequire(import.meta.url).resolve("@types/react/index.d.ts");
|
|
500
|
-
return path.dirname(indexDts);
|
|
501
|
-
} catch {
|
|
502
|
-
return null;
|
|
503
|
-
}
|
|
504
|
-
}
|
|
505
|
-
function findPackageDistDir(pkg) {
|
|
506
|
-
try {
|
|
507
|
-
const pkgJson = createRequire(import.meta.url).resolve(`${pkg}/package.json`);
|
|
508
|
-
const dist = path.join(path.dirname(pkgJson), "dist");
|
|
509
|
-
return fs.existsSync(dist) ? dist : null;
|
|
510
|
-
} catch {
|
|
511
|
-
return null;
|
|
512
|
-
}
|
|
513
|
-
}
|
|
514
|
-
async function loadVfs() {
|
|
515
|
-
if (vfsCache) return vfsCache;
|
|
516
|
-
const vfs = /* @__PURE__ */ new Map();
|
|
517
|
-
const require_ = createRequire(import.meta.url);
|
|
518
|
-
const tsDir = path.dirname(require_.resolve("typescript/lib/typescript.js"));
|
|
519
|
-
const libFiles = fs.readdirSync(tsDir).filter((f) => /^lib\..*\.d\.ts$/.test(f));
|
|
520
|
-
for (const file of libFiles) {
|
|
521
|
-
const content = fs.readFileSync(path.join(tsDir, file), "utf-8");
|
|
522
|
-
parseAndStore(vfs, file, content);
|
|
523
|
-
}
|
|
524
|
-
const reactDir = findReactTypesDir();
|
|
525
|
-
if (reactDir) {
|
|
526
|
-
const reactDts = fs.readdirSync(reactDir).filter((f) => f.endsWith(".d.ts"));
|
|
527
|
-
for (const file of reactDts) {
|
|
528
|
-
const content = fs.readFileSync(path.join(reactDir, file), "utf-8");
|
|
529
|
-
parseAndStore(vfs, `node_modules/@types/react/${file}`, content);
|
|
530
|
-
}
|
|
531
|
-
}
|
|
532
|
-
const designDist = findPackageDistDir("@ggui-ai/design");
|
|
533
|
-
if (designDist) {
|
|
534
|
-
const dtsFiles = walkDir(designDist, (f) => f.endsWith(".d.ts") && !f.endsWith(".d.ts.map"));
|
|
535
|
-
for (const file of dtsFiles) {
|
|
536
|
-
const rel = path.relative(designDist, file);
|
|
537
|
-
const content = fs.readFileSync(file, "utf-8");
|
|
538
|
-
parseAndStore(vfs, `node_modules/@ggui-ai/design/${rel}`, content);
|
|
539
|
-
}
|
|
540
|
-
}
|
|
541
|
-
const wireDist = findPackageDistDir("@ggui-ai/wire");
|
|
542
|
-
if (wireDist) {
|
|
543
|
-
const dtsFiles = walkDir(wireDist, (f) => f.endsWith(".d.ts") && !f.endsWith(".d.ts.map"));
|
|
544
|
-
for (const file of dtsFiles) {
|
|
545
|
-
const rel = path.relative(wireDist, file);
|
|
546
|
-
const content = fs.readFileSync(file, "utf-8");
|
|
547
|
-
parseAndStore(vfs, `node_modules/@ggui-ai/wire/${rel}`, content);
|
|
548
|
-
}
|
|
549
|
-
}
|
|
550
|
-
const clientLibsDist = findPackageDistDir("@ggui-ai/gadgets");
|
|
551
|
-
if (clientLibsDist) {
|
|
552
|
-
const dtsFiles = walkDir(
|
|
553
|
-
clientLibsDist,
|
|
554
|
-
(f) => f.endsWith(".d.ts") && !f.endsWith(".d.ts.map")
|
|
555
|
-
);
|
|
556
|
-
for (const file of dtsFiles) {
|
|
557
|
-
const rel = path.relative(clientLibsDist, file);
|
|
558
|
-
const content = fs.readFileSync(file, "utf-8");
|
|
559
|
-
parseAndStore(vfs, `node_modules/@ggui-ai/gadgets/${rel}`, content);
|
|
560
|
-
}
|
|
561
|
-
}
|
|
562
|
-
vfsCache = vfs;
|
|
563
|
-
return vfs;
|
|
564
|
-
}
|
|
565
|
-
function resolveRelativeInVfs(vfs, containingFile, importPath) {
|
|
566
|
-
const normalized = containingFile.startsWith("/") ? containingFile.slice(1) : containingFile;
|
|
567
|
-
const dir = path.posix.dirname(normalized);
|
|
568
|
-
const specifier = importPath.replace(/\.(?:m|c)?js$/, "");
|
|
569
|
-
const resolved2 = path.posix.normalize(`${dir}/${specifier}`);
|
|
570
|
-
const withDts = `${resolved2}.d.ts`;
|
|
571
|
-
if (vfs.has(withDts)) return withDts;
|
|
572
|
-
const withIndexDts = `${resolved2}/index.d.ts`;
|
|
573
|
-
if (vfs.has(withIndexDts)) return withIndexDts;
|
|
574
|
-
const withTs = `${resolved2}.ts`;
|
|
575
|
-
if (vfs.has(withTs)) return withTs;
|
|
576
|
-
if (vfs.has(resolved2)) return resolved2;
|
|
577
|
-
return void 0;
|
|
578
|
-
}
|
|
579
|
-
var COMPILER_OPTIONS = {
|
|
580
|
-
target: ts4.ScriptTarget.ES2020,
|
|
581
|
-
module: ts4.ModuleKind.ESNext,
|
|
582
|
-
// Classic React JSX mode. The synthetic prefix (SYNTHETIC_PREFIX)
|
|
583
|
-
// supplies `import React from 'react'` plus a `declare global` JSX
|
|
584
|
-
// namespace shim: `@types/react` v19 removed the *global* `JSX`
|
|
585
|
-
// namespace (it lives at `React.JSX` now), which classic mode needs
|
|
586
|
-
// for `JSX.IntrinsicElements` and the `key`/`ref` carve-out. Without
|
|
587
|
-
// the shim, `<div>` degrades to `any` and every typed component
|
|
588
|
-
// falsely rejects the intrinsic `key` prop.
|
|
589
|
-
jsx: ts4.JsxEmit.React,
|
|
590
|
-
moduleResolution: ts4.ModuleResolutionKind.Bundler,
|
|
591
|
-
strict: false,
|
|
592
|
-
strictNullChecks: true,
|
|
593
|
-
// Catch undefined.foo() errors that cause runtime crashes
|
|
594
|
-
noEmit: true,
|
|
595
|
-
skipLibCheck: true,
|
|
596
|
-
noImplicitAny: true,
|
|
597
|
-
types: [],
|
|
598
|
-
esModuleInterop: true
|
|
599
|
-
};
|
|
600
|
-
function normalizeVfsPath(f) {
|
|
601
|
-
if (f.startsWith("/")) {
|
|
602
|
-
return f.slice(1);
|
|
603
|
-
}
|
|
604
|
-
return null;
|
|
605
|
-
}
|
|
606
|
-
function resolveModuleName(vfs, name, containingFile) {
|
|
607
|
-
if (name === "react") {
|
|
608
|
-
return resolved("node_modules/@types/react/index.d.ts");
|
|
609
|
-
}
|
|
610
|
-
if (name === "react/jsx-runtime") {
|
|
611
|
-
return resolved("node_modules/@types/react/jsx-runtime.d.ts");
|
|
612
|
-
}
|
|
613
|
-
if (name === "react/jsx-dev-runtime") {
|
|
614
|
-
const p = "node_modules/@types/react/jsx-dev-runtime.d.ts";
|
|
615
|
-
if (vfs.has(p)) return resolved(p);
|
|
616
|
-
return resolved("node_modules/@types/react/jsx-runtime.d.ts");
|
|
617
|
-
}
|
|
618
|
-
const designPrefix = "@ggui-ai/design/";
|
|
619
|
-
if (name.startsWith(designPrefix)) {
|
|
620
|
-
const subpath = name.slice(designPrefix.length);
|
|
621
|
-
const indexPath = `node_modules/@ggui-ai/design/${subpath}/index.d.ts`;
|
|
622
|
-
if (vfs.has(indexPath)) return resolved(indexPath);
|
|
623
|
-
const directPath = `node_modules/@ggui-ai/design/${subpath}.d.ts`;
|
|
624
|
-
if (vfs.has(directPath)) return resolved(directPath);
|
|
625
|
-
}
|
|
626
|
-
if (name === "@ggui-ai/design") {
|
|
627
|
-
return resolved("node_modules/@ggui-ai/design/index.d.ts");
|
|
628
|
-
}
|
|
629
|
-
if (name === "@ggui-ai/wire") {
|
|
630
|
-
return resolved("node_modules/@ggui-ai/wire/index.d.ts");
|
|
631
|
-
}
|
|
632
|
-
if (name === "@ggui-ai/gadgets") {
|
|
633
|
-
return resolved("node_modules/@ggui-ai/gadgets/index.d.ts");
|
|
634
|
-
}
|
|
635
|
-
if (name.startsWith("./") || name.startsWith("../")) {
|
|
636
|
-
const resolvedPath = resolveRelativeInVfs(vfs, containingFile, name);
|
|
637
|
-
if (resolvedPath) return resolved(resolvedPath);
|
|
638
|
-
}
|
|
639
|
-
const bareIndex = `node_modules/${name}/index.d.ts`;
|
|
640
|
-
if (vfs.has(bareIndex)) return resolved(bareIndex);
|
|
641
|
-
return { resolvedModule: void 0 };
|
|
642
|
-
}
|
|
643
|
-
function createVfsHost(vfs, componentCode) {
|
|
644
|
-
const componentFile = ts4.createSourceFile(
|
|
645
|
-
"Component.tsx",
|
|
646
|
-
componentCode,
|
|
647
|
-
ts4.ScriptTarget.ES2020,
|
|
648
|
-
true,
|
|
649
|
-
ts4.ScriptKind.TSX
|
|
650
|
-
);
|
|
651
|
-
const host = {
|
|
652
|
-
getSourceFile(fileName) {
|
|
653
|
-
if (fileName === "Component.tsx") return componentFile;
|
|
654
|
-
const entry = vfs.get(fileName);
|
|
655
|
-
if (entry) return entry.sourceFile;
|
|
656
|
-
const normalized = normalizeVfsPath(fileName);
|
|
657
|
-
if (normalized) return vfs.get(normalized)?.sourceFile;
|
|
658
|
-
return void 0;
|
|
659
|
-
},
|
|
660
|
-
getDefaultLibFileName() {
|
|
661
|
-
return "lib.es2020.full.d.ts";
|
|
662
|
-
},
|
|
663
|
-
writeFile() {
|
|
664
|
-
},
|
|
665
|
-
getCurrentDirectory() {
|
|
666
|
-
return "/";
|
|
667
|
-
},
|
|
668
|
-
getCanonicalFileName(f) {
|
|
669
|
-
return f;
|
|
670
|
-
},
|
|
671
|
-
useCaseSensitiveFileNames() {
|
|
672
|
-
return true;
|
|
673
|
-
},
|
|
674
|
-
getNewLine() {
|
|
675
|
-
return "\n";
|
|
676
|
-
},
|
|
677
|
-
fileExists(f) {
|
|
678
|
-
if (f === "Component.tsx" || vfs.has(f)) return true;
|
|
679
|
-
const normalized = normalizeVfsPath(f);
|
|
680
|
-
if (normalized && vfs.has(normalized)) return true;
|
|
681
|
-
return false;
|
|
682
|
-
},
|
|
683
|
-
readFile(f) {
|
|
684
|
-
if (f === "Component.tsx") return componentCode;
|
|
685
|
-
const direct = vfs.get(f);
|
|
686
|
-
if (direct) return direct.content;
|
|
687
|
-
const normalized = normalizeVfsPath(f);
|
|
688
|
-
if (normalized) return vfs.get(normalized)?.content;
|
|
689
|
-
return void 0;
|
|
690
|
-
},
|
|
691
|
-
resolveModuleNameLiterals(moduleLiterals, containingFile) {
|
|
692
|
-
return moduleLiterals.map(
|
|
693
|
-
(literal) => resolveModuleName(vfs, literal.text, containingFile)
|
|
694
|
-
);
|
|
695
|
-
}
|
|
696
|
-
};
|
|
697
|
-
return host;
|
|
698
|
-
}
|
|
699
|
-
function resolved(resolvedFileName) {
|
|
700
|
-
return {
|
|
701
|
-
resolvedModule: {
|
|
702
|
-
resolvedFileName,
|
|
703
|
-
isExternalLibraryImport: true,
|
|
704
|
-
extension: ts4.Extension.Dts
|
|
705
|
-
}
|
|
706
|
-
};
|
|
707
|
-
}
|
|
708
|
-
async function typecheck(code, dtsMap) {
|
|
709
|
-
const vfs = await loadVfs();
|
|
710
|
-
const effectiveVfs = (() => {
|
|
711
|
-
const dtsEntries = dtsMap !== void 0 ? Object.entries(dtsMap) : [];
|
|
712
|
-
if (dtsEntries.length === 0) {
|
|
713
|
-
return vfs;
|
|
714
|
-
}
|
|
715
|
-
const overlay = new Map(vfs);
|
|
716
|
-
for (const [pkg, content] of dtsEntries) {
|
|
717
|
-
parseAndStore(overlay, `node_modules/${pkg}/index.d.ts`, content);
|
|
718
|
-
}
|
|
719
|
-
return overlay;
|
|
720
|
-
})();
|
|
721
|
-
const globalJsxShim = "declare global { namespace JSX { type ElementType = string | ((props: any) => any) | (new (props: any) => any); interface Element { type: any; props: any; key: string | number | null; } interface ElementClass { render(): any; } interface ElementAttributesProperty { props: object; } interface ElementChildrenAttribute { children: object; } interface IntrinsicAttributes { key?: string | number | bigint | null; } interface IntrinsicClassAttributes<T> { ref?: any; } interface IntrinsicElements { [elem: string]: any; } } }\n";
|
|
722
|
-
const SYNTHETIC_PREFIX = "import React from 'react';\n" + globalJsxShim;
|
|
723
|
-
const prefixedCode = SYNTHETIC_PREFIX + code;
|
|
724
|
-
const lineOffset = SYNTHETIC_PREFIX.split("\n").length - 1;
|
|
725
|
-
const host = createVfsHost(effectiveVfs, prefixedCode);
|
|
726
|
-
const program = ts4.createProgram(["Component.tsx"], COMPILER_OPTIONS, host);
|
|
727
|
-
const diagnostics = ts4.getPreEmitDiagnostics(program);
|
|
728
|
-
const errors = [];
|
|
729
|
-
const warnings = [];
|
|
730
|
-
for (const diag of diagnostics) {
|
|
731
|
-
if (diag.file && diag.file.fileName !== "Component.tsx") continue;
|
|
732
|
-
if (!diag.file) continue;
|
|
733
|
-
const diagLine = ts4.getLineAndCharacterOfPosition(diag.file, diag.start ?? 0).line;
|
|
734
|
-
if (diagLine < lineOffset) continue;
|
|
735
|
-
const rawLine = diag.file ? ts4.getLineAndCharacterOfPosition(diag.file, diag.start ?? 0).line + 1 : 0;
|
|
736
|
-
const line = Math.max(1, rawLine - lineOffset);
|
|
737
|
-
const message = ts4.flattenDiagnosticMessageText(diag.messageText, "\n");
|
|
738
|
-
const code_ = diag.code;
|
|
739
|
-
const sourceLines = diag.file.text.split("\n");
|
|
740
|
-
const sourceLine = rawLine >= 1 && rawLine <= sourceLines.length ? sourceLines[rawLine - 1]?.trim() ?? "" : "";
|
|
741
|
-
const entry = {
|
|
742
|
-
code: code_,
|
|
743
|
-
line,
|
|
744
|
-
message,
|
|
745
|
-
fix: generateFix(code_, message, sourceLine)
|
|
746
|
-
};
|
|
747
|
-
if (BLOCKING_CODES.has(code_)) {
|
|
748
|
-
errors.push(entry);
|
|
749
|
-
} else {
|
|
750
|
-
warnings.push(entry);
|
|
751
|
-
}
|
|
752
|
-
}
|
|
753
|
-
return { errors, warnings };
|
|
754
|
-
}
|
|
755
|
-
var linterInstance = null;
|
|
756
|
-
async function getLinter() {
|
|
757
|
-
if (!linterInstance) {
|
|
758
|
-
linterInstance = new Linter();
|
|
759
|
-
const tsParser = await import('@typescript-eslint/parser');
|
|
760
|
-
linterInstance.defineParser("@typescript-eslint/parser", tsParser);
|
|
761
|
-
const reactHooksPlugin = await import('eslint-plugin-react-hooks');
|
|
762
|
-
const hooksRules = reactHooksPlugin.default?.rules ?? reactHooksPlugin.rules;
|
|
763
|
-
for (const [name, rule] of Object.entries(hooksRules)) {
|
|
764
|
-
linterInstance.defineRule(`react-hooks/${name}`, rule);
|
|
765
|
-
}
|
|
766
|
-
const reactPlugin = await import('eslint-plugin-react');
|
|
767
|
-
const reactRules = reactPlugin.default?.rules ?? reactPlugin.rules;
|
|
768
|
-
for (const [name, rule] of Object.entries(reactRules)) {
|
|
769
|
-
linterInstance.defineRule(`react/${name}`, rule);
|
|
770
|
-
}
|
|
771
|
-
}
|
|
772
|
-
return linterInstance;
|
|
773
|
-
}
|
|
774
|
-
async function lintReactHooks(code) {
|
|
775
|
-
const linter = await getLinter();
|
|
776
|
-
let messages;
|
|
777
|
-
try {
|
|
778
|
-
const config = {
|
|
779
|
-
parser: "@typescript-eslint/parser",
|
|
780
|
-
parserOptions: {
|
|
781
|
-
ecmaVersion: 2022,
|
|
782
|
-
sourceType: "module",
|
|
783
|
-
ecmaFeatures: { jsx: true }
|
|
784
|
-
},
|
|
785
|
-
rules: {
|
|
786
|
-
"react-hooks/rules-of-hooks": 2,
|
|
787
|
-
"react-hooks/exhaustive-deps": 1,
|
|
788
|
-
// React rules
|
|
789
|
-
"react/jsx-no-undef": 2,
|
|
790
|
-
// undefined JSX components
|
|
791
|
-
"react/jsx-key": 1,
|
|
792
|
-
// missing key in lists
|
|
793
|
-
"react/no-direct-mutation-state": 2,
|
|
794
|
-
// direct state mutation
|
|
795
|
-
// An unused `const submit = useAction('submit')` is a
|
|
796
|
-
// dead contract wire. Narrow the rule to variable bindings we
|
|
797
|
-
// care about: skip function args (LLMs legitimately omit unused
|
|
798
|
-
// params), skip destructured `rest` collectors, respect a `_`-
|
|
799
|
-
// prefix escape hatch for intentionally-ignored declarations,
|
|
800
|
-
// and silence the caught-error slot (async error handling can
|
|
801
|
-
// leave `catch (err)` unused at a boundary).
|
|
802
|
-
"no-unused-vars": [2, {
|
|
803
|
-
vars: "all",
|
|
804
|
-
args: "none",
|
|
805
|
-
ignoreRestSiblings: true,
|
|
806
|
-
varsIgnorePattern: "^_",
|
|
807
|
-
caughtErrors: "none"
|
|
808
|
-
}]
|
|
809
|
-
},
|
|
810
|
-
settings: {
|
|
811
|
-
react: { version: "19.0" }
|
|
812
|
-
}
|
|
813
|
-
};
|
|
814
|
-
messages = linter.verify(code, config, { filename: "component.tsx" });
|
|
815
|
-
} catch {
|
|
816
|
-
return [];
|
|
817
|
-
}
|
|
818
|
-
const diagnostics = [];
|
|
819
|
-
const ADMITTED_RULES = (id) => id.startsWith("react-hooks/") || id.startsWith("react/") || id === "no-unused-vars";
|
|
820
|
-
for (const msg of messages) {
|
|
821
|
-
if (!msg.ruleId) continue;
|
|
822
|
-
if (!ADMITTED_RULES(msg.ruleId)) continue;
|
|
823
|
-
diagnostics.push({
|
|
824
|
-
rule: msg.ruleId,
|
|
825
|
-
line: msg.line,
|
|
826
|
-
message: msg.message,
|
|
827
|
-
fix: generateReactFix(msg.ruleId, msg.message),
|
|
828
|
-
severity: resolveSeverity(msg, code)
|
|
829
|
-
});
|
|
830
|
-
}
|
|
831
|
-
return diagnostics;
|
|
832
|
-
}
|
|
833
|
-
function resolveSeverity(msg, code) {
|
|
834
|
-
const base = msg.severity === 2 ? "error" : "warning";
|
|
835
|
-
if (msg.ruleId !== "no-unused-vars" || base !== "error") return base;
|
|
836
|
-
const name = msg.message.match(/'([^']+)'/)?.[1];
|
|
837
|
-
if (name === void 0) return "warning";
|
|
838
|
-
const safe = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
839
|
-
const directRe = new RegExp(
|
|
840
|
-
`\\bconst\\s+${safe}\\s*=\\s*use(Action|Stream|GguiContext)\\s*[<(]`
|
|
841
|
-
);
|
|
842
|
-
const destructuredRe = new RegExp(
|
|
843
|
-
`\\bconst\\s+\\[[^\\]]*\\b${safe}\\b[^\\]]*\\]\\s*=\\s*use(Action|Stream|GguiContext)\\s*[<(]`
|
|
844
|
-
);
|
|
845
|
-
return directRe.test(code) || destructuredRe.test(code) ? "error" : "warning";
|
|
846
|
-
}
|
|
847
|
-
function generateReactFix(ruleId, message) {
|
|
848
|
-
if (ruleId === "react-hooks/rules-of-hooks") {
|
|
849
|
-
if (message.includes("called conditionally")) {
|
|
850
|
-
return "Move this hook to the top level of the component, before any early returns or conditionals. Hooks must run in the same order every render.";
|
|
851
|
-
}
|
|
852
|
-
return "Hooks can only be called at the top level of a React function component or custom hook. Move it out of any conditional, loop, nested function, or callback.";
|
|
853
|
-
}
|
|
854
|
-
if (ruleId === "react-hooks/exhaustive-deps") {
|
|
855
|
-
return "Add the missing dependencies to the dependency array, or remove the array to run on every render.";
|
|
856
|
-
}
|
|
857
|
-
if (ruleId === "react/jsx-no-undef") {
|
|
858
|
-
const match = message.match(/'(\w+)'/);
|
|
859
|
-
const name = match?.[1] ?? "Component";
|
|
860
|
-
return `'${name}' is not defined. Import it from the design system or define it in the file.`;
|
|
861
|
-
}
|
|
862
|
-
if (ruleId === "react/jsx-key") {
|
|
863
|
-
return 'Add a unique "key" prop to each element rendered inside a .map() or iterator.';
|
|
864
|
-
}
|
|
865
|
-
if (ruleId === "react/no-direct-mutation-state") {
|
|
866
|
-
return "Do not mutate state directly. Use setState or the setter from useState instead.";
|
|
867
|
-
}
|
|
868
|
-
if (ruleId === "no-unused-vars") {
|
|
869
|
-
const match = message.match(/'([^']+)'/);
|
|
870
|
-
const name = match?.[1] ?? "variable";
|
|
871
|
-
const isLikelyWireBinding = /\bconst\s+[A-Za-z_$][\w$]*\s*=\s*use(Action|Stream)\s*\(/.test(message);
|
|
872
|
-
if (isLikelyWireBinding || /^(submit|cancel|search|progress|snapshot)$/.test(name)) {
|
|
873
|
-
return `'${name}' is declared but never used. If this binding came from a wire hook (useAction / useStream) or a clientCapabilities hook (useGeolocation / useCamera / etc. from @ggui-ai/gadgets), consume it somewhere in the component \u2014 render its value in JSX, bind it to a callback prop, or use it in an effect. A contract-declared hook without consumption is a dead wire.`;
|
|
874
|
-
}
|
|
875
|
-
return `'${name}' is declared but never used. Remove the declaration, or consume it somewhere in the component. Prefix with '_' (e.g. '_${name}') to mark it intentionally unused.`;
|
|
876
|
-
}
|
|
877
|
-
return "Fix the React violation.";
|
|
878
|
-
}
|
|
879
|
-
|
|
880
|
-
// src/design-mode.ts
|
|
881
|
-
var DEFAULT_DESIGN_MODE = "constrained";
|
|
882
|
-
var NOT_YET_TAUGHT_TOKENS = /* @__PURE__ */ new Set([]);
|
|
883
|
-
var taughtTokenManifest = consumedTokenManifest.filter(
|
|
884
|
-
(token) => !NOT_YET_TAUGHT_TOKENS.has(token)
|
|
885
|
-
);
|
|
886
|
-
|
|
887
|
-
// src/evaluation/types-public.ts
|
|
888
|
-
function priorityForIssue(category) {
|
|
889
|
-
if (category === "interactivity" || category === "accessibility" || category === "layout" || category === "loading" || category === "visual") {
|
|
890
|
-
return "P2";
|
|
891
|
-
}
|
|
892
|
-
if (category === "tokens" || category === "crash" || category === "functionality") {
|
|
893
|
-
return "P1";
|
|
894
|
-
}
|
|
895
|
-
return "P0";
|
|
896
|
-
}
|
|
897
|
-
|
|
898
|
-
// src/check/run-tier0.ts
|
|
899
|
-
function stripComments(code) {
|
|
900
|
-
const scanner = ts4.createScanner(
|
|
901
|
-
ts4.ScriptTarget.Latest,
|
|
902
|
-
/* skipTrivia */
|
|
903
|
-
false,
|
|
904
|
-
ts4.LanguageVariant.JSX,
|
|
905
|
-
code
|
|
906
|
-
);
|
|
907
|
-
let out = "";
|
|
908
|
-
for (let token = scanner.scan(); token !== ts4.SyntaxKind.EndOfFileToken; token = scanner.scan()) {
|
|
909
|
-
const text = scanner.getTokenText();
|
|
910
|
-
out += token === ts4.SyntaxKind.SingleLineCommentTrivia || token === ts4.SyntaxKind.MultiLineCommentTrivia ? text.replace(/[^\n]/g, " ") : text;
|
|
911
|
-
}
|
|
912
|
-
return out;
|
|
913
|
-
}
|
|
914
|
-
function detectCertainDoubleWiredActions(sourceCode, actionBindings, designMode = DEFAULT_DESIGN_MODE) {
|
|
915
|
-
const issues = [];
|
|
916
|
-
if (actionBindings.length === 0) return issues;
|
|
917
|
-
const bindings = new Set(actionBindings);
|
|
918
|
-
const anyElementIsInteractive = designMode === "free";
|
|
919
|
-
const TRAIT_HOST_TAGS = /* @__PURE__ */ new Set(["Card", "Box", "Stack", "Row"]);
|
|
920
|
-
const TRAITS_THAT_FIRE = /* @__PURE__ */ new Set(["Clickable", "Pressable"]);
|
|
921
|
-
const INTERACTIVE_DESCENDANTS = /* @__PURE__ */ new Set([
|
|
922
|
-
"Button",
|
|
923
|
-
"Checkbox",
|
|
924
|
-
"Input",
|
|
925
|
-
"Toggle",
|
|
926
|
-
"Slider",
|
|
927
|
-
"RadioGroup",
|
|
928
|
-
"Select",
|
|
929
|
-
"TextArea",
|
|
930
|
-
"Link"
|
|
931
|
-
]);
|
|
932
|
-
const HANDLER_ATTRS = ["onClick", "onChange", "onPress", "onSelect"];
|
|
933
|
-
const sf = ts4.createSourceFile(
|
|
934
|
-
"source.tsx",
|
|
935
|
-
sourceCode,
|
|
936
|
-
ts4.ScriptTarget.Latest,
|
|
937
|
-
/* setParentNodes */
|
|
938
|
-
true,
|
|
939
|
-
ts4.ScriptKind.TSX
|
|
940
|
-
);
|
|
941
|
-
function tagNameText(node) {
|
|
942
|
-
return ts4.isIdentifier(node.tagName) ? node.tagName.text : void 0;
|
|
943
|
-
}
|
|
944
|
-
function attrExpression(node, attrName) {
|
|
945
|
-
for (const attr of node.attributes.properties) {
|
|
946
|
-
if (ts4.isJsxAttribute(attr) && ts4.isIdentifier(attr.name) && attr.name.text === attrName && attr.initializer !== void 0 && ts4.isJsxExpression(attr.initializer)) {
|
|
947
|
-
return attr.initializer.expression;
|
|
948
|
-
}
|
|
949
|
-
}
|
|
950
|
-
return void 0;
|
|
951
|
-
}
|
|
952
|
-
function isTraitHostWithFiringAs(node) {
|
|
953
|
-
const tag = tagNameText(node);
|
|
954
|
-
if (tag === void 0) return false;
|
|
955
|
-
if (anyElementIsInteractive) return true;
|
|
956
|
-
if (!TRAIT_HOST_TAGS.has(tag)) return false;
|
|
957
|
-
const asExpr = attrExpression(node, "as");
|
|
958
|
-
return asExpr !== void 0 && ts4.isIdentifier(asExpr) && TRAITS_THAT_FIRE.has(asExpr.text);
|
|
959
|
-
}
|
|
960
|
-
function extractCalleeName(expr) {
|
|
961
|
-
if (expr === void 0) return void 0;
|
|
962
|
-
if (ts4.isIdentifier(expr)) return expr.text;
|
|
963
|
-
if (ts4.isArrowFunction(expr) || ts4.isFunctionExpression(expr)) {
|
|
964
|
-
const body = expr.body;
|
|
965
|
-
if (ts4.isCallExpression(body) && ts4.isIdentifier(body.expression)) {
|
|
966
|
-
return body.expression.text;
|
|
967
|
-
}
|
|
968
|
-
if (ts4.isBlock(body)) {
|
|
969
|
-
for (const stmt of body.statements) {
|
|
970
|
-
if (ts4.isExpressionStatement(stmt) && ts4.isCallExpression(stmt.expression) && ts4.isIdentifier(stmt.expression.expression)) {
|
|
971
|
-
return stmt.expression.expression.text;
|
|
972
|
-
}
|
|
973
|
-
}
|
|
974
|
-
}
|
|
975
|
-
}
|
|
976
|
-
return void 0;
|
|
977
|
-
}
|
|
978
|
-
function findMatchingInteractiveDescendant(outer, expectedBinding) {
|
|
979
|
-
let found;
|
|
980
|
-
function walk(node) {
|
|
981
|
-
if (found !== void 0) return;
|
|
982
|
-
const opening = ts4.isJsxElement(node) ? node.openingElement : ts4.isJsxSelfClosingElement(node) ? node : void 0;
|
|
983
|
-
if (opening !== void 0) {
|
|
984
|
-
const tag = tagNameText(opening);
|
|
985
|
-
if (tag !== void 0 && (anyElementIsInteractive || INTERACTIVE_DESCENDANTS.has(tag))) {
|
|
986
|
-
for (const attrName of HANDLER_ATTRS) {
|
|
987
|
-
const callee = extractCalleeName(attrExpression(opening, attrName));
|
|
988
|
-
if (callee === expectedBinding) {
|
|
989
|
-
found = {
|
|
990
|
-
tag,
|
|
991
|
-
line: sf.getLineAndCharacterOfPosition(opening.getStart()).line + 1
|
|
992
|
-
};
|
|
993
|
-
return;
|
|
994
|
-
}
|
|
995
|
-
}
|
|
996
|
-
}
|
|
997
|
-
}
|
|
998
|
-
ts4.forEachChild(node, walk);
|
|
999
|
-
}
|
|
1000
|
-
ts4.forEachChild(outer, walk);
|
|
1001
|
-
return found;
|
|
1002
|
-
}
|
|
1003
|
-
function visit(node) {
|
|
1004
|
-
if (ts4.isJsxElement(node) && isTraitHostWithFiringAs(node.openingElement)) {
|
|
1005
|
-
const outerCallee = extractCalleeName(attrExpression(node.openingElement, "onClick")) ?? extractCalleeName(attrExpression(node.openingElement, "onPress"));
|
|
1006
|
-
if (outerCallee !== void 0 && bindings.has(outerCallee)) {
|
|
1007
|
-
const match = findMatchingInteractiveDescendant(node, outerCallee);
|
|
1008
|
-
if (match !== void 0) {
|
|
1009
|
-
const outerTag = tagNameText(node.openingElement);
|
|
1010
|
-
const outerLine = sf.getLineAndCharacterOfPosition(node.openingElement.getStart()).line + 1;
|
|
1011
|
-
const outerLabel = anyElementIsInteractive ? `<${outerTag}>` : `<${outerTag} as={...}>`;
|
|
1012
|
-
issues.push({
|
|
1013
|
-
tier: 0,
|
|
1014
|
-
result: "fail",
|
|
1015
|
-
category: "interactivity",
|
|
1016
|
-
subcategory: "double-wired-action:certain",
|
|
1017
|
-
severity: "critical",
|
|
1018
|
-
description: `Nested-interactive double-wire: outer ${outerLabel} at line ${outerLine} and inner <${match.tag}> at line ${match.line} both dispatch the same useAction binding '${outerCallee}'. One user click on the inner control fires its handler AND bubbles to the outer handler \u2014 '${outerCallee}' dispatches TWICE, the action runs back-to-back, and a toggle-style action silently reverts the user's change.`,
|
|
1019
|
-
fix: anyElementIsInteractive ? `Pick ONE gesture surface for '${outerCallee}': either remove the onClick from the outer <${outerTag}> and let the inner <${match.tag}> own the gesture, OR drop the inner <${match.tag}>'s handler and let the outer <${outerTag}> own it. Never wire both to the same useAction binding \u2014 stopPropagation is not a fix.` : `Pick ONE surface for the gesture: either drop \`as={...}\` + onClick on the outer <${outerTag}> and let the inner <${match.tag}> own the gesture, OR remove the inner <${match.tag}> and let the outer <${outerTag} as={...}> own it. Don't wire both to the same useAction binding.`,
|
|
1020
|
-
line: outerLine
|
|
1021
|
-
});
|
|
1022
|
-
}
|
|
1023
|
-
}
|
|
1024
|
-
}
|
|
1025
|
-
ts4.forEachChild(node, visit);
|
|
1026
|
-
}
|
|
1027
|
-
visit(sf);
|
|
1028
|
-
return issues;
|
|
1029
|
-
}
|
|
1030
|
-
var MANIFEST_TOKENS = new Set(consumedTokenManifest);
|
|
1031
|
-
var RETIRED_TOKEN_SUCCESSORS = /* @__PURE__ */ new Map([
|
|
1032
|
-
["--ggui-color-surface", "--ggui-color-container"],
|
|
1033
|
-
["--ggui-color-onSurface", "--ggui-color-onContainer"],
|
|
1034
|
-
["--ggui-color-surfaceVariant", "--ggui-color-sunken"],
|
|
1035
|
-
["--ggui-color-onSurfaceVariant", "--ggui-color-onSunken"]
|
|
1036
|
-
]);
|
|
1037
|
-
function suggestManifestTokens(name) {
|
|
1038
|
-
const successor = RETIRED_TOKEN_SUCCESSORS.get(name);
|
|
1039
|
-
if (successor !== void 0) return [successor];
|
|
1040
|
-
const segments = name.split("-").filter((segment) => segment.length > 0);
|
|
1041
|
-
const own = name.endsWith("-") ? segments.length : segments.length - 1;
|
|
1042
|
-
for (let n = own; n >= 2; n--) {
|
|
1043
|
-
const prefix = `--${segments.slice(0, n).join("-")}-`;
|
|
1044
|
-
const family = taughtTokenManifest.filter((token) => token.startsWith(prefix));
|
|
1045
|
-
if (family.length > 0) return wordHitsFirst(name, rolesFirst(family));
|
|
1046
|
-
}
|
|
1047
|
-
const words = new Set(
|
|
1048
|
-
segments.slice(1).filter((segment) => segment.length >= 4).map((segment) => segment.toLowerCase())
|
|
1049
|
-
);
|
|
1050
|
-
return rolesFirst(
|
|
1051
|
-
taughtTokenManifest.filter(
|
|
1052
|
-
(token) => token.split("-").some((segment) => words.has(segment.toLowerCase()))
|
|
1053
|
-
)
|
|
1054
|
-
);
|
|
1055
|
-
}
|
|
1056
|
-
function wordHitsFirst(name, family) {
|
|
1057
|
-
const word = name.slice(name.lastIndexOf("-") + 1).toLowerCase();
|
|
1058
|
-
if (word.length < 4) return family;
|
|
1059
|
-
const hit = (token) => token.slice(token.lastIndexOf("-") + 1).toLowerCase().includes(word);
|
|
1060
|
-
return [...family.filter(hit), ...family.filter((token) => !hit(token))];
|
|
1061
|
-
}
|
|
1062
|
-
function rolesFirst(tokens) {
|
|
1063
|
-
const isRole = (token) => !/^\d+$/.test(token.slice(token.lastIndexOf("-") + 1));
|
|
1064
|
-
return [...tokens.filter(isRole), ...tokens.filter((token) => !isRole(token))];
|
|
1065
|
-
}
|
|
1066
|
-
function offManifestHint(name) {
|
|
1067
|
-
const successor = RETIRED_TOKEN_SUCCESSORS.get(name);
|
|
1068
|
-
if (successor !== void 0) {
|
|
1069
|
-
return `"${name}" was retired (ggui#987); the current name is var(${successor}). `;
|
|
1070
|
-
}
|
|
1071
|
-
const near = suggestManifestTokens(name);
|
|
1072
|
-
if (near.length === 0) return "";
|
|
1073
|
-
const shown = near.slice(0, 8).map((token) => `var(${token})`).join(", ");
|
|
1074
|
-
const more = near.length > 8 ? ` (and ${near.length - 8} more in that family)` : "";
|
|
1075
|
-
return `Nearest names in the manifest: ${shown}${more}. `;
|
|
1076
|
-
}
|
|
1077
|
-
var TOKEN_REF_RE = /var\(\s*(--ggui-[a-zA-Z0-9-]+)/g;
|
|
1078
|
-
var HOOK_NAME_FOR = {
|
|
1079
|
-
action: "useAction",
|
|
1080
|
-
stream: "useStream",
|
|
1081
|
-
context: "useGguiContext"
|
|
1082
|
-
};
|
|
1083
|
-
var CONTRACT_FIELD_FOR = {
|
|
1084
|
-
action: "actionSpec",
|
|
1085
|
-
stream: "streamSpec",
|
|
1086
|
-
context: "contextSpec"
|
|
1087
|
-
};
|
|
1088
|
-
function isGadgetExportImported(source, pkg, exportName) {
|
|
1089
|
-
const esc = pkg.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1090
|
-
const re = new RegExp(
|
|
1091
|
-
`import\\s*(?:[A-Za-z_$][\\w$]*\\s*,\\s*)?\\{([^}]*)\\}\\s*from\\s*['"]${esc}['"]`,
|
|
1092
|
-
"g"
|
|
1093
|
-
);
|
|
1094
|
-
let m;
|
|
1095
|
-
while ((m = re.exec(source)) !== null) {
|
|
1096
|
-
for (const raw of m[1].split(",")) {
|
|
1097
|
-
if (raw.trim().split(/\s+as\s+/)[0]?.trim() === exportName) return true;
|
|
1098
|
-
}
|
|
1099
|
-
}
|
|
1100
|
-
return false;
|
|
1101
|
-
}
|
|
1102
|
-
function detectComponentNeverReturns(sourceCode) {
|
|
1103
|
-
const sf = ts4.createSourceFile(
|
|
1104
|
-
"source.tsx",
|
|
1105
|
-
sourceCode,
|
|
1106
|
-
ts4.ScriptTarget.Latest,
|
|
1107
|
-
/* setParentNodes */
|
|
1108
|
-
true,
|
|
1109
|
-
ts4.ScriptKind.TSX
|
|
1110
|
-
);
|
|
1111
|
-
const isFunctionLike = (n) => ts4.isFunctionDeclaration(n) || ts4.isFunctionExpression(n) || ts4.isArrowFunction(n) || ts4.isMethodDeclaration(n) || ts4.isClassDeclaration(n) || ts4.isClassExpression(n);
|
|
1112
|
-
function resolveTopLevel(name) {
|
|
1113
|
-
for (const st of sf.statements) {
|
|
1114
|
-
if (ts4.isFunctionDeclaration(st) && st.name?.text === name) return st;
|
|
1115
|
-
if (ts4.isVariableStatement(st)) {
|
|
1116
|
-
for (const decl of st.declarationList.declarations) {
|
|
1117
|
-
if (ts4.isIdentifier(decl.name) && decl.name.text === name && decl.initializer !== void 0 && (ts4.isArrowFunction(decl.initializer) || ts4.isFunctionExpression(decl.initializer))) {
|
|
1118
|
-
return decl.initializer;
|
|
1119
|
-
}
|
|
1120
|
-
}
|
|
1121
|
-
}
|
|
1122
|
-
}
|
|
1123
|
-
return void 0;
|
|
1124
|
-
}
|
|
1125
|
-
let component;
|
|
1126
|
-
for (const st of sf.statements) {
|
|
1127
|
-
if (ts4.isFunctionDeclaration(st) && st.modifiers?.some((m) => m.kind === ts4.SyntaxKind.DefaultKeyword)) {
|
|
1128
|
-
component = st;
|
|
1129
|
-
break;
|
|
1130
|
-
}
|
|
1131
|
-
if (ts4.isExportAssignment(st) && st.isExportEquals !== true) {
|
|
1132
|
-
const expr = st.expression;
|
|
1133
|
-
if (ts4.isArrowFunction(expr) || ts4.isFunctionExpression(expr)) component = expr;
|
|
1134
|
-
else if (ts4.isIdentifier(expr)) component = resolveTopLevel(expr.text);
|
|
1135
|
-
break;
|
|
1136
|
-
}
|
|
1137
|
-
}
|
|
1138
|
-
if (component === void 0) return null;
|
|
1139
|
-
const body = component.body;
|
|
1140
|
-
if (body === void 0 || !ts4.isBlock(body)) return null;
|
|
1141
|
-
let returnsValue = false;
|
|
1142
|
-
let jsxStatementLine;
|
|
1143
|
-
const visit = (node) => {
|
|
1144
|
-
if (returnsValue) return;
|
|
1145
|
-
if (ts4.isReturnStatement(node)) {
|
|
1146
|
-
if (node.expression !== void 0) returnsValue = true;
|
|
1147
|
-
return;
|
|
1148
|
-
}
|
|
1149
|
-
if (jsxStatementLine === void 0 && ts4.isExpressionStatement(node) && (ts4.isJsxElement(node.expression) || ts4.isJsxFragment(node.expression) || ts4.isJsxSelfClosingElement(node.expression))) {
|
|
1150
|
-
jsxStatementLine = sf.getLineAndCharacterOfPosition(node.getStart(sf)).line + 1;
|
|
1151
|
-
}
|
|
1152
|
-
node.forEachChild((child) => {
|
|
1153
|
-
if (!isFunctionLike(child)) visit(child);
|
|
1154
|
-
});
|
|
1155
|
-
};
|
|
1156
|
-
for (const st of body.statements) if (!isFunctionLike(st)) visit(st);
|
|
1157
|
-
return returnsValue ? null : { jsxStatementLine };
|
|
1158
|
-
}
|
|
1159
|
-
var KIT_CONTROLS = ["Button", "Input", "Select", "TextArea", "Checkbox", "RadioGroup", "Toggle", "SearchField"];
|
|
1160
|
-
function kitControlTags(source) {
|
|
1161
|
-
const out = [];
|
|
1162
|
-
const open = new RegExp(`<(${KIT_CONTROLS.join("|")})\\b`, "g");
|
|
1163
|
-
for (const m of source.matchAll(open)) {
|
|
1164
|
-
const start = (m.index ?? 0) + m[0].length;
|
|
1165
|
-
let depth = 0;
|
|
1166
|
-
let quote = null;
|
|
1167
|
-
let i = start;
|
|
1168
|
-
for (; i < source.length; i += 1) {
|
|
1169
|
-
const c = source[i];
|
|
1170
|
-
if (quote !== null) {
|
|
1171
|
-
if (c === quote && source[i - 1] !== "\\") quote = null;
|
|
1172
|
-
} else if (c === '"' || c === "'" || c === "`") quote = c;
|
|
1173
|
-
else if (c === "{") depth += 1;
|
|
1174
|
-
else if (c === "}") depth -= 1;
|
|
1175
|
-
else if (c === ">" && depth === 0) break;
|
|
1176
|
-
}
|
|
1177
|
-
out.push({ name: m[1] ?? "", attrs: source.slice(start, i), line: source.slice(0, m.index ?? 0).split("\n").length });
|
|
1178
|
-
}
|
|
1179
|
-
return out;
|
|
1180
|
-
}
|
|
1181
|
-
function inlineBorderKey(attrs, source) {
|
|
1182
|
-
const BORDER = /\b(border(?:Top|Right|Bottom|Left)?(?:Color|Width|Style)?)\s*:/;
|
|
1183
|
-
const style = attrs.match(/\bstyle=\{\{([\s\S]*)\}\}/);
|
|
1184
|
-
if (style !== null) {
|
|
1185
|
-
const hit2 = (style[1] ?? "").match(BORDER);
|
|
1186
|
-
if (hit2 !== null) return hit2[1] ?? null;
|
|
1187
|
-
}
|
|
1188
|
-
const named = attrs.match(/\bstyle=\{\s*([A-Za-z_$][\w$]*)\s*\}/);
|
|
1189
|
-
if (named === null) return null;
|
|
1190
|
-
const decl = source.match(new RegExp(`\\b(?:const|let)\\s+${named[1]}\\b[^=]*=\\s*\\{([^{}]*)\\}`));
|
|
1191
|
-
const hit = decl !== null ? (decl[1] ?? "").match(BORDER) : null;
|
|
1192
|
-
return hit !== null ? hit[1] ?? null : null;
|
|
1193
|
-
}
|
|
1194
|
-
async function runTier0Checks(sourceCode, contract, buildErrors, gadgetTypes, designMode = DEFAULT_DESIGN_MODE) {
|
|
1195
|
-
const issues = [];
|
|
1196
|
-
const lines = sourceCode.split("\n");
|
|
1197
|
-
const enforceDesignVocabulary = designMode === "constrained";
|
|
1198
|
-
if (buildErrors && buildErrors.length > 0) {
|
|
1199
|
-
const errorDetail = buildErrors.map((e) => {
|
|
1200
|
-
const match = e.match(/<stdin>:(\d+):\d+: ERROR: (.+)/);
|
|
1201
|
-
return match ? `Line ${match[1]}: ${match[2]}` : e.slice(0, 200);
|
|
1202
|
-
}).join("\n");
|
|
1203
|
-
issues.push({
|
|
1204
|
-
tier: 0,
|
|
1205
|
-
result: "fail",
|
|
1206
|
-
category: "compile",
|
|
1207
|
-
subcategory: "build-error",
|
|
1208
|
-
severity: "critical",
|
|
1209
|
-
description: `Component failed to compile:
|
|
1210
|
-
${errorDetail}`,
|
|
1211
|
-
fix: "Fix the JSX/TypeScript syntax errors listed above"
|
|
1212
|
-
});
|
|
1213
|
-
}
|
|
1214
|
-
const assetEscapedLines = /* @__PURE__ */ new Set();
|
|
1215
|
-
const jsxOpenRegex = /<([A-Z][A-Za-z0-9]*)\b([^>]*)>/gs;
|
|
1216
|
-
for (const match of sourceCode.matchAll(jsxOpenRegex)) {
|
|
1217
|
-
const tagAttrs = match[2];
|
|
1218
|
-
const assetColorAttr = tagAttrs.match(/\bassetColor\s*=\s*(?:["']([^"']*)["']|\{[^}]*\})/);
|
|
1219
|
-
if (!assetColorAttr) continue;
|
|
1220
|
-
const assetSemanticAttr = tagAttrs.match(/\bassetSemantic\s*=\s*["']([^"']*)["']/);
|
|
1221
|
-
const startLine = sourceCode.slice(0, match.index ?? 0).split("\n").length;
|
|
1222
|
-
const tagLineCount = match[0].split("\n").length;
|
|
1223
|
-
if (!assetSemanticAttr || assetSemanticAttr[1].length === 0) {
|
|
1224
|
-
issues.push({
|
|
1225
|
-
tier: 0,
|
|
1226
|
-
result: "fail",
|
|
1227
|
-
category: "tokens",
|
|
1228
|
-
subcategory: "asset-color-pair",
|
|
1229
|
-
severity: "critical",
|
|
1230
|
-
description: "Box `assetColor` set without a non-empty `assetSemantic` \u2014 the typed brand-color escape requires both. `assetSemantic` is a human-readable label that documents why this color bypasses the theme.",
|
|
1231
|
-
fix: 'Pair the `assetColor` with a non-empty `assetSemantic` literal, e.g. `<Box assetColor="#635BFF" assetSemantic="stripe-brand-purple">`. Reach for `surface="..."` first whenever the color SHOULD track the operator\'s theme.',
|
|
1232
|
-
line: startLine
|
|
1233
|
-
});
|
|
1234
|
-
continue;
|
|
1235
|
-
}
|
|
1236
|
-
for (let l = 0; l < tagLineCount; l++) {
|
|
1237
|
-
assetEscapedLines.add(startLine + l);
|
|
1238
|
-
}
|
|
1239
|
-
}
|
|
1240
|
-
const allowedGadgetPackages = new Set(
|
|
1241
|
-
Object.keys(contract?.clientCapabilities?.gadgets ?? {})
|
|
1242
|
-
);
|
|
1243
|
-
for (let i = 0; i < lines.length; i++) {
|
|
1244
|
-
const line = lines[i];
|
|
1245
|
-
const lineNum = i + 1;
|
|
1246
|
-
if (/\beval\s*\(/.test(line)) {
|
|
1247
|
-
issues.push({
|
|
1248
|
-
tier: 0,
|
|
1249
|
-
result: "fail",
|
|
1250
|
-
category: "security",
|
|
1251
|
-
subcategory: "eval",
|
|
1252
|
-
severity: "critical",
|
|
1253
|
-
description: "eval() is forbidden",
|
|
1254
|
-
fix: "Remove eval() call entirely",
|
|
1255
|
-
line: lineNum
|
|
1256
|
-
});
|
|
1257
|
-
}
|
|
1258
|
-
if (/\bfetch\s*\(/.test(line)) {
|
|
1259
|
-
issues.push({
|
|
1260
|
-
tier: 0,
|
|
1261
|
-
result: "fail",
|
|
1262
|
-
category: "security",
|
|
1263
|
-
subcategory: "fetch",
|
|
1264
|
-
severity: "critical",
|
|
1265
|
-
description: "fetch() is forbidden \u2014 use props for data",
|
|
1266
|
-
fix: "Remove fetch() and pass data via props",
|
|
1267
|
-
line: lineNum
|
|
1268
|
-
});
|
|
1269
|
-
}
|
|
1270
|
-
const trimmedLine = line.trim();
|
|
1271
|
-
if (trimmedLine.startsWith("//") || trimmedLine.startsWith("*")) continue;
|
|
1272
|
-
const importMatch = trimmedLine.match(/import\s+.*from\s+['"]([^'"]+)['"]/);
|
|
1273
|
-
if (importMatch) {
|
|
1274
|
-
const pkg = importMatch[1];
|
|
1275
|
-
if (!isAllowedImport(pkg, allowedGadgetPackages)) {
|
|
1276
|
-
issues.push({
|
|
1277
|
-
tier: 0,
|
|
1278
|
-
result: "fail",
|
|
1279
|
-
category: "imports",
|
|
1280
|
-
subcategory: "disallowed-package",
|
|
1281
|
-
severity: "critical",
|
|
1282
|
-
description: `Import from "${pkg}" is not allowed`,
|
|
1283
|
-
fix: `Only import from: ${describeAllowedImports()}`,
|
|
1284
|
-
line: lineNum
|
|
1285
|
-
});
|
|
1286
|
-
}
|
|
1287
|
-
}
|
|
1288
|
-
const literalFallbackMatch = line.match(
|
|
1289
|
-
/var\(\s*(--ggui-[a-zA-Z0-9-]+)\s*,\s*([^)]+)\)/
|
|
1290
|
-
);
|
|
1291
|
-
const literalFallback = literalFallbackMatch !== null && !literalFallbackMatch[2].trim().startsWith("var(") ? { token: literalFallbackMatch[1], literal: literalFallbackMatch[2].trim() } : null;
|
|
1292
|
-
if (literalFallback !== null && !assetEscapedLines.has(lineNum)) {
|
|
1293
|
-
issues.push({
|
|
1294
|
-
tier: 0,
|
|
1295
|
-
result: "fail",
|
|
1296
|
-
category: "tokens",
|
|
1297
|
-
subcategory: "token-fallback",
|
|
1298
|
-
severity: "critical",
|
|
1299
|
-
description: `Literal fallback "${literalFallback.literal}" inside a token reference \u2014 the runtime injects every token, and the fallback paints the wrong value exactly when the theme matters most.`,
|
|
1300
|
-
fix: `Drop the fallback: write var(${literalFallback.token}) bare. If the token might not exist, that is a vocabulary problem to fix by choosing a token from the design-system docs, never by a literal.`,
|
|
1301
|
-
line: lineNum
|
|
1302
|
-
});
|
|
1303
|
-
}
|
|
1304
|
-
const hexMatch = line.match(/#[0-9a-fA-F]{3,8}\b/);
|
|
1305
|
-
if (hexMatch && !assetEscapedLines.has(lineNum)) {
|
|
1306
|
-
if (literalFallback !== null && literalFallback.literal.includes(hexMatch[0])) ; else if (!line.includes("// fallback")) {
|
|
1307
|
-
issues.push({
|
|
1308
|
-
tier: 0,
|
|
1309
|
-
result: "fail",
|
|
1310
|
-
category: "tokens",
|
|
1311
|
-
subcategory: "hex-color",
|
|
1312
|
-
severity: "critical",
|
|
1313
|
-
description: `Hardcoded color "${hexMatch[0]}" breaks theme switching \u2014 use design tokens.`,
|
|
1314
|
-
fix: `Replace with a primitive variant (Button variant="primary"|Badge variant="success"|...) OR a bare token reference like var(--ggui-color-primary-500) \u2014 never a literal fallback. Hardcoded colors mean the operator's theme has no effect on this surface.`,
|
|
1315
|
-
line: lineNum
|
|
1316
|
-
});
|
|
1317
|
-
}
|
|
1318
|
-
}
|
|
1319
|
-
const colorFnMatch = line.match(/\b(rgba?|hsla?)\s*\(/);
|
|
1320
|
-
if (colorFnMatch && !line.includes("var(--ggui-") && !line.includes("// fallback") && !line.includes("$value") && !assetEscapedLines.has(lineNum)) {
|
|
1321
|
-
issues.push({
|
|
1322
|
-
tier: 0,
|
|
1323
|
-
result: "fail",
|
|
1324
|
-
category: "tokens",
|
|
1325
|
-
subcategory: "hardcoded-color-fn",
|
|
1326
|
-
severity: "critical",
|
|
1327
|
-
description: `Hardcoded ${colorFnMatch[1]}() breaks theme switching \u2014 use design tokens.`,
|
|
1328
|
-
fix: "Replace with a primitive variant OR a layering role: var(--ggui-color-container), var(--ggui-color-onContainer), var(--ggui-color-outline), etc.",
|
|
1329
|
-
line: lineNum
|
|
1330
|
-
});
|
|
1331
|
-
}
|
|
1332
|
-
const offManifest = /* @__PURE__ */ new Set();
|
|
1333
|
-
for (const m of line.matchAll(TOKEN_REF_RE)) {
|
|
1334
|
-
const name = m[1];
|
|
1335
|
-
if (!MANIFEST_TOKENS.has(name)) offManifest.add(name);
|
|
1336
|
-
}
|
|
1337
|
-
for (const name of offManifest) {
|
|
1338
|
-
issues.push({
|
|
1339
|
-
tier: 0,
|
|
1340
|
-
result: "fail",
|
|
1341
|
-
category: "tokens",
|
|
1342
|
-
subcategory: "off-manifest-token",
|
|
1343
|
-
severity: "critical",
|
|
1344
|
-
description: `Token "${name}" is not in the design system's token manifest \u2014 no theme defines it, so it renders unset.`,
|
|
1345
|
-
fix: `${offManifestHint(name)}Choose a token that exists: the closed vocabulary is the manifest the design-system docs list (colors: var(--ggui-color-primary-500), var(--ggui-color-onContainer), \u2026; spacing: var(--ggui-spacing-4); type: var(--ggui-font-size-sm)). Never invent a name and never add a literal fallback.`,
|
|
1346
|
-
line: lineNum
|
|
1347
|
-
});
|
|
1348
|
-
}
|
|
1349
|
-
const rawSpacingMatch = enforceDesignVocabulary ? line.match(
|
|
1350
|
-
/\b(gap|padding|paddingX|paddingY|margin|radius)\s*=\s*["'][\d.]+(?:px|rem|em)["']/
|
|
1351
|
-
) : null;
|
|
1352
|
-
if (rawSpacingMatch) {
|
|
1353
|
-
issues.push({
|
|
1354
|
-
tier: 0,
|
|
1355
|
-
result: "fail",
|
|
1356
|
-
category: "tokens",
|
|
1357
|
-
subcategory: "raw-spacing",
|
|
1358
|
-
severity: "critical",
|
|
1359
|
-
description: `\`${rawSpacingMatch[1]}\` uses a raw CSS length \u2014 bypasses the design spacing/radius scale.`,
|
|
1360
|
-
fix: `Use a scale name: ${rawSpacingMatch[1]}="xs|sm|md|lg|xl" (spacing also has none|2xl; radius none|sm|md|lg|xl). For an exact off-scale pixel value pass a number \u2014 ${rawSpacingMatch[1]}={12}.`,
|
|
1361
|
-
line: lineNum
|
|
1362
|
-
});
|
|
1363
|
-
}
|
|
1364
|
-
const namedColorMatch = line.match(
|
|
1365
|
-
/(?:^|[^a-zA-Z-])(?:color|background|backgroundColor|borderColor)\s*:\s*['"]([a-z][a-zA-Z]+)['"]/
|
|
1366
|
-
);
|
|
1367
|
-
if (namedColorMatch) {
|
|
1368
|
-
const named = namedColorMatch[1].toLowerCase();
|
|
1369
|
-
const allowed = /* @__PURE__ */ new Set([
|
|
1370
|
-
"inherit",
|
|
1371
|
-
"currentcolor",
|
|
1372
|
-
"transparent",
|
|
1373
|
-
"unset",
|
|
1374
|
-
"initial",
|
|
1375
|
-
"revert",
|
|
1376
|
-
"none"
|
|
1377
|
-
]);
|
|
1378
|
-
const namedCssColors = /* @__PURE__ */ new Set([
|
|
1379
|
-
"red",
|
|
1380
|
-
"green",
|
|
1381
|
-
"blue",
|
|
1382
|
-
"yellow",
|
|
1383
|
-
"orange",
|
|
1384
|
-
"purple",
|
|
1385
|
-
"pink",
|
|
1386
|
-
"cyan",
|
|
1387
|
-
"magenta",
|
|
1388
|
-
"lime",
|
|
1389
|
-
"teal",
|
|
1390
|
-
"navy",
|
|
1391
|
-
"maroon",
|
|
1392
|
-
"olive",
|
|
1393
|
-
"aqua",
|
|
1394
|
-
"fuchsia",
|
|
1395
|
-
"silver",
|
|
1396
|
-
"gray",
|
|
1397
|
-
"grey",
|
|
1398
|
-
"white",
|
|
1399
|
-
"black",
|
|
1400
|
-
"brown",
|
|
1401
|
-
"gold",
|
|
1402
|
-
"indigo",
|
|
1403
|
-
"violet",
|
|
1404
|
-
"crimson",
|
|
1405
|
-
"tomato",
|
|
1406
|
-
"coral",
|
|
1407
|
-
"salmon",
|
|
1408
|
-
"orchid",
|
|
1409
|
-
"plum",
|
|
1410
|
-
"tan",
|
|
1411
|
-
"khaki",
|
|
1412
|
-
"beige",
|
|
1413
|
-
"ivory",
|
|
1414
|
-
"lavender",
|
|
1415
|
-
"mint",
|
|
1416
|
-
"turquoise",
|
|
1417
|
-
"azure",
|
|
1418
|
-
"royalblue",
|
|
1419
|
-
"darkblue",
|
|
1420
|
-
"lightblue",
|
|
1421
|
-
"skyblue",
|
|
1422
|
-
"steelblue",
|
|
1423
|
-
"darkred",
|
|
1424
|
-
"lightgreen",
|
|
1425
|
-
"darkgreen",
|
|
1426
|
-
"forestgreen",
|
|
1427
|
-
"darkorange",
|
|
1428
|
-
"lightgray",
|
|
1429
|
-
"lightgrey",
|
|
1430
|
-
"darkgray",
|
|
1431
|
-
"darkgrey",
|
|
1432
|
-
"hotpink",
|
|
1433
|
-
"deeppink",
|
|
1434
|
-
"lightpink",
|
|
1435
|
-
"lightyellow"
|
|
1436
|
-
]);
|
|
1437
|
-
if (!allowed.has(named) && namedCssColors.has(named)) {
|
|
1438
|
-
issues.push({
|
|
1439
|
-
tier: 0,
|
|
1440
|
-
result: "fail",
|
|
1441
|
-
category: "tokens",
|
|
1442
|
-
subcategory: "named-color",
|
|
1443
|
-
severity: "critical",
|
|
1444
|
-
description: `Hardcoded CSS named color "${named}" breaks theme switching \u2014 use design tokens.`,
|
|
1445
|
-
fix: `Replace with a typed slot (Text tone="muted" / Box surface="accent" / Badge variant="success") OR a semantic token: var(--ggui-color-onSunken), var(--ggui-color-primary-500), etc. The keyword "${named}" maps to a fixed RGB; the operator's theme has no effect on it.`,
|
|
1446
|
-
line: lineNum
|
|
1447
|
-
});
|
|
1448
|
-
}
|
|
1449
|
-
}
|
|
1450
|
-
const pxMatch = enforceDesignVocabulary ? line.match(/(?:padding|margin|gap|borderRadius)\s*:\s*['"]?\d+px/) : null;
|
|
1451
|
-
if (pxMatch && !line.includes("var(--ggui-")) {
|
|
1452
|
-
issues.push({
|
|
1453
|
-
tier: 0,
|
|
1454
|
-
result: "warn",
|
|
1455
|
-
category: "tokens",
|
|
1456
|
-
subcategory: "raw-pixels",
|
|
1457
|
-
description: "Raw pixel value in spacing \u2014 must use design tokens",
|
|
1458
|
-
fix: 'Replace with a bare var(--ggui-spacing-*) token \u2014 no literal fallback (the runtime injects every token) \u2014 or the scale name on the primitive prop (gap="md", padding="lg")',
|
|
1459
|
-
line: lineNum
|
|
1460
|
-
});
|
|
1461
|
-
}
|
|
1462
|
-
const numericSpacingMatch = enforceDesignVocabulary ? line.match(/\b(padding|paddingX|paddingY|gap|margin)\s*=\s*\{?\s*(\d+)\s*\}?/) : null;
|
|
1463
|
-
if (numericSpacingMatch && !line.includes("var(--ggui-")) {
|
|
1464
|
-
issues.push({
|
|
1465
|
-
tier: 0,
|
|
1466
|
-
result: "warn",
|
|
1467
|
-
category: "tokens",
|
|
1468
|
-
subcategory: "numeric-spacing-prop",
|
|
1469
|
-
description: `Numeric spacing prop ${numericSpacingMatch[1]}={${numericSpacingMatch[2]}} \u2014 use design tokens instead`,
|
|
1470
|
-
fix: `Replace with ${numericSpacingMatch[1]}="var(--ggui-spacing-*)"`,
|
|
1471
|
-
line: lineNum
|
|
1472
|
-
});
|
|
1473
|
-
}
|
|
1474
|
-
}
|
|
1475
|
-
const clickablePattern = /<(?:Card|Box|Stack|Row)\s[^>]*onClick/;
|
|
1476
|
-
for (let i = 0; enforceDesignVocabulary && i < lines.length; i++) {
|
|
1477
|
-
if (clickablePattern.test(lines[i]) && !lines[i].includes("as={Clickable}") && !lines[i].includes("as={Pressable}")) {
|
|
1478
|
-
issues.push({
|
|
1479
|
-
tier: 0,
|
|
1480
|
-
result: "warn",
|
|
1481
|
-
category: "contract",
|
|
1482
|
-
subcategory: "clickable-wrapper",
|
|
1483
|
-
description: `onClick on a structural primitive without as={Clickable} \u2014 the bare primitive has no click or keyboard handling`,
|
|
1484
|
-
fix: `Add as={Clickable} to the element, e.g., <Card as={Clickable} onClick={handler}>`,
|
|
1485
|
-
line: i + 1
|
|
1486
|
-
});
|
|
1487
|
-
}
|
|
1488
|
-
}
|
|
1489
|
-
const bindingRe = designMode === "free" ? /(?:const|let)\s+(\w+)\s*=\s*useAction\s*(?:<[^>]*>)?\s*\(/g : /(?:const|let)\s+(\w+)\s*=\s*useAction\s*\(/g;
|
|
1490
|
-
const actionBindings = [...sourceCode.matchAll(bindingRe)].map((m) => m[1]).filter((name) => typeof name === "string");
|
|
1491
|
-
for (const binding of actionBindings) {
|
|
1492
|
-
const callSites = (sourceCode.match(new RegExp(`\\b${binding}\\s*\\(`, "g")) ?? []).length;
|
|
1493
|
-
if (callSites >= 2) {
|
|
1494
|
-
issues.push({
|
|
1495
|
-
tier: 0,
|
|
1496
|
-
result: "warn",
|
|
1497
|
-
category: "interactivity",
|
|
1498
|
-
subcategory: "double-wired-action",
|
|
1499
|
-
description: `Action '${binding}' is dispatched from ${callSites} call sites \u2014 if an interactive element nests inside another, one gesture fires the action twice (the inner gesture bubbles to the outer handler).`,
|
|
1500
|
-
fix: `Wire each useAction callback to exactly ONE interactive surface; never nest interactive elements (e.g. a Checkbox inside a Card as={Clickable}).`
|
|
1501
|
-
});
|
|
1502
|
-
}
|
|
1503
|
-
}
|
|
1504
|
-
if (actionBindings.length > 0) {
|
|
1505
|
-
issues.push(
|
|
1506
|
-
...detectCertainDoubleWiredActions(sourceCode, actionBindings, designMode)
|
|
1507
|
-
);
|
|
1508
|
-
}
|
|
1509
|
-
if (/\buseRender\s*\(/.test(sourceCode)) {
|
|
1510
|
-
const suppressionRe = /(?<![\w-])disabled\s*=\s*\{([^}]*)\}/gs;
|
|
1511
|
-
for (const m of sourceCode.matchAll(suppressionRe)) {
|
|
1512
|
-
const expr = m[1];
|
|
1513
|
-
if (!/\bisConnected\b/.test(expr)) continue;
|
|
1514
|
-
const attrLine = sourceCode.slice(0, m.index ?? 0).split("\n").length;
|
|
1515
|
-
issues.push({
|
|
1516
|
-
tier: 0,
|
|
1517
|
-
result: "fail",
|
|
1518
|
-
category: "interactivity",
|
|
1519
|
-
subcategory: "connection-suppression",
|
|
1520
|
-
severity: "critical",
|
|
1521
|
-
description: `\`disabled\` is bound to the connection state (\`${expr.trim().replace(/\s+/g, " ")}\`) \u2014 that strips the click handler, and the click is how the runtime detects that the host can relay actions again. The control stays dead after recovery.`,
|
|
1522
|
-
fix: "Leave the control enabled \u2014 never bind `disabled` to the connection state. The runtime presents the dead zone itself (a standing notice, then a per-gesture cue) and the attempt is how it detects recovery; a suppressed control stays dead after the host recovers.",
|
|
1523
|
-
line: attrLine
|
|
1524
|
-
});
|
|
1525
|
-
}
|
|
1526
|
-
}
|
|
1527
|
-
const propsInterfaceMatch = sourceCode.match(/interface Props\s*\{([^}]+)\}/);
|
|
1528
|
-
if (propsInterfaceMatch) {
|
|
1529
|
-
const propsBody = propsInterfaceMatch[1];
|
|
1530
|
-
const optionalProps = [...propsBody.matchAll(/(\w+)\?:/g)].map((m) => m[1]);
|
|
1531
|
-
for (const prop of optionalProps) {
|
|
1532
|
-
const accessPattern = new RegExp(`props\\.${prop}[.\\[]`, "g");
|
|
1533
|
-
const guardPattern = new RegExp(`props\\.${prop}(\\?[.[]|\\s*&&|\\s*\\?\\?)`, "g");
|
|
1534
|
-
const accesses = (sourceCode.match(accessPattern) || []).length;
|
|
1535
|
-
const guards = (sourceCode.match(guardPattern) || []).length;
|
|
1536
|
-
if (accesses > 0 && guards === 0) {
|
|
1537
|
-
issues.push({
|
|
1538
|
-
tier: 0,
|
|
1539
|
-
result: "warn",
|
|
1540
|
-
category: "crash",
|
|
1541
|
-
subcategory: `optional-prop:${prop}`,
|
|
1542
|
-
description: `Optional prop 'props.${prop}' accessed without null guard \u2014 will crash if undefined`,
|
|
1543
|
-
fix: `Use props.${prop}?.field or props.${prop} ?? fallback or {props.${prop} && ...}`
|
|
1544
|
-
});
|
|
1545
|
-
}
|
|
1546
|
-
}
|
|
1547
|
-
}
|
|
1548
|
-
const ALIGN_VALUES = /* @__PURE__ */ new Set(["start", "center", "end", "stretch"]);
|
|
1549
|
-
const JUSTIFY_VALUES = /* @__PURE__ */ new Set(["start", "center", "end", "between", "around", "evenly"]);
|
|
1550
|
-
const tagRegex = /<(Stack|Row)\b([^>]*)>/gs;
|
|
1551
|
-
for (const tag of enforceDesignVocabulary ? sourceCode.matchAll(tagRegex) : []) {
|
|
1552
|
-
const tagName = tag[1];
|
|
1553
|
-
const attrs = tag[2];
|
|
1554
|
-
const tagLine = sourceCode.slice(0, tag.index).split("\n").length;
|
|
1555
|
-
if (/\bpadding=/.test(attrs)) {
|
|
1556
|
-
issues.push({
|
|
1557
|
-
tier: 0,
|
|
1558
|
-
result: "fail",
|
|
1559
|
-
category: "types",
|
|
1560
|
-
subcategory: "stack-row-padding",
|
|
1561
|
-
description: `<${tagName}> does not accept a 'padding' prop`,
|
|
1562
|
-
fix: `Wrap the children in <Box padding="..."> or use the parent <Card padding="...">`,
|
|
1563
|
-
line: tagLine
|
|
1564
|
-
});
|
|
1565
|
-
}
|
|
1566
|
-
const alignAttr = attrs.match(/\balign=["']([^"']+)["']/);
|
|
1567
|
-
if (alignAttr && !ALIGN_VALUES.has(alignAttr[1])) {
|
|
1568
|
-
issues.push({
|
|
1569
|
-
tier: 0,
|
|
1570
|
-
result: "fail",
|
|
1571
|
-
category: "types",
|
|
1572
|
-
subcategory: "stack-row-align",
|
|
1573
|
-
description: `align="${alignAttr[1]}" is invalid on <${tagName}>`,
|
|
1574
|
-
fix: `Use align="start" | "center" | "end" | "stretch" (NOT flex-start/flex-end/space-between)`,
|
|
1575
|
-
line: tagLine
|
|
1576
|
-
});
|
|
1577
|
-
}
|
|
1578
|
-
const justifyAttr = attrs.match(/\bjustify=["']([^"']+)["']/);
|
|
1579
|
-
if (justifyAttr && !JUSTIFY_VALUES.has(justifyAttr[1])) {
|
|
1580
|
-
issues.push({
|
|
1581
|
-
tier: 0,
|
|
1582
|
-
result: "fail",
|
|
1583
|
-
category: "types",
|
|
1584
|
-
subcategory: "stack-row-justify",
|
|
1585
|
-
description: `justify="${justifyAttr[1]}" is invalid on <${tagName}>`,
|
|
1586
|
-
fix: `Use justify="start" | "center" | "end" | "between" | "around" | "evenly"`,
|
|
1587
|
-
line: tagLine
|
|
1588
|
-
});
|
|
1589
|
-
}
|
|
1590
|
-
}
|
|
1591
|
-
for (const tag of enforceDesignVocabulary ? kitControlTags(sourceCode) : []) {
|
|
1592
|
-
const key = inlineBorderKey(tag.attrs, sourceCode);
|
|
1593
|
-
if (key === null) continue;
|
|
1594
|
-
issues.push({
|
|
1595
|
-
tier: 0,
|
|
1596
|
-
result: "fail",
|
|
1597
|
-
category: "tokens",
|
|
1598
|
-
subcategory: "control-inline-border",
|
|
1599
|
-
severity: "critical",
|
|
1600
|
-
description: `control-inline-border: <${tag.name}> sets an inline \`${key}\` \u2014 a kit control draws its own boundary, and an inline border paints over it`,
|
|
1601
|
-
fix: `Remove the border from <${tag.name}>'s style (keep borderRadius if you need it). An outline chip is <Button variant="outline"> exactly as it comes; a control-like edge on your own element uses var(--ggui-color-controlOutline).`,
|
|
1602
|
-
line: tag.line
|
|
1603
|
-
});
|
|
1604
|
-
}
|
|
1605
|
-
if (!sourceCode.includes("interface Props") && !sourceCode.includes("type Props")) {
|
|
1606
|
-
issues.push({
|
|
1607
|
-
tier: 0,
|
|
1608
|
-
result: "fail",
|
|
1609
|
-
category: "types",
|
|
1610
|
-
subcategory: "props-interface",
|
|
1611
|
-
severity: "critical",
|
|
1612
|
-
description: "No Props interface found \u2014 data is likely hardcoded",
|
|
1613
|
-
fix: "Add interface Props { ... } with typed fields and default values in the function signature"
|
|
1614
|
-
});
|
|
1615
|
-
}
|
|
1616
|
-
if (!sourceCode.includes("export default function")) {
|
|
1617
|
-
issues.push({
|
|
1618
|
-
tier: 0,
|
|
1619
|
-
result: "fail",
|
|
1620
|
-
category: "compile",
|
|
1621
|
-
subcategory: "default-export",
|
|
1622
|
-
severity: "critical",
|
|
1623
|
-
description: "Missing default export function",
|
|
1624
|
-
fix: "Add export default function Component(props: Props) { ... }"
|
|
1625
|
-
});
|
|
1626
|
-
}
|
|
1627
|
-
const neverReturns = detectComponentNeverReturns(sourceCode);
|
|
1628
|
-
if (neverReturns !== null) {
|
|
1629
|
-
issues.push({
|
|
1630
|
-
tier: 0,
|
|
1631
|
-
result: "fail",
|
|
1632
|
-
category: "compile",
|
|
1633
|
-
subcategory: "renders-nothing",
|
|
1634
|
-
severity: "critical",
|
|
1635
|
-
description: neverReturns.jsxStatementLine === void 0 ? "The component never returns its tree \u2014 it renders a blank frame" : `The component never returns its tree: the JSX at line ${neverReturns.jsxStatementLine} sits at statement position, so it is built and thrown away and the frame renders blank`,
|
|
1636
|
-
fix: "Wrap the tree in the return the boilerplate gave you: `return (` before the root element and `);` after it.",
|
|
1637
|
-
...neverReturns.jsxStatementLine === void 0 ? {} : { line: neverReturns.jsxStatementLine }
|
|
1638
|
-
});
|
|
1639
|
-
}
|
|
1640
|
-
if (contract) {
|
|
1641
|
-
try {
|
|
1642
|
-
const contractIssues = validateAllContracts(sourceCode, contract);
|
|
1643
|
-
for (const ci of contractIssues) {
|
|
1644
|
-
issues.push({
|
|
1645
|
-
tier: 0,
|
|
1646
|
-
result: ci.severity === "error" ? "fail" : "warn",
|
|
1647
|
-
category: "contract",
|
|
1648
|
-
subcategory: ci.field,
|
|
1649
|
-
severity: ci.severity === "error" ? "critical" : "major",
|
|
1650
|
-
description: ci.message,
|
|
1651
|
-
fix: ci.fix
|
|
1652
|
-
});
|
|
1653
|
-
}
|
|
1654
|
-
} catch {
|
|
1655
|
-
}
|
|
1656
|
-
const fnBody = sourceCode.slice(sourceCode.indexOf("export default function"));
|
|
1657
|
-
if (contract.actionSpec) {
|
|
1658
|
-
for (const actionName of Object.keys(contract.actionSpec)) {
|
|
1659
|
-
if (!fnBody.includes(actionName)) {
|
|
1660
|
-
issues.push({
|
|
1661
|
-
tier: 0,
|
|
1662
|
-
result: "warn",
|
|
1663
|
-
category: "contract",
|
|
1664
|
-
subcategory: `action:${actionName}`,
|
|
1665
|
-
description: `Action hook '${actionName}' from contract is declared but never used in the component`,
|
|
1666
|
-
fix: `Wire ${actionName}() to a Button onClick, form onSubmit, or other user interaction`
|
|
1667
|
-
});
|
|
1668
|
-
}
|
|
1669
|
-
}
|
|
1670
|
-
}
|
|
1671
|
-
if (contract.streamSpec) {
|
|
1672
|
-
for (const channelName of Object.keys(contract.streamSpec)) {
|
|
1673
|
-
if (!fnBody.includes(channelName)) {
|
|
1674
|
-
issues.push({
|
|
1675
|
-
tier: 0,
|
|
1676
|
-
result: "warn",
|
|
1677
|
-
category: "contract",
|
|
1678
|
-
subcategory: `stream:${channelName}`,
|
|
1679
|
-
description: `Stream hook '${channelName}' from contract is declared but never rendered in the component`,
|
|
1680
|
-
fix: `Render ${channelName}.latest data in the JSX (with null guard: ${channelName}.latest && ...)`
|
|
1681
|
-
});
|
|
1682
|
-
}
|
|
1683
|
-
}
|
|
1684
|
-
}
|
|
1685
|
-
if (contract.clientCapabilities) {
|
|
1686
|
-
for (const use of listContractGadgets(contract)) {
|
|
1687
|
-
if (!fnBody.includes(use.name)) {
|
|
1688
|
-
issues.push({
|
|
1689
|
-
tier: 0,
|
|
1690
|
-
result: "warn",
|
|
1691
|
-
category: "contract",
|
|
1692
|
-
subcategory: `clientCapability:${use.name}`,
|
|
1693
|
-
description: `Client capability '${use.name}' from contract is declared but the export is never used`,
|
|
1694
|
-
fix: HOOK_NAME_RE.test(use.name) ? `Import \`${use.name}\` from \`${use.package}\` and call it inside the component \u2014 bind the return value and surface its \`.value\` / \`.status\` in JSX.` : `Import \`${use.name}\` from \`${use.package}\` and render it as a JSX element (\`<${use.name} \u2026 />\`).`
|
|
1695
|
-
});
|
|
1696
|
-
}
|
|
1697
|
-
}
|
|
1698
|
-
}
|
|
1699
|
-
try {
|
|
1700
|
-
const report = checkWirePreservation(sourceCode, contract);
|
|
1701
|
-
for (const site of report.missing) {
|
|
1702
|
-
const hook = HOOK_NAME_FOR[site.kind];
|
|
1703
|
-
const fix = site.kind === "context" ? `Restore \`const [${site.name}, set${site.name.charAt(0).toUpperCase() + site.name.slice(1)}] = ${hook}('${site.name}')\` at the top of the component body. The boilerplate auto-emits this destructure for every declared contextSpec slot \u2014 do not delete it.` : `Restore \`const ${site.name} = ${hook}('${site.name}')\` at the top of the component body and consume the returned binding (in JSX, a callback, or an effect).`;
|
|
1704
|
-
issues.push({
|
|
1705
|
-
tier: 0,
|
|
1706
|
-
result: "fail",
|
|
1707
|
-
category: "contract",
|
|
1708
|
-
subcategory: `wire_preservation:${site.kind}:${site.name}`,
|
|
1709
|
-
severity: "critical",
|
|
1710
|
-
description: `Contract declares ${site.kind} '${site.name}' but no ${hook}('${site.name}') call exists in the component. The boilerplate placed this hook for you \u2014 do not delete it.`,
|
|
1711
|
-
fix
|
|
1712
|
-
});
|
|
1713
|
-
}
|
|
1714
|
-
} catch {
|
|
1715
|
-
}
|
|
1716
|
-
if (contract.propsSpec) {
|
|
1717
|
-
const propsProperties = contract.propsSpec.properties ?? {};
|
|
1718
|
-
for (const propName of Object.keys(propsProperties)) {
|
|
1719
|
-
if (!fnBody.includes(`props.${propName}`) && !fnBody.includes(`{props.${propName}}`)) {
|
|
1720
|
-
issues.push({
|
|
1721
|
-
tier: 0,
|
|
1722
|
-
result: "warn",
|
|
1723
|
-
category: "contract",
|
|
1724
|
-
subcategory: `prop:${propName}`,
|
|
1725
|
-
description: `Props field '${propName}' from contract is never rendered \u2014 data is wasted`,
|
|
1726
|
-
fix: `Render props.${propName} in the JSX (e.g., <Text>{props.${propName}}</Text>)`
|
|
1727
|
-
});
|
|
1728
|
-
}
|
|
1729
|
-
}
|
|
1730
|
-
}
|
|
1731
|
-
}
|
|
1732
|
-
try {
|
|
1733
|
-
const contractsForExtraCheck = contract ?? {};
|
|
1734
|
-
const extras = checkWirePreservation(sourceCode, contractsForExtraCheck).extra;
|
|
1735
|
-
for (const site of extras) {
|
|
1736
|
-
const hook = HOOK_NAME_FOR[site.kind];
|
|
1737
|
-
const field = CONTRACT_FIELD_FOR[site.kind];
|
|
1738
|
-
issues.push({
|
|
1739
|
-
tier: 0,
|
|
1740
|
-
result: "fail",
|
|
1741
|
-
category: "contract",
|
|
1742
|
-
subcategory: `wire_undeclared:${site.kind}:${site.name}`,
|
|
1743
|
-
severity: "critical",
|
|
1744
|
-
description: `Component calls ${hook}('${site.name}') but the contract does not declare ${site.kind} '${site.name}'. Every wire reference in the generated code MUST correspond to a declared entry on the agent-authored contract \u2014 the runtime mounts/registers wire surfaces from the contract, not from the code. ` + (contract === void 0 ? "No contract authored at all \u2014 describing the contract in the prompt text is NOT enough; the agent MUST pass a structured `contract` field on the ggui_render call." : ""),
|
|
1745
|
-
fix: `Either (a) declare \`${field}.${site.name}\` on the \`contract\` input so the runtime registers the surface for this UI, or (b) remove the ${hook}('${site.name}') call from the component if it isn't part of the intended wire surface.`
|
|
1746
|
-
});
|
|
1747
|
-
}
|
|
1748
|
-
} catch {
|
|
1749
|
-
}
|
|
1750
|
-
try {
|
|
1751
|
-
const importReport = checkWireImports(sourceCode);
|
|
1752
|
-
for (const miss of importReport.missing) {
|
|
1753
|
-
issues.push({
|
|
1754
|
-
tier: 0,
|
|
1755
|
-
result: "fail",
|
|
1756
|
-
category: "imports",
|
|
1757
|
-
subcategory: `wire_import_missing:${miss.hook}`,
|
|
1758
|
-
severity: "critical",
|
|
1759
|
-
description: `Component calls ${miss.hook}(...) but does not import it from '@ggui-ai/wire'. Without the import, rewriteImports has no specifier to attach the data-URL shim to, so the hook is undeclared at browser eval time and the component crashes on mount with \`ReferenceError: ${miss.hook} is not defined\`.`,
|
|
1760
|
-
fix: `Add \`import { ${miss.hook} } from '@ggui-ai/wire';\` at the top of the file alongside the other imports.`
|
|
1761
|
-
});
|
|
1762
|
-
}
|
|
1763
|
-
} catch {
|
|
1764
|
-
}
|
|
1765
|
-
{
|
|
1766
|
-
const requireRe = /require\s*\(\s*['"](@[^'"]+)['"]\s*\)/g;
|
|
1767
|
-
const seen = /* @__PURE__ */ new Set();
|
|
1768
|
-
let m;
|
|
1769
|
-
while ((m = requireRe.exec(sourceCode)) !== null) {
|
|
1770
|
-
const pkg = m[1];
|
|
1771
|
-
if (pkg === void 0 || seen.has(pkg)) continue;
|
|
1772
|
-
seen.add(pkg);
|
|
1773
|
-
issues.push({
|
|
1774
|
-
tier: 0,
|
|
1775
|
-
result: "fail",
|
|
1776
|
-
category: "imports",
|
|
1777
|
-
subcategory: `require_disallowed:${pkg}`,
|
|
1778
|
-
severity: "critical",
|
|
1779
|
-
description: `Component uses \`require('${pkg}')\` \u2014 CommonJS require is not available in the iframe's browser ESM runtime, and the import-rewriter only attaches data-URL shims to STATIC import specifiers. The component will fail to mount with \`ReferenceError: require is not defined\` at the first call.`,
|
|
1780
|
-
fix: `Replace with a top-level static \`import { \u2026 } from '${pkg}'\` at the top of the file. The boilerplate emits this import for you when the contract declares a matching \`clientCapabilities.gadgets[*]\` entry \u2014 restore the line instead of inlining a require() call.`
|
|
1781
|
-
});
|
|
1782
|
-
}
|
|
1783
|
-
}
|
|
1784
|
-
{
|
|
1785
|
-
const gadgetScanSource = stripComments(sourceCode);
|
|
1786
|
-
for (const use of contract ? listContractGadgets(contract) : []) {
|
|
1787
|
-
const exportName = use.name;
|
|
1788
|
-
if (isGadgetExportImported(gadgetScanSource, use.package, exportName)) {
|
|
1789
|
-
continue;
|
|
1790
|
-
}
|
|
1791
|
-
const isComponent = !HOOK_NAME_RE.test(exportName);
|
|
1792
|
-
const kind = isComponent ? "component" : "hook";
|
|
1793
|
-
const bindingName = exportName.length > 3 ? exportName.charAt(3).toLowerCase() + exportName.slice(4) : exportName;
|
|
1794
|
-
const fix = isComponent ? `The component \`${exportName}\` is REAL and CORRECT \u2014 do NOT remove it. Restore the gadget plumbing in 2 steps:
|
|
1795
|
-
(1) Keep \`import { ${exportName} } from '${use.package}';\` at the top of the file.
|
|
1796
|
-
(2) RENDER \`<${exportName} \u2026 />\` as a JSX element in the tree you return \u2014 pass its props from the contract. Do NOT call it like a hook.
|
|
1797
|
-
Import \`${exportName}\` ONLY from '${use.package}' \u2014 that is the package it is registered under. If you remove the import again, this check will fail again \u2014 \`${exportName}\` is not optional.` : `The hook \`${exportName}\` is REAL and CORRECT \u2014 do NOT remove it. Restore the gadget plumbing in 2 steps:
|
|
1798
|
-
(1) Keep \`import { ${exportName} } from '${use.package}';\` at the top of the file.
|
|
1799
|
-
(2) CALL \`${exportName}(...)\` inside the component body and render its return value. Example:
|
|
1800
|
-
\`const ${bindingName} = ${exportName}({ /* props from contract */ });\`
|
|
1801
|
-
\`return <div>{/* render ${bindingName} */}</div>;\`
|
|
1802
|
-
Import \`${exportName}\` ONLY from '${use.package}' \u2014 that is the package it is registered under. If you remove the import again, this check will fail again \u2014 \`${exportName}\` is not optional.`;
|
|
1803
|
-
issues.push({
|
|
1804
|
-
tier: 0,
|
|
1805
|
-
result: "fail",
|
|
1806
|
-
category: "imports",
|
|
1807
|
-
subcategory: `gadget_preservation:${exportName}`,
|
|
1808
|
-
severity: "critical",
|
|
1809
|
-
description: `Contract declares \`clientCapabilities.gadgets['${use.package}']['${exportName}']\` (a ${kind}) but the component does not import \`${exportName}\` from \`${use.package}\`. The boilerplate emits \`import { ${exportName} } from '${use.package}';\` \u2014 keep it. Without the import the iframe runtime cannot resolve the ${kind} and the registered gadget is unreachable.`,
|
|
1810
|
-
fix
|
|
1811
|
-
});
|
|
1812
|
-
}
|
|
1813
|
-
}
|
|
1814
|
-
const [typeResult, reactResult] = await Promise.all([
|
|
1815
|
-
typecheck(sourceCode, gadgetTypes).catch((err) => {
|
|
1816
|
-
console.warn("[runTier0Checks] TypeChecker failed:", err instanceof Error ? err.message : String(err));
|
|
1817
|
-
return null;
|
|
1818
|
-
}),
|
|
1819
|
-
lintReactHooks(sourceCode).catch((err) => {
|
|
1820
|
-
console.warn("[runTier0Checks] React linter failed:", err instanceof Error ? err.message : String(err));
|
|
1821
|
-
return [];
|
|
1822
|
-
})
|
|
1823
|
-
]);
|
|
1824
|
-
if (typeResult) {
|
|
1825
|
-
for (const error of typeResult.errors) {
|
|
1826
|
-
issues.push({
|
|
1827
|
-
tier: 0,
|
|
1828
|
-
result: "fail",
|
|
1829
|
-
category: "types",
|
|
1830
|
-
subcategory: `ts${error.code}`,
|
|
1831
|
-
description: error.message,
|
|
1832
|
-
fix: error.fix,
|
|
1833
|
-
line: error.line
|
|
1834
|
-
});
|
|
1835
|
-
}
|
|
1836
|
-
for (const warning of typeResult.warnings) {
|
|
1837
|
-
issues.push({
|
|
1838
|
-
tier: 0,
|
|
1839
|
-
result: "warn",
|
|
1840
|
-
category: "types",
|
|
1841
|
-
subcategory: `ts${warning.code}`,
|
|
1842
|
-
description: warning.message,
|
|
1843
|
-
fix: warning.fix,
|
|
1844
|
-
line: warning.line
|
|
1845
|
-
});
|
|
1846
|
-
}
|
|
1847
|
-
}
|
|
1848
|
-
for (const diag of reactResult) {
|
|
1849
|
-
issues.push({
|
|
1850
|
-
tier: 0,
|
|
1851
|
-
result: diag.severity === "error" ? "fail" : "warn",
|
|
1852
|
-
category: "types",
|
|
1853
|
-
subcategory: diag.rule,
|
|
1854
|
-
description: diag.message,
|
|
1855
|
-
fix: diag.fix,
|
|
1856
|
-
line: diag.line
|
|
1857
|
-
});
|
|
1858
|
-
}
|
|
1859
|
-
for (const issue of issues) {
|
|
1860
|
-
if (!issue.priority) issue.priority = priorityForIssue(issue.category);
|
|
1861
|
-
}
|
|
1862
|
-
return issues;
|
|
1863
|
-
}
|
|
1864
|
-
async function runTier0(sourceCode, contract, designMode = DEFAULT_DESIGN_MODE) {
|
|
1865
|
-
const issues = await runTier0Checks(sourceCode, contract, void 0, void 0, designMode);
|
|
1866
|
-
const pass = [];
|
|
1867
|
-
const failedCategories = new Set(issues.map((i) => i.category));
|
|
1868
|
-
const tier0Categories = ["compile", "security", "contract", "types", "imports", "tokens"];
|
|
1869
|
-
for (const cat of tier0Categories) {
|
|
1870
|
-
if (!failedCategories.has(cat)) {
|
|
1871
|
-
pass.push(cat);
|
|
1872
|
-
}
|
|
1873
|
-
}
|
|
1874
|
-
return { issues, pass };
|
|
1875
|
-
}
|
|
1876
|
-
|
|
1877
|
-
// src/check/sample-props.ts
|
|
1878
|
-
function generateSampleProps(spec) {
|
|
1879
|
-
const props = {};
|
|
1880
|
-
for (const [name, entry] of Object.entries(spec.properties)) {
|
|
1881
|
-
if (entry.example !== void 0) {
|
|
1882
|
-
props[name] = entry.example;
|
|
1883
|
-
continue;
|
|
1884
|
-
}
|
|
1885
|
-
if (entry.default !== void 0) {
|
|
1886
|
-
props[name] = entry.default;
|
|
1887
|
-
continue;
|
|
1888
|
-
}
|
|
1889
|
-
props[name] = synthesizeFromSchema(entry.schema);
|
|
1890
|
-
}
|
|
1891
|
-
return props;
|
|
1892
|
-
}
|
|
1893
|
-
function synthesizeFromSchema(schema) {
|
|
1894
|
-
const declared = Array.isArray(schema.type) ? schema.type.find((t) => t !== "null") : schema.type;
|
|
1895
|
-
switch (declared) {
|
|
1896
|
-
case "string":
|
|
1897
|
-
return "sample";
|
|
1898
|
-
case "number":
|
|
1899
|
-
case "integer":
|
|
1900
|
-
return 0;
|
|
1901
|
-
case "boolean":
|
|
1902
|
-
return false;
|
|
1903
|
-
case "null":
|
|
1904
|
-
return null;
|
|
1905
|
-
case "array": {
|
|
1906
|
-
if (schema.items) return [synthesizeFromSchema(schema.items) ?? null];
|
|
1907
|
-
return [];
|
|
1908
|
-
}
|
|
1909
|
-
case "object": {
|
|
1910
|
-
const obj = {};
|
|
1911
|
-
if (schema.properties) {
|
|
1912
|
-
for (const [k, v] of Object.entries(schema.properties)) {
|
|
1913
|
-
obj[k] = synthesizeFromSchema(v);
|
|
1914
|
-
}
|
|
1915
|
-
}
|
|
1916
|
-
return obj;
|
|
1917
|
-
}
|
|
1918
|
-
default:
|
|
1919
|
-
return void 0;
|
|
1920
|
-
}
|
|
1921
|
-
}
|
|
1922
|
-
|
|
1923
|
-
export { checkWireImports, checkWirePreservation, collectExpectedWires, extractPropsInterface, extractWireCallSites, extractWireImports, generateSampleProps, inferPropsSpecFromSampleData, jsonSchemaTypeToTs, lintReactHooks, propsSpecToTypeScript, runTier0, runTier0Checks, typecheck, validateActionSpecConformance, validateAllContracts, validatePropsAgainstSchema, validateStreamSpecConformance };
|
|
1
|
+
export { checkWireImports, checkWirePreservation, collectExpectedWires, extractPropsInterface, extractWireCallSites, extractWireImports, generateSampleProps, inferPropsSpecFromSampleData, lintReactHooks, propsSpecToTypeScript, runTier0, runTier0Checks, typecheck, validateActionSpecConformance, validateAllContracts, validatePropsAgainstSchema, validateStreamSpecConformance } from '../chunk-FWJIMKFZ.js';
|
|
2
|
+
import '../chunk-33B3BAWS.js';
|
|
3
|
+
export { jsonSchemaTypeToTs } from '../chunk-Z3CXME3K.js';
|
|
4
|
+
import '../chunk-PUH4CXGZ.js';
|
|
5
|
+
import '../chunk-3T2HMTON.js';
|
|
6
|
+
import '../chunk-G3PMV62Z.js';
|
|
1924
7
|
//# sourceMappingURL=index.js.map
|
|
1925
8
|
//# sourceMappingURL=index.js.map
|