@ggui-ai/ui-gen 0.22.0 → 0.24.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-Du7P_5Fx.d.ts +201 -0
- package/dist/adapters/generation-dispatch.d.ts +33 -12
- package/dist/adapters/generation-dispatch.js +549 -129
- package/dist/adapters/generation-dispatch.js.map +1 -1
- package/dist/adapters/index.js +1 -1
- package/dist/adapters/index.js.map +1 -1
- package/dist/advanced/index.d.ts +5 -3
- package/dist/advanced/index.js +601 -144
- package/dist/advanced/index.js.map +1 -1
- package/dist/blueprint-validator.js +178 -54
- package/dist/blueprint-validator.js.map +1 -1
- package/dist/boilerplate.js +1 -0
- package/dist/boilerplate.js.map +1 -1
- package/dist/check/index.d.ts +2 -1
- package/dist/check/index.js +47 -1
- package/dist/check/index.js.map +1 -1
- package/dist/{create-ui-generator-BEkzEtNz.d.ts → create-ui-generator-GIhzx08x.d.ts} +34 -10
- package/dist/{dispatch-DJNpQ7DI.d.ts → dispatch-BnAH56p4.d.ts} +1 -1
- package/dist/evaluation/axis-checks/index.d.ts +4 -3
- package/dist/evaluation/axis-checks/index.js +19 -4
- package/dist/evaluation/axis-checks/index.js.map +1 -1
- package/dist/evaluation/axis-checks/registry.d.ts +2 -1
- package/dist/evaluation/axis-checks/registry.js +19 -4
- package/dist/evaluation/axis-checks/registry.js.map +1 -1
- package/dist/evaluation/index.d.ts +4 -3
- package/dist/evaluation/index.js +21 -6
- package/dist/evaluation/index.js.map +1 -1
- package/dist/harness/check/runtime-render/index.d.ts +27 -8
- package/dist/harness/check/runtime-render/index.js +217 -54
- package/dist/harness/check/runtime-render/index.js.map +1 -1
- package/dist/harness/check/runtime-render/render-check-worker.d.ts +8 -1
- package/dist/harness/check/runtime-render/render-check-worker.js +72 -32
- package/dist/harness/check/runtime-render/render-check-worker.js.map +1 -1
- package/dist/harness/index.d.ts +2 -1
- package/dist/harness/index.js +3 -2
- package/dist/harness/index.js.map +1 -1
- package/dist/harness/prompts.js.map +1 -1
- package/dist/harness/result-types.d.ts +2 -1
- package/dist/harness/runtime.d.ts +2 -1
- package/dist/harness/runtime.js +2 -1
- package/dist/harness/runtime.js.map +1 -1
- package/dist/harness/types-public.d.ts +18 -5
- package/dist/harness/types-public.js +1 -0
- package/dist/harness/types-public.js.map +1 -1
- package/dist/harness/validator-trace-sink.d.ts +2 -1
- package/dist/index.d.ts +38 -3
- package/dist/index.js +592 -144
- package/dist/index.js.map +1 -1
- package/dist/patch.d.ts +2 -1
- package/dist/policy.d.ts +7 -4
- package/dist/policy.js.map +1 -1
- package/dist/tools/get-primitives-ts.d.ts +1 -1
- package/dist/tools/get-primitives-ts.js +1 -1
- package/dist/tools/get-primitives-ts.js.map +1 -1
- package/dist/tools/render-check-worker.d.ts +7 -1
- package/dist/tools/render-check-worker.js +55 -23
- package/dist/tools/render-check-worker.js.map +1 -1
- package/dist/{types-public-mM_-1Yk3.d.ts → types-public-Bk6d1WLN.d.ts} +98 -9
- package/dist/validation/index.d.ts +1 -1
- package/dist/validation/index.js +1 -1
- package/dist/validation/index.js.map +1 -1
- package/dist/workflows.d.ts +2 -1
- package/package.json +12 -12
- package/src/adapters/generation-dispatch.ts +137 -96
- package/src/advanced/generator.ts +16 -1
- package/src/blueprint-validator.ts +21 -6
- package/src/boilerplate/hard-sections.ts +1 -0
- package/src/check/run-tier0.ts +80 -1
- package/src/coding-agent/tools.ts +6 -1
- package/src/create-ui-generator.ts +185 -95
- package/src/evaluation/axis-checks/checks/state-payload.ts +29 -4
- package/src/evaluation/types-public.ts +94 -8
- package/src/harness/check/runtime-render/adapter.ts +223 -58
- package/src/harness/check/runtime-render/index.ts +75 -2
- package/src/harness/check/runtime-render/render-check-host.ts +111 -26
- package/src/harness/check/runtime-render/render-check-worker.ts +83 -29
- package/src/harness/check/runtime-render/render-check.ts +173 -117
- package/src/harness/coding/generate-task-runner.ts +57 -4
- package/src/harness/coding/init-session.ts +13 -0
- package/src/harness/coding/run-eval-round.ts +238 -13
- package/src/harness/llm-router.ts +3 -3
- package/src/harness/policy.ts +11 -0
- package/src/harness/primitive-slice.ts +104 -0
- package/src/harness/types-public.ts +17 -5
- package/src/index.ts +4 -0
- package/src/policy.ts +7 -4
- package/src/tools/get-primitives-ts.ts +1 -1
- package/src/tools/render-check-worker.ts +74 -23
- package/src/validation/primitives.ts +1 -1
- package/dist/render-check-C0CqVceF.d.ts +0 -87
|
@@ -15,18 +15,11 @@
|
|
|
15
15
|
// All checks are best-effort: if a heuristic can't find an element, the
|
|
16
16
|
// check fails with a descriptive reason rather than throwing.
|
|
17
17
|
|
|
18
|
-
import type {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
PropsSpec,
|
|
22
|
-
} from "@ggui-ai/protocol";
|
|
18
|
+
import type { RenderCheckHostOptions } from "./render-check-host.js";
|
|
19
|
+
import type { GenerationRuntimeProbeCheck } from "@ggui-ai/mcp-server-core";
|
|
20
|
+
import type { DataContract, JsonObject, PropsSpec } from "@ggui-ai/protocol";
|
|
23
21
|
import { HOOK_NAME_RE, listContractGadgets } from "@ggui-ai/protocol";
|
|
24
|
-
import {
|
|
25
|
-
createProbe,
|
|
26
|
-
createProbeWireConfig,
|
|
27
|
-
type ActionFiredEvent,
|
|
28
|
-
type Probe,
|
|
29
|
-
} from "./probe.js";
|
|
22
|
+
import { createProbe, createProbeWireConfig, type ActionFiredEvent, type Probe } from "./probe.js";
|
|
30
23
|
import { loadComponent } from "./load-component.js";
|
|
31
24
|
import { findWiring, type WiringDetection } from "./find-wiring.js";
|
|
32
25
|
import { installProductionActShim } from "./production-act-shim.js";
|
|
@@ -45,6 +38,7 @@ import {
|
|
|
45
38
|
type ParentLink,
|
|
46
39
|
} from "./host-boundary.js";
|
|
47
40
|
import { hostGlobals, openRecord } from "../../../internal/open-record.js";
|
|
41
|
+
import type { ProbeHostLoad } from "../../../evaluation/types-public.js";
|
|
48
42
|
import { primeInputs } from "@ggui-ai/ui-visual-tester/prime-inputs";
|
|
49
43
|
|
|
50
44
|
/**
|
|
@@ -72,15 +66,32 @@ type ProbeModuleNamespace = {
|
|
|
72
66
|
*/
|
|
73
67
|
export type CheckOutcome = "verified" | "failed" | "unverified" | "skipped";
|
|
74
68
|
|
|
69
|
+
/**
|
|
70
|
+
* The checks this probe runs, by name (ggui#1380). ONE union: the same
|
|
71
|
+
* values a generation's metadata lists in `failChecks`
|
|
72
|
+
* (`GenerationRuntimeProbeCheck` in `@ggui-ai/mcp-server-core`), declared
|
|
73
|
+
* there and named here — the same pattern as `RuntimeProbeStatus`.
|
|
74
|
+
*/
|
|
75
|
+
export type RenderCheckKind = GenerationRuntimeProbeCheck;
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Every {@link RenderCheckKind}, once, in the order the union declares them.
|
|
79
|
+
* The order is the order a probe's `failChecks` lists the checks that failed,
|
|
80
|
+
* so a render crash always reads first. `satisfies` refuses a kind outside
|
|
81
|
+
* the union; the pin beside the adapter refuses a member missing here.
|
|
82
|
+
*/
|
|
83
|
+
export const RENDER_CHECK_KINDS = [
|
|
84
|
+
"render-no-throw",
|
|
85
|
+
"prop-sensitivity",
|
|
86
|
+
"action-wiring",
|
|
87
|
+
"selection-identity",
|
|
88
|
+
"prop-coverage",
|
|
89
|
+
"optional-props-omitted",
|
|
90
|
+
"stream-rerender",
|
|
91
|
+
] as const satisfies readonly RenderCheckKind[];
|
|
92
|
+
|
|
75
93
|
export interface RenderCheckIssue {
|
|
76
|
-
readonly check:
|
|
77
|
-
| "render-no-throw"
|
|
78
|
-
| "action-wiring"
|
|
79
|
-
| "selection-identity"
|
|
80
|
-
| "prop-coverage"
|
|
81
|
-
| "prop-sensitivity"
|
|
82
|
-
| "stream-rerender"
|
|
83
|
-
| "optional-props-omitted";
|
|
94
|
+
readonly check: RenderCheckKind;
|
|
84
95
|
readonly outcome: CheckOutcome;
|
|
85
96
|
readonly subject?: string;
|
|
86
97
|
readonly reason: string;
|
|
@@ -114,17 +125,33 @@ export interface RenderCheckIssue {
|
|
|
114
125
|
* priming failure never aborts the check — it is recorded here, on every
|
|
115
126
|
* action-wiring issue, so it is visible in the feedback instead of vanishing.
|
|
116
127
|
*/
|
|
117
|
-
export type InputPrimingDiagnostic =
|
|
118
|
-
|
|
119
|
-
|
|
128
|
+
export type InputPrimingDiagnostic = { readonly primed: number } | { readonly error: string };
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* The check started but did not finish (ggui#1299). The only kind is a
|
|
132
|
+
* wall-clock timeout in the isolated worker. It is not a verdict on the
|
|
133
|
+
* component: `issues` is empty and `ok` is false because nothing was
|
|
134
|
+
* verified, never because something failed.
|
|
135
|
+
*/
|
|
136
|
+
export interface RenderCheckIncomplete {
|
|
137
|
+
readonly kind: "timeout";
|
|
138
|
+
/** How long the worker ran before it was stopped. */
|
|
139
|
+
readonly elapsedMs: number;
|
|
140
|
+
/** The wall-clock bound it crossed. */
|
|
141
|
+
readonly boundMs: number;
|
|
142
|
+
}
|
|
120
143
|
|
|
121
144
|
export interface RenderCheckResult {
|
|
122
145
|
readonly ok: boolean;
|
|
123
146
|
readonly issues: readonly RenderCheckIssue[];
|
|
147
|
+
/** Present only when the check did not finish; see {@link RenderCheckIncomplete}. */
|
|
148
|
+
readonly incomplete?: RenderCheckIncomplete;
|
|
124
149
|
readonly stats: {
|
|
125
150
|
readonly actionsChecked: number;
|
|
126
151
|
readonly streamsChecked: number;
|
|
127
152
|
readonly renderMs: number;
|
|
153
|
+
/** Host load around an isolated check (absent in-process). */
|
|
154
|
+
readonly hostLoad?: ProbeHostLoad;
|
|
128
155
|
};
|
|
129
156
|
}
|
|
130
157
|
|
|
@@ -134,6 +161,14 @@ export interface RunRenderCheckInput {
|
|
|
134
161
|
readonly contract?: DataContract;
|
|
135
162
|
}
|
|
136
163
|
|
|
164
|
+
/**
|
|
165
|
+
* Options of {@link runRenderCheck} (ggui#1380): the host's options —
|
|
166
|
+
* `bounds` are the isolated subprocess's wall-clock, heap and grace (see
|
|
167
|
+
* `RenderCheckHostBounds`); they apply to the worker path only and are
|
|
168
|
+
* ignored in-process. One shape, declared once on the host.
|
|
169
|
+
*/
|
|
170
|
+
export type RunRenderCheckOptions = RenderCheckHostOptions;
|
|
171
|
+
|
|
137
172
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
138
173
|
// Public entry
|
|
139
174
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -160,13 +195,15 @@ export interface RunRenderCheckInput {
|
|
|
160
195
|
*/
|
|
161
196
|
export async function runRenderCheck(
|
|
162
197
|
input: RunRenderCheckInput,
|
|
198
|
+
options: RunRenderCheckOptions = {}
|
|
163
199
|
): Promise<RenderCheckResult> {
|
|
164
200
|
const g = hostGlobals();
|
|
165
201
|
if ("window" in g && "document" in g) {
|
|
202
|
+
// In-process: nothing to bound — the check shares the caller's process.
|
|
166
203
|
return runRenderCheckInProcess(input);
|
|
167
204
|
}
|
|
168
205
|
const { runRenderCheckViaWorker } = await import("./render-check-host.js");
|
|
169
|
-
return runRenderCheckViaWorker(input);
|
|
206
|
+
return runRenderCheckViaWorker(input, options);
|
|
170
207
|
}
|
|
171
208
|
|
|
172
209
|
/**
|
|
@@ -179,7 +216,7 @@ export async function runRenderCheck(
|
|
|
179
216
|
* dispatcher explicitly.
|
|
180
217
|
*/
|
|
181
218
|
export async function runRenderCheckInProcess(
|
|
182
|
-
input: RunRenderCheckInput
|
|
219
|
+
input: RunRenderCheckInput
|
|
183
220
|
): Promise<RenderCheckResult> {
|
|
184
221
|
const t0 = Date.now();
|
|
185
222
|
const issues: RenderCheckIssue[] = [];
|
|
@@ -207,7 +244,7 @@ export async function runRenderCheckInProcess(
|
|
|
207
244
|
const { GguiWireProvider } = Wire;
|
|
208
245
|
|
|
209
246
|
const moduleResolutions: Record<string, unknown> = {
|
|
210
|
-
|
|
247
|
+
react: React,
|
|
211
248
|
"react/jsx-runtime": ReactJsxRuntime,
|
|
212
249
|
"@ggui-ai/wire": Wire,
|
|
213
250
|
};
|
|
@@ -243,9 +280,7 @@ export async function runRenderCheckInProcess(
|
|
|
243
280
|
// a direct dep. Swallow on miss —
|
|
244
281
|
// same posture as design subpaths above.
|
|
245
282
|
try {
|
|
246
|
-
moduleResolutions["@ggui-ai/gadgets"] = await import(
|
|
247
|
-
"@ggui-ai/gadgets"
|
|
248
|
-
);
|
|
283
|
+
moduleResolutions["@ggui-ai/gadgets"] = await import("@ggui-ai/gadgets");
|
|
249
284
|
} catch {
|
|
250
285
|
/* skip — sandbox will report a clear error if the component imports it */
|
|
251
286
|
}
|
|
@@ -347,11 +382,7 @@ export async function runRenderCheckInProcess(
|
|
|
347
382
|
if (!capturedComponentStack) {
|
|
348
383
|
// Try to find any arg that looks like a " at X (...)" stack.
|
|
349
384
|
for (const arg of args) {
|
|
350
|
-
if (
|
|
351
|
-
typeof arg === "string" &&
|
|
352
|
-
/^\s*at\s+\S/m.test(arg) &&
|
|
353
|
-
arg.length > 20
|
|
354
|
-
) {
|
|
385
|
+
if (typeof arg === "string" && /^\s*at\s+\S/m.test(arg) && arg.length > 20) {
|
|
355
386
|
capturedComponentStack = arg;
|
|
356
387
|
break;
|
|
357
388
|
}
|
|
@@ -426,11 +457,19 @@ export async function runRenderCheckInProcess(
|
|
|
426
457
|
children: React.createElement(ProbeErrorBoundary, {
|
|
427
458
|
children: React.createElement(Component, input.mockupProps),
|
|
428
459
|
}),
|
|
429
|
-
})
|
|
430
|
-
)
|
|
460
|
+
})
|
|
461
|
+
)
|
|
431
462
|
);
|
|
432
463
|
const timeoutPromise = new Promise<never>((_, reject) => {
|
|
433
|
-
setTimeout(
|
|
464
|
+
setTimeout(
|
|
465
|
+
() =>
|
|
466
|
+
reject(
|
|
467
|
+
new Error(
|
|
468
|
+
`[runtime-render] render exceeded ${RENDER_TIMEOUT_MS}ms — classified as runtime-hang`
|
|
469
|
+
)
|
|
470
|
+
),
|
|
471
|
+
RENDER_TIMEOUT_MS
|
|
472
|
+
).unref?.();
|
|
434
473
|
});
|
|
435
474
|
renderResult = await Promise.race([renderPromise, timeoutPromise]);
|
|
436
475
|
} catch (e) {
|
|
@@ -457,7 +496,10 @@ export async function runRenderCheckInProcess(
|
|
|
457
496
|
.split("\n")
|
|
458
497
|
.map((l) => l.trim())
|
|
459
498
|
.filter((l) => l.startsWith("at "))
|
|
460
|
-
.filter(
|
|
499
|
+
.filter(
|
|
500
|
+
(l) =>
|
|
501
|
+
!/(node_modules|happy-dom|@testing-library|react-dom|node:internal|node:async)/.test(l)
|
|
502
|
+
)
|
|
461
503
|
.slice(0, 5);
|
|
462
504
|
// Prefer the ErrorBoundary-captured stack (richer + names component
|
|
463
505
|
// wrappers like memo/forwardRef), fall back to the spy-captured one.
|
|
@@ -497,9 +539,7 @@ export async function runRenderCheckInProcess(
|
|
|
497
539
|
const compBlock = componentFrames.length
|
|
498
540
|
? `\n Component stack: ${componentFrames.join(" > ")}`
|
|
499
541
|
: "";
|
|
500
|
-
const errMsg = boundaryRef.error
|
|
501
|
-
? boundaryRef.error.message
|
|
502
|
-
: "(no error message)";
|
|
542
|
+
const errMsg = boundaryRef.error ? boundaryRef.error.message : "(no error message)";
|
|
503
543
|
issues.push({
|
|
504
544
|
check: "render-no-throw",
|
|
505
545
|
outcome: "failed",
|
|
@@ -577,7 +617,7 @@ export async function runRenderCheckInProcess(
|
|
|
577
617
|
if (input.contract?.actionSpec) {
|
|
578
618
|
for (const [name, entry] of Object.entries(input.contract.actionSpec)) {
|
|
579
619
|
const idField = idLikeRequiredStringField(
|
|
580
|
-
entry.schema as Record<string, unknown> | undefined
|
|
620
|
+
entry.schema as Record<string, unknown> | undefined
|
|
581
621
|
);
|
|
582
622
|
if (idField === null) continue;
|
|
583
623
|
const issue = await checkSelectionIdentity({
|
|
@@ -634,8 +674,7 @@ export async function runRenderCheckInProcess(
|
|
|
634
674
|
// prop — not dead), and non-scalar props are all skipped.
|
|
635
675
|
if (input.contract?.propsSpec) {
|
|
636
676
|
const propsSpec5c = input.contract.propsSpec as PropsSpec;
|
|
637
|
-
const normalize = (s: string | null): string =>
|
|
638
|
-
(s ?? "").replace(/\s+/g, " ").trim();
|
|
677
|
+
const normalize = (s: string | null): string => (s ?? "").replace(/\s+/g, " ").trim();
|
|
639
678
|
const baseText = normalize(container.textContent);
|
|
640
679
|
for (const [propName, entry] of Object.entries(propsSpec5c.properties)) {
|
|
641
680
|
if (!entry.required) continue;
|
|
@@ -663,7 +702,7 @@ export async function runRenderCheckInProcess(
|
|
|
663
702
|
[propName]: varied,
|
|
664
703
|
}),
|
|
665
704
|
}),
|
|
666
|
-
})
|
|
705
|
+
})
|
|
667
706
|
);
|
|
668
707
|
} catch {
|
|
669
708
|
continue; // crash on varied value ⇒ the prop is read, not dead
|
|
@@ -676,8 +715,7 @@ export async function runRenderCheckInProcess(
|
|
|
676
715
|
check: "prop-sensitivity",
|
|
677
716
|
outcome: "failed",
|
|
678
717
|
subject: propName,
|
|
679
|
-
reason:
|
|
680
|
-
`props.${propName} is displayed ('${marker}' appears in the DOM) but changing its value to '${String(varied)}' left the rendered output BYTE-IDENTICAL — 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.`,
|
|
718
|
+
reason: `props.${propName} is displayed ('${marker}' appears in the DOM) but changing its value to '${String(varied)}' left the rendered output BYTE-IDENTICAL — 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.`,
|
|
681
719
|
});
|
|
682
720
|
}
|
|
683
721
|
}
|
|
@@ -711,9 +749,10 @@ export async function runRenderCheckInProcess(
|
|
|
711
749
|
const declaredProps = input.contract?.propsSpec?.properties;
|
|
712
750
|
if (declaredProps) {
|
|
713
751
|
const optionalFilled = Object.entries(declaredProps)
|
|
714
|
-
.filter(
|
|
715
|
-
(entry
|
|
716
|
-
|
|
752
|
+
.filter(
|
|
753
|
+
([name, entry]) =>
|
|
754
|
+
(entry as { required?: boolean }).required !== true &&
|
|
755
|
+
input.mockupProps[name] !== undefined
|
|
717
756
|
)
|
|
718
757
|
.map(([name]) => name);
|
|
719
758
|
if (optionalFilled.length > 0) {
|
|
@@ -743,11 +782,10 @@ export async function runRenderCheckInProcess(
|
|
|
743
782
|
children: React.createElement(OmitErrorBoundary, {
|
|
744
783
|
children: React.createElement(Component, requiredOnlyProps),
|
|
745
784
|
}),
|
|
746
|
-
})
|
|
785
|
+
})
|
|
747
786
|
);
|
|
748
787
|
} catch (e) {
|
|
749
|
-
omitBoundaryRef.error ??=
|
|
750
|
-
e instanceof Error ? e : new Error(String(e));
|
|
788
|
+
omitBoundaryRef.error ??= e instanceof Error ? e : new Error(String(e));
|
|
751
789
|
}
|
|
752
790
|
if (omitBoundaryRef.error) {
|
|
753
791
|
issues.push({
|
|
@@ -769,8 +807,16 @@ export async function runRenderCheckInProcess(
|
|
|
769
807
|
|
|
770
808
|
return finalize(issues, t0, actionsChecked, streamsChecked);
|
|
771
809
|
} finally {
|
|
772
|
-
try {
|
|
773
|
-
|
|
810
|
+
try {
|
|
811
|
+
uninstallSpy?.();
|
|
812
|
+
} catch {
|
|
813
|
+
/* ignore */
|
|
814
|
+
}
|
|
815
|
+
try {
|
|
816
|
+
uninstallGadgetRegistry?.();
|
|
817
|
+
} catch {
|
|
818
|
+
/* ignore */
|
|
819
|
+
}
|
|
774
820
|
teardown();
|
|
775
821
|
}
|
|
776
822
|
}
|
|
@@ -814,9 +860,7 @@ export async function runRenderCheckInProcess(
|
|
|
814
860
|
* Returns an uninstaller; the caller MUST invoke it — `globalThis` is
|
|
815
861
|
* a process global and a leaked slot taints the next check.
|
|
816
862
|
*/
|
|
817
|
-
export function installGadgetStubRegistry(
|
|
818
|
-
contract: DataContract | undefined,
|
|
819
|
-
): () => void {
|
|
863
|
+
export function installGadgetStubRegistry(contract: DataContract | undefined): () => void {
|
|
820
864
|
const declared = contract?.clientCapabilities?.gadgets;
|
|
821
865
|
if (!declared || Object.keys(declared).length === 0) {
|
|
822
866
|
return () => {};
|
|
@@ -829,9 +873,8 @@ export function installGadgetStubRegistry(
|
|
|
829
873
|
const valueStub = new Proxy(
|
|
830
874
|
{},
|
|
831
875
|
{
|
|
832
|
-
get: (_t, key) =>
|
|
833
|
-
|
|
834
|
-
},
|
|
876
|
+
get: (_t, key) => (key === "then" || typeof key === "symbol" ? undefined : noop),
|
|
877
|
+
}
|
|
835
878
|
);
|
|
836
879
|
const result = { status: "idle", value: valueStub, start: noop };
|
|
837
880
|
// Unknown top-level keys resolve to a no-op (covers `map.foo()`);
|
|
@@ -886,9 +929,7 @@ export function installGadgetStubRegistry(
|
|
|
886
929
|
* `__ggui__.gadgets` registry). Only operator-registered wrapper
|
|
887
930
|
* packages need a sandbox shim.
|
|
888
931
|
*/
|
|
889
|
-
function collectThirdPartyGadgetPackages(
|
|
890
|
-
contract: DataContract | undefined,
|
|
891
|
-
): string[] {
|
|
932
|
+
function collectThirdPartyGadgetPackages(contract: DataContract | undefined): string[] {
|
|
892
933
|
if (!contract) return [];
|
|
893
934
|
const packages = new Set<string>();
|
|
894
935
|
for (const use of listContractGadgets(contract)) {
|
|
@@ -934,7 +975,7 @@ function buildGadgetPackageProbeShim(packageName: string): ProbeModuleNamespace
|
|
|
934
975
|
const impl = resolveExport(name);
|
|
935
976
|
if (typeof impl !== "function") {
|
|
936
977
|
throw new Error(
|
|
937
|
-
`[gadget] export '${name}' from '${packageName}' is not loaded in the render-check probe — the component imports it but the contract's clientCapabilities.gadgets never declared it, so installGadgetStubRegistry planted no stub
|
|
978
|
+
`[gadget] export '${name}' from '${packageName}' is not loaded in the render-check probe — the component imports it but the contract's clientCapabilities.gadgets never declared it, so installGadgetStubRegistry planted no stub.`
|
|
938
979
|
);
|
|
939
980
|
}
|
|
940
981
|
return (impl as (...a: unknown[]) => unknown)(...args);
|
|
@@ -950,9 +991,8 @@ function buildGadgetPackageProbeShim(packageName: string): ProbeModuleNamespace
|
|
|
950
991
|
},
|
|
951
992
|
// `key in ns` checks (esbuild interop may probe) report true so
|
|
952
993
|
// the named import binds to the thunk rather than `undefined`.
|
|
953
|
-
has: (_t, key) =>
|
|
954
|
-
|
|
955
|
-
},
|
|
994
|
+
has: (_t, key) => typeof key !== "symbol" && key !== "then" && key !== "default",
|
|
995
|
+
}
|
|
956
996
|
) as ProbeModuleNamespace;
|
|
957
997
|
}
|
|
958
998
|
|
|
@@ -1020,12 +1060,16 @@ async function setupHappyDom(): Promise<() => void> {
|
|
|
1020
1060
|
return;
|
|
1021
1061
|
}
|
|
1022
1062
|
// Not ours — re-throw on next tick so the default handler runs.
|
|
1023
|
-
process.nextTick(() => {
|
|
1063
|
+
process.nextTick(() => {
|
|
1064
|
+
throw err;
|
|
1065
|
+
});
|
|
1024
1066
|
};
|
|
1025
1067
|
const unhandledHandler = (reason: unknown): void => {
|
|
1026
1068
|
if (isTeardownArtifact(reason)) return;
|
|
1027
1069
|
const err = reason instanceof Error ? reason : new Error(String(reason));
|
|
1028
|
-
process.nextTick(() => {
|
|
1070
|
+
process.nextTick(() => {
|
|
1071
|
+
throw err;
|
|
1072
|
+
});
|
|
1029
1073
|
};
|
|
1030
1074
|
process.on("uncaughtException", uncaughtHandler);
|
|
1031
1075
|
process.on("unhandledRejection", unhandledHandler);
|
|
@@ -1125,9 +1169,9 @@ function finalize(
|
|
|
1125
1169
|
issues: RenderCheckIssue[],
|
|
1126
1170
|
t0: number,
|
|
1127
1171
|
actionsChecked: number,
|
|
1128
|
-
streamsChecked: number
|
|
1172
|
+
streamsChecked: number
|
|
1129
1173
|
): RenderCheckResult {
|
|
1130
|
-
const ok = !issues.some(i => i.outcome === "failed");
|
|
1174
|
+
const ok = !issues.some((i) => i.outcome === "failed");
|
|
1131
1175
|
return {
|
|
1132
1176
|
ok,
|
|
1133
1177
|
issues,
|
|
@@ -1167,7 +1211,9 @@ async function checkActionWiring(input: CheckActionInput): Promise<RenderCheckIs
|
|
|
1167
1211
|
check: "action-wiring",
|
|
1168
1212
|
outcome: "failed",
|
|
1169
1213
|
subject: actionName,
|
|
1170
|
-
reason:
|
|
1214
|
+
reason:
|
|
1215
|
+
wiring.reason ??
|
|
1216
|
+
`Action '${actionName}' is declared in contract but useAction('${actionName}') is not wired to any UI element`,
|
|
1171
1217
|
diagnostics: baseDiagnostics,
|
|
1172
1218
|
};
|
|
1173
1219
|
}
|
|
@@ -1178,7 +1224,9 @@ async function checkActionWiring(input: CheckActionInput): Promise<RenderCheckIs
|
|
|
1178
1224
|
check: "action-wiring",
|
|
1179
1225
|
outcome: "unverified",
|
|
1180
1226
|
subject: actionName,
|
|
1181
|
-
reason:
|
|
1227
|
+
reason:
|
|
1228
|
+
wiring.reason ??
|
|
1229
|
+
"Source indicates non-click or non-native wiring; static probe did not verify execution deterministically.",
|
|
1182
1230
|
diagnostics: baseDiagnostics,
|
|
1183
1231
|
};
|
|
1184
1232
|
}
|
|
@@ -1229,24 +1277,20 @@ async function checkActionWiring(input: CheckActionInput): Promise<RenderCheckIs
|
|
|
1229
1277
|
* `id`/`Id`/`ID` and whose declared type is string — the shape the
|
|
1230
1278
|
* matcher's booking/selection contracts carry (`bookSlot { slotId }`).
|
|
1231
1279
|
*/
|
|
1232
|
-
function idLikeRequiredStringField(
|
|
1233
|
-
schema: Record<string, unknown> | undefined,
|
|
1234
|
-
): string | null {
|
|
1280
|
+
function idLikeRequiredStringField(schema: Record<string, unknown> | undefined): string | null {
|
|
1235
1281
|
if (schema === undefined) return null;
|
|
1236
|
-
const properties = schema[
|
|
1237
|
-
const required = schema[
|
|
1238
|
-
if (properties === null || typeof properties !==
|
|
1282
|
+
const properties = schema["properties"];
|
|
1283
|
+
const required = schema["required"];
|
|
1284
|
+
if (properties === null || typeof properties !== "object") return null;
|
|
1239
1285
|
if (!Array.isArray(required)) return null;
|
|
1240
|
-
for (const [propName, propSchema] of Object.entries(
|
|
1241
|
-
properties as Record<string, unknown>,
|
|
1242
|
-
)) {
|
|
1286
|
+
for (const [propName, propSchema] of Object.entries(properties as Record<string, unknown>)) {
|
|
1243
1287
|
if (!/id$/i.test(propName)) continue;
|
|
1244
1288
|
if (!required.includes(propName)) continue;
|
|
1245
1289
|
const t =
|
|
1246
|
-
propSchema !== null && typeof propSchema ===
|
|
1247
|
-
? (propSchema as Record<string, unknown>)[
|
|
1290
|
+
propSchema !== null && typeof propSchema === "object"
|
|
1291
|
+
? (propSchema as Record<string, unknown>)["type"]
|
|
1248
1292
|
: undefined;
|
|
1249
|
-
if (t ===
|
|
1293
|
+
if (t === "string") return propName;
|
|
1250
1294
|
}
|
|
1251
1295
|
return null;
|
|
1252
1296
|
}
|
|
@@ -1269,16 +1313,16 @@ interface CheckSelectionIdentityInput {
|
|
|
1269
1313
|
* null (covered by other checks or genuinely unjudgeable here).
|
|
1270
1314
|
*/
|
|
1271
1315
|
async function checkSelectionIdentity(
|
|
1272
|
-
input: CheckSelectionIdentityInput
|
|
1316
|
+
input: CheckSelectionIdentityInput
|
|
1273
1317
|
): Promise<RenderCheckIssue | null> {
|
|
1274
1318
|
const { container, probe, user, actionName, idField } = input;
|
|
1275
1319
|
|
|
1276
1320
|
const clickables = Array.from(
|
|
1277
|
-
container.querySelectorAll<MinimalElement>('button, [role="button"]')
|
|
1321
|
+
container.querySelectorAll<MinimalElement>('button, [role="button"]')
|
|
1278
1322
|
);
|
|
1279
1323
|
const byLabel = new Map<string, MinimalElement[]>();
|
|
1280
1324
|
for (const el of clickables) {
|
|
1281
|
-
const label = (el.textContent ??
|
|
1325
|
+
const label = (el.textContent ?? "").replace(/\s+/g, " ").trim();
|
|
1282
1326
|
if (label.length === 0) continue;
|
|
1283
1327
|
const group = byLabel.get(label);
|
|
1284
1328
|
if (group) group.push(el);
|
|
@@ -1288,9 +1332,7 @@ async function checkSelectionIdentity(
|
|
|
1288
1332
|
for (const [label, group] of byLabel) {
|
|
1289
1333
|
if (group.length < 2) continue;
|
|
1290
1334
|
|
|
1291
|
-
const payloadFor = async (
|
|
1292
|
-
el: MinimalElement,
|
|
1293
|
-
): Promise<unknown | undefined> => {
|
|
1335
|
+
const payloadFor = async (el: MinimalElement): Promise<unknown | undefined> => {
|
|
1294
1336
|
const before = probe.getFireLog().length;
|
|
1295
1337
|
try {
|
|
1296
1338
|
await user.click(el);
|
|
@@ -1301,10 +1343,7 @@ async function checkSelectionIdentity(
|
|
|
1301
1343
|
const events = probe
|
|
1302
1344
|
.getFireLog()
|
|
1303
1345
|
.slice(before)
|
|
1304
|
-
.filter(
|
|
1305
|
-
(e): e is ActionFiredEvent =>
|
|
1306
|
-
e.kind === 'action.fired' && e.name === actionName,
|
|
1307
|
-
);
|
|
1346
|
+
.filter((e): e is ActionFiredEvent => e.kind === "action.fired" && e.name === actionName);
|
|
1308
1347
|
return events.length > 0 ? events[events.length - 1]!.payload : undefined;
|
|
1309
1348
|
};
|
|
1310
1349
|
|
|
@@ -1315,9 +1354,9 @@ async function checkSelectionIdentity(
|
|
|
1315
1354
|
if (first === undefined || second === undefined) continue;
|
|
1316
1355
|
|
|
1317
1356
|
const idOf = (payload: unknown): string | undefined => {
|
|
1318
|
-
if (payload === null || typeof payload !==
|
|
1357
|
+
if (payload === null || typeof payload !== "object") return undefined;
|
|
1319
1358
|
const v = (payload as Record<string, unknown>)[idField];
|
|
1320
|
-
return typeof v ===
|
|
1359
|
+
return typeof v === "string" ? v : undefined;
|
|
1321
1360
|
};
|
|
1322
1361
|
const firstId = idOf(first);
|
|
1323
1362
|
const secondId = idOf(second);
|
|
@@ -1326,11 +1365,10 @@ async function checkSelectionIdentity(
|
|
|
1326
1365
|
|
|
1327
1366
|
if (firstId === secondId) {
|
|
1328
1367
|
return {
|
|
1329
|
-
check:
|
|
1330
|
-
outcome:
|
|
1368
|
+
check: "selection-identity",
|
|
1369
|
+
outcome: "failed",
|
|
1331
1370
|
subject: actionName,
|
|
1332
|
-
reason:
|
|
1333
|
-
`Two clickable elements share the visible label '${label}', and clicking EACH fired '${actionName}' with the same ${idField} ('${firstId}') — 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}".`,
|
|
1371
|
+
reason: `Two clickable elements share the visible label '${label}', and clicking EACH fired '${actionName}' with the same ${idField} ('${firstId}') — 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}".`,
|
|
1334
1372
|
elementHint: describeElement(group[0]!),
|
|
1335
1373
|
};
|
|
1336
1374
|
}
|
|
@@ -1377,16 +1415,16 @@ async function simulateAndCheck(input: SimulateAndCheckInput): Promise<SimulateR
|
|
|
1377
1415
|
}
|
|
1378
1416
|
await flushPromises();
|
|
1379
1417
|
const newEvents = probe.getFireLog().slice(before);
|
|
1380
|
-
const matched = newEvents.some(e => e.kind === eventKind && e.name === actionName);
|
|
1418
|
+
const matched = newEvents.some((e) => e.kind === eventKind && e.name === actionName);
|
|
1381
1419
|
if (matched) return { fired: true };
|
|
1382
1420
|
}
|
|
1383
1421
|
|
|
1384
1422
|
// Capture what DID fire — useful diagnostic.
|
|
1385
1423
|
const allFired = probe
|
|
1386
1424
|
.getFireLog()
|
|
1387
|
-
.filter(e => e.kind === "action.fired")
|
|
1388
|
-
.map(e => e.name);
|
|
1389
|
-
const otherActionsFired = Array.from(new Set(allFired)).filter(n => n !== actionName);
|
|
1425
|
+
.filter((e) => e.kind === "action.fired")
|
|
1426
|
+
.map((e) => e.name);
|
|
1427
|
+
const otherActionsFired = Array.from(new Set(allFired)).filter((n) => n !== actionName);
|
|
1390
1428
|
|
|
1391
1429
|
return {
|
|
1392
1430
|
fired: false,
|
|
@@ -1398,7 +1436,7 @@ async function simulateAndCheck(input: SimulateAndCheckInput): Promise<SimulateR
|
|
|
1398
1436
|
async function dispatchTrigger(
|
|
1399
1437
|
el: MinimalElement,
|
|
1400
1438
|
kind: "click" | "submit" | "change" | "keyboard-enter",
|
|
1401
|
-
user: { click: (el: MinimalElement) => Promise<void> }
|
|
1439
|
+
user: { click: (el: MinimalElement) => Promise<void> }
|
|
1402
1440
|
): Promise<void> {
|
|
1403
1441
|
switch (kind) {
|
|
1404
1442
|
case "click":
|
|
@@ -1409,7 +1447,10 @@ async function dispatchTrigger(
|
|
|
1409
1447
|
// dispatch a 'submit' event on it. happy-dom respects this.
|
|
1410
1448
|
const form = closestForm(el);
|
|
1411
1449
|
if (!form) return;
|
|
1412
|
-
const ev = new (globalThis as { Event: typeof Event }).Event("submit", {
|
|
1450
|
+
const ev = new (globalThis as { Event: typeof Event }).Event("submit", {
|
|
1451
|
+
bubbles: true,
|
|
1452
|
+
cancelable: true,
|
|
1453
|
+
});
|
|
1413
1454
|
toEventDispatcher(form).dispatchEvent(ev);
|
|
1414
1455
|
return;
|
|
1415
1456
|
}
|
|
@@ -1425,8 +1466,14 @@ async function dispatchTrigger(
|
|
|
1425
1466
|
}
|
|
1426
1467
|
case "keyboard-enter": {
|
|
1427
1468
|
// Click first to focus, then dispatch an Enter keydown.
|
|
1428
|
-
try {
|
|
1429
|
-
|
|
1469
|
+
try {
|
|
1470
|
+
await user.click(el);
|
|
1471
|
+
} catch {
|
|
1472
|
+
/* ignore */
|
|
1473
|
+
}
|
|
1474
|
+
const KeyboardEventCtor = (
|
|
1475
|
+
globalThis as { KeyboardEvent?: new (type: string, init: object) => Event }
|
|
1476
|
+
).KeyboardEvent;
|
|
1430
1477
|
if (KeyboardEventCtor) {
|
|
1431
1478
|
const ev = new KeyboardEventCtor("keydown", { key: "Enter", bubbles: true });
|
|
1432
1479
|
toEventDispatcher(el).dispatchEvent(ev);
|
|
@@ -1463,7 +1510,10 @@ function setSyntheticValue(el: MinimalElement): void {
|
|
|
1463
1510
|
const current = node.value ?? "";
|
|
1464
1511
|
let pick = opts[0]!.value;
|
|
1465
1512
|
for (let i = 0; i < opts.length; i++) {
|
|
1466
|
-
if (opts[i]!.value !== current) {
|
|
1513
|
+
if (opts[i]!.value !== current) {
|
|
1514
|
+
pick = opts[i]!.value;
|
|
1515
|
+
break;
|
|
1516
|
+
}
|
|
1467
1517
|
}
|
|
1468
1518
|
node.value = pick;
|
|
1469
1519
|
}
|
|
@@ -1514,7 +1564,7 @@ function findCandidateElements(
|
|
|
1514
1564
|
container: MinimalElement,
|
|
1515
1565
|
kind: "click" | "submit" | "change" | "keyboard-enter",
|
|
1516
1566
|
name: string,
|
|
1517
|
-
label: string
|
|
1567
|
+
label: string
|
|
1518
1568
|
): MinimalElement[] {
|
|
1519
1569
|
switch (kind) {
|
|
1520
1570
|
case "click":
|
|
@@ -1522,7 +1572,7 @@ function findCandidateElements(
|
|
|
1522
1572
|
case "submit": {
|
|
1523
1573
|
// submit-eligible: <form>, button[type=submit], input[type=submit]
|
|
1524
1574
|
const all = container.querySelectorAll<MinimalElement>(
|
|
1525
|
-
'form, button[type="submit"], input[type="submit"]'
|
|
1575
|
+
'form, button[type="submit"], input[type="submit"]'
|
|
1526
1576
|
);
|
|
1527
1577
|
return Array.from(all);
|
|
1528
1578
|
}
|
|
@@ -1575,7 +1625,9 @@ interface CheckStreamRerenderInput {
|
|
|
1575
1625
|
React: typeof import("react");
|
|
1576
1626
|
}
|
|
1577
1627
|
|
|
1578
|
-
async function checkStreamRerender(
|
|
1628
|
+
async function checkStreamRerender(
|
|
1629
|
+
input: CheckStreamRerenderInput
|
|
1630
|
+
): Promise<RenderCheckIssue | null> {
|
|
1579
1631
|
const { container, eventName, probe } = input;
|
|
1580
1632
|
|
|
1581
1633
|
if (!probe.getRegistered().streams.includes(eventName)) {
|
|
@@ -1594,7 +1646,7 @@ async function checkStreamRerender(input: CheckStreamRerenderInput): Promise<Ren
|
|
|
1594
1646
|
// in multiple common field names so downstream consumers find it.
|
|
1595
1647
|
const payload = { id: marker, text: marker, value: marker, message: marker, name: marker };
|
|
1596
1648
|
|
|
1597
|
-
await new Promise<void>(resolve => {
|
|
1649
|
+
await new Promise<void>((resolve) => {
|
|
1598
1650
|
setTimeout(() => {
|
|
1599
1651
|
probe.emitStream(eventName, payload);
|
|
1600
1652
|
resolve();
|
|
@@ -1624,7 +1676,11 @@ async function checkStreamRerender(input: CheckStreamRerenderInput): Promise<Ren
|
|
|
1624
1676
|
// Helpers
|
|
1625
1677
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
1626
1678
|
|
|
1627
|
-
function findActionElements(
|
|
1679
|
+
function findActionElements(
|
|
1680
|
+
container: MinimalElement,
|
|
1681
|
+
name: string,
|
|
1682
|
+
label: string
|
|
1683
|
+
): MinimalElement[] {
|
|
1628
1684
|
const candidates: MinimalElement[] = [];
|
|
1629
1685
|
const seen = new Set<MinimalElement>();
|
|
1630
1686
|
|
|
@@ -1633,7 +1689,7 @@ function findActionElements(container: MinimalElement, name: string, label: stri
|
|
|
1633
1689
|
const labelKey = norm(label);
|
|
1634
1690
|
|
|
1635
1691
|
const allClickable = container.querySelectorAll<MinimalElement>(
|
|
1636
|
-
'button, [role="button"], input[type="submit"], input[type="button"], a[href]'
|
|
1692
|
+
'button, [role="button"], input[type="submit"], input[type="button"], a[href]'
|
|
1637
1693
|
);
|
|
1638
1694
|
|
|
1639
1695
|
// Pass 1: text/aria-label/data-action match
|
|
@@ -1696,5 +1752,5 @@ function pickScalarMarker(value: unknown): string | null {
|
|
|
1696
1752
|
}
|
|
1697
1753
|
|
|
1698
1754
|
function flushPromises(): Promise<void> {
|
|
1699
|
-
return new Promise(resolve => setTimeout(resolve, 0));
|
|
1755
|
+
return new Promise((resolve) => setTimeout(resolve, 0));
|
|
1700
1756
|
}
|