@ggui-ai/ui-gen 0.28.0 → 0.29.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{adapter-h4_V69LI.d.ts → adapter-DLQ-5COm.d.ts} +1 -1
- package/dist/adapters/base.js +2 -27
- package/dist/adapters/base.js.map +1 -1
- package/dist/adapters/generation-dispatch.d.ts +14 -6
- package/dist/adapters/generation-dispatch.js +40 -19260
- package/dist/adapters/generation-dispatch.js.map +1 -1
- package/dist/adapters/index.d.ts +1 -1
- package/dist/adapters/index.js +25 -3561
- package/dist/adapters/index.js.map +1 -1
- package/dist/adapters/types.js +2 -9
- package/dist/adapters/types.js.map +1 -1
- package/dist/advanced/index.d.ts +13 -12
- package/dist/advanced/index.js +63 -21954
- package/dist/advanced/index.js.map +1 -1
- package/dist/{bank-Ctdp3FUA.d.ts → bank-LWaiAtaA.d.ts} +1 -1
- package/dist/blueprint-validator.js +6 -2301
- package/dist/blueprint-validator.js.map +1 -1
- package/dist/boilerplate.d.ts +3 -2
- package/dist/boilerplate.js +6 -1867
- package/dist/boilerplate.js.map +1 -1
- package/dist/check/index.d.ts +3 -2
- package/dist/check/index.js +6 -1923
- package/dist/check/index.js.map +1 -1
- package/dist/chunk-33B3BAWS.js +24 -0
- package/dist/chunk-33B3BAWS.js.map +1 -0
- package/dist/chunk-3HSGA6NI.js +591 -0
- package/dist/chunk-3HSGA6NI.js.map +1 -0
- package/dist/chunk-3NNNGNTO.js +1707 -0
- package/dist/chunk-3NNNGNTO.js.map +1 -0
- package/dist/chunk-3T2HMTON.js +79 -0
- package/dist/chunk-3T2HMTON.js.map +1 -0
- package/dist/chunk-3ZSSDV3L.js +123 -0
- package/dist/chunk-3ZSSDV3L.js.map +1 -0
- package/dist/chunk-4MBR5GA3.js +99 -0
- package/dist/chunk-4MBR5GA3.js.map +1 -0
- package/dist/chunk-56NJFD55.js +337 -0
- package/dist/chunk-56NJFD55.js.map +1 -0
- package/dist/chunk-5AGTY555.js +26 -0
- package/dist/chunk-5AGTY555.js.map +1 -0
- package/dist/chunk-5EJM7EGY.js +115 -0
- package/dist/chunk-5EJM7EGY.js.map +1 -0
- package/dist/chunk-5KXCQLLR.js +529 -0
- package/dist/chunk-5KXCQLLR.js.map +1 -0
- package/dist/chunk-5VBXHVEH.js +171 -0
- package/dist/chunk-5VBXHVEH.js.map +1 -0
- package/dist/chunk-6KDDEING.js +515 -0
- package/dist/chunk-6KDDEING.js.map +1 -0
- package/dist/chunk-6VDRWAP6.js +123 -0
- package/dist/chunk-6VDRWAP6.js.map +1 -0
- package/dist/chunk-732IBFEJ.js +2400 -0
- package/dist/chunk-732IBFEJ.js.map +1 -0
- package/dist/chunk-73AURS3I.js +46 -0
- package/dist/chunk-73AURS3I.js.map +1 -0
- package/dist/chunk-77H5SKLX.js +8 -0
- package/dist/chunk-77H5SKLX.js.map +1 -0
- package/dist/chunk-7QE3AHLE.js +35 -0
- package/dist/chunk-7QE3AHLE.js.map +1 -0
- package/dist/chunk-A375NNWN.js +11 -0
- package/dist/chunk-A375NNWN.js.map +1 -0
- package/dist/chunk-AOU33EFS.js +140 -0
- package/dist/chunk-AOU33EFS.js.map +1 -0
- package/dist/chunk-BA4QYA6E.js +461 -0
- package/dist/chunk-BA4QYA6E.js.map +1 -0
- package/dist/chunk-DBP764GL.js +3 -0
- package/dist/chunk-DBP764GL.js.map +1 -0
- package/dist/chunk-DUU3ZJDQ.js +41 -0
- package/dist/chunk-DUU3ZJDQ.js.map +1 -0
- package/dist/chunk-DXKRBXNU.js +30 -0
- package/dist/chunk-DXKRBXNU.js.map +1 -0
- package/dist/chunk-FB7OEEAH.js +1467 -0
- package/dist/chunk-FB7OEEAH.js.map +1 -0
- package/dist/chunk-FLRNMZV2.js +18 -0
- package/dist/chunk-FLRNMZV2.js.map +1 -0
- package/dist/chunk-FWJIMKFZ.js +2032 -0
- package/dist/chunk-FWJIMKFZ.js.map +1 -0
- package/dist/chunk-FXRHWYIQ.js +163 -0
- package/dist/chunk-FXRHWYIQ.js.map +1 -0
- package/dist/chunk-G3PMV62Z.js +33 -0
- package/dist/chunk-G3PMV62Z.js.map +1 -0
- package/dist/chunk-GAJI7Y3P.js +13 -0
- package/dist/chunk-GAJI7Y3P.js.map +1 -0
- package/dist/chunk-GYINWMPR.js +2052 -0
- package/dist/chunk-GYINWMPR.js.map +1 -0
- package/dist/chunk-HD6ABIK7.js +484 -0
- package/dist/chunk-HD6ABIK7.js.map +1 -0
- package/dist/chunk-HDQVHYO6.js +100 -0
- package/dist/chunk-HDQVHYO6.js.map +1 -0
- package/dist/chunk-IHUT3SIW.js +7 -0
- package/dist/chunk-IHUT3SIW.js.map +1 -0
- package/dist/chunk-JDH5EGNZ.js +126 -0
- package/dist/chunk-JDH5EGNZ.js.map +1 -0
- package/dist/chunk-JMD4RGSV.js +37 -0
- package/dist/chunk-JMD4RGSV.js.map +1 -0
- package/dist/chunk-KJWGZL5B.js +33 -0
- package/dist/chunk-KJWGZL5B.js.map +1 -0
- package/dist/chunk-MKMDQFZ7.js +86 -0
- package/dist/chunk-MKMDQFZ7.js.map +1 -0
- package/dist/chunk-NF6JUZWF.js +85 -0
- package/dist/chunk-NF6JUZWF.js.map +1 -0
- package/dist/chunk-O2YC6XDC.js +1933 -0
- package/dist/chunk-O2YC6XDC.js.map +1 -0
- package/dist/chunk-OVLH5ICV.js +29 -0
- package/dist/chunk-OVLH5ICV.js.map +1 -0
- package/dist/chunk-PBOYHGSD.js +106 -0
- package/dist/chunk-PBOYHGSD.js.map +1 -0
- package/dist/chunk-PJFH7QWA.js +81 -0
- package/dist/chunk-PJFH7QWA.js.map +1 -0
- package/dist/chunk-PUH4CXGZ.js +284 -0
- package/dist/chunk-PUH4CXGZ.js.map +1 -0
- package/dist/chunk-QLXXPGTV.js +18 -0
- package/dist/chunk-QLXXPGTV.js.map +1 -0
- package/dist/chunk-R5GIGPW2.js +2845 -0
- package/dist/chunk-R5GIGPW2.js.map +1 -0
- package/dist/chunk-RH4IUSTO.js +83 -0
- package/dist/chunk-RH4IUSTO.js.map +1 -0
- package/dist/chunk-S2GVXKKY.js +153 -0
- package/dist/chunk-S2GVXKKY.js.map +1 -0
- package/dist/chunk-TVHGKFJS.js +58 -0
- package/dist/chunk-TVHGKFJS.js.map +1 -0
- package/dist/chunk-U7GALWLG.js +66 -0
- package/dist/chunk-U7GALWLG.js.map +1 -0
- package/dist/chunk-VTC263LT.js +256 -0
- package/dist/chunk-VTC263LT.js.map +1 -0
- package/dist/chunk-WEOXFEVE.js +739 -0
- package/dist/chunk-WEOXFEVE.js.map +1 -0
- package/dist/chunk-WYHP46LT.js +28 -0
- package/dist/chunk-WYHP46LT.js.map +1 -0
- package/dist/chunk-XGLO5LGG.js +274 -0
- package/dist/chunk-XGLO5LGG.js.map +1 -0
- package/dist/chunk-XRBCRKA2.js +29 -0
- package/dist/chunk-XRBCRKA2.js.map +1 -0
- package/dist/chunk-XW7ADFFV.js +340 -0
- package/dist/chunk-XW7ADFFV.js.map +1 -0
- package/dist/chunk-YIQ42IUX.js +34 -0
- package/dist/chunk-YIQ42IUX.js.map +1 -0
- package/dist/chunk-YJZTUY7B.js +101 -0
- package/dist/chunk-YJZTUY7B.js.map +1 -0
- package/dist/chunk-Z3CXME3K.js +107 -0
- package/dist/chunk-Z3CXME3K.js.map +1 -0
- package/dist/classifier/index.js +2 -527
- package/dist/classifier/index.js.map +1 -1
- package/dist/client-JBPCXU7W.js +4 -0
- package/dist/client-JBPCXU7W.js.map +1 -0
- package/dist/compose.js +3 -367
- package/dist/compose.js.map +1 -1
- package/dist/cost-tracker-HHGQ4FTW.js +67 -0
- package/dist/cost-tracker-HHGQ4FTW.js.map +1 -0
- package/dist/{create-ui-generator-BrZHF0cf.d.ts → create-ui-generator-BY-WeaPi.d.ts} +3 -3
- package/dist/{design-mode-DGWN_Gkm.d.ts → design-mode-XyOQmzE0.d.ts} +4 -3
- package/dist/{dispatch-B07TI30d.d.ts → dispatch-8LzTddKw.d.ts} +3 -2
- package/dist/dist-UALN6ZVB.js +543 -0
- package/dist/dist-UALN6ZVB.js.map +1 -0
- package/dist/dupe-break-X6K7QFIS.js +4 -0
- package/dist/dupe-break-X6K7QFIS.js.map +1 -0
- package/dist/evaluation/axis-checks/index.d.ts +5 -4
- package/dist/evaluation/axis-checks/index.js +6 -1914
- package/dist/evaluation/axis-checks/index.js.map +1 -1
- package/dist/evaluation/axis-checks/registry.d.ts +3 -2
- package/dist/evaluation/axis-checks/registry.js +3 -1844
- package/dist/evaluation/axis-checks/registry.js.map +1 -1
- package/dist/evaluation/index.d.ts +49 -132
- package/dist/evaluation/index.js +302 -6466
- package/dist/evaluation/index.js.map +1 -1
- package/dist/evaluation/types.d.ts +3 -2
- package/dist/evaluation/types.js +2 -5
- package/dist/evaluation/types.js.map +1 -1
- package/dist/fragments/index.js +2 -338
- package/dist/fragments/index.js.map +1 -1
- package/dist/{generate-tSO_shQ0.d.ts → generate-DQ4ci90s.d.ts} +1 -1
- package/dist/harness/check/runtime-render/index.d.ts +35 -15
- package/dist/harness/check/runtime-render/index.js +6 -2390
- package/dist/harness/check/runtime-render/index.js.map +1 -1
- package/dist/harness/check/runtime-render/render-check-worker.js +4 -1739
- package/dist/harness/check/runtime-render/render-check-worker.js.map +1 -1
- package/dist/harness/index.d.ts +3 -2
- package/dist/harness/index.js +23 -5126
- package/dist/harness/index.js.map +1 -1
- package/dist/harness/llm-trace-sink.js +2 -24
- package/dist/harness/llm-trace-sink.js.map +1 -1
- package/dist/harness/prompts.d.ts +2 -2
- package/dist/harness/prompts.js +8 -324
- package/dist/harness/prompts.js.map +1 -1
- package/dist/harness/result-types.d.ts +6 -5
- package/dist/harness/result-types.js +2 -0
- package/dist/harness/result-types.js.map +1 -1
- package/dist/harness/runtime.d.ts +22 -5
- package/dist/harness/runtime.js +10 -2313
- package/dist/harness/runtime.js.map +1 -1
- package/dist/harness/types-public.d.ts +3 -2
- package/dist/harness/types-public.js +16 -2954
- package/dist/harness/types-public.js.map +1 -1
- package/dist/harness/validator-trace-sink.d.ts +3 -2
- package/dist/harness/validator-trace-sink.js +2 -26
- package/dist/harness/validator-trace-sink.js.map +1 -1
- package/dist/hash.js +2 -39
- package/dist/hash.js.map +1 -1
- package/dist/hashline-5OKREIRM.js +4 -0
- package/dist/hashline-5OKREIRM.js.map +1 -0
- package/dist/index.d.ts +50 -11
- package/dist/index.js +57 -22015
- package/dist/index.js.map +1 -1
- package/dist/llm-evaluator-PN3WFCS4.js +653 -0
- package/dist/llm-evaluator-PN3WFCS4.js.map +1 -0
- package/dist/magic-string.es-2NZTC4GZ.js +1307 -0
- package/dist/magic-string.es-2NZTC4GZ.js.map +1 -0
- package/dist/patch.d.ts +3 -2
- package/dist/patch.js +2 -56
- package/dist/patch.js.map +1 -1
- package/dist/policy-VCYKGSAC.js +5 -0
- package/dist/policy-VCYKGSAC.js.map +1 -0
- package/dist/policy.js +2 -35
- package/dist/policy.js.map +1 -1
- package/dist/primitive-index-2OF6F4TZ.js +131 -0
- package/dist/primitive-index-2OF6F4TZ.js.map +1 -0
- package/dist/primitive-slice-WTMBLYFJ.js +235 -0
- package/dist/primitive-slice-WTMBLYFJ.js.map +1 -0
- package/dist/{prompts-BhNUuRoZ.d.ts → prompts-CLZGywMP.d.ts} +1 -1
- package/dist/provider-adapter-contract.js +10407 -14204
- package/dist/provider-adapter-contract.js.map +1 -1
- package/dist/provider-adapter.js +2 -44
- package/dist/provider-adapter.js.map +1 -1
- package/dist/providers/index.d.ts +89 -1
- package/dist/providers/index.js +115 -44
- package/dist/providers/index.js.map +1 -1
- package/dist/raw-C6KBH5I3.js +141 -0
- package/dist/raw-C6KBH5I3.js.map +1 -0
- package/dist/raw-FXSBXQTZ.js +7 -0
- package/dist/raw-FXSBXQTZ.js.map +1 -0
- package/dist/raw-MURDUPGK.js +8 -0
- package/dist/raw-MURDUPGK.js.map +1 -0
- package/dist/raw-T3GW3UVX.js +8 -0
- package/dist/raw-T3GW3UVX.js.map +1 -0
- package/dist/render-check-host-ZNIS6BJ5.js +5 -0
- package/dist/render-check-host-ZNIS6BJ5.js.map +1 -0
- package/dist/sdk-4JNSDNSR.js +6 -0
- package/dist/sdk-4JNSDNSR.js.map +1 -0
- package/dist/sdk-DIVJMHCX.js +7 -0
- package/dist/sdk-DIVJMHCX.js.map +1 -0
- package/dist/sdk-QRJOCRTS.js +7 -0
- package/dist/sdk-QRJOCRTS.js.map +1 -0
- package/dist/tag-balance-CTHCTRJ7.js +123 -0
- package/dist/tag-balance-CTHCTRJ7.js.map +1 -0
- package/dist/tools/get-primitives-ts.d.ts +1 -1
- package/dist/tools/get-primitives-ts.js +3 -1
- package/dist/tools/get-primitives-ts.js.map +1 -1
- package/dist/tools/render-check-worker.js +3 -40
- package/dist/tools/render-check-worker.js.map +1 -1
- package/dist/{types-public-h8IMnYuh.d.ts → types-public-hYlrerZh.d.ts} +35 -2
- package/dist/validation/index.d.ts +2 -2
- package/dist/validation/index.js +3 -498
- package/dist/validation/index.js.map +1 -1
- package/dist/{visual-evaluator-C2strxqD.d.ts → visual-evaluator-KskajxD5.d.ts} +14 -4
- package/dist/visual-evaluator-TGEXYQTP.js +12 -0
- package/dist/visual-evaluator-TGEXYQTP.js.map +1 -0
- package/dist/workflows.d.ts +3 -2
- package/dist/workflows.js +2 -151
- package/dist/workflows.js.map +1 -1
- package/package.json +20 -16
- package/src/adapters/claude/sdk.ts +4 -2
- package/src/adapters/claude/tool-bridge.ts +2 -1
- package/src/adapters/generation-dispatch.ts +9 -0
- package/src/adapters/openai/raw.ts +4 -0
- package/src/adapters/openai/sdk.ts +6 -1
- package/src/adapters/openai/tokens.ts +21 -7
- package/src/adapters/tool-bridge.ts +1 -1
- package/src/adapters/tools.ts +1 -1
- package/src/advanced/feedback.ts +1 -2
- package/src/advanced/generator.ts +4 -3
- package/src/boilerplate/free-design-prompt.ts +7 -0
- package/src/boilerplate/generate.ts +18 -5
- package/src/boilerplate/hard-sections.ts +8 -3
- package/src/boilerplate/react-hooks.ts +56 -0
- package/src/boilerplate/system-prompt.ts +12 -3
- package/src/check/run-tier0.ts +233 -8
- package/src/check/type-checker.ts +128 -16
- package/src/coding-agent/tools.ts +26 -3
- package/src/design-mode.ts +4 -9
- package/src/evaluation/axis-checks/checks/state-merge.ts +2 -4
- package/src/evaluation/axis-checks/checks/state-payload.ts +1 -28
- package/src/evaluation/axis-checks/checks/tint.ts +197 -0
- package/src/evaluation/axis-checks/helpers.ts +121 -0
- package/src/evaluation/axis-checks/registry.ts +2 -0
- package/src/evaluation/criteria/__fixtures__/edge-v2-draw2.json +4629 -0
- package/src/evaluation/criteria/bank.ts +1 -1
- package/src/evaluation/criteria/edge-instruments.ts +14 -0
- package/src/evaluation/criteria/edge-probe-v2.ts +352 -0
- package/src/evaluation/criteria/edge-v2.ts +242 -0
- package/src/evaluation/criteria/resolve.ts +24 -5
- package/src/evaluation/index.ts +3 -5
- package/src/evaluation/mcp-server.ts +0 -50
- package/src/evaluation/types-public.ts +6 -1
- package/src/evaluation/visual-evaluator.ts +161 -30
- package/src/harness/check/runtime-render/index.ts +28 -4
- package/src/harness/check/runtime-render/render-check.ts +20 -21
- package/src/harness/claude-agent-sdk-loader.ts +93 -0
- package/src/harness/claude-code-login-query.ts +1 -1
- package/src/harness/coding/generate-task-runner.ts +1 -0
- package/src/harness/coding/run-coding-turn.ts +3 -0
- package/src/harness/coding/run-eval-round.ts +9 -0
- package/src/harness/llm-router.ts +50 -2
- package/src/harness/runtime.ts +17 -0
- package/src/index.ts +9 -0
- package/src/providers/anthropic-tool-call.ts +263 -0
- package/src/providers/index.ts +13 -0
- package/src/tools/get-primitives-ts.ts +1 -1
- package/src/tools/render-check.ts +4 -4
- package/src/validation/primitives.ts +2 -1
- package/src/evaluation/loop.ts +0 -218
|
@@ -1,2304 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
import
|
|
7
|
-
import ts from 'typescript';
|
|
8
|
-
import { primeInputs } from '@ggui-ai/ui-visual-tester/prime-inputs';
|
|
9
|
-
|
|
10
|
-
var __defProp = Object.defineProperty;
|
|
11
|
-
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
12
|
-
var __esm = (fn, res) => function __init() {
|
|
13
|
-
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
14
|
-
};
|
|
15
|
-
var __export = (target, all) => {
|
|
16
|
-
for (var name in all)
|
|
17
|
-
__defProp(target, name, { get: all[name], enumerable: true });
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
// src/internal/verdict-line.ts
|
|
21
|
-
function parseVerdictLine(stdout) {
|
|
22
|
-
const lines = stdout.split("\n");
|
|
23
|
-
for (let i = lines.length - 1; i >= 0; i -= 1) {
|
|
24
|
-
const line = lines[i];
|
|
25
|
-
if (line.startsWith(`${VERDICT_PREFIX} `)) return JSON.parse(line.slice(VERDICT_PREFIX.length + 1));
|
|
26
|
-
}
|
|
27
|
-
return void 0;
|
|
28
|
-
}
|
|
29
|
-
var VERDICT_PREFIX;
|
|
30
|
-
var init_verdict_line = __esm({
|
|
31
|
-
"src/internal/verdict-line.ts"() {
|
|
32
|
-
VERDICT_PREFIX = "@@ggui-render-check-verdict@@";
|
|
33
|
-
}
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
// src/harness/check/runtime-render/render-check-host.ts
|
|
37
|
-
var render_check_host_exports = {};
|
|
38
|
-
__export(render_check_host_exports, {
|
|
39
|
-
DEFAULT_RENDER_CHECK_HOST_BOUNDS: () => DEFAULT_RENDER_CHECK_HOST_BOUNDS,
|
|
40
|
-
mapSandboxResultToCheckResult: () => mapSandboxResultToCheckResult,
|
|
41
|
-
resolveRenderCheckHostBounds: () => resolveRenderCheckHostBounds,
|
|
42
|
-
resolveWorkerSpawn: () => resolveWorkerSpawn,
|
|
43
|
-
runRenderCheckViaWorker: () => runRenderCheckViaWorker
|
|
44
|
-
});
|
|
45
|
-
function resolveRenderCheckHostBounds(bounds = {}) {
|
|
46
|
-
const timeoutMs = bounds.timeoutMs ?? DEFAULT_RENDER_CHECK_HOST_BOUNDS.timeoutMs;
|
|
47
|
-
const heapMb = bounds.heapMb ?? DEFAULT_RENDER_CHECK_HOST_BOUNDS.heapMb;
|
|
48
|
-
if (!Number.isInteger(timeoutMs) || timeoutMs < 2) {
|
|
49
|
-
throw new RangeError(`render-check: timeoutMs must be an integer \u2265 2, got ${timeoutMs}`);
|
|
50
|
-
}
|
|
51
|
-
if (!Number.isInteger(heapMb) || heapMb < 1) {
|
|
52
|
-
throw new RangeError(`render-check: heapMb must be a positive integer, got ${heapMb}`);
|
|
53
|
-
}
|
|
54
|
-
const requestedGrace = bounds.gracePeriodMs ?? DEFAULT_RENDER_CHECK_HOST_BOUNDS.gracePeriodMs;
|
|
55
|
-
const gracePeriodMs = Math.max(1, Math.min(requestedGrace, Math.floor(timeoutMs / 2)));
|
|
56
|
-
return { timeoutMs, heapMb, gracePeriodMs };
|
|
57
|
-
}
|
|
58
|
-
function resolveWorkerSpawn() {
|
|
59
|
-
const jsCandidates = [
|
|
60
|
-
// Sibling — when import.meta.url is already .../harness/check/runtime-render/.
|
|
61
|
-
new URL(`./${WORKER_BASENAME}.js`, import.meta.url),
|
|
62
|
-
// From dist/index.js.
|
|
63
|
-
new URL(`./harness/check/runtime-render/${WORKER_BASENAME}.js`, import.meta.url),
|
|
64
|
-
// From dist/harness/index.js.
|
|
65
|
-
new URL(`./check/runtime-render/${WORKER_BASENAME}.js`, import.meta.url),
|
|
66
|
-
// From dist/check/index.js, dist/advanced/index.js, dist/<flat>.js.
|
|
67
|
-
new URL(`../harness/check/runtime-render/${WORKER_BASENAME}.js`, import.meta.url)
|
|
68
|
-
];
|
|
69
|
-
for (const jsUrl of jsCandidates) {
|
|
70
|
-
const jsPath = fileURLToPath(jsUrl);
|
|
71
|
-
if (existsSync(jsPath)) {
|
|
72
|
-
return { command: process.execPath, args: [jsPath] };
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
const tsCandidates = [
|
|
76
|
-
new URL(`./${WORKER_BASENAME}.ts`, import.meta.url),
|
|
77
|
-
new URL(`../harness/check/runtime-render/${WORKER_BASENAME}.ts`, import.meta.url)
|
|
78
|
-
];
|
|
79
|
-
for (const tsUrl of tsCandidates) {
|
|
80
|
-
const tsPath = fileURLToPath(tsUrl);
|
|
81
|
-
if (existsSync(tsPath)) {
|
|
82
|
-
const require_ = createRequire(import.meta.url);
|
|
83
|
-
const tsxLoader = pathToFileURL(require_.resolve("tsx")).href;
|
|
84
|
-
return { command: process.execPath, args: ["--import", tsxLoader, tsPath] };
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
const tried = [...jsCandidates, ...tsCandidates].map((u) => fileURLToPath(u)).join(", ");
|
|
88
|
-
throw new Error(
|
|
89
|
-
`render-check: isolated worker not found at any of: ${tried}. Did \`pnpm build\` run?`
|
|
90
|
-
);
|
|
91
|
-
}
|
|
92
|
-
function failed(reason, t0) {
|
|
93
|
-
return {
|
|
94
|
-
ok: false,
|
|
95
|
-
issues: [{ check: "render-no-throw", outcome: "failed", reason }],
|
|
96
|
-
stats: { actionsChecked: 0, streamsChecked: 0, renderMs: Date.now() - t0 }
|
|
97
|
-
};
|
|
98
|
-
}
|
|
99
|
-
function unverified(reason, t0) {
|
|
100
|
-
return {
|
|
101
|
-
ok: false,
|
|
102
|
-
issues: [{ check: "render-no-throw", outcome: "unverified", reason }],
|
|
103
|
-
stats: { actionsChecked: 0, streamsChecked: 0, renderMs: Date.now() - t0 }
|
|
104
|
-
};
|
|
105
|
-
}
|
|
106
|
-
function mapSandboxResultToCheckResult(result, t0, boundMs = DEFAULT_RENDER_CHECK_HOST_BOUNDS.timeoutMs) {
|
|
107
|
-
if (result.outcome === "timeout") {
|
|
108
|
-
return {
|
|
109
|
-
ok: false,
|
|
110
|
-
issues: [],
|
|
111
|
-
incomplete: { kind: "timeout", elapsedMs: result.durationMs, boundMs },
|
|
112
|
-
stats: { actionsChecked: 0, streamsChecked: 0, renderMs: Date.now() - t0 }
|
|
113
|
-
};
|
|
114
|
-
}
|
|
115
|
-
if (result.outcome === "overflow-stdout" || result.outcome === "overflow-stderr") {
|
|
116
|
-
return failed(
|
|
117
|
-
`render check worker produced excessive output (${result.outcome}) \u2014 the component is likely in a pathological logging or error loop.`,
|
|
118
|
-
t0
|
|
119
|
-
);
|
|
120
|
-
}
|
|
121
|
-
if (result.outcome === "spawn-error") {
|
|
122
|
-
return unverified(
|
|
123
|
-
`render check worker failed to start \u2014 ${result.errorMessage}`,
|
|
124
|
-
t0
|
|
125
|
-
);
|
|
126
|
-
}
|
|
127
|
-
if (result.outcome === "canceled") {
|
|
128
|
-
return unverified("render check worker was canceled before completing.", t0);
|
|
129
|
-
}
|
|
130
|
-
if (result.outcome !== "exit") {
|
|
131
|
-
return unverified(
|
|
132
|
-
`render check worker ended with unexpected sandbox outcome '${result.outcome}'.`,
|
|
133
|
-
t0
|
|
134
|
-
);
|
|
135
|
-
}
|
|
136
|
-
if (result.exitCode !== 0) {
|
|
137
|
-
const tail = result.stderr.trim() || result.stdout.trim();
|
|
138
|
-
return unverified(
|
|
139
|
-
`render check worker exited ${result.exitCode}${tail ? ` \u2014 ${tail.slice(0, 600)}` : ""}`,
|
|
140
|
-
t0
|
|
141
|
-
);
|
|
142
|
-
}
|
|
143
|
-
let verdict;
|
|
144
|
-
try {
|
|
145
|
-
verdict = parseVerdictLine(result.stdout);
|
|
146
|
-
} catch (err) {
|
|
147
|
-
return unverified(
|
|
148
|
-
`render check worker verdict was not valid JSON \u2014 ${err instanceof Error ? err.message : String(err)}`,
|
|
149
|
-
t0
|
|
150
|
-
);
|
|
151
|
-
}
|
|
152
|
-
if (verdict === void 0) {
|
|
153
|
-
return unverified("render check worker exited without producing a verdict.", t0);
|
|
154
|
-
}
|
|
155
|
-
return verdict;
|
|
156
|
-
}
|
|
157
|
-
async function runRenderCheckViaWorker(input, options = {}) {
|
|
158
|
-
const t0 = Date.now();
|
|
159
|
-
const bounds = resolveRenderCheckHostBounds(options.bounds);
|
|
160
|
-
let spawn;
|
|
161
|
-
try {
|
|
162
|
-
spawn = resolveWorkerSpawn();
|
|
163
|
-
} catch (err) {
|
|
164
|
-
return unverified(err instanceof Error ? err.message : String(err), t0);
|
|
165
|
-
}
|
|
166
|
-
const loadAtStart = loadavg()[0] ?? 0;
|
|
167
|
-
const result = await runSandboxed({
|
|
168
|
-
command: spawn.command,
|
|
169
|
-
args: spawn.args,
|
|
170
|
-
timeoutMs: bounds.timeoutMs,
|
|
171
|
-
gracePeriodMs: bounds.gracePeriodMs,
|
|
172
|
-
maxStdoutBytes: CHECK_STDOUT_CAP,
|
|
173
|
-
nodeHeapMb: bounds.heapMb,
|
|
174
|
-
// ggui#1398: the check learns its own deadline, so its report-only
|
|
175
|
-
// pending phase can step aside instead of pushing it past the bound.
|
|
176
|
-
stdin: JSON.stringify({ ...input, deadlineAt: t0 + bounds.timeoutMs }),
|
|
177
|
-
// NODE_ENV steers React's production vs development build — keep
|
|
178
|
-
// parity with the caller; everything else stays on the sandbox's
|
|
179
|
-
// default allowlist.
|
|
180
|
-
env: { NODE_ENV: process.env.NODE_ENV ?? "production" }
|
|
181
|
-
});
|
|
182
|
-
const hostLoad = {
|
|
183
|
-
start: loadAtStart,
|
|
184
|
-
end: loadavg()[0] ?? 0,
|
|
185
|
-
cores: availableParallelism()
|
|
186
|
-
};
|
|
187
|
-
const mapped = mapSandboxResultToCheckResult(result, t0, bounds.timeoutMs);
|
|
188
|
-
return { ...mapped, stats: { ...mapped.stats, hostLoad } };
|
|
189
|
-
}
|
|
190
|
-
var DEFAULT_RENDER_CHECK_HOST_BOUNDS, CHECK_STDOUT_CAP, WORKER_BASENAME;
|
|
191
|
-
var init_render_check_host = __esm({
|
|
192
|
-
"src/harness/check/runtime-render/render-check-host.ts"() {
|
|
193
|
-
init_verdict_line();
|
|
194
|
-
DEFAULT_RENDER_CHECK_HOST_BOUNDS = {
|
|
195
|
-
timeoutMs: 3e4,
|
|
196
|
-
heapMb: 512,
|
|
197
|
-
gracePeriodMs: 2e3
|
|
198
|
-
};
|
|
199
|
-
CHECK_STDOUT_CAP = 2 * 1024 * 1024;
|
|
200
|
-
WORKER_BASENAME = "render-check-worker";
|
|
201
|
-
}
|
|
202
|
-
});
|
|
203
|
-
var installed = false;
|
|
204
|
-
function installProductionActShim() {
|
|
205
|
-
if (installed) return;
|
|
206
|
-
installed = true;
|
|
207
|
-
const require_ = createRequire(import.meta.url);
|
|
208
|
-
let reactCjs;
|
|
209
|
-
try {
|
|
210
|
-
reactCjs = require_("react");
|
|
211
|
-
} catch {
|
|
212
|
-
return;
|
|
213
|
-
}
|
|
214
|
-
if (typeof reactCjs.act === "function") {
|
|
215
|
-
return;
|
|
216
|
-
}
|
|
217
|
-
try {
|
|
218
|
-
const testUtils = require_("react-dom/test-utils");
|
|
219
|
-
const reactDom = require_("react-dom");
|
|
220
|
-
testUtils.act = createFlushSyncAct(reactDom.flushSync);
|
|
221
|
-
} catch {
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
function createFlushSyncAct(flushSync) {
|
|
225
|
-
return (callback) => {
|
|
226
|
-
let callbackResult;
|
|
227
|
-
flushSync(() => {
|
|
228
|
-
callbackResult = callback();
|
|
229
|
-
});
|
|
230
|
-
if (isThenable(callbackResult)) {
|
|
231
|
-
const pending = callbackResult;
|
|
232
|
-
return new Promise((resolve, reject) => {
|
|
233
|
-
pending.then(() => resolve(), reject);
|
|
234
|
-
});
|
|
235
|
-
}
|
|
236
|
-
return new Promise((resolve) => {
|
|
237
|
-
queueMicrotask(resolve);
|
|
238
|
-
});
|
|
239
|
-
};
|
|
240
|
-
}
|
|
241
|
-
function isThenable(value) {
|
|
242
|
-
return typeof value === "object" && value !== null && typeof value.then === "function";
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
// src/harness/check/runtime-render/probe.ts
|
|
246
|
-
function makeInternals() {
|
|
247
|
-
return {
|
|
248
|
-
fireLog: [],
|
|
249
|
-
streamHandlers: /* @__PURE__ */ new Map(),
|
|
250
|
-
registeredStreams: /* @__PURE__ */ new Set()
|
|
251
|
-
};
|
|
252
|
-
}
|
|
253
|
-
function createProbe() {
|
|
254
|
-
let internals = makeInternals();
|
|
255
|
-
const probe = {
|
|
256
|
-
emitStream: (eventName, payload) => {
|
|
257
|
-
const handlers = internals.streamHandlers.get(eventName);
|
|
258
|
-
if (!handlers) return;
|
|
259
|
-
for (const handler of handlers) handler(payload);
|
|
260
|
-
},
|
|
261
|
-
getFireLog: () => internals.fireLog.slice(),
|
|
262
|
-
getRegistered: () => ({
|
|
263
|
-
streams: Array.from(internals.registeredStreams)
|
|
264
|
-
}),
|
|
265
|
-
fired: (actionName) => internals.fireLog.some((e) => e.kind === "action.fired" && e.name === actionName),
|
|
266
|
-
installPostMessageSpy: () => installPostMessageSpy(probe),
|
|
267
|
-
reset: () => {
|
|
268
|
-
internals = makeInternals();
|
|
269
|
-
probe.__internals = internals;
|
|
270
|
-
},
|
|
271
|
-
__internals: internals
|
|
272
|
-
};
|
|
273
|
-
return probe;
|
|
274
|
-
}
|
|
275
|
-
function installPostMessageSpy(probe) {
|
|
276
|
-
const parent = globalThis.window?.parent;
|
|
277
|
-
if (!parent || typeof parent.postMessage !== "function") {
|
|
278
|
-
return () => {
|
|
279
|
-
};
|
|
280
|
-
}
|
|
281
|
-
const original = parent.postMessage;
|
|
282
|
-
const spy = (...args) => {
|
|
283
|
-
const envelope = args[0];
|
|
284
|
-
recordEnvelope(probe.__internals, envelope);
|
|
285
|
-
return original.apply(parent, args);
|
|
286
|
-
};
|
|
287
|
-
try {
|
|
288
|
-
Object.defineProperty(parent, "postMessage", {
|
|
289
|
-
value: spy,
|
|
290
|
-
configurable: true,
|
|
291
|
-
writable: true
|
|
292
|
-
});
|
|
293
|
-
} catch {
|
|
294
|
-
return () => {
|
|
295
|
-
};
|
|
296
|
-
}
|
|
297
|
-
return () => {
|
|
298
|
-
try {
|
|
299
|
-
Object.defineProperty(parent, "postMessage", {
|
|
300
|
-
value: original,
|
|
301
|
-
configurable: true,
|
|
302
|
-
writable: true
|
|
303
|
-
});
|
|
304
|
-
} catch {
|
|
305
|
-
}
|
|
306
|
-
};
|
|
307
|
-
}
|
|
308
|
-
function recordEnvelope(internals, envelope) {
|
|
309
|
-
if (!envelope || typeof envelope !== "object") return;
|
|
310
|
-
const e = envelope;
|
|
311
|
-
if (typeof e.method !== "string") return;
|
|
312
|
-
const params = e.params ?? {};
|
|
313
|
-
switch (e.method) {
|
|
314
|
-
case "ui/open-link": {
|
|
315
|
-
const url = typeof params.url === "string" ? params.url : "";
|
|
316
|
-
internals.fireLog.push({
|
|
317
|
-
kind: "link.opened",
|
|
318
|
-
url,
|
|
319
|
-
ts: Date.now()
|
|
320
|
-
});
|
|
321
|
-
return;
|
|
322
|
-
}
|
|
323
|
-
case "ui/request-display-mode": {
|
|
324
|
-
const mode = typeof params.mode === "string" ? params.mode : "";
|
|
325
|
-
internals.fireLog.push({
|
|
326
|
-
kind: "displayMode.requested",
|
|
327
|
-
mode,
|
|
328
|
-
ts: Date.now()
|
|
329
|
-
});
|
|
330
|
-
return;
|
|
331
|
-
}
|
|
332
|
-
case "tools/call": {
|
|
333
|
-
return;
|
|
334
|
-
}
|
|
335
|
-
default:
|
|
336
|
-
return;
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
function createProbeWireConfig(probe) {
|
|
340
|
-
return {
|
|
341
|
-
app: {
|
|
342
|
-
appId: "probe-app",
|
|
343
|
-
appName: "Probe",
|
|
344
|
-
appDescription: "Eval-time probe wire config"
|
|
345
|
-
},
|
|
346
|
-
render: {
|
|
347
|
-
sessionId: "probe-render",
|
|
348
|
-
isConnected: true
|
|
349
|
-
},
|
|
350
|
-
auth: {
|
|
351
|
-
isAuthenticated: false
|
|
352
|
-
},
|
|
353
|
-
dispatch: (actionName, data) => {
|
|
354
|
-
probe.__internals.fireLog.push({
|
|
355
|
-
kind: "action.fired",
|
|
356
|
-
name: actionName,
|
|
357
|
-
payload: data,
|
|
358
|
-
ts: Date.now()
|
|
359
|
-
});
|
|
360
|
-
},
|
|
361
|
-
subscribe: (eventType, handler) => {
|
|
362
|
-
probe.__internals.registeredStreams.add(eventType);
|
|
363
|
-
let set = probe.__internals.streamHandlers.get(eventType);
|
|
364
|
-
if (!set) {
|
|
365
|
-
set = /* @__PURE__ */ new Set();
|
|
366
|
-
probe.__internals.streamHandlers.set(eventType, set);
|
|
367
|
-
}
|
|
368
|
-
const wrapped = handler;
|
|
369
|
-
set.add(wrapped);
|
|
370
|
-
return () => {
|
|
371
|
-
set.delete(wrapped);
|
|
372
|
-
};
|
|
373
|
-
}
|
|
374
|
-
// The WireConfig surface is dispatch + subscribe only. Capability
|
|
375
|
-
// hooks live in `@ggui-ai/gadgets` and own their own lifecycle —
|
|
376
|
-
// they never route through WireConfig.
|
|
377
|
-
};
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
// src/internal/evaluate-component-source.ts
|
|
381
|
-
async function evaluateComponentSource(sourceCode, sandboxRequire) {
|
|
382
|
-
const esbuild = await import('esbuild');
|
|
383
|
-
const { Script } = await import('vm');
|
|
384
|
-
const cjsResult = await esbuild.transform(sourceCode, {
|
|
385
|
-
loader: "tsx",
|
|
386
|
-
target: "es2020",
|
|
387
|
-
format: "cjs",
|
|
388
|
-
jsx: "automatic",
|
|
389
|
-
jsxImportSource: "react",
|
|
390
|
-
sourcefile: "Component.tsx"
|
|
391
|
-
});
|
|
392
|
-
const sandboxModule = { exports: {} };
|
|
393
|
-
const wrappedCode = `(function(require, exports, module) {
|
|
394
|
-
${cjsResult.code}
|
|
395
|
-
})`;
|
|
396
|
-
const script = new Script(wrappedCode, { filename: "Component.cjs" });
|
|
397
|
-
const fn = script.runInThisContext();
|
|
398
|
-
fn(sandboxRequire, sandboxModule.exports, sandboxModule);
|
|
399
|
-
return sandboxModule.exports;
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
// src/harness/check/runtime-render/load-component.ts
|
|
403
|
-
async function loadComponent(input) {
|
|
404
|
-
const { sourceCode, moduleResolutions = {} } = input;
|
|
405
|
-
const { createRequire: createRequire3 } = await import('module');
|
|
406
|
-
const require_ = createRequire3(import.meta.url);
|
|
407
|
-
const sandboxRequire = (id) => {
|
|
408
|
-
if (id in moduleResolutions) return moduleResolutions[id];
|
|
409
|
-
if (id === "react/jsx-runtime" || id === "react/jsx-dev-runtime") {
|
|
410
|
-
return require_("react/jsx-runtime");
|
|
411
|
-
}
|
|
412
|
-
if (id === "react") return require_("react");
|
|
413
|
-
if (id === "react-dom" || id.startsWith("react-dom/")) {
|
|
414
|
-
return require_(id);
|
|
415
|
-
}
|
|
416
|
-
if (id === "@ggui-ai/wire") return require_("@ggui-ai/wire");
|
|
417
|
-
if (id.startsWith("@ggui-ai/design")) {
|
|
418
|
-
try {
|
|
419
|
-
return require_(id);
|
|
420
|
-
} catch {
|
|
421
|
-
throw new Error(
|
|
422
|
-
`Required dependency not available in render-check sandbox: ${id}. Install @ggui-ai/design or run render-check from an environment that has it.`
|
|
423
|
-
);
|
|
424
|
-
}
|
|
425
|
-
}
|
|
426
|
-
if (id === "@ggui-ai/gadgets") {
|
|
427
|
-
try {
|
|
428
|
-
return require_("@ggui-ai/gadgets");
|
|
429
|
-
} catch {
|
|
430
|
-
throw new Error(
|
|
431
|
-
`Required dependency not available in render-check sandbox: ${id}. Install @ggui-ai/gadgets or pass it through moduleResolutions.`
|
|
432
|
-
);
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
throw new Error(`Import not allowed in render-check sandbox: ${id}`);
|
|
436
|
-
};
|
|
437
|
-
const exportsRecord = await evaluateComponentSource(sourceCode, sandboxRequire);
|
|
438
|
-
const candidate = exportsRecord.default;
|
|
439
|
-
if (typeof candidate !== "function") {
|
|
440
|
-
throw new Error("Compiled component has no default-exported function");
|
|
441
|
-
}
|
|
442
|
-
return { Component: candidate };
|
|
443
|
-
}
|
|
444
|
-
var NATIVE_CLICK_PROPS = /* @__PURE__ */ new Set(["onClick"]);
|
|
445
|
-
var NATIVE_SUBMIT_PROPS = /* @__PURE__ */ new Set(["onSubmit"]);
|
|
446
|
-
var NATIVE_CHANGE_PROPS = /* @__PURE__ */ new Set(["onChange"]);
|
|
447
|
-
var NATIVE_KEY_PROPS = /* @__PURE__ */ new Set(["onKeyDown", "onKeyUp", "onKeyPress"]);
|
|
448
|
-
var HOST_CLICK_TAGS = /* @__PURE__ */ new Set(["button", "a", "div", "span", "li", "input"]);
|
|
449
|
-
var HOST_CHANGE_TAGS = /* @__PURE__ */ new Set(["select", "input", "textarea"]);
|
|
450
|
-
function findWiring(input) {
|
|
451
|
-
const { sourceCode, hookName, hookArg } = input;
|
|
452
|
-
const sf = ts.createSourceFile("Component.tsx", sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
|
|
453
|
-
const hookBindings = findHookBindings(sf, hookName, hookArg);
|
|
454
|
-
if (hookBindings.length === 0) {
|
|
455
|
-
return {
|
|
456
|
-
kind: "missing",
|
|
457
|
-
reason: `Hook ${hookName}('${hookArg}') is not destructured in the component`,
|
|
458
|
-
observedJsxElements: [],
|
|
459
|
-
observedNativeProps: [],
|
|
460
|
-
observedCustomProps: []
|
|
461
|
-
};
|
|
462
|
-
}
|
|
463
|
-
const callableNames = expandAliases(sf, hookBindings);
|
|
464
|
-
const observedJsxElements = [];
|
|
465
|
-
const observedNativeProps = [];
|
|
466
|
-
const observedCustomProps = [];
|
|
467
|
-
let sawClickOnHost = false;
|
|
468
|
-
let sawSubmitOnForm = false;
|
|
469
|
-
let sawChangeOnNativeInput = false;
|
|
470
|
-
let sawKeyOnAnything = false;
|
|
471
|
-
let sawNonNativeProp = false;
|
|
472
|
-
const customPropElements = [];
|
|
473
|
-
const submitButton = { found: false };
|
|
474
|
-
function visit(node) {
|
|
475
|
-
if (ts.isJsxAttribute(node) && node.initializer) {
|
|
476
|
-
const attrName = node.name.getText(sf);
|
|
477
|
-
const initRefs = findReferencedNames(node.initializer, callableNames);
|
|
478
|
-
if (initRefs.size > 0) {
|
|
479
|
-
const parent = node.parent.parent;
|
|
480
|
-
const tagName = getTagName(parent);
|
|
481
|
-
if (tagName) observedJsxElements.push(tagName);
|
|
482
|
-
const isHostTag = isHostElementTag(tagName ?? "");
|
|
483
|
-
if (NATIVE_CLICK_PROPS.has(attrName)) {
|
|
484
|
-
observedNativeProps.push(attrName);
|
|
485
|
-
if (isHostTag && (HOST_CLICK_TAGS.has((tagName ?? "").toLowerCase()) || tagName === "button")) {
|
|
486
|
-
sawClickOnHost = true;
|
|
487
|
-
} else {
|
|
488
|
-
sawClickOnHost = true;
|
|
489
|
-
}
|
|
490
|
-
} else if (NATIVE_SUBMIT_PROPS.has(attrName)) {
|
|
491
|
-
observedNativeProps.push(attrName);
|
|
492
|
-
if ((tagName ?? "").toLowerCase() === "form") sawSubmitOnForm = true;
|
|
493
|
-
else sawSubmitOnForm = true;
|
|
494
|
-
} else if (NATIVE_CHANGE_PROPS.has(attrName)) {
|
|
495
|
-
observedNativeProps.push(attrName);
|
|
496
|
-
if (HOST_CHANGE_TAGS.has((tagName ?? "").toLowerCase())) {
|
|
497
|
-
sawChangeOnNativeInput = true;
|
|
498
|
-
} else {
|
|
499
|
-
observedCustomProps.push(attrName);
|
|
500
|
-
sawNonNativeProp = true;
|
|
501
|
-
if (tagName) customPropElements.push(tagName);
|
|
502
|
-
}
|
|
503
|
-
} else if (NATIVE_KEY_PROPS.has(attrName)) {
|
|
504
|
-
observedNativeProps.push(attrName);
|
|
505
|
-
sawKeyOnAnything = true;
|
|
506
|
-
} else {
|
|
507
|
-
observedCustomProps.push(attrName);
|
|
508
|
-
sawNonNativeProp = true;
|
|
509
|
-
if (tagName) customPropElements.push(tagName);
|
|
510
|
-
}
|
|
511
|
-
}
|
|
512
|
-
}
|
|
513
|
-
if (ts.isJsxOpeningLikeElement(node)) {
|
|
514
|
-
const tag = getTagName(node);
|
|
515
|
-
if ((tag ?? "").toLowerCase() === "button") {
|
|
516
|
-
const typeAttr = node.attributes.properties.find(
|
|
517
|
-
(p) => ts.isJsxAttribute(p) && p.name.getText(sf) === "type"
|
|
518
|
-
);
|
|
519
|
-
if (typeAttr && ts.isJsxAttribute(typeAttr) && typeAttr.initializer) {
|
|
520
|
-
const v = typeAttr.initializer.getText(sf).toLowerCase();
|
|
521
|
-
if (v.includes("submit")) submitButton.found = true;
|
|
522
|
-
}
|
|
523
|
-
}
|
|
524
|
-
}
|
|
525
|
-
ts.forEachChild(node, visit);
|
|
526
|
-
}
|
|
527
|
-
visit(sf);
|
|
528
|
-
if (sawClickOnHost) {
|
|
529
|
-
return wrapDetection("click", observedJsxElements, observedNativeProps, observedCustomProps);
|
|
530
|
-
}
|
|
531
|
-
if (sawSubmitOnForm || submitButton.found) {
|
|
532
|
-
return wrapDetection("submit", observedJsxElements, observedNativeProps, observedCustomProps);
|
|
533
|
-
}
|
|
534
|
-
if (sawChangeOnNativeInput) {
|
|
535
|
-
return wrapDetection("change", observedJsxElements, observedNativeProps, observedCustomProps);
|
|
536
|
-
}
|
|
537
|
-
if (sawKeyOnAnything) {
|
|
538
|
-
return wrapDetection("keyboard-enter", observedJsxElements, observedNativeProps, observedCustomProps);
|
|
539
|
-
}
|
|
540
|
-
if (sawNonNativeProp) {
|
|
541
|
-
const props = Array.from(new Set(observedCustomProps)).join(", ");
|
|
542
|
-
const tags = Array.from(new Set(customPropElements)).join(", ");
|
|
543
|
-
return {
|
|
544
|
-
kind: "unverified",
|
|
545
|
-
reason: `Source indicates non-click or non-native wiring; static probe did not verify execution deterministically. Callback flows into ${props} on <${tags}>.`,
|
|
546
|
-
observedJsxElements: dedupe(observedJsxElements),
|
|
547
|
-
observedNativeProps: dedupe(observedNativeProps),
|
|
548
|
-
observedCustomProps: dedupe(observedCustomProps)
|
|
549
|
-
};
|
|
550
|
-
}
|
|
551
|
-
return {
|
|
552
|
-
kind: "missing",
|
|
553
|
-
reason: `Hook ${hookName}('${hookArg}') is destructured but never referenced in any JSX attribute`,
|
|
554
|
-
observedJsxElements: [],
|
|
555
|
-
observedNativeProps: [],
|
|
556
|
-
observedCustomProps: []
|
|
557
|
-
};
|
|
558
|
-
}
|
|
559
|
-
function findHookBindings(sf, hookName, hookArg) {
|
|
560
|
-
const bindings = [];
|
|
561
|
-
function visit(node) {
|
|
562
|
-
if (ts.isVariableDeclaration(node) && node.initializer && ts.isCallExpression(node.initializer)) {
|
|
563
|
-
const call = node.initializer;
|
|
564
|
-
const callee = call.expression.getText(sf);
|
|
565
|
-
if (callee !== hookName) {
|
|
566
|
-
ts.forEachChild(node, visit);
|
|
567
|
-
return;
|
|
568
|
-
}
|
|
569
|
-
const firstArg = call.arguments[0];
|
|
570
|
-
if (!firstArg || !ts.isStringLiteral(firstArg)) {
|
|
571
|
-
ts.forEachChild(node, visit);
|
|
572
|
-
return;
|
|
573
|
-
}
|
|
574
|
-
if (firstArg.text !== hookArg) {
|
|
575
|
-
ts.forEachChild(node, visit);
|
|
576
|
-
return;
|
|
577
|
-
}
|
|
578
|
-
if (ts.isIdentifier(node.name)) {
|
|
579
|
-
bindings.push(node.name.text);
|
|
580
|
-
} else if (ts.isObjectBindingPattern(node.name)) {
|
|
581
|
-
for (const elem of node.name.elements) {
|
|
582
|
-
const target = elem.name;
|
|
583
|
-
if (ts.isIdentifier(target)) bindings.push(target.text);
|
|
584
|
-
}
|
|
585
|
-
}
|
|
586
|
-
}
|
|
587
|
-
ts.forEachChild(node, visit);
|
|
588
|
-
}
|
|
589
|
-
visit(sf);
|
|
590
|
-
return bindings;
|
|
591
|
-
}
|
|
592
|
-
function expandAliases(sf, bindings) {
|
|
593
|
-
const all = new Set(bindings);
|
|
594
|
-
function visit(node) {
|
|
595
|
-
if (ts.isVariableDeclaration(node) && node.initializer && ts.isIdentifier(node.name)) {
|
|
596
|
-
const newName = node.name.text;
|
|
597
|
-
const init = node.initializer;
|
|
598
|
-
if (ts.isIdentifier(init) && all.has(init.text)) {
|
|
599
|
-
all.add(newName);
|
|
600
|
-
}
|
|
601
|
-
if (ts.isArrowFunction(init) || ts.isFunctionExpression(init)) {
|
|
602
|
-
if (bodyReferences(init.body, all)) {
|
|
603
|
-
all.add(newName);
|
|
604
|
-
}
|
|
605
|
-
}
|
|
606
|
-
if (ts.isCallExpression(init)) {
|
|
607
|
-
if (bodyReferences(init, all)) {
|
|
608
|
-
all.add(newName);
|
|
609
|
-
}
|
|
610
|
-
}
|
|
611
|
-
}
|
|
612
|
-
ts.forEachChild(node, visit);
|
|
613
|
-
}
|
|
614
|
-
visit(sf);
|
|
615
|
-
return all;
|
|
616
|
-
}
|
|
617
|
-
function bodyReferences(body, names) {
|
|
618
|
-
let found = false;
|
|
619
|
-
function walk(n) {
|
|
620
|
-
if (found) return;
|
|
621
|
-
if (ts.isIdentifier(n) && names.has(n.text)) {
|
|
622
|
-
found = true;
|
|
623
|
-
return;
|
|
624
|
-
}
|
|
625
|
-
ts.forEachChild(n, walk);
|
|
626
|
-
}
|
|
627
|
-
walk(body);
|
|
628
|
-
return found;
|
|
629
|
-
}
|
|
630
|
-
function findReferencedNames(node, names) {
|
|
631
|
-
const found = /* @__PURE__ */ new Set();
|
|
632
|
-
function walk(n) {
|
|
633
|
-
if (ts.isIdentifier(n) && names.has(n.text)) {
|
|
634
|
-
found.add(n.text);
|
|
635
|
-
}
|
|
636
|
-
ts.forEachChild(n, walk);
|
|
637
|
-
}
|
|
638
|
-
walk(node);
|
|
639
|
-
return found;
|
|
640
|
-
}
|
|
641
|
-
function getTagName(el) {
|
|
642
|
-
if (ts.isJsxOpeningLikeElement(el)) {
|
|
643
|
-
return el.tagName.getText();
|
|
644
|
-
}
|
|
645
|
-
return null;
|
|
646
|
-
}
|
|
647
|
-
function isHostElementTag(name) {
|
|
648
|
-
if (!name) return false;
|
|
649
|
-
return name[0] === name[0].toLowerCase();
|
|
650
|
-
}
|
|
651
|
-
function wrapDetection(kind, jsx, native, custom) {
|
|
652
|
-
return {
|
|
653
|
-
kind,
|
|
654
|
-
observedJsxElements: dedupe(jsx),
|
|
655
|
-
observedNativeProps: dedupe(native),
|
|
656
|
-
observedCustomProps: dedupe(custom)
|
|
657
|
-
};
|
|
658
|
-
}
|
|
659
|
-
function dedupe(arr) {
|
|
660
|
-
return Array.from(new Set(arr));
|
|
661
|
-
}
|
|
662
|
-
|
|
663
|
-
// src/internal/open-record.ts
|
|
664
|
-
function openRecord(o) {
|
|
665
|
-
return o;
|
|
666
|
-
}
|
|
667
|
-
function hostGlobals() {
|
|
668
|
-
return openRecord(globalThis);
|
|
669
|
-
}
|
|
670
|
-
|
|
671
|
-
// src/harness/check/runtime-render/host-boundary.ts
|
|
672
|
-
function getMethod(o, name) {
|
|
673
|
-
if (typeof o !== "object" && typeof o !== "function") return void 0;
|
|
674
|
-
if (o === null) return void 0;
|
|
675
|
-
const v = openRecord(o)[name];
|
|
676
|
-
if (typeof v !== "function") return void 0;
|
|
677
|
-
return v.bind(o);
|
|
678
|
-
}
|
|
679
|
-
function toMinimalElement(v) {
|
|
680
|
-
if (typeof v === "object" && v !== null && typeof openRecord(v).tagName === "string" && typeof openRecord(v).getAttribute === "function" && typeof openRecord(v).querySelectorAll === "function") {
|
|
681
|
-
return v;
|
|
682
|
-
}
|
|
683
|
-
throw new Error(
|
|
684
|
-
"[runtime-render] host value lacks the element surface (tagName/getAttribute/querySelectorAll)"
|
|
685
|
-
);
|
|
686
|
-
}
|
|
687
|
-
function buildClickUser(userEventModule, doc) {
|
|
688
|
-
const setup = getMethod(userEventModule, "setup");
|
|
689
|
-
const instance = setup ? setup({ document: doc }) : userEventModule;
|
|
690
|
-
const click = getMethod(instance, "click");
|
|
691
|
-
if (!click) {
|
|
692
|
-
throw new Error(
|
|
693
|
-
"[runtime-render] user-event module exposes neither setup().click nor click()"
|
|
694
|
-
);
|
|
695
|
-
}
|
|
696
|
-
return {
|
|
697
|
-
click: async (el) => {
|
|
698
|
-
await click(el);
|
|
699
|
-
}
|
|
700
|
-
};
|
|
701
|
-
}
|
|
702
|
-
function toEventDispatcher(el) {
|
|
703
|
-
const dispatch = getMethod(el, "dispatchEvent");
|
|
704
|
-
if (!dispatch) {
|
|
705
|
-
throw new Error(
|
|
706
|
-
`[runtime-render] element <${el.tagName.toLowerCase()}> has no dispatchEvent`
|
|
707
|
-
);
|
|
708
|
-
}
|
|
709
|
-
return {
|
|
710
|
-
dispatchEvent: (ev) => {
|
|
711
|
-
dispatch(ev);
|
|
712
|
-
}
|
|
713
|
-
};
|
|
714
|
-
}
|
|
715
|
-
function editableSurface(el) {
|
|
716
|
-
return el;
|
|
717
|
-
}
|
|
718
|
-
function parentLink(node) {
|
|
719
|
-
return node;
|
|
720
|
-
}
|
|
721
|
-
|
|
722
|
-
// src/harness/check/runtime-render/probe-pending.ts
|
|
723
|
-
function createProbePendingSource() {
|
|
724
|
-
const pending = /* @__PURE__ */ new Set();
|
|
725
|
-
const listeners = /* @__PURE__ */ new Set();
|
|
726
|
-
const notify = () => {
|
|
727
|
-
for (const listener of [...listeners]) listener();
|
|
728
|
-
};
|
|
729
|
-
return {
|
|
730
|
-
isPending: (actionName) => pending.has(actionName),
|
|
731
|
-
subscribe: (listener) => {
|
|
732
|
-
listeners.add(listener);
|
|
733
|
-
return () => {
|
|
734
|
-
listeners.delete(listener);
|
|
735
|
-
};
|
|
736
|
-
},
|
|
737
|
-
mark: (actionName) => {
|
|
738
|
-
pending.add(actionName);
|
|
739
|
-
notify();
|
|
740
|
-
},
|
|
741
|
-
clear: () => {
|
|
742
|
-
if (pending.size === 0) return;
|
|
743
|
-
pending.clear();
|
|
744
|
-
notify();
|
|
745
|
-
}
|
|
746
|
-
};
|
|
747
|
-
}
|
|
748
|
-
function lookOf(el) {
|
|
749
|
-
return {
|
|
750
|
-
connected: el.isConnected !== false,
|
|
751
|
-
// React renders `disabled={true}` as `disabled=""`, so presence is the test.
|
|
752
|
-
disabled: el.getAttribute("disabled") !== null || el.getAttribute("aria-disabled") === "true",
|
|
753
|
-
ariaBusy: el.getAttribute("aria-busy") === "true",
|
|
754
|
-
text: el.tagName?.toLowerCase() === "input" ? String(typeof el.value === "string" ? el.value : el.getAttribute("value") ?? "") : el.textContent ?? ""
|
|
755
|
-
};
|
|
756
|
-
}
|
|
757
|
-
async function settlesWithin(ms, work) {
|
|
758
|
-
let timer;
|
|
759
|
-
const expired = new Promise((resolve) => {
|
|
760
|
-
timer = setTimeout(() => resolve(false), Math.max(0, ms));
|
|
761
|
-
});
|
|
762
|
-
try {
|
|
763
|
-
return await Promise.race([work.then(() => true), expired]);
|
|
764
|
-
} finally {
|
|
765
|
-
if (timer !== void 0) clearTimeout(timer);
|
|
766
|
-
}
|
|
767
|
-
}
|
|
768
|
-
function lookChanged(before, after) {
|
|
769
|
-
if (before.connected && !after.connected) return true;
|
|
770
|
-
if (!before.disabled && after.disabled) return true;
|
|
771
|
-
if (!before.ariaBusy && after.ariaBusy) return true;
|
|
772
|
-
return before.text.trim() !== after.text.trim();
|
|
773
|
-
}
|
|
774
|
-
|
|
775
|
-
// src/harness/check/runtime-render/walk.ts
|
|
776
|
-
var LAST_PRESSED_KEPT = 4;
|
|
777
|
-
function isDisabled(el) {
|
|
778
|
-
return el.getAttribute("disabled") !== null || el.getAttribute("aria-disabled") === "true";
|
|
779
|
-
}
|
|
780
|
-
function labelOf(el) {
|
|
781
|
-
const named = el.getAttribute("aria-label") ?? el.getAttribute("placeholder");
|
|
782
|
-
return (named ?? el.textContent ?? "").replace(/\s+/g, " ").trim().slice(0, 60);
|
|
783
|
-
}
|
|
784
|
-
function screenFingerprint(container) {
|
|
785
|
-
const parts = [];
|
|
786
|
-
for (const el of container.querySelectorAll(
|
|
787
|
-
'button, [role="button"], a[href], input, select, textarea, form'
|
|
788
|
-
)) {
|
|
789
|
-
parts.push(
|
|
790
|
-
[el.tagName.toLowerCase(), el.getAttribute("type") ?? "", labelOf(el), isDisabled(el) ? "disabled" : ""].join("|")
|
|
791
|
-
);
|
|
792
|
-
}
|
|
793
|
-
return parts.join("\n");
|
|
794
|
-
}
|
|
795
|
-
function keyed(controls, describe) {
|
|
796
|
-
const seen = /* @__PURE__ */ new Map();
|
|
797
|
-
const out = /* @__PURE__ */ new Map();
|
|
798
|
-
for (const c of controls) {
|
|
799
|
-
const d = describe(c.element);
|
|
800
|
-
const n = seen.get(d) ?? 0;
|
|
801
|
-
seen.set(d, n + 1);
|
|
802
|
-
out.set(`${d}#${n}`, c);
|
|
803
|
-
}
|
|
804
|
-
return out;
|
|
805
|
-
}
|
|
806
|
-
function firstStepToUnpressed(screens, from) {
|
|
807
|
-
const firstStep = /* @__PURE__ */ new Map([[from, void 0]]);
|
|
808
|
-
const queue = [from];
|
|
809
|
-
while (queue.length > 0) {
|
|
810
|
-
const at = queue.shift();
|
|
811
|
-
const screen = screens.get(at);
|
|
812
|
-
if (screen === void 0) continue;
|
|
813
|
-
if (at !== from && screen.order.some((k) => !screen.led.has(k))) return firstStep.get(at);
|
|
814
|
-
for (const [key, to] of screen.led) {
|
|
815
|
-
if (to === at || firstStep.has(to)) continue;
|
|
816
|
-
firstStep.set(to, firstStep.get(at) ?? key);
|
|
817
|
-
queue.push(to);
|
|
818
|
-
}
|
|
819
|
-
}
|
|
820
|
-
return void 0;
|
|
821
|
-
}
|
|
822
|
-
async function walkToAction(input) {
|
|
823
|
-
const lastPressed = [];
|
|
824
|
-
const screenTexts = [];
|
|
825
|
-
const screens = /* @__PURE__ */ new Map();
|
|
826
|
-
let presses = 0;
|
|
827
|
-
let namedControlSeen = false;
|
|
828
|
-
const outcome = (stoppedBy, extra = {}) => ({
|
|
829
|
-
fired: false,
|
|
830
|
-
presses,
|
|
831
|
-
screens: screens.size,
|
|
832
|
-
stoppedBy,
|
|
833
|
-
namedControlSeen,
|
|
834
|
-
lastPressed: [...lastPressed],
|
|
835
|
-
screenTexts: [...screenTexts],
|
|
836
|
-
...extra
|
|
837
|
-
});
|
|
838
|
-
const mounted = await input.mount();
|
|
839
|
-
if (mounted === void 0) return outcome("no-mount");
|
|
840
|
-
const { container } = mounted;
|
|
841
|
-
const arrive = () => {
|
|
842
|
-
mounted.prime();
|
|
843
|
-
const controls = keyed(input.controls(container), input.describe);
|
|
844
|
-
if ([...controls.values()].some((c) => c.namesAction)) namedControlSeen = true;
|
|
845
|
-
const id = screenFingerprint(container);
|
|
846
|
-
if (!screens.has(id)) {
|
|
847
|
-
screens.set(id, { order: [...controls.keys()], led: /* @__PURE__ */ new Map() });
|
|
848
|
-
screenTexts.push(container.textContent ?? "");
|
|
849
|
-
}
|
|
850
|
-
return { id, controls };
|
|
851
|
-
};
|
|
852
|
-
let here = arrive();
|
|
853
|
-
const first = here.id;
|
|
854
|
-
for (; ; ) {
|
|
855
|
-
const screen = screens.get(here.id);
|
|
856
|
-
const onOpenFirstScreen = here.id === first && screen.order.some((k) => !screen.led.has(k));
|
|
857
|
-
if (!onOpenFirstScreen) {
|
|
858
|
-
if (presses >= input.maxPresses) return outcome("press-cap");
|
|
859
|
-
if (Date.now() >= input.stopAt) return outcome("deadline");
|
|
860
|
-
}
|
|
861
|
-
const key = screen.order.find((k) => !screen.led.has(k)) ?? firstStepToUnpressed(screens, here.id);
|
|
862
|
-
if (key === void 0) return outcome("explored");
|
|
863
|
-
const control = here.controls.get(key);
|
|
864
|
-
if (control === void 0) {
|
|
865
|
-
screen.led.set(key, here.id);
|
|
866
|
-
continue;
|
|
867
|
-
}
|
|
868
|
-
presses += 1;
|
|
869
|
-
lastPressed.push(input.describe(control.element));
|
|
870
|
-
if (lastPressed.length > LAST_PRESSED_KEPT) lastPressed.shift();
|
|
871
|
-
if (await input.press(control)) {
|
|
872
|
-
return outcome("dispatched", { fired: true, element: control.element, container });
|
|
873
|
-
}
|
|
874
|
-
here = arrive();
|
|
875
|
-
if (!screen.led.has(key)) screen.led.set(key, here.id);
|
|
876
|
-
}
|
|
877
|
-
}
|
|
878
|
-
|
|
879
|
-
// src/harness/check/runtime-render/render-check.ts
|
|
880
|
-
var PENDING_PHASE_RESERVE_MS = 5e3;
|
|
881
|
-
var WALK_MAX_PRESSES = 60;
|
|
882
|
-
var WALK_ACTION_CAP_MS = 3e3;
|
|
883
|
-
var WALK_DEADLINE_MARGIN_MS = 2e3;
|
|
884
|
-
async function runRenderCheck(input, options = {}) {
|
|
885
|
-
const g = hostGlobals();
|
|
886
|
-
if ("window" in g && "document" in g) {
|
|
887
|
-
return runRenderCheckInProcess(input);
|
|
888
|
-
}
|
|
889
|
-
const { runRenderCheckViaWorker: runRenderCheckViaWorker2 } = await Promise.resolve().then(() => (init_render_check_host(), render_check_host_exports));
|
|
890
|
-
return runRenderCheckViaWorker2(input, options);
|
|
891
|
-
}
|
|
892
|
-
async function runRenderCheckInProcess(input) {
|
|
893
|
-
const t0 = Date.now();
|
|
894
|
-
const issues = [];
|
|
895
|
-
const teardown = await setupHappyDom();
|
|
896
|
-
let uninstallSpy;
|
|
897
|
-
let uninstallGadgetRegistry;
|
|
898
|
-
try {
|
|
899
|
-
const React = await import('react');
|
|
900
|
-
const ReactJsxRuntime = await import('react/jsx-runtime');
|
|
901
|
-
const Wire = await import('@ggui-ai/wire');
|
|
902
|
-
const { GguiWireProvider } = Wire;
|
|
903
|
-
const moduleResolutions = {
|
|
904
|
-
react: React,
|
|
905
|
-
"react/jsx-runtime": ReactJsxRuntime,
|
|
906
|
-
"@ggui-ai/wire": Wire
|
|
907
|
-
};
|
|
908
|
-
const designSpecifiers = [
|
|
909
|
-
"@ggui-ai/design",
|
|
910
|
-
"@ggui-ai/design/primitives",
|
|
911
|
-
"@ggui-ai/design/components",
|
|
912
|
-
"@ggui-ai/design/compositions",
|
|
913
|
-
"@ggui-ai/design/interact"
|
|
914
|
-
];
|
|
915
|
-
for (const id of designSpecifiers) {
|
|
916
|
-
try {
|
|
917
|
-
moduleResolutions[id] = await import(id);
|
|
918
|
-
} catch {
|
|
919
|
-
}
|
|
920
|
-
}
|
|
921
|
-
try {
|
|
922
|
-
moduleResolutions["@ggui-ai/gadgets"] = await import('@ggui-ai/gadgets');
|
|
923
|
-
} catch {
|
|
924
|
-
}
|
|
925
|
-
uninstallGadgetRegistry = installGadgetStubRegistry(input.contract);
|
|
926
|
-
for (const pkg of collectThirdPartyGadgetPackages(input.contract)) {
|
|
927
|
-
if (pkg in moduleResolutions) continue;
|
|
928
|
-
moduleResolutions[pkg] = buildGadgetPackageProbeShim(pkg);
|
|
929
|
-
}
|
|
930
|
-
let Component;
|
|
931
|
-
try {
|
|
932
|
-
const loaded = await loadComponent({
|
|
933
|
-
sourceCode: input.sourceCode,
|
|
934
|
-
moduleResolutions
|
|
935
|
-
});
|
|
936
|
-
Component = loaded.Component;
|
|
937
|
-
} catch (e) {
|
|
938
|
-
issues.push({
|
|
939
|
-
check: "render-no-throw",
|
|
940
|
-
outcome: "failed",
|
|
941
|
-
reason: `Failed to load component: ${e instanceof Error ? e.message : String(e)}`
|
|
942
|
-
});
|
|
943
|
-
return finalize(issues, t0, 0, 0);
|
|
944
|
-
}
|
|
945
|
-
const probe = createProbe();
|
|
946
|
-
const wireConfig = createProbeWireConfig(probe);
|
|
947
|
-
uninstallSpy = probe.installPostMessageSpy();
|
|
948
|
-
installProductionActShim();
|
|
949
|
-
const { render, cleanup, act } = await import('@testing-library/react');
|
|
950
|
-
const { ActionPendingContext } = await import('@ggui-ai/wire/internal');
|
|
951
|
-
const pendingSource = createProbePendingSource();
|
|
952
|
-
const userEventModule = await import('@testing-library/user-event');
|
|
953
|
-
const userEvent = userEventModule.default ?? userEventModule;
|
|
954
|
-
const originalConsoleError = console.error;
|
|
955
|
-
let loopSignature = null;
|
|
956
|
-
let capturedComponentStack = null;
|
|
957
|
-
const loopPatterns = [
|
|
958
|
-
[/Maximum update depth exceeded/i, "max-update-depth"],
|
|
959
|
-
[/Too many re-renders/i, "too-many-renders"],
|
|
960
|
-
[/Rendered more hooks than during the previous render/i, "hook-count-drift"]
|
|
961
|
-
];
|
|
962
|
-
console.error = (...args) => {
|
|
963
|
-
const text = args.map((a) => typeof a === "string" ? a : "").join(" ");
|
|
964
|
-
if (!capturedComponentStack) {
|
|
965
|
-
for (const arg of args) {
|
|
966
|
-
if (typeof arg === "string" && /^\s*at\s+\S/m.test(arg) && arg.length > 20) {
|
|
967
|
-
capturedComponentStack = arg;
|
|
968
|
-
break;
|
|
969
|
-
}
|
|
970
|
-
}
|
|
971
|
-
if (!capturedComponentStack) {
|
|
972
|
-
const stackMatch = text.match(/(?:^|\n)((?:\s*at\s+\S[^\n]*\n?){2,})/);
|
|
973
|
-
if (stackMatch) {
|
|
974
|
-
capturedComponentStack = stackMatch[1].trim();
|
|
975
|
-
}
|
|
976
|
-
}
|
|
977
|
-
}
|
|
978
|
-
for (const [pattern, tag] of loopPatterns) {
|
|
979
|
-
if (pattern.test(text)) {
|
|
980
|
-
loopSignature = tag;
|
|
981
|
-
throw new Error(`[runtime-render] infinite render loop detected (${tag})`);
|
|
982
|
-
}
|
|
983
|
-
}
|
|
984
|
-
originalConsoleError.apply(console, args);
|
|
985
|
-
};
|
|
986
|
-
const boundaryRef = {
|
|
987
|
-
stack: null,
|
|
988
|
-
error: null,
|
|
989
|
-
catches: 0
|
|
990
|
-
};
|
|
991
|
-
class ProbeErrorBoundary extends React.Component {
|
|
992
|
-
state = { caught: false };
|
|
993
|
-
static getDerivedStateFromError() {
|
|
994
|
-
return { caught: true };
|
|
995
|
-
}
|
|
996
|
-
componentDidCatch(error, errorInfo) {
|
|
997
|
-
boundaryRef.catches += 1;
|
|
998
|
-
if (errorInfo.componentStack && !boundaryRef.stack) {
|
|
999
|
-
boundaryRef.stack = errorInfo.componentStack;
|
|
1000
|
-
}
|
|
1001
|
-
if (!boundaryRef.error) {
|
|
1002
|
-
boundaryRef.error = error;
|
|
1003
|
-
}
|
|
1004
|
-
}
|
|
1005
|
-
render() {
|
|
1006
|
-
if (this.state.caught) return null;
|
|
1007
|
-
return this.props.children;
|
|
1008
|
-
}
|
|
1009
|
-
}
|
|
1010
|
-
const cardTree = () => React.createElement(GguiWireProvider, {
|
|
1011
|
-
config: wireConfig,
|
|
1012
|
-
children: React.createElement(ProbeErrorBoundary, {
|
|
1013
|
-
children: React.createElement(ActionPendingContext.Provider, {
|
|
1014
|
-
value: pendingSource,
|
|
1015
|
-
children: React.createElement(Component, input.mockupProps)
|
|
1016
|
-
})
|
|
1017
|
-
})
|
|
1018
|
-
});
|
|
1019
|
-
let renderResult;
|
|
1020
|
-
const RENDER_TIMEOUT_MS = 5e3;
|
|
1021
|
-
try {
|
|
1022
|
-
const renderPromise = Promise.resolve().then(() => render(cardTree()));
|
|
1023
|
-
const timeoutPromise = new Promise((_, reject) => {
|
|
1024
|
-
setTimeout(
|
|
1025
|
-
() => reject(
|
|
1026
|
-
new Error(
|
|
1027
|
-
`[runtime-render] render exceeded ${RENDER_TIMEOUT_MS}ms \u2014 classified as runtime-hang`
|
|
1028
|
-
)
|
|
1029
|
-
),
|
|
1030
|
-
RENDER_TIMEOUT_MS
|
|
1031
|
-
).unref?.();
|
|
1032
|
-
});
|
|
1033
|
-
renderResult = await Promise.race([renderPromise, timeoutPromise]);
|
|
1034
|
-
} catch (e) {
|
|
1035
|
-
const msg = e instanceof Error ? e.message : String(e);
|
|
1036
|
-
const errStack = e instanceof Error ? e.stack ?? "" : "";
|
|
1037
|
-
const baseReason = loopSignature ? `Infinite render loop (${loopSignature}) \u2014 likely setState/dispatch inside useEffect with missing or unstable dependency` : /runtime-hang/.test(msg) ? `Render wall-clock exceeded ${RENDER_TIMEOUT_MS}ms \u2014 likely async effect loop or unresolved promise` : `Render threw: ${msg}`;
|
|
1038
|
-
const userFrames = errStack.split("\n").map((l) => l.trim()).filter((l) => l.startsWith("at ")).filter(
|
|
1039
|
-
(l) => !/(node_modules|happy-dom|@testing-library|react-dom|node:internal|node:async)/.test(l)
|
|
1040
|
-
).slice(0, 5);
|
|
1041
|
-
const stackSource = boundaryRef.stack ?? capturedComponentStack ?? "";
|
|
1042
|
-
const componentFrames = stackSource.split("\n").map((l) => l.trim()).filter((l) => l.startsWith("at ")).slice(0, 4);
|
|
1043
|
-
const stackBlock = userFrames.length ? `
|
|
1044
|
-
Stack (user frames): ${userFrames.join(" | ")}` : "";
|
|
1045
|
-
const componentBlock = componentFrames.length ? `
|
|
1046
|
-
Component stack: ${componentFrames.join(" > ")}` : "";
|
|
1047
|
-
const loopReason = `${baseReason}${stackBlock}${componentBlock}`;
|
|
1048
|
-
issues.push({
|
|
1049
|
-
check: "render-no-throw",
|
|
1050
|
-
outcome: "failed",
|
|
1051
|
-
reason: loopReason
|
|
1052
|
-
});
|
|
1053
|
-
console.error = originalConsoleError;
|
|
1054
|
-
return finalize(issues, t0, 0, 0);
|
|
1055
|
-
}
|
|
1056
|
-
console.error = originalConsoleError;
|
|
1057
|
-
if (boundaryRef.stack || boundaryRef.error) {
|
|
1058
|
-
const componentFrames = (boundaryRef.stack ?? "").split("\n").map((l) => l.trim()).filter((l) => l.startsWith("at ")).slice(0, 4);
|
|
1059
|
-
const compBlock = componentFrames.length ? `
|
|
1060
|
-
Component stack: ${componentFrames.join(" > ")}` : "";
|
|
1061
|
-
const errMsg = boundaryRef.error ? boundaryRef.error.message : "(no error message)";
|
|
1062
|
-
issues.push({
|
|
1063
|
-
check: "render-no-throw",
|
|
1064
|
-
outcome: "failed",
|
|
1065
|
-
reason: `Render threw: ${errMsg}${compBlock}`
|
|
1066
|
-
});
|
|
1067
|
-
cleanup();
|
|
1068
|
-
return finalize(issues, t0, 0, 0);
|
|
1069
|
-
}
|
|
1070
|
-
const container = toMinimalElement(renderResult.container);
|
|
1071
|
-
const user = buildClickUser(userEvent, hostGlobals().document);
|
|
1072
|
-
let actionsChecked = 0;
|
|
1073
|
-
let streamsChecked = 0;
|
|
1074
|
-
let pendingTally;
|
|
1075
|
-
const pendingTargets = [];
|
|
1076
|
-
const walkedTexts = [];
|
|
1077
|
-
try {
|
|
1078
|
-
let inputPriming;
|
|
1079
|
-
try {
|
|
1080
|
-
inputPriming = { primed: primeInputs(renderResult.container) };
|
|
1081
|
-
} catch (err) {
|
|
1082
|
-
inputPriming = { error: err instanceof Error ? err.message : String(err) };
|
|
1083
|
-
}
|
|
1084
|
-
if (input.contract?.actionSpec) {
|
|
1085
|
-
const actionSpec = input.contract.actionSpec;
|
|
1086
|
-
for (const [name, entry] of Object.entries(actionSpec)) {
|
|
1087
|
-
actionsChecked++;
|
|
1088
|
-
const wiring = findWiring({
|
|
1089
|
-
sourceCode: input.sourceCode,
|
|
1090
|
-
hookName: "useAction",
|
|
1091
|
-
hookArg: name
|
|
1092
|
-
});
|
|
1093
|
-
const walkPriming = {};
|
|
1094
|
-
const mount = async () => {
|
|
1095
|
-
let mounted;
|
|
1096
|
-
try {
|
|
1097
|
-
mounted = render(cardTree());
|
|
1098
|
-
} catch {
|
|
1099
|
-
return void 0;
|
|
1100
|
-
}
|
|
1101
|
-
await flushPromises();
|
|
1102
|
-
const root = mounted.container;
|
|
1103
|
-
return {
|
|
1104
|
-
container: toMinimalElement(root),
|
|
1105
|
-
prime: () => {
|
|
1106
|
-
try {
|
|
1107
|
-
primeInputs(root);
|
|
1108
|
-
} catch (err) {
|
|
1109
|
-
walkPriming.error ??= err instanceof Error ? err.message : String(err);
|
|
1110
|
-
}
|
|
1111
|
-
}
|
|
1112
|
-
};
|
|
1113
|
-
};
|
|
1114
|
-
const issue = await checkActionWiring({
|
|
1115
|
-
mount,
|
|
1116
|
-
actionName: name,
|
|
1117
|
-
actionLabel: entry.label,
|
|
1118
|
-
wiring,
|
|
1119
|
-
probe,
|
|
1120
|
-
user,
|
|
1121
|
-
pendingTargets,
|
|
1122
|
-
stopAt: walkStopAt(input.deadlineAt),
|
|
1123
|
-
walkedTexts
|
|
1124
|
-
});
|
|
1125
|
-
if (issue) {
|
|
1126
|
-
const priming = "primed" in inputPriming && walkPriming.error !== void 0 ? { error: `on a screen the action walk reached: ${walkPriming.error}` } : inputPriming;
|
|
1127
|
-
issues.push({ ...issue, diagnostics: { ...issue.diagnostics, inputPriming: priming } });
|
|
1128
|
-
}
|
|
1129
|
-
}
|
|
1130
|
-
}
|
|
1131
|
-
if (input.contract?.actionSpec) {
|
|
1132
|
-
for (const [name, entry] of Object.entries(input.contract.actionSpec)) {
|
|
1133
|
-
const idField = idLikeRequiredStringField(
|
|
1134
|
-
entry.schema
|
|
1135
|
-
);
|
|
1136
|
-
if (idField === null) continue;
|
|
1137
|
-
const issue = await checkSelectionIdentity({
|
|
1138
|
-
container,
|
|
1139
|
-
probe,
|
|
1140
|
-
user,
|
|
1141
|
-
actionName: name,
|
|
1142
|
-
idField
|
|
1143
|
-
});
|
|
1144
|
-
if (issue) issues.push(issue);
|
|
1145
|
-
}
|
|
1146
|
-
}
|
|
1147
|
-
if (input.contract?.propsSpec) {
|
|
1148
|
-
for (const issue of checkPropCoverage({
|
|
1149
|
-
container,
|
|
1150
|
-
walkedTexts,
|
|
1151
|
-
propsSpec: input.contract.propsSpec,
|
|
1152
|
-
mockupProps: input.mockupProps
|
|
1153
|
-
})) {
|
|
1154
|
-
issues.push(issue);
|
|
1155
|
-
}
|
|
1156
|
-
}
|
|
1157
|
-
if (input.contract?.propsSpec) {
|
|
1158
|
-
const propsSpec5c = input.contract.propsSpec;
|
|
1159
|
-
const normalize = (s) => (s ?? "").replace(/\s+/g, " ").trim();
|
|
1160
|
-
const baseText = normalize(container.textContent);
|
|
1161
|
-
for (const [propName, entry] of Object.entries(propsSpec5c.properties)) {
|
|
1162
|
-
if (!entry.required) continue;
|
|
1163
|
-
const orig = input.mockupProps[propName];
|
|
1164
|
-
if (typeof orig !== "string" && typeof orig !== "number") continue;
|
|
1165
|
-
const marker = pickScalarMarker(orig);
|
|
1166
|
-
if (marker === null || !baseText.includes(marker)) continue;
|
|
1167
|
-
const varied = typeof orig === "string" ? "Zzyzx Qw" : orig + 137731;
|
|
1168
|
-
const variedProbe = createProbe();
|
|
1169
|
-
const variedConfig = createProbeWireConfig(variedProbe);
|
|
1170
|
-
let variedResult;
|
|
1171
|
-
try {
|
|
1172
|
-
variedResult = render(
|
|
1173
|
-
React.createElement(GguiWireProvider, {
|
|
1174
|
-
config: variedConfig,
|
|
1175
|
-
children: React.createElement(ProbeErrorBoundary, {
|
|
1176
|
-
children: React.createElement(Component, {
|
|
1177
|
-
...input.mockupProps,
|
|
1178
|
-
[propName]: varied
|
|
1179
|
-
})
|
|
1180
|
-
})
|
|
1181
|
-
})
|
|
1182
|
-
);
|
|
1183
|
-
} catch {
|
|
1184
|
-
continue;
|
|
1185
|
-
}
|
|
1186
|
-
await flushPromises();
|
|
1187
|
-
const variedText = normalize(variedResult.container.textContent);
|
|
1188
|
-
variedResult.unmount();
|
|
1189
|
-
if (variedText === baseText) {
|
|
1190
|
-
issues.push({
|
|
1191
|
-
check: "prop-sensitivity",
|
|
1192
|
-
outcome: "failed",
|
|
1193
|
-
subject: propName,
|
|
1194
|
-
reason: `props.${propName} is displayed ('${marker}' appears in the DOM) but changing its value to '${String(varied)}' left the rendered output BYTE-IDENTICAL \u2014 the value is baked into the component instead of derived from the prop. A cached reuse of this blueprint under a different ${propName} would show the original value.`
|
|
1195
|
-
});
|
|
1196
|
-
}
|
|
1197
|
-
}
|
|
1198
|
-
}
|
|
1199
|
-
if (input.contract?.streamSpec) {
|
|
1200
|
-
const streamSpec = input.contract.streamSpec;
|
|
1201
|
-
for (const [name] of Object.entries(streamSpec)) {
|
|
1202
|
-
streamsChecked++;
|
|
1203
|
-
const issue = await checkStreamRerender({
|
|
1204
|
-
container,
|
|
1205
|
-
eventName: name,
|
|
1206
|
-
probe,
|
|
1207
|
-
React
|
|
1208
|
-
});
|
|
1209
|
-
if (issue) issues.push(issue);
|
|
1210
|
-
}
|
|
1211
|
-
}
|
|
1212
|
-
const declaredProps = input.contract?.propsSpec?.properties;
|
|
1213
|
-
if (declaredProps) {
|
|
1214
|
-
const optionalFilled = Object.entries(declaredProps).filter(
|
|
1215
|
-
([name, entry]) => entry.required !== true && input.mockupProps[name] !== void 0
|
|
1216
|
-
).map(([name]) => name);
|
|
1217
|
-
if (optionalFilled.length > 0) {
|
|
1218
|
-
const requiredOnlyProps = { ...input.mockupProps };
|
|
1219
|
-
for (const name of optionalFilled) delete requiredOnlyProps[name];
|
|
1220
|
-
const omitBoundaryRef = {};
|
|
1221
|
-
class OmitErrorBoundary extends React.Component {
|
|
1222
|
-
state = { caught: false };
|
|
1223
|
-
static getDerivedStateFromError() {
|
|
1224
|
-
return { caught: true };
|
|
1225
|
-
}
|
|
1226
|
-
componentDidCatch(error) {
|
|
1227
|
-
omitBoundaryRef.error ??= error;
|
|
1228
|
-
}
|
|
1229
|
-
render() {
|
|
1230
|
-
if (this.state.caught) return null;
|
|
1231
|
-
return this.props.children;
|
|
1232
|
-
}
|
|
1233
|
-
}
|
|
1234
|
-
try {
|
|
1235
|
-
render(
|
|
1236
|
-
React.createElement(GguiWireProvider, {
|
|
1237
|
-
config: wireConfig,
|
|
1238
|
-
children: React.createElement(OmitErrorBoundary, {
|
|
1239
|
-
children: React.createElement(Component, requiredOnlyProps)
|
|
1240
|
-
})
|
|
1241
|
-
})
|
|
1242
|
-
);
|
|
1243
|
-
} catch (e) {
|
|
1244
|
-
omitBoundaryRef.error ??= e instanceof Error ? e : new Error(String(e));
|
|
1245
|
-
}
|
|
1246
|
-
if (omitBoundaryRef.error) {
|
|
1247
|
-
issues.push({
|
|
1248
|
-
check: "optional-props-omitted",
|
|
1249
|
-
outcome: "failed",
|
|
1250
|
-
subject: optionalFilled.join(", "),
|
|
1251
|
-
reason: `Render crashed when the optional prop(s) ${optionalFilled.map((n) => `'${n}'`).join(", ")} were omitted: ${omitBoundaryRef.error.message}. The contract marks them optional (no \`required: true\`), so a legitimate render may leave them out.`
|
|
1252
|
-
});
|
|
1253
|
-
}
|
|
1254
|
-
}
|
|
1255
|
-
}
|
|
1256
|
-
const timeLeft = input.deadlineAt === void 0 ? Number.POSITIVE_INFINITY : input.deadlineAt - Date.now();
|
|
1257
|
-
if (input.contract?.actionSpec && timeLeft >= PENDING_PHASE_RESERVE_MS) {
|
|
1258
|
-
pendingTally = await runPendingPhase({
|
|
1259
|
-
targets: pendingTargets,
|
|
1260
|
-
source: pendingSource,
|
|
1261
|
-
act,
|
|
1262
|
-
catches: () => boundaryRef.catches,
|
|
1263
|
-
actions: Object.entries(input.contract.actionSpec).map(([name, entry]) => ({ name, label: entry.label })),
|
|
1264
|
-
...input.deadlineAt !== void 0 ? { deadlineAt: input.deadlineAt } : {}
|
|
1265
|
-
});
|
|
1266
|
-
}
|
|
1267
|
-
} finally {
|
|
1268
|
-
cleanup();
|
|
1269
|
-
}
|
|
1270
|
-
return finalize(issues, t0, actionsChecked, streamsChecked, pendingTally);
|
|
1271
|
-
} finally {
|
|
1272
|
-
try {
|
|
1273
|
-
uninstallSpy?.();
|
|
1274
|
-
} catch {
|
|
1275
|
-
}
|
|
1276
|
-
try {
|
|
1277
|
-
uninstallGadgetRegistry?.();
|
|
1278
|
-
} catch {
|
|
1279
|
-
}
|
|
1280
|
-
teardown();
|
|
1281
|
-
}
|
|
1282
|
-
}
|
|
1283
|
-
function installGadgetStubRegistry(contract) {
|
|
1284
|
-
const declared = contract?.clientCapabilities?.gadgets;
|
|
1285
|
-
if (!declared || Object.keys(declared).length === 0) {
|
|
1286
|
-
return () => {
|
|
1287
|
-
};
|
|
1288
|
-
}
|
|
1289
|
-
const noop = () => {
|
|
1290
|
-
};
|
|
1291
|
-
const valueStub = new Proxy(
|
|
1292
|
-
{},
|
|
1293
|
-
{
|
|
1294
|
-
get: (_t, key) => key === "then" || typeof key === "symbol" ? void 0 : noop
|
|
1295
|
-
}
|
|
1296
|
-
);
|
|
1297
|
-
const result = { status: "idle", value: valueStub, start: noop };
|
|
1298
|
-
const resultStub = new Proxy(result, {
|
|
1299
|
-
get: (t, key) => {
|
|
1300
|
-
if (key === "status" || key === "value" || key === "start") {
|
|
1301
|
-
return t[key];
|
|
1302
|
-
}
|
|
1303
|
-
return key === "then" || typeof key === "symbol" ? void 0 : noop;
|
|
1304
|
-
}
|
|
1305
|
-
});
|
|
1306
|
-
const gadgets = {};
|
|
1307
|
-
for (const use of contract ? listContractGadgets(contract) : []) {
|
|
1308
|
-
const pkgSlot = gadgets[use.package] ??= {};
|
|
1309
|
-
if (HOOK_NAME_RE.test(use.name)) {
|
|
1310
|
-
pkgSlot[use.name] = () => resultStub;
|
|
1311
|
-
} else {
|
|
1312
|
-
pkgSlot[use.name] = () => null;
|
|
1313
|
-
}
|
|
1314
|
-
}
|
|
1315
|
-
const root = globalThis;
|
|
1316
|
-
const prior = root.__ggui__;
|
|
1317
|
-
root.__ggui__ = { gadgets, publicEnv: {} };
|
|
1318
|
-
return () => {
|
|
1319
|
-
if (prior === void 0) {
|
|
1320
|
-
delete root.__ggui__;
|
|
1321
|
-
} else {
|
|
1322
|
-
root.__ggui__ = prior;
|
|
1323
|
-
}
|
|
1324
|
-
};
|
|
1325
|
-
}
|
|
1326
|
-
function collectThirdPartyGadgetPackages(contract) {
|
|
1327
|
-
if (!contract) return [];
|
|
1328
|
-
const packages = /* @__PURE__ */ new Set();
|
|
1329
|
-
for (const use of listContractGadgets(contract)) {
|
|
1330
|
-
if (use.package !== "@ggui-ai/gadgets") packages.add(use.package);
|
|
1331
|
-
}
|
|
1332
|
-
return [...packages];
|
|
1333
|
-
}
|
|
1334
|
-
function buildGadgetPackageProbeShim(packageName) {
|
|
1335
|
-
const resolveExport = (name) => {
|
|
1336
|
-
const root = globalThis;
|
|
1337
|
-
return root.__ggui__?.gadgets?.[packageName]?.[name];
|
|
1338
|
-
};
|
|
1339
|
-
const makeThunk = (name) => (...args) => {
|
|
1340
|
-
const impl = resolveExport(name);
|
|
1341
|
-
if (typeof impl !== "function") {
|
|
1342
|
-
throw new Error(
|
|
1343
|
-
`[gadget] export '${name}' from '${packageName}' is not loaded in the render-check probe \u2014 the component imports it but the contract's clientCapabilities.gadgets never declared it, so installGadgetStubRegistry planted no stub.`
|
|
1344
|
-
);
|
|
1345
|
-
}
|
|
1346
|
-
return impl(...args);
|
|
1347
|
-
};
|
|
1348
|
-
return new Proxy(
|
|
1349
|
-
{},
|
|
1350
|
-
{
|
|
1351
|
-
get: (_t, key) => {
|
|
1352
|
-
if (typeof key === "symbol" || key === "default" || key === "then") {
|
|
1353
|
-
return void 0;
|
|
1354
|
-
}
|
|
1355
|
-
return makeThunk(key);
|
|
1356
|
-
},
|
|
1357
|
-
// `key in ns` checks (esbuild interop may probe) report true so
|
|
1358
|
-
// the named import binds to the thunk rather than `undefined`.
|
|
1359
|
-
has: (_t, key) => typeof key !== "symbol" && key !== "then" && key !== "default"
|
|
1360
|
-
}
|
|
1361
|
-
);
|
|
1362
|
-
}
|
|
1363
|
-
async function setupHappyDom() {
|
|
1364
|
-
const g = hostGlobals();
|
|
1365
|
-
const pendingTimeouts = /* @__PURE__ */ new Set();
|
|
1366
|
-
const pendingIntervals = /* @__PURE__ */ new Set();
|
|
1367
|
-
const origSetTimeout = globalThis.setTimeout;
|
|
1368
|
-
const origSetInterval = globalThis.setInterval;
|
|
1369
|
-
const origClearTimeout = globalThis.clearTimeout;
|
|
1370
|
-
const origClearInterval = globalThis.clearInterval;
|
|
1371
|
-
globalThis.setTimeout = ((...args) => {
|
|
1372
|
-
const id = origSetTimeout(...args);
|
|
1373
|
-
pendingTimeouts.add(id);
|
|
1374
|
-
return id;
|
|
1375
|
-
});
|
|
1376
|
-
globalThis.setInterval = ((...args) => {
|
|
1377
|
-
const id = origSetInterval(...args);
|
|
1378
|
-
pendingIntervals.add(id);
|
|
1379
|
-
return id;
|
|
1380
|
-
});
|
|
1381
|
-
globalThis.clearTimeout = (id) => {
|
|
1382
|
-
if (id !== void 0) pendingTimeouts.delete(id);
|
|
1383
|
-
return origClearTimeout(id);
|
|
1384
|
-
};
|
|
1385
|
-
globalThis.clearInterval = (id) => {
|
|
1386
|
-
if (id !== void 0) pendingIntervals.delete(id);
|
|
1387
|
-
return origClearInterval(id);
|
|
1388
|
-
};
|
|
1389
|
-
const isTeardownArtifact = (err) => {
|
|
1390
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
1391
|
-
return /window is not defined/i.test(msg) || /document is not defined/i.test(msg) || /Cannot read propert(?:y|ies) of undefined \(reading 'event'\)/i.test(msg) || /requestAnimationFrame is not defined/i.test(msg);
|
|
1392
|
-
};
|
|
1393
|
-
const uncaughtHandler = (err) => {
|
|
1394
|
-
if (isTeardownArtifact(err)) {
|
|
1395
|
-
return;
|
|
1396
|
-
}
|
|
1397
|
-
process.nextTick(() => {
|
|
1398
|
-
throw err;
|
|
1399
|
-
});
|
|
1400
|
-
};
|
|
1401
|
-
const unhandledHandler = (reason) => {
|
|
1402
|
-
if (isTeardownArtifact(reason)) return;
|
|
1403
|
-
const err = reason instanceof Error ? reason : new Error(String(reason));
|
|
1404
|
-
process.nextTick(() => {
|
|
1405
|
-
throw err;
|
|
1406
|
-
});
|
|
1407
|
-
};
|
|
1408
|
-
process.on("uncaughtException", uncaughtHandler);
|
|
1409
|
-
process.on("unhandledRejection", unhandledHandler);
|
|
1410
|
-
const cleanupAsyncInfra = () => {
|
|
1411
|
-
for (const id of pendingTimeouts) origClearTimeout(id);
|
|
1412
|
-
for (const id of pendingIntervals) origClearInterval(id);
|
|
1413
|
-
pendingTimeouts.clear();
|
|
1414
|
-
pendingIntervals.clear();
|
|
1415
|
-
globalThis.setTimeout = origSetTimeout;
|
|
1416
|
-
globalThis.setInterval = origSetInterval;
|
|
1417
|
-
globalThis.clearTimeout = origClearTimeout;
|
|
1418
|
-
globalThis.clearInterval = origClearInterval;
|
|
1419
|
-
origSetTimeout(() => {
|
|
1420
|
-
process.off("uncaughtException", uncaughtHandler);
|
|
1421
|
-
process.off("unhandledRejection", unhandledHandler);
|
|
1422
|
-
}, 100).unref?.();
|
|
1423
|
-
};
|
|
1424
|
-
if ("window" in g && "document" in g) {
|
|
1425
|
-
const priorActFlag = g.IS_REACT_ACT_ENVIRONMENT;
|
|
1426
|
-
g.IS_REACT_ACT_ENVIRONMENT = true;
|
|
1427
|
-
return () => {
|
|
1428
|
-
cleanupAsyncInfra();
|
|
1429
|
-
if (priorActFlag === void 0) delete g.IS_REACT_ACT_ENVIRONMENT;
|
|
1430
|
-
else g.IS_REACT_ACT_ENVIRONMENT = priorActFlag;
|
|
1431
|
-
};
|
|
1432
|
-
}
|
|
1433
|
-
const { Window } = await import('happy-dom');
|
|
1434
|
-
const window = new Window({ url: "https://render-check.local" });
|
|
1435
|
-
const windowAny = openRecord(window);
|
|
1436
|
-
const keys = [
|
|
1437
|
-
"window",
|
|
1438
|
-
"document",
|
|
1439
|
-
"navigator",
|
|
1440
|
-
"HTMLElement",
|
|
1441
|
-
"Node",
|
|
1442
|
-
"Element",
|
|
1443
|
-
"Event",
|
|
1444
|
-
"MouseEvent",
|
|
1445
|
-
"KeyboardEvent",
|
|
1446
|
-
"getComputedStyle",
|
|
1447
|
-
"requestAnimationFrame",
|
|
1448
|
-
"cancelAnimationFrame"
|
|
1449
|
-
];
|
|
1450
|
-
const prior = {};
|
|
1451
|
-
for (const k of keys) {
|
|
1452
|
-
prior[k] = { value: g[k], existed: k in g };
|
|
1453
|
-
try {
|
|
1454
|
-
Object.defineProperty(g, k, {
|
|
1455
|
-
value: windowAny[k],
|
|
1456
|
-
writable: true,
|
|
1457
|
-
configurable: true
|
|
1458
|
-
});
|
|
1459
|
-
} catch {
|
|
1460
|
-
}
|
|
1461
|
-
}
|
|
1462
|
-
g.IS_REACT_ACT_ENVIRONMENT = true;
|
|
1463
|
-
return () => {
|
|
1464
|
-
cleanupAsyncInfra();
|
|
1465
|
-
for (const k of keys) {
|
|
1466
|
-
try {
|
|
1467
|
-
if (prior[k].existed) {
|
|
1468
|
-
Object.defineProperty(g, k, {
|
|
1469
|
-
value: prior[k].value,
|
|
1470
|
-
writable: true,
|
|
1471
|
-
configurable: true
|
|
1472
|
-
});
|
|
1473
|
-
} else {
|
|
1474
|
-
delete g[k];
|
|
1475
|
-
}
|
|
1476
|
-
} catch {
|
|
1477
|
-
}
|
|
1478
|
-
}
|
|
1479
|
-
delete g.IS_REACT_ACT_ENVIRONMENT;
|
|
1480
|
-
};
|
|
1481
|
-
}
|
|
1482
|
-
function finalize(issues, t0, actionsChecked, streamsChecked, pendingTally) {
|
|
1483
|
-
const ok = !issues.some((i) => i.outcome === "failed");
|
|
1484
|
-
return {
|
|
1485
|
-
ok,
|
|
1486
|
-
issues,
|
|
1487
|
-
stats: {
|
|
1488
|
-
actionsChecked,
|
|
1489
|
-
streamsChecked,
|
|
1490
|
-
renderMs: Date.now() - t0,
|
|
1491
|
-
...pendingTally !== void 0 ? {
|
|
1492
|
-
pendingAffordance: {
|
|
1493
|
-
dispatched: pendingTally.dispatched,
|
|
1494
|
-
visible: pendingTally.visible,
|
|
1495
|
-
missing: [...pendingTally.missing],
|
|
1496
|
-
gone: [...pendingTally.gone]
|
|
1497
|
-
}
|
|
1498
|
-
} : {}
|
|
1499
|
-
}
|
|
1500
|
-
};
|
|
1501
|
-
}
|
|
1502
|
-
var PENDING_TARGET_CAP_MS = 2e3;
|
|
1503
|
-
var PENDING_PHASE_TAIL_MS = 1e3;
|
|
1504
|
-
async function runPendingPhase(input) {
|
|
1505
|
-
const tally = { dispatched: 0, visible: 0, missing: [], gone: [] };
|
|
1506
|
-
let stopped = false;
|
|
1507
|
-
for (const target of input.targets) {
|
|
1508
|
-
const budget = Math.min(
|
|
1509
|
-
PENDING_TARGET_CAP_MS,
|
|
1510
|
-
input.deadlineAt === void 0 ? Number.POSITIVE_INFINITY : input.deadlineAt - Date.now() - PENDING_PHASE_TAIL_MS
|
|
1511
|
-
);
|
|
1512
|
-
const found = stopped || budget <= 0 ? void 0 : pendingControlOf(target, input.actions);
|
|
1513
|
-
if (found === void 0) {
|
|
1514
|
-
tally.gone.push(target.actionName);
|
|
1515
|
-
if (budget <= 0) stopped = true;
|
|
1516
|
-
continue;
|
|
1517
|
-
}
|
|
1518
|
-
const el = controlToRead(found, target);
|
|
1519
|
-
const before = lookOf(el);
|
|
1520
|
-
const caughtBefore = input.catches();
|
|
1521
|
-
const settled = await settlesWithin(
|
|
1522
|
-
budget,
|
|
1523
|
-
(async () => {
|
|
1524
|
-
await input.act(async () => {
|
|
1525
|
-
input.source.mark(target.actionName);
|
|
1526
|
-
});
|
|
1527
|
-
await flushPromises();
|
|
1528
|
-
})()
|
|
1529
|
-
);
|
|
1530
|
-
const crashed = input.catches() !== caughtBefore;
|
|
1531
|
-
tally.dispatched += 1;
|
|
1532
|
-
if (settled && !crashed && lookChanged(before, lookOf(el))) tally.visible += 1;
|
|
1533
|
-
else tally.missing.push(target.actionName);
|
|
1534
|
-
if (!settled) {
|
|
1535
|
-
stopped = true;
|
|
1536
|
-
continue;
|
|
1537
|
-
}
|
|
1538
|
-
await input.act(async () => {
|
|
1539
|
-
input.source.clear();
|
|
1540
|
-
});
|
|
1541
|
-
await flushPromises();
|
|
1542
|
-
if (crashed) stopped = true;
|
|
1543
|
-
}
|
|
1544
|
-
return tally;
|
|
1545
|
-
}
|
|
1546
|
-
function pendingControlOf(target, actions) {
|
|
1547
|
-
const movedToAnother = !matchesActionIdentity(target.element, target.actionName, target.actionLabel) && actions.some((a) => a.name !== target.actionName && matchesActionIdentity(target.element, a.name, a.label));
|
|
1548
|
-
if (lookOf(target.element).connected && !movedToAnother) return target.element;
|
|
1549
|
-
if (target.wiringKind !== "click") return void 0;
|
|
1550
|
-
return findActionElements(target.container, target.actionName, target.actionLabel, { identityOnly: true })[0];
|
|
1551
|
-
}
|
|
1552
|
-
function controlToRead(el, target) {
|
|
1553
|
-
if (el.tagName.toLowerCase() !== "form") return el;
|
|
1554
|
-
for (const c of el.querySelectorAll('button, input[type="submit"], input[type="button"], [role="button"]')) {
|
|
1555
|
-
if (matchesActionIdentity(c, target.actionName, target.actionLabel)) return c;
|
|
1556
|
-
}
|
|
1557
|
-
for (const submitter of el.querySelectorAll(
|
|
1558
|
-
'button[type="submit"], input[type="submit"], button:not([type])'
|
|
1559
|
-
)) {
|
|
1560
|
-
return submitter;
|
|
1561
|
-
}
|
|
1562
|
-
return el;
|
|
1563
|
-
}
|
|
1564
|
-
function walkStopAt(deadlineAt) {
|
|
1565
|
-
const cap = Date.now() + WALK_ACTION_CAP_MS;
|
|
1566
|
-
return deadlineAt === void 0 ? cap : Math.min(cap, deadlineAt - PENDING_PHASE_RESERVE_MS - WALK_DEADLINE_MARGIN_MS);
|
|
1567
|
-
}
|
|
1568
|
-
async function checkActionWiring(input) {
|
|
1569
|
-
const { actionName, actionLabel, wiring, probe, user } = input;
|
|
1570
|
-
const baseDiagnostics = {
|
|
1571
|
-
observedJsxElements: wiring.observedJsxElements,
|
|
1572
|
-
observedNativeProps: wiring.observedNativeProps,
|
|
1573
|
-
observedCustomProps: wiring.observedCustomProps
|
|
1574
|
-
};
|
|
1575
|
-
if (wiring.kind === "missing") {
|
|
1576
|
-
return {
|
|
1577
|
-
check: "action-wiring",
|
|
1578
|
-
outcome: "failed",
|
|
1579
|
-
subject: actionName,
|
|
1580
|
-
reason: wiring.reason ?? `Action '${actionName}' is declared in contract but useAction('${actionName}') is not wired to any UI element`,
|
|
1581
|
-
diagnostics: baseDiagnostics
|
|
1582
|
-
};
|
|
1583
|
-
}
|
|
1584
|
-
if (wiring.kind === "unverified") {
|
|
1585
|
-
return {
|
|
1586
|
-
check: "action-wiring",
|
|
1587
|
-
outcome: "unverified",
|
|
1588
|
-
subject: actionName,
|
|
1589
|
-
reason: wiring.reason ?? "Source indicates non-click or non-native wiring; static probe did not verify execution deterministically.",
|
|
1590
|
-
diagnostics: baseDiagnostics
|
|
1591
|
-
};
|
|
1592
|
-
}
|
|
1593
|
-
const kind = wiring.kind;
|
|
1594
|
-
const walk = await walkToAction({
|
|
1595
|
-
mount: input.mount,
|
|
1596
|
-
controls: (container) => actionControls(container, kind, actionName, actionLabel),
|
|
1597
|
-
press: async (control) => {
|
|
1598
|
-
const before = probe.getFireLog().length;
|
|
1599
|
-
try {
|
|
1600
|
-
await dispatchTrigger(control.element, control.how, user);
|
|
1601
|
-
} catch {
|
|
1602
|
-
return false;
|
|
1603
|
-
}
|
|
1604
|
-
await flushPromises();
|
|
1605
|
-
return probe.getFireLog().slice(before).some((e) => e.kind === "action.fired" && e.name === actionName);
|
|
1606
|
-
},
|
|
1607
|
-
describe: describeElement,
|
|
1608
|
-
stopAt: input.stopAt,
|
|
1609
|
-
maxPresses: WALK_MAX_PRESSES
|
|
1610
|
-
});
|
|
1611
|
-
input.walkedTexts.push(...walk.screenTexts);
|
|
1612
|
-
if (walk.fired) {
|
|
1613
|
-
if (input.pendingTargets && walk.element && walk.container && (kind === "click" || kind === "submit")) {
|
|
1614
|
-
input.pendingTargets.push({
|
|
1615
|
-
actionName,
|
|
1616
|
-
actionLabel,
|
|
1617
|
-
wiringKind: kind,
|
|
1618
|
-
element: walk.element,
|
|
1619
|
-
container: walk.container
|
|
1620
|
-
});
|
|
1621
|
-
}
|
|
1622
|
-
return null;
|
|
1623
|
-
}
|
|
1624
|
-
const allFired = probe.getFireLog().filter((e) => e.kind === "action.fired").map((e) => e.name);
|
|
1625
|
-
const otherActionsFired = Array.from(new Set(allFired)).filter((n) => n !== actionName);
|
|
1626
|
-
const stoppedBy = walk.stoppedBy === "dispatched" ? "explored" : walk.stoppedBy;
|
|
1627
|
-
return {
|
|
1628
|
-
check: "action-wiring",
|
|
1629
|
-
outcome: "unverified",
|
|
1630
|
-
subject: actionName,
|
|
1631
|
-
reason: `Source confirms ${kind} wiring exists for action '${actionName}', but synthetic ${kind} did not dispatch it. Likely cause: conditional rendering, required input/form fill, or a multi-step interaction the static probe cannot complete.`,
|
|
1632
|
-
...walk.presses === 0 && stoppedBy === "explored" ? { elementHint: `No ${kind}-eligible element found in DOM` } : {},
|
|
1633
|
-
diagnostics: {
|
|
1634
|
-
...baseDiagnostics,
|
|
1635
|
-
actionsFiredFromClicks: otherActionsFired,
|
|
1636
|
-
walk: {
|
|
1637
|
-
presses: walk.presses,
|
|
1638
|
-
screens: walk.screens,
|
|
1639
|
-
stoppedBy,
|
|
1640
|
-
namedControlSeen: walk.namedControlSeen,
|
|
1641
|
-
lastPressed: walk.lastPressed
|
|
1642
|
-
}
|
|
1643
|
-
}
|
|
1644
|
-
};
|
|
1645
|
-
}
|
|
1646
|
-
function idLikeRequiredStringField(schema) {
|
|
1647
|
-
if (schema === void 0) return null;
|
|
1648
|
-
const properties = schema["properties"];
|
|
1649
|
-
const required = schema["required"];
|
|
1650
|
-
if (properties === null || typeof properties !== "object") return null;
|
|
1651
|
-
if (!Array.isArray(required)) return null;
|
|
1652
|
-
for (const [propName, propSchema] of Object.entries(properties)) {
|
|
1653
|
-
if (!/id$/i.test(propName)) continue;
|
|
1654
|
-
if (!required.includes(propName)) continue;
|
|
1655
|
-
const t = propSchema !== null && typeof propSchema === "object" ? propSchema["type"] : void 0;
|
|
1656
|
-
if (t === "string") return propName;
|
|
1657
|
-
}
|
|
1658
|
-
return null;
|
|
1659
|
-
}
|
|
1660
|
-
async function checkSelectionIdentity(input) {
|
|
1661
|
-
const { container, probe, user, actionName, idField } = input;
|
|
1662
|
-
const clickables = Array.from(
|
|
1663
|
-
container.querySelectorAll('button, [role="button"]')
|
|
1664
|
-
);
|
|
1665
|
-
const byLabel = /* @__PURE__ */ new Map();
|
|
1666
|
-
for (const el of clickables) {
|
|
1667
|
-
const label = (el.textContent ?? "").replace(/\s+/g, " ").trim();
|
|
1668
|
-
if (label.length === 0) continue;
|
|
1669
|
-
const group = byLabel.get(label);
|
|
1670
|
-
if (group) group.push(el);
|
|
1671
|
-
else byLabel.set(label, [el]);
|
|
1672
|
-
}
|
|
1673
|
-
for (const [label, group] of byLabel) {
|
|
1674
|
-
if (group.length < 2) continue;
|
|
1675
|
-
const payloadFor = async (el) => {
|
|
1676
|
-
const before = probe.getFireLog().length;
|
|
1677
|
-
try {
|
|
1678
|
-
await user.click(el);
|
|
1679
|
-
} catch {
|
|
1680
|
-
return void 0;
|
|
1681
|
-
}
|
|
1682
|
-
await flushPromises();
|
|
1683
|
-
const events = probe.getFireLog().slice(before).filter((e) => e.kind === "action.fired" && e.name === actionName);
|
|
1684
|
-
return events.length > 0 ? events[events.length - 1].payload : void 0;
|
|
1685
|
-
};
|
|
1686
|
-
const first = await payloadFor(group[0]);
|
|
1687
|
-
const second = await payloadFor(group[1]);
|
|
1688
|
-
if (first === void 0 || second === void 0) continue;
|
|
1689
|
-
const idOf = (payload) => {
|
|
1690
|
-
if (payload === null || typeof payload !== "object") return void 0;
|
|
1691
|
-
const v = payload[idField];
|
|
1692
|
-
return typeof v === "string" ? v : void 0;
|
|
1693
|
-
};
|
|
1694
|
-
const firstId = idOf(first);
|
|
1695
|
-
const secondId = idOf(second);
|
|
1696
|
-
if (firstId === void 0 || secondId === void 0) continue;
|
|
1697
|
-
if (firstId === secondId) {
|
|
1698
|
-
return {
|
|
1699
|
-
check: "selection-identity",
|
|
1700
|
-
outcome: "failed",
|
|
1701
|
-
subject: actionName,
|
|
1702
|
-
reason: `Two clickable elements share the visible label '${label}', and clicking EACH fired '${actionName}' with the same ${idField} ('${firstId}') \u2014 the action payload is keyed on the display value, not the element's identity. Distinct cells must send their own ${idField} (the contract requires it), or selecting one "${label}" acts on every "${label}".`,
|
|
1703
|
-
elementHint: describeElement(group[0])
|
|
1704
|
-
};
|
|
1705
|
-
}
|
|
1706
|
-
return null;
|
|
1707
|
-
}
|
|
1708
|
-
return null;
|
|
1709
|
-
}
|
|
1710
|
-
async function dispatchTrigger(el, kind, user) {
|
|
1711
|
-
switch (kind) {
|
|
1712
|
-
case "click":
|
|
1713
|
-
await user.click(el);
|
|
1714
|
-
return;
|
|
1715
|
-
case "submit": {
|
|
1716
|
-
const form = closestForm(el);
|
|
1717
|
-
if (!form) return;
|
|
1718
|
-
const ev = new globalThis.Event("submit", {
|
|
1719
|
-
bubbles: true,
|
|
1720
|
-
cancelable: true
|
|
1721
|
-
});
|
|
1722
|
-
toEventDispatcher(form).dispatchEvent(ev);
|
|
1723
|
-
return;
|
|
1724
|
-
}
|
|
1725
|
-
case "change": {
|
|
1726
|
-
setSyntheticValue(el);
|
|
1727
|
-
const ev = new globalThis.Event("change", { bubbles: true });
|
|
1728
|
-
toEventDispatcher(el).dispatchEvent(ev);
|
|
1729
|
-
return;
|
|
1730
|
-
}
|
|
1731
|
-
case "keyboard-enter": {
|
|
1732
|
-
try {
|
|
1733
|
-
await user.click(el);
|
|
1734
|
-
} catch {
|
|
1735
|
-
}
|
|
1736
|
-
const KeyboardEventCtor = globalThis.KeyboardEvent;
|
|
1737
|
-
if (KeyboardEventCtor) {
|
|
1738
|
-
const ev = new KeyboardEventCtor("keydown", { key: "Enter", bubbles: true });
|
|
1739
|
-
toEventDispatcher(el).dispatchEvent(ev);
|
|
1740
|
-
}
|
|
1741
|
-
return;
|
|
1742
|
-
}
|
|
1743
|
-
}
|
|
1744
|
-
}
|
|
1745
|
-
function setSyntheticValue(el) {
|
|
1746
|
-
try {
|
|
1747
|
-
const tag = el.tagName.toLowerCase();
|
|
1748
|
-
const node = editableSurface(el);
|
|
1749
|
-
if (tag === "select") {
|
|
1750
|
-
const opts = node.options;
|
|
1751
|
-
if (opts && opts.length > 0) {
|
|
1752
|
-
const current = node.value ?? "";
|
|
1753
|
-
let pick = opts[0].value;
|
|
1754
|
-
for (let i = 0; i < opts.length; i++) {
|
|
1755
|
-
if (opts[i].value !== current) {
|
|
1756
|
-
pick = opts[i].value;
|
|
1757
|
-
break;
|
|
1758
|
-
}
|
|
1759
|
-
}
|
|
1760
|
-
node.value = pick;
|
|
1761
|
-
}
|
|
1762
|
-
return;
|
|
1763
|
-
}
|
|
1764
|
-
if (tag === "input") {
|
|
1765
|
-
const type = (node.type ?? "text").toLowerCase();
|
|
1766
|
-
if (type === "checkbox" || type === "radio") {
|
|
1767
|
-
node.checked = !node.checked;
|
|
1768
|
-
return;
|
|
1769
|
-
}
|
|
1770
|
-
if (type === "number" || type === "range") {
|
|
1771
|
-
const cur = Number(node.value ?? "");
|
|
1772
|
-
node.value = String(Number.isFinite(cur) ? cur + 1 : 1);
|
|
1773
|
-
return;
|
|
1774
|
-
}
|
|
1775
|
-
node.value = node.value && node.value.length > 0 ? `${node.value}x` : "test";
|
|
1776
|
-
return;
|
|
1777
|
-
}
|
|
1778
|
-
if (tag === "textarea") {
|
|
1779
|
-
node.value = node.value && node.value.length > 0 ? `${node.value}x` : "test";
|
|
1780
|
-
return;
|
|
1781
|
-
}
|
|
1782
|
-
} catch {
|
|
1783
|
-
}
|
|
1784
|
-
}
|
|
1785
|
-
function closestForm(el) {
|
|
1786
|
-
let cur = parentLink(el);
|
|
1787
|
-
while (cur) {
|
|
1788
|
-
if (cur.tagName && cur.tagName.toLowerCase() === "form") {
|
|
1789
|
-
return toMinimalElement(cur);
|
|
1790
|
-
}
|
|
1791
|
-
cur = typeof cur.parentNode === "object" && cur.parentNode !== null ? parentLink(cur.parentNode) : null;
|
|
1792
|
-
}
|
|
1793
|
-
return null;
|
|
1794
|
-
}
|
|
1795
|
-
function actionControls(container, kind, name, label) {
|
|
1796
|
-
const clickables = findActionElements(container, name, label);
|
|
1797
|
-
const named = (el) => matchesActionIdentity(el, name, label);
|
|
1798
|
-
if (kind === "click" || kind === "keyboard-enter") {
|
|
1799
|
-
return clickables.map((element) => ({ element, namesAction: named(element), how: kind }));
|
|
1800
|
-
}
|
|
1801
|
-
const eligible = Array.from(
|
|
1802
|
-
container.querySelectorAll(
|
|
1803
|
-
kind === "submit" ? 'form, button[type="submit"], input[type="submit"]' : "select, input, textarea"
|
|
1804
|
-
)
|
|
1805
|
-
);
|
|
1806
|
-
const seen = new Set(eligible);
|
|
1807
|
-
return [
|
|
1808
|
-
...eligible.map((element) => ({ element, namesAction: named(element), how: kind })),
|
|
1809
|
-
...clickables.filter((el) => !seen.has(el)).map((element) => ({ element, namesAction: named(element), how: "click" }))
|
|
1810
|
-
];
|
|
1811
|
-
}
|
|
1812
|
-
function checkPropCoverage(input) {
|
|
1813
|
-
const { container, walkedTexts, propsSpec, mockupProps } = input;
|
|
1814
|
-
const issues = [];
|
|
1815
|
-
const texts = [container.textContent ?? "", ...walkedTexts];
|
|
1816
|
-
for (const [propName, entry] of Object.entries(propsSpec.properties)) {
|
|
1817
|
-
if (!entry.required) continue;
|
|
1818
|
-
const value = mockupProps[propName];
|
|
1819
|
-
if (value === void 0 || value === null) continue;
|
|
1820
|
-
const marker = pickScalarMarker(value);
|
|
1821
|
-
if (marker === null) continue;
|
|
1822
|
-
if (!texts.some((text) => text.includes(marker))) {
|
|
1823
|
-
const where = walkedTexts.length > 0 ? ", on the first screen or any screen the action walk reached" : "";
|
|
1824
|
-
issues.push({
|
|
1825
|
-
check: "prop-coverage",
|
|
1826
|
-
outcome: "unverified",
|
|
1827
|
-
subject: propName,
|
|
1828
|
-
reason: `Required prop '${propName}' value (${JSON.stringify(marker).slice(0, 60)}) not visible in rendered DOM${where}`
|
|
1829
|
-
});
|
|
1830
|
-
}
|
|
1831
|
-
}
|
|
1832
|
-
return issues;
|
|
1833
|
-
}
|
|
1834
|
-
async function checkStreamRerender(input) {
|
|
1835
|
-
const { container, eventName, probe } = input;
|
|
1836
|
-
if (!probe.getRegistered().streams.includes(eventName)) {
|
|
1837
|
-
return {
|
|
1838
|
-
check: "stream-rerender",
|
|
1839
|
-
outcome: "unverified",
|
|
1840
|
-
subject: eventName,
|
|
1841
|
-
reason: `useStream('${eventName}') was never called \u2014 the stream event is declared in the contract but the component does not subscribe to it`
|
|
1842
|
-
};
|
|
1843
|
-
}
|
|
1844
|
-
const before = container.textContent ?? "";
|
|
1845
|
-
const marker = `__probe_marker_${Date.now()}__`;
|
|
1846
|
-
const payload = { id: marker, text: marker, value: marker, message: marker, name: marker };
|
|
1847
|
-
await new Promise((resolve) => {
|
|
1848
|
-
setTimeout(() => {
|
|
1849
|
-
probe.emitStream(eventName, payload);
|
|
1850
|
-
resolve();
|
|
1851
|
-
}, 0);
|
|
1852
|
-
});
|
|
1853
|
-
await flushPromises();
|
|
1854
|
-
const after = container.textContent ?? "";
|
|
1855
|
-
if (after === before) {
|
|
1856
|
-
return {
|
|
1857
|
-
check: "stream-rerender",
|
|
1858
|
-
outcome: "unverified",
|
|
1859
|
-
subject: eventName,
|
|
1860
|
-
reason: `useStream('${eventName}') is subscribed but emitting a payload did not change the DOM`
|
|
1861
|
-
};
|
|
1862
|
-
}
|
|
1863
|
-
if (!after.includes(marker)) {
|
|
1864
|
-
return null;
|
|
1865
|
-
}
|
|
1866
|
-
return null;
|
|
1867
|
-
}
|
|
1868
|
-
function matchesActionIdentity(el, name, label) {
|
|
1869
|
-
const norm = (s) => s.toLowerCase().replace(/[\s_-]+/g, "");
|
|
1870
|
-
const aria = (el.getAttribute("aria-label") ?? "").toLowerCase();
|
|
1871
|
-
const txt = (el.textContent ?? "").toLowerCase();
|
|
1872
|
-
const dataAction = (el.getAttribute("data-action") ?? "").toLowerCase();
|
|
1873
|
-
return aria.includes(label.toLowerCase()) || txt.includes(label.toLowerCase()) || norm(aria).includes(norm(name)) || norm(txt).includes(norm(name)) || dataAction === name.toLowerCase() || norm(txt).includes(norm(label));
|
|
1874
|
-
}
|
|
1875
|
-
function findActionElements(container, name, label, options = {}) {
|
|
1876
|
-
const candidates = [];
|
|
1877
|
-
const seen = /* @__PURE__ */ new Set();
|
|
1878
|
-
const allClickable = container.querySelectorAll(
|
|
1879
|
-
'button, [role="button"], input[type="submit"], input[type="button"], a[href]'
|
|
1880
|
-
);
|
|
1881
|
-
for (const el of allClickable) {
|
|
1882
|
-
if (matchesActionIdentity(el, name, label)) {
|
|
1883
|
-
if (!seen.has(el)) {
|
|
1884
|
-
seen.add(el);
|
|
1885
|
-
candidates.push(el);
|
|
1886
|
-
}
|
|
1887
|
-
}
|
|
1888
|
-
}
|
|
1889
|
-
if (options.identityOnly) return candidates;
|
|
1890
|
-
for (const el of allClickable) {
|
|
1891
|
-
if (!seen.has(el)) {
|
|
1892
|
-
seen.add(el);
|
|
1893
|
-
candidates.push(el);
|
|
1894
|
-
}
|
|
1895
|
-
}
|
|
1896
|
-
return candidates;
|
|
1897
|
-
}
|
|
1898
|
-
function describeElement(el) {
|
|
1899
|
-
const tag = el.tagName.toLowerCase();
|
|
1900
|
-
const aria = el.getAttribute("aria-label");
|
|
1901
|
-
const txt = (el.textContent ?? "").trim().slice(0, 30);
|
|
1902
|
-
if (aria) return `<${tag} aria-label="${aria}">`;
|
|
1903
|
-
if (txt) return `<${tag}>${txt}</${tag}>`;
|
|
1904
|
-
return `<${tag}>`;
|
|
1905
|
-
}
|
|
1906
|
-
function pickScalarMarker(value) {
|
|
1907
|
-
if (typeof value === "string" && value.length > 0) return value;
|
|
1908
|
-
if (typeof value === "number") return String(value);
|
|
1909
|
-
if (Array.isArray(value)) {
|
|
1910
|
-
for (const item of value) {
|
|
1911
|
-
const m = pickScalarMarker(item);
|
|
1912
|
-
if (m) return m;
|
|
1913
|
-
}
|
|
1914
|
-
return null;
|
|
1915
|
-
}
|
|
1916
|
-
if (value && typeof value === "object") {
|
|
1917
|
-
for (const v of Object.values(value)) {
|
|
1918
|
-
const m = pickScalarMarker(v);
|
|
1919
|
-
if (m) return m;
|
|
1920
|
-
}
|
|
1921
|
-
}
|
|
1922
|
-
return null;
|
|
1923
|
-
}
|
|
1924
|
-
function flushPromises() {
|
|
1925
|
-
return new Promise((resolve) => setTimeout(resolve, 0));
|
|
1926
|
-
}
|
|
1927
|
-
|
|
1928
|
-
// src/harness/check/runtime-render/prepare-mockup.ts
|
|
1929
|
-
function prepareMockupProps(input) {
|
|
1930
|
-
const { contract, fixtureProps } = input;
|
|
1931
|
-
const propsSpec = contract?.propsSpec;
|
|
1932
|
-
const props = {};
|
|
1933
|
-
const source = {};
|
|
1934
|
-
const warnings = [];
|
|
1935
|
-
if (!propsSpec || !propsSpec.properties) {
|
|
1936
|
-
if (fixtureProps) {
|
|
1937
|
-
Object.assign(props, fixtureProps);
|
|
1938
|
-
for (const k of Object.keys(fixtureProps)) source[k] = "fixture";
|
|
1939
|
-
}
|
|
1940
|
-
return { props, source, warnings };
|
|
1941
|
-
}
|
|
1942
|
-
for (const [key, entry] of Object.entries(propsSpec.properties)) {
|
|
1943
|
-
if (fixtureProps && Object.prototype.hasOwnProperty.call(fixtureProps, key)) {
|
|
1944
|
-
props[key] = fixtureProps[key];
|
|
1945
|
-
source[key] = "fixture";
|
|
1946
|
-
continue;
|
|
1947
|
-
}
|
|
1948
|
-
const synth = synthesizePropValue(entry, key, warnings);
|
|
1949
|
-
if (synth.kind === "ok") {
|
|
1950
|
-
props[key] = synth.value;
|
|
1951
|
-
source[key] = synth.source;
|
|
1952
|
-
} else if (entry.required) {
|
|
1953
|
-
warnings.push(`Required prop '${key}' could not be synthesized (${synth.reason})`);
|
|
1954
|
-
}
|
|
1955
|
-
}
|
|
1956
|
-
return { props, source, warnings };
|
|
1957
|
-
}
|
|
1958
|
-
function synthesizePropValue(entry, fieldName, warnings) {
|
|
1959
|
-
if (entry.example !== void 0) {
|
|
1960
|
-
return { kind: "ok", value: entry.example, source: "entry-example" };
|
|
1961
|
-
}
|
|
1962
|
-
if (entry.default !== void 0) {
|
|
1963
|
-
return { kind: "ok", value: entry.default, source: "entry-default" };
|
|
1964
|
-
}
|
|
1965
|
-
return synthesizeFromSchema(entry.schema, fieldName, warnings, 0);
|
|
1966
|
-
}
|
|
1967
|
-
var MAX_DEPTH = 6;
|
|
1968
|
-
function synthesizeFromSchema(schema, hint, warnings, depth, ordinal) {
|
|
1969
|
-
if (!schema) return { kind: "fail", reason: "no schema" };
|
|
1970
|
-
if (depth > MAX_DEPTH) return { kind: "fail", reason: "schema too deep" };
|
|
1971
|
-
if (schema.default !== void 0) {
|
|
1972
|
-
return { kind: "ok", value: schema.default, source: "schema-default" };
|
|
1973
|
-
}
|
|
1974
|
-
if (schema.example !== void 0) {
|
|
1975
|
-
return { kind: "ok", value: schema.example, source: "schema-example" };
|
|
1976
|
-
}
|
|
1977
|
-
if (schema.enum && schema.enum.length > 0) {
|
|
1978
|
-
return { kind: "ok", value: schema.enum[0], source: "schema-enum" };
|
|
1979
|
-
}
|
|
1980
|
-
if (schema.oneOf && schema.oneOf.length > 0) {
|
|
1981
|
-
return synthesizeFromSchema(schema.oneOf[0], hint, warnings, depth + 1, ordinal);
|
|
1982
|
-
}
|
|
1983
|
-
if (schema.anyOf && schema.anyOf.length > 0) {
|
|
1984
|
-
return synthesizeFromSchema(schema.anyOf[0], hint, warnings, depth + 1, ordinal);
|
|
1985
|
-
}
|
|
1986
|
-
if (Array.isArray(schema.type)) {
|
|
1987
|
-
const primary = schema.type.find((t) => t !== "null") ?? "null";
|
|
1988
|
-
return synthesizeFromSchema({ ...schema, type: primary }, hint, warnings, depth + 1, ordinal);
|
|
1989
|
-
}
|
|
1990
|
-
switch (schema.type) {
|
|
1991
|
-
case "string": {
|
|
1992
|
-
switch (schema.format) {
|
|
1993
|
-
case "date":
|
|
1994
|
-
case "date-time":
|
|
1995
|
-
return { kind: "ok", value: "2026-04-13T12:00:00Z", source: "schema-synth" };
|
|
1996
|
-
case "email":
|
|
1997
|
-
return { kind: "ok", value: "user@example.com", source: "schema-synth" };
|
|
1998
|
-
case "uri":
|
|
1999
|
-
case "url":
|
|
2000
|
-
return { kind: "ok", value: "https://example.com", source: "schema-synth" };
|
|
2001
|
-
case "uuid":
|
|
2002
|
-
return { kind: "ok", value: "00000000-0000-0000-0000-000000000000", source: "schema-synth" };
|
|
2003
|
-
default: {
|
|
2004
|
-
const cap = hint.charAt(0).toUpperCase() + hint.slice(1);
|
|
2005
|
-
const value = ordinal === void 0 ? `Sample ${cap}` : `Sample ${cap} ${ordinal}`;
|
|
2006
|
-
return { kind: "ok", value, source: "schema-synth" };
|
|
2007
|
-
}
|
|
2008
|
-
}
|
|
2009
|
-
}
|
|
2010
|
-
case "integer":
|
|
2011
|
-
case "number": {
|
|
2012
|
-
const min = typeof schema.minimum === "number" ? schema.minimum : 1;
|
|
2013
|
-
const max = typeof schema.maximum === "number" ? schema.maximum : 100;
|
|
2014
|
-
const value = Math.min(max, Math.max(min, 42));
|
|
2015
|
-
return { kind: "ok", value, source: "schema-synth" };
|
|
2016
|
-
}
|
|
2017
|
-
case "boolean":
|
|
2018
|
-
return { kind: "ok", value: true, source: "schema-synth" };
|
|
2019
|
-
case "null":
|
|
2020
|
-
return { kind: "ok", value: null, source: "schema-synth" };
|
|
2021
|
-
case "array": {
|
|
2022
|
-
if (!schema.items) {
|
|
2023
|
-
return { kind: "ok", value: [], source: "schema-synth" };
|
|
2024
|
-
}
|
|
2025
|
-
const items = [];
|
|
2026
|
-
for (let i = 0; i < 2; i++) {
|
|
2027
|
-
const itemHint = `${hint}Item${i + 1}`;
|
|
2028
|
-
const itemSynth = synthesizeFromSchema(schema.items, itemHint, warnings, depth + 1, i + 1);
|
|
2029
|
-
if (itemSynth.kind === "ok") {
|
|
2030
|
-
if (itemSynth.value !== null && typeof itemSynth.value === "object" && !Array.isArray(itemSynth.value) && !("id" in itemSynth.value)) {
|
|
2031
|
-
itemSynth.value.id = `${hint}-${i + 1}`;
|
|
2032
|
-
}
|
|
2033
|
-
items.push(itemSynth.value);
|
|
2034
|
-
}
|
|
2035
|
-
}
|
|
2036
|
-
return { kind: "ok", value: items, source: "schema-synth" };
|
|
2037
|
-
}
|
|
2038
|
-
case "object": {
|
|
2039
|
-
const obj = {};
|
|
2040
|
-
const propsMap = schema.properties ?? {};
|
|
2041
|
-
const required = new Set(schema.required ?? []);
|
|
2042
|
-
for (const [k, sub] of Object.entries(propsMap)) {
|
|
2043
|
-
const subSynth = synthesizeFromSchema(sub, k, warnings, depth + 1, ordinal);
|
|
2044
|
-
if (subSynth.kind === "ok") {
|
|
2045
|
-
obj[k] = subSynth.value;
|
|
2046
|
-
} else if (required.has(k)) {
|
|
2047
|
-
warnings.push(
|
|
2048
|
-
`Required object field '${hint}.${k}' could not be synthesized (${subSynth.reason})`
|
|
2049
|
-
);
|
|
2050
|
-
}
|
|
2051
|
-
}
|
|
2052
|
-
return { kind: "ok", value: obj, source: "schema-synth" };
|
|
2053
|
-
}
|
|
2054
|
-
default:
|
|
2055
|
-
return { kind: "fail", reason: `unsupported schema type: ${schema.type ?? "unknown"}` };
|
|
2056
|
-
}
|
|
2057
|
-
}
|
|
2058
|
-
|
|
2059
|
-
// src/harness/check/runtime-render/adapter.ts
|
|
2060
|
-
init_render_check_host();
|
|
2061
|
-
function createSlotLimiter(maxConcurrent) {
|
|
2062
|
-
if (maxConcurrent === void 0) return (task) => task(0);
|
|
2063
|
-
if (!Number.isInteger(maxConcurrent) || maxConcurrent < 1) {
|
|
2064
|
-
throw new RangeError(
|
|
2065
|
-
`createRuntimeRenderCheck: maxConcurrent must be a positive integer, got ${maxConcurrent}`
|
|
2066
|
-
);
|
|
2067
|
-
}
|
|
2068
|
-
let active = 0;
|
|
2069
|
-
const waiting = [];
|
|
2070
|
-
const acquire = () => {
|
|
2071
|
-
if (active < maxConcurrent) {
|
|
2072
|
-
active += 1;
|
|
2073
|
-
return Promise.resolve(0);
|
|
2074
|
-
}
|
|
2075
|
-
const enqueuedAt = Date.now();
|
|
2076
|
-
return new Promise((resolve) => {
|
|
2077
|
-
waiting.push(() => {
|
|
2078
|
-
active += 1;
|
|
2079
|
-
resolve(Date.now() - enqueuedAt);
|
|
2080
|
-
});
|
|
2081
|
-
});
|
|
2082
|
-
};
|
|
2083
|
-
const release = () => {
|
|
2084
|
-
active -= 1;
|
|
2085
|
-
const next = waiting.shift();
|
|
2086
|
-
if (next !== void 0) next();
|
|
2087
|
-
};
|
|
2088
|
-
return async (task) => {
|
|
2089
|
-
const waitedMs = await acquire();
|
|
2090
|
-
try {
|
|
2091
|
-
return await task(waitedMs);
|
|
2092
|
-
} finally {
|
|
2093
|
-
release();
|
|
2094
|
-
}
|
|
2095
|
-
};
|
|
2096
|
-
}
|
|
2097
|
-
function queuedField(queuedMs) {
|
|
2098
|
-
return queuedMs > 0 ? { queuedMs } : {};
|
|
2099
|
-
}
|
|
2100
|
-
function toRenderCheckOptions(config) {
|
|
2101
|
-
if (config.timeoutMs === void 0 && config.heapMb === void 0) return void 0;
|
|
2102
|
-
return {
|
|
2103
|
-
bounds: {
|
|
2104
|
-
...config.timeoutMs !== void 0 ? { timeoutMs: config.timeoutMs } : {},
|
|
2105
|
-
...config.heapMb !== void 0 ? { heapMb: config.heapMb } : {}
|
|
2106
|
-
}
|
|
2107
|
-
};
|
|
2108
|
-
}
|
|
2109
|
-
function createRuntimeRenderCheck(config = {}) {
|
|
2110
|
-
const limit = createSlotLimiter(config.maxConcurrent);
|
|
2111
|
-
const options = toRenderCheckOptions(config);
|
|
2112
|
-
if (options !== void 0) resolveRenderCheckHostBounds(options.bounds);
|
|
2113
|
-
return {
|
|
2114
|
-
id: "runtime-render",
|
|
2115
|
-
run: async (input) => {
|
|
2116
|
-
const { sourceCode, compiledCode, contract, fixtureProps } = input;
|
|
2117
|
-
if (compiledCode === null) {
|
|
2118
|
-
return { status: "not-applicable", issues: [], reason: "no compiled code" };
|
|
2119
|
-
}
|
|
2120
|
-
if (!contract) {
|
|
2121
|
-
return { status: "not-applicable", issues: [], reason: "no contract surface" };
|
|
2122
|
-
}
|
|
2123
|
-
const mockup = prepareMockupProps({ contract, fixtureProps });
|
|
2124
|
-
let queuedMs = 0;
|
|
2125
|
-
let t0 = Date.now();
|
|
2126
|
-
let result;
|
|
2127
|
-
try {
|
|
2128
|
-
result = await limit((waitedMs) => {
|
|
2129
|
-
queuedMs = waitedMs;
|
|
2130
|
-
t0 = Date.now();
|
|
2131
|
-
return runRenderCheck(
|
|
2132
|
-
{
|
|
2133
|
-
sourceCode,
|
|
2134
|
-
mockupProps: mockup.props,
|
|
2135
|
-
contract
|
|
2136
|
-
},
|
|
2137
|
-
options
|
|
2138
|
-
);
|
|
2139
|
-
});
|
|
2140
|
-
} catch (e) {
|
|
2141
|
-
const message = e instanceof Error ? e.message : String(e);
|
|
2142
|
-
console.warn(`[runtime-render] probe skipped \u2014 infra failure: ${message}`);
|
|
2143
|
-
return { status: "infra-skipped", issues: [], reason: message, elapsedMs: Date.now() - t0, ...queuedField(queuedMs) };
|
|
2144
|
-
}
|
|
2145
|
-
const hostLoad = result.stats.hostLoad;
|
|
2146
|
-
const load = hostLoad !== void 0 ? { hostLoad } : {};
|
|
2147
|
-
if (result.incomplete !== void 0) {
|
|
2148
|
-
const { elapsedMs, boundMs } = result.incomplete;
|
|
2149
|
-
const loadNote = hostLoad !== void 0 ? `; host load ${hostLoad.start.toFixed(1)} \u2192 ${hostLoad.end.toFixed(1)} on ${hostLoad.cores} CPUs` : "";
|
|
2150
|
-
const reason = `render check did not finish within ${boundMs} ms (stopped at ${elapsedMs} ms)${loadNote}`;
|
|
2151
|
-
console.warn(`[runtime-render] probe timed out \u2014 ${reason}`);
|
|
2152
|
-
return { status: "timed-out", issues: [], reason, elapsedMs: Date.now() - t0, ...queuedField(queuedMs), ...load };
|
|
2153
|
-
}
|
|
2154
|
-
return {
|
|
2155
|
-
status: "ran",
|
|
2156
|
-
issues: result.issues.map(toEvalIssue).filter((x) => x !== null),
|
|
2157
|
-
elapsedMs: Date.now() - t0,
|
|
2158
|
-
...queuedField(queuedMs),
|
|
2159
|
-
renderMs: result.stats.renderMs,
|
|
2160
|
-
...load,
|
|
2161
|
-
// ggui#1398: report-only — carried to the probe's meta for the bench, never an issue.
|
|
2162
|
-
...result.stats.pendingAffordance !== void 0 ? { pendingAffordance: result.stats.pendingAffordance } : {}
|
|
2163
|
-
};
|
|
2164
|
-
}
|
|
2165
|
-
};
|
|
2166
|
-
}
|
|
2167
|
-
var DEFAULT_RUNTIME_RENDER_CHECK = createRuntimeRenderCheck();
|
|
2168
|
-
function classifyRenderCrashFix(reason) {
|
|
2169
|
-
const r = reason.toLowerCase();
|
|
2170
|
-
if (r.includes("too many re-renders") || r.includes("maximum update depth") || r.includes("infinite render loop")) {
|
|
2171
|
-
return "Find the setState/dispatch call in your render body or useEffect. Either: (a) move the setState into an event handler, (b) add a proper useEffect dependency array so it doesn't fire every render, or (c) guard the setState with an equality check `if (next !== current) setX(next)`. Do NOT call setState in render or useEffect-without-deps.";
|
|
2172
|
-
}
|
|
2173
|
-
if (r.includes("cannot access") && r.includes("before initialization")) {
|
|
2174
|
-
const symMatch = reason.match(/'([^']+)'/);
|
|
2175
|
-
const sym = symMatch ? symMatch[1] : "the variable";
|
|
2176
|
-
return `Temporal-dead-zone error on \`${sym}\`. You're referencing ${sym} (likely in a useEffect dependency array or a default value) BEFORE its \`const\`/\`let\` declaration. Move the declaration of ${sym} to the top of the component body, before any useEffect/useMemo/useCallback that reads it.`;
|
|
2177
|
-
}
|
|
2178
|
-
if (r.includes("is not defined")) {
|
|
2179
|
-
const symMatch = reason.match(/(\w+) is not defined/);
|
|
2180
|
-
const sym = symMatch ? symMatch[1] : "the symbol";
|
|
2181
|
-
return `ReferenceError: \`${sym}\` is used in JSX/expression but never declared. Either: (a) add a \`const ${sym} = ...\` declaration in the component body, (b) destructure it from props/state, or (c) fix the typo if you meant a similarly-named local.`;
|
|
2182
|
-
}
|
|
2183
|
-
if (r.includes("is not iterable") || r.includes("symbol(symbol.iterator)")) {
|
|
2184
|
-
return "Render iterated over a non-array. Find the `for...of`, `[...spread]`, or `.map`/`.filter`/`.reduce` call that crashed and either: (a) default the value to `[]` (`const items = props.items ?? []`), or (b) check it's an array before iterating (`Array.isArray(x) && ...`).";
|
|
2185
|
-
}
|
|
2186
|
-
if (r.includes("cannot read") || r.includes("undefined is not") || r.includes("null is not")) {
|
|
2187
|
-
return "Null/undefined access. Add optional chaining (`obj?.field`) and default values for optional props/state before reading nested fields. Check your destructure patterns \u2014 destructuring undefined throws.";
|
|
2188
|
-
}
|
|
2189
|
-
return "Add null guards on optional props, handle empty arrays/strings, and verify all hook outputs before destructuring.";
|
|
2190
|
-
}
|
|
2191
|
-
function toEvalIssue(issue) {
|
|
2192
|
-
if (issue.outcome === "verified" || issue.outcome === "skipped") return null;
|
|
2193
|
-
const result = issue.outcome === "failed" ? "fail" : "warn";
|
|
2194
|
-
const subject = issue.subject ?? "";
|
|
2195
|
-
const subcategory = subject ? `runtime:${issue.check}:${subject}` : `runtime:${issue.check}`;
|
|
2196
|
-
const elementHint = issue.elementHint ? ` (element: ${issue.elementHint})` : "";
|
|
2197
|
-
const diag = issue.diagnostics;
|
|
2198
|
-
const diagParts = [];
|
|
2199
|
-
if (diag?.observedNativeProps?.length) {
|
|
2200
|
-
diagParts.push(`native props: ${diag.observedNativeProps.join(", ")}`);
|
|
2201
|
-
}
|
|
2202
|
-
if (diag?.observedCustomProps?.length) {
|
|
2203
|
-
diagParts.push(`custom props: ${diag.observedCustomProps.join(", ")}`);
|
|
2204
|
-
}
|
|
2205
|
-
if (diag?.observedJsxElements?.length) {
|
|
2206
|
-
diagParts.push(`elements: ${diag.observedJsxElements.slice(0, 4).join(", ")}`);
|
|
2207
|
-
}
|
|
2208
|
-
if (diag?.actionsFiredFromClicks?.length) {
|
|
2209
|
-
diagParts.push(`other actions fired from clicks: ${diag.actionsFiredFromClicks.join(", ")}`);
|
|
2210
|
-
}
|
|
2211
|
-
if (diag?.inputPriming) {
|
|
2212
|
-
diagParts.push(
|
|
2213
|
-
"primed" in diag.inputPriming ? `input priming: ${diag.inputPriming.primed} control(s) filled before the click` : `input priming failed: ${diag.inputPriming.error}`
|
|
2214
|
-
);
|
|
2215
|
-
}
|
|
2216
|
-
if (diag?.walk) {
|
|
2217
|
-
const w = diag.walk;
|
|
2218
|
-
const stopped = {
|
|
2219
|
-
explored: "every reachable control pressed",
|
|
2220
|
-
"press-cap": "stopped at the press cap",
|
|
2221
|
-
deadline: "stopped at the time budget",
|
|
2222
|
-
"no-mount": "the card could not be mounted for the walk"
|
|
2223
|
-
}[w.stoppedBy];
|
|
2224
|
-
const named = w.namedControlSeen ? "a control naming the action rendered, but pressing it did not dispatch the action" : "no screen rendered a control naming the action";
|
|
2225
|
-
const last = w.lastPressed.length ? `; last pressed: ${w.lastPressed.join(", ")}` : "";
|
|
2226
|
-
diagParts.push(`walk: ${w.presses} press(es) across ${w.screens} screen(s), ${stopped}; ${named}${last}`);
|
|
2227
|
-
}
|
|
2228
|
-
const diagSuffix = diagParts.length ? ` [observed: ${diagParts.join("; ")}]` : "";
|
|
2229
|
-
switch (issue.check) {
|
|
2230
|
-
case "render-no-throw":
|
|
2231
|
-
return {
|
|
2232
|
-
tier: 0,
|
|
2233
|
-
result,
|
|
2234
|
-
category: "crash",
|
|
2235
|
-
subcategory,
|
|
2236
|
-
severity: "critical",
|
|
2237
|
-
description: `Component crashed at runtime: ${issue.reason}`,
|
|
2238
|
-
fix: classifyRenderCrashFix(issue.reason)
|
|
2239
|
-
};
|
|
2240
|
-
case "optional-props-omitted":
|
|
2241
|
-
return {
|
|
2242
|
-
tier: 0,
|
|
2243
|
-
result,
|
|
2244
|
-
category: "crash",
|
|
2245
|
-
subcategory,
|
|
2246
|
-
severity: "critical",
|
|
2247
|
-
description: `Component crashed when optional props were omitted: ${issue.reason}`,
|
|
2248
|
-
fix: `Guard every optional prop before member access (props.${subject.split(",")[0]?.trim() ?? "x"}?.map(...), a default via destructuring, or an early empty-state return). Optional props (no \`required: true\` in the contract) may be absent at runtime even though the harness fills them.`
|
|
2249
|
-
};
|
|
2250
|
-
case "action-wiring": {
|
|
2251
|
-
const fix = issue.outcome === "unverified" ? `Source shows the action callback flowing into a non-native or custom-component prop. If wiring is intentional (e.g., Dropdown.onChange, drag-drop), this warn is informational \u2014 manual/browser verification is required to confirm. Otherwise wire to a native onClick={() => ${subject}(payload)} on <button> or design-system <Button>.` : `Wire ${subject}() to a native event prop. Common fix: <Button onClick={() => ${subject}(payload)}>Label</Button>. Source-AST analysis didn't find this wiring in your JSX.`;
|
|
2252
|
-
return {
|
|
2253
|
-
tier: 0,
|
|
2254
|
-
result,
|
|
2255
|
-
category: "contract",
|
|
2256
|
-
subcategory,
|
|
2257
|
-
severity: result === "fail" ? "critical" : "major",
|
|
2258
|
-
description: `${issue.reason}${elementHint}${diagSuffix}`,
|
|
2259
|
-
fix
|
|
2260
|
-
};
|
|
2261
|
-
}
|
|
2262
|
-
case "selection-identity":
|
|
2263
|
-
return {
|
|
2264
|
-
tier: 0,
|
|
2265
|
-
result,
|
|
2266
|
-
category: "contract",
|
|
2267
|
-
subcategory,
|
|
2268
|
-
severity: "critical",
|
|
2269
|
-
description: `${issue.reason}${elementHint}${diagSuffix}`,
|
|
2270
|
-
fix: `Key the action payload (and any selection state) on the item's ID from the data, never on its display text. Common fix: map over items and pass the item's id into the handler \u2014 onClick={() => ${subject}({ ...payload, id: item.id })} \u2014 and store selection as the id (setSelected(item.id)), not the label.`
|
|
2271
|
-
};
|
|
2272
|
-
case "prop-sensitivity":
|
|
2273
|
-
return {
|
|
2274
|
-
tier: 0,
|
|
2275
|
-
result,
|
|
2276
|
-
category: "contract",
|
|
2277
|
-
subcategory,
|
|
2278
|
-
severity: "critical",
|
|
2279
|
-
description: `${issue.reason}${diagSuffix}`,
|
|
2280
|
-
fix: `Derive the display AND any logic from props.${subject} \u2014 render {props.${subject}} and branch on the prop's value, never on the literal value the request happened to mention (e.g. props.${subject} ?? 'January' followed by January-special-cased logic bakes the request into the component). The contract delivers the value; trust it.`
|
|
2281
|
-
};
|
|
2282
|
-
case "prop-coverage":
|
|
2283
|
-
return {
|
|
2284
|
-
tier: 0,
|
|
2285
|
-
result,
|
|
2286
|
-
category: "contract",
|
|
2287
|
-
subcategory,
|
|
2288
|
-
description: `${issue.reason}${diagSuffix}`,
|
|
2289
|
-
fix: `Render props.${subject} somewhere in the JSX (e.g., <Text>{props.${subject}}</Text>). If you display a derived/formatted version, this warn may be a false positive.`
|
|
2290
|
-
};
|
|
2291
|
-
case "stream-rerender":
|
|
2292
|
-
return {
|
|
2293
|
-
tier: 0,
|
|
2294
|
-
result,
|
|
2295
|
-
category: "contract",
|
|
2296
|
-
subcategory,
|
|
2297
|
-
description: `${issue.reason}${diagSuffix}`,
|
|
2298
|
-
fix: `Subscribe with const ${subject} = useStream('${subject}'); render ${subject}.latest && <Text>{${subject}.latest.field}</Text> or .all.map(...).`
|
|
2299
|
-
};
|
|
2300
|
-
}
|
|
2301
|
-
}
|
|
1
|
+
import { DEFAULT_RUNTIME_RENDER_CHECK } from './chunk-BA4QYA6E.js';
|
|
2
|
+
import './chunk-3NNNGNTO.js';
|
|
3
|
+
import './chunk-FXRHWYIQ.js';
|
|
4
|
+
import './chunk-KJWGZL5B.js';
|
|
5
|
+
import './chunk-FLRNMZV2.js';
|
|
6
|
+
import './chunk-G3PMV62Z.js';
|
|
2302
7
|
|
|
2303
8
|
// src/blueprint-validator.ts
|
|
2304
9
|
function probeDidNotRunCode(status) {
|