@ggui-ai/ui-gen 0.23.0 → 0.25.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-Cg_rkpGq.d.ts +201 -0
- package/dist/adapters/generation-dispatch.d.ts +58 -15
- package/dist/adapters/generation-dispatch.js +1418 -180
- package/dist/adapters/generation-dispatch.js.map +1 -1
- package/dist/adapters/index.js +3 -1
- package/dist/adapters/index.js.map +1 -1
- package/dist/advanced/index.d.ts +12 -8
- package/dist/advanced/index.js +1467 -239
- package/dist/advanced/index.js.map +1 -1
- package/dist/{axes-CzLEMDeB.d.ts → axes-CKVCzR_x.d.ts} +1 -1
- package/dist/bank-CT4eEkta.d.ts +118 -0
- package/dist/blueprint-validator.js +153 -59
- package/dist/blueprint-validator.js.map +1 -1
- package/dist/boilerplate.js +24 -12
- package/dist/boilerplate.js.map +1 -1
- package/dist/check/index.d.ts +12 -4
- package/dist/check/index.js +94 -2
- package/dist/check/index.js.map +1 -1
- package/dist/classifier/index.d.ts +2 -2
- package/dist/compose.d.ts +2 -2
- package/dist/{create-ui-generator-D0kbWwln.d.ts → create-ui-generator-DFnnArrv.d.ts} +43 -11
- package/dist/{dispatch-wxaIMwZt.d.ts → dispatch-CStl9eAw.d.ts} +5 -3
- package/dist/evaluation/axis-checks/index.d.ts +6 -5
- package/dist/evaluation/axis-checks/index.js +90 -2
- package/dist/evaluation/axis-checks/index.js.map +1 -1
- package/dist/evaluation/axis-checks/registry.d.ts +4 -3
- package/dist/evaluation/axis-checks/registry.js +88 -1
- package/dist/evaluation/axis-checks/registry.js.map +1 -1
- package/dist/evaluation/index.d.ts +301 -23
- package/dist/evaluation/index.js +746 -30
- package/dist/evaluation/index.js.map +1 -1
- package/dist/evaluation/types.d.ts +9 -1
- package/dist/evaluation/types.js.map +1 -1
- package/dist/fragments/index.d.ts +3 -3
- package/dist/harness/check/runtime-render/index.d.ts +30 -11
- package/dist/harness/check/runtime-render/index.js +203 -60
- 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 +5 -4
- package/dist/harness/index.js +33 -15
- package/dist/harness/index.js.map +1 -1
- package/dist/harness/prompts.js.map +1 -1
- package/dist/harness/result-types.d.ts +4 -3
- package/dist/harness/runtime.d.ts +11 -4
- package/dist/harness/runtime.js +26 -14
- package/dist/harness/runtime.js.map +1 -1
- package/dist/harness/types-public.d.ts +13 -4
- package/dist/harness/types-public.js +33 -15
- package/dist/harness/types-public.js.map +1 -1
- package/dist/harness/validator-trace-sink.d.ts +4 -3
- package/dist/hash.d.ts +1 -1
- package/dist/index.d.ts +59 -8
- package/dist/index.js +1476 -241
- package/dist/index.js.map +1 -1
- package/dist/{llm-router-DwwUK1bz.d.ts → llm-router-nfO7wPec.d.ts} +3 -1
- package/dist/patch.d.ts +4 -3
- package/dist/policy.d.ts +8 -5
- 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-BOvHNG7K.d.ts → types-Dh1s4K9j.d.ts} +1 -1
- package/dist/{types-public-Ow-tNtFE.d.ts → types-public-BJTLpzWn.d.ts} +208 -15
- 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 +4 -3
- package/package.json +12 -12
- package/src/adapters/generation-dispatch.ts +173 -98
- package/src/adapters/tools.ts +2 -1
- package/src/boilerplate/free-design-prompt.ts +1 -1
- package/src/boilerplate/generate.ts +26 -1
- package/src/boilerplate/hard-sections.ts +13 -13
- package/src/boilerplate/system-prompt.ts +1 -1
- package/src/check/index.ts +1 -0
- package/src/check/run-tier0.ts +80 -1
- package/src/check/sample-props.ts +63 -0
- package/src/coding-agent/tools.ts +4 -1
- package/src/create-harness.ts +3 -3
- package/src/create-ui-generator.ts +204 -95
- package/src/evaluation/__fixtures__/png.ts +100 -0
- package/src/evaluation/axis-checks/checks/state-merge.ts +1 -1
- package/src/evaluation/axis-checks/checks/universal.ts +111 -0
- package/src/evaluation/axis-checks/dispatch.ts +4 -1
- package/src/evaluation/criteria/bank.ts +85 -0
- package/src/evaluation/criteria/context.ts +52 -0
- package/src/evaluation/criteria/resolve.ts +159 -0
- package/src/evaluation/criteria/select.ts +79 -0
- package/src/evaluation/index.ts +23 -0
- package/src/evaluation/ink-extent.ts +200 -0
- package/src/evaluation/theme-css.ts +12 -5
- package/src/evaluation/types-public.ts +200 -11
- package/src/evaluation/types.ts +9 -0
- package/src/evaluation/visual-evaluator.ts +636 -30
- package/src/harness/check/runtime-render/adapter.ts +221 -75
- package/src/harness/check/runtime-render/index.ts +75 -2
- package/src/harness/check/runtime-render/render-check-host.ts +82 -19
- package/src/harness/check/runtime-render/render-check-worker.ts +83 -29
- package/src/harness/check/runtime-render/render-check.ts +154 -117
- package/src/harness/coding/generate-task-runner.ts +60 -4
- package/src/harness/coding/init-session.ts +13 -0
- package/src/harness/coding/run-coding-turn.ts +7 -3
- package/src/harness/coding/run-eval-round.ts +257 -15
- package/src/harness/policy.ts +11 -0
- package/src/harness/primitive-slice.ts +104 -0
- package/src/harness/runtime.ts +5 -0
- package/src/harness/types-public.ts +9 -1
- package/src/harness-tool-names.ts +38 -0
- package/src/index.ts +16 -0
- package/src/policy.ts +7 -4
- package/src/run-check.ts +2 -0
- package/src/tools/get-primitives-ts.ts +1 -1
- package/src/tools/get-wire.ts +1 -1
- package/src/tools/render-check-worker.ts +74 -23
- package/src/tools/render-check.ts +0 -60
- package/src/tools.ts +11 -1
- package/src/validation/primitives.ts +1 -1
- package/dist/render-check-Blwj8gB6.d.ts +0 -105
|
@@ -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";
|
|
@@ -73,15 +66,32 @@ type ProbeModuleNamespace = {
|
|
|
73
66
|
*/
|
|
74
67
|
export type CheckOutcome = "verified" | "failed" | "unverified" | "skipped";
|
|
75
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
|
+
|
|
76
93
|
export interface RenderCheckIssue {
|
|
77
|
-
readonly check:
|
|
78
|
-
| "render-no-throw"
|
|
79
|
-
| "action-wiring"
|
|
80
|
-
| "selection-identity"
|
|
81
|
-
| "prop-coverage"
|
|
82
|
-
| "prop-sensitivity"
|
|
83
|
-
| "stream-rerender"
|
|
84
|
-
| "optional-props-omitted";
|
|
94
|
+
readonly check: RenderCheckKind;
|
|
85
95
|
readonly outcome: CheckOutcome;
|
|
86
96
|
readonly subject?: string;
|
|
87
97
|
readonly reason: string;
|
|
@@ -115,9 +125,7 @@ export interface RenderCheckIssue {
|
|
|
115
125
|
* priming failure never aborts the check — it is recorded here, on every
|
|
116
126
|
* action-wiring issue, so it is visible in the feedback instead of vanishing.
|
|
117
127
|
*/
|
|
118
|
-
export type InputPrimingDiagnostic =
|
|
119
|
-
| { readonly primed: number }
|
|
120
|
-
| { readonly error: string };
|
|
128
|
+
export type InputPrimingDiagnostic = { readonly primed: number } | { readonly error: string };
|
|
121
129
|
|
|
122
130
|
/**
|
|
123
131
|
* The check started but did not finish (ggui#1299). The only kind is a
|
|
@@ -153,6 +161,14 @@ export interface RunRenderCheckInput {
|
|
|
153
161
|
readonly contract?: DataContract;
|
|
154
162
|
}
|
|
155
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
|
+
|
|
156
172
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
157
173
|
// Public entry
|
|
158
174
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -179,13 +195,15 @@ export interface RunRenderCheckInput {
|
|
|
179
195
|
*/
|
|
180
196
|
export async function runRenderCheck(
|
|
181
197
|
input: RunRenderCheckInput,
|
|
198
|
+
options: RunRenderCheckOptions = {}
|
|
182
199
|
): Promise<RenderCheckResult> {
|
|
183
200
|
const g = hostGlobals();
|
|
184
201
|
if ("window" in g && "document" in g) {
|
|
202
|
+
// In-process: nothing to bound — the check shares the caller's process.
|
|
185
203
|
return runRenderCheckInProcess(input);
|
|
186
204
|
}
|
|
187
205
|
const { runRenderCheckViaWorker } = await import("./render-check-host.js");
|
|
188
|
-
return runRenderCheckViaWorker(input);
|
|
206
|
+
return runRenderCheckViaWorker(input, options);
|
|
189
207
|
}
|
|
190
208
|
|
|
191
209
|
/**
|
|
@@ -198,7 +216,7 @@ export async function runRenderCheck(
|
|
|
198
216
|
* dispatcher explicitly.
|
|
199
217
|
*/
|
|
200
218
|
export async function runRenderCheckInProcess(
|
|
201
|
-
input: RunRenderCheckInput
|
|
219
|
+
input: RunRenderCheckInput
|
|
202
220
|
): Promise<RenderCheckResult> {
|
|
203
221
|
const t0 = Date.now();
|
|
204
222
|
const issues: RenderCheckIssue[] = [];
|
|
@@ -226,7 +244,7 @@ export async function runRenderCheckInProcess(
|
|
|
226
244
|
const { GguiWireProvider } = Wire;
|
|
227
245
|
|
|
228
246
|
const moduleResolutions: Record<string, unknown> = {
|
|
229
|
-
|
|
247
|
+
react: React,
|
|
230
248
|
"react/jsx-runtime": ReactJsxRuntime,
|
|
231
249
|
"@ggui-ai/wire": Wire,
|
|
232
250
|
};
|
|
@@ -262,9 +280,7 @@ export async function runRenderCheckInProcess(
|
|
|
262
280
|
// a direct dep. Swallow on miss —
|
|
263
281
|
// same posture as design subpaths above.
|
|
264
282
|
try {
|
|
265
|
-
moduleResolutions["@ggui-ai/gadgets"] = await import(
|
|
266
|
-
"@ggui-ai/gadgets"
|
|
267
|
-
);
|
|
283
|
+
moduleResolutions["@ggui-ai/gadgets"] = await import("@ggui-ai/gadgets");
|
|
268
284
|
} catch {
|
|
269
285
|
/* skip — sandbox will report a clear error if the component imports it */
|
|
270
286
|
}
|
|
@@ -366,11 +382,7 @@ export async function runRenderCheckInProcess(
|
|
|
366
382
|
if (!capturedComponentStack) {
|
|
367
383
|
// Try to find any arg that looks like a " at X (...)" stack.
|
|
368
384
|
for (const arg of args) {
|
|
369
|
-
if (
|
|
370
|
-
typeof arg === "string" &&
|
|
371
|
-
/^\s*at\s+\S/m.test(arg) &&
|
|
372
|
-
arg.length > 20
|
|
373
|
-
) {
|
|
385
|
+
if (typeof arg === "string" && /^\s*at\s+\S/m.test(arg) && arg.length > 20) {
|
|
374
386
|
capturedComponentStack = arg;
|
|
375
387
|
break;
|
|
376
388
|
}
|
|
@@ -445,11 +457,19 @@ export async function runRenderCheckInProcess(
|
|
|
445
457
|
children: React.createElement(ProbeErrorBoundary, {
|
|
446
458
|
children: React.createElement(Component, input.mockupProps),
|
|
447
459
|
}),
|
|
448
|
-
})
|
|
449
|
-
)
|
|
460
|
+
})
|
|
461
|
+
)
|
|
450
462
|
);
|
|
451
463
|
const timeoutPromise = new Promise<never>((_, reject) => {
|
|
452
|
-
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?.();
|
|
453
473
|
});
|
|
454
474
|
renderResult = await Promise.race([renderPromise, timeoutPromise]);
|
|
455
475
|
} catch (e) {
|
|
@@ -476,7 +496,10 @@ export async function runRenderCheckInProcess(
|
|
|
476
496
|
.split("\n")
|
|
477
497
|
.map((l) => l.trim())
|
|
478
498
|
.filter((l) => l.startsWith("at "))
|
|
479
|
-
.filter(
|
|
499
|
+
.filter(
|
|
500
|
+
(l) =>
|
|
501
|
+
!/(node_modules|happy-dom|@testing-library|react-dom|node:internal|node:async)/.test(l)
|
|
502
|
+
)
|
|
480
503
|
.slice(0, 5);
|
|
481
504
|
// Prefer the ErrorBoundary-captured stack (richer + names component
|
|
482
505
|
// wrappers like memo/forwardRef), fall back to the spy-captured one.
|
|
@@ -516,9 +539,7 @@ export async function runRenderCheckInProcess(
|
|
|
516
539
|
const compBlock = componentFrames.length
|
|
517
540
|
? `\n Component stack: ${componentFrames.join(" > ")}`
|
|
518
541
|
: "";
|
|
519
|
-
const errMsg = boundaryRef.error
|
|
520
|
-
? boundaryRef.error.message
|
|
521
|
-
: "(no error message)";
|
|
542
|
+
const errMsg = boundaryRef.error ? boundaryRef.error.message : "(no error message)";
|
|
522
543
|
issues.push({
|
|
523
544
|
check: "render-no-throw",
|
|
524
545
|
outcome: "failed",
|
|
@@ -596,7 +617,7 @@ export async function runRenderCheckInProcess(
|
|
|
596
617
|
if (input.contract?.actionSpec) {
|
|
597
618
|
for (const [name, entry] of Object.entries(input.contract.actionSpec)) {
|
|
598
619
|
const idField = idLikeRequiredStringField(
|
|
599
|
-
entry.schema as Record<string, unknown> | undefined
|
|
620
|
+
entry.schema as Record<string, unknown> | undefined
|
|
600
621
|
);
|
|
601
622
|
if (idField === null) continue;
|
|
602
623
|
const issue = await checkSelectionIdentity({
|
|
@@ -653,8 +674,7 @@ export async function runRenderCheckInProcess(
|
|
|
653
674
|
// prop — not dead), and non-scalar props are all skipped.
|
|
654
675
|
if (input.contract?.propsSpec) {
|
|
655
676
|
const propsSpec5c = input.contract.propsSpec as PropsSpec;
|
|
656
|
-
const normalize = (s: string | null): string =>
|
|
657
|
-
(s ?? "").replace(/\s+/g, " ").trim();
|
|
677
|
+
const normalize = (s: string | null): string => (s ?? "").replace(/\s+/g, " ").trim();
|
|
658
678
|
const baseText = normalize(container.textContent);
|
|
659
679
|
for (const [propName, entry] of Object.entries(propsSpec5c.properties)) {
|
|
660
680
|
if (!entry.required) continue;
|
|
@@ -682,7 +702,7 @@ export async function runRenderCheckInProcess(
|
|
|
682
702
|
[propName]: varied,
|
|
683
703
|
}),
|
|
684
704
|
}),
|
|
685
|
-
})
|
|
705
|
+
})
|
|
686
706
|
);
|
|
687
707
|
} catch {
|
|
688
708
|
continue; // crash on varied value ⇒ the prop is read, not dead
|
|
@@ -695,8 +715,7 @@ export async function runRenderCheckInProcess(
|
|
|
695
715
|
check: "prop-sensitivity",
|
|
696
716
|
outcome: "failed",
|
|
697
717
|
subject: propName,
|
|
698
|
-
reason:
|
|
699
|
-
`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.`,
|
|
700
719
|
});
|
|
701
720
|
}
|
|
702
721
|
}
|
|
@@ -730,9 +749,10 @@ export async function runRenderCheckInProcess(
|
|
|
730
749
|
const declaredProps = input.contract?.propsSpec?.properties;
|
|
731
750
|
if (declaredProps) {
|
|
732
751
|
const optionalFilled = Object.entries(declaredProps)
|
|
733
|
-
.filter(
|
|
734
|
-
(entry
|
|
735
|
-
|
|
752
|
+
.filter(
|
|
753
|
+
([name, entry]) =>
|
|
754
|
+
(entry as { required?: boolean }).required !== true &&
|
|
755
|
+
input.mockupProps[name] !== undefined
|
|
736
756
|
)
|
|
737
757
|
.map(([name]) => name);
|
|
738
758
|
if (optionalFilled.length > 0) {
|
|
@@ -762,11 +782,10 @@ export async function runRenderCheckInProcess(
|
|
|
762
782
|
children: React.createElement(OmitErrorBoundary, {
|
|
763
783
|
children: React.createElement(Component, requiredOnlyProps),
|
|
764
784
|
}),
|
|
765
|
-
})
|
|
785
|
+
})
|
|
766
786
|
);
|
|
767
787
|
} catch (e) {
|
|
768
|
-
omitBoundaryRef.error ??=
|
|
769
|
-
e instanceof Error ? e : new Error(String(e));
|
|
788
|
+
omitBoundaryRef.error ??= e instanceof Error ? e : new Error(String(e));
|
|
770
789
|
}
|
|
771
790
|
if (omitBoundaryRef.error) {
|
|
772
791
|
issues.push({
|
|
@@ -788,8 +807,16 @@ export async function runRenderCheckInProcess(
|
|
|
788
807
|
|
|
789
808
|
return finalize(issues, t0, actionsChecked, streamsChecked);
|
|
790
809
|
} finally {
|
|
791
|
-
try {
|
|
792
|
-
|
|
810
|
+
try {
|
|
811
|
+
uninstallSpy?.();
|
|
812
|
+
} catch {
|
|
813
|
+
/* ignore */
|
|
814
|
+
}
|
|
815
|
+
try {
|
|
816
|
+
uninstallGadgetRegistry?.();
|
|
817
|
+
} catch {
|
|
818
|
+
/* ignore */
|
|
819
|
+
}
|
|
793
820
|
teardown();
|
|
794
821
|
}
|
|
795
822
|
}
|
|
@@ -833,9 +860,7 @@ export async function runRenderCheckInProcess(
|
|
|
833
860
|
* Returns an uninstaller; the caller MUST invoke it — `globalThis` is
|
|
834
861
|
* a process global and a leaked slot taints the next check.
|
|
835
862
|
*/
|
|
836
|
-
export function installGadgetStubRegistry(
|
|
837
|
-
contract: DataContract | undefined,
|
|
838
|
-
): () => void {
|
|
863
|
+
export function installGadgetStubRegistry(contract: DataContract | undefined): () => void {
|
|
839
864
|
const declared = contract?.clientCapabilities?.gadgets;
|
|
840
865
|
if (!declared || Object.keys(declared).length === 0) {
|
|
841
866
|
return () => {};
|
|
@@ -848,9 +873,8 @@ export function installGadgetStubRegistry(
|
|
|
848
873
|
const valueStub = new Proxy(
|
|
849
874
|
{},
|
|
850
875
|
{
|
|
851
|
-
get: (_t, key) =>
|
|
852
|
-
|
|
853
|
-
},
|
|
876
|
+
get: (_t, key) => (key === "then" || typeof key === "symbol" ? undefined : noop),
|
|
877
|
+
}
|
|
854
878
|
);
|
|
855
879
|
const result = { status: "idle", value: valueStub, start: noop };
|
|
856
880
|
// Unknown top-level keys resolve to a no-op (covers `map.foo()`);
|
|
@@ -905,9 +929,7 @@ export function installGadgetStubRegistry(
|
|
|
905
929
|
* `__ggui__.gadgets` registry). Only operator-registered wrapper
|
|
906
930
|
* packages need a sandbox shim.
|
|
907
931
|
*/
|
|
908
|
-
function collectThirdPartyGadgetPackages(
|
|
909
|
-
contract: DataContract | undefined,
|
|
910
|
-
): string[] {
|
|
932
|
+
function collectThirdPartyGadgetPackages(contract: DataContract | undefined): string[] {
|
|
911
933
|
if (!contract) return [];
|
|
912
934
|
const packages = new Set<string>();
|
|
913
935
|
for (const use of listContractGadgets(contract)) {
|
|
@@ -953,7 +975,7 @@ function buildGadgetPackageProbeShim(packageName: string): ProbeModuleNamespace
|
|
|
953
975
|
const impl = resolveExport(name);
|
|
954
976
|
if (typeof impl !== "function") {
|
|
955
977
|
throw new Error(
|
|
956
|
-
`[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.`
|
|
957
979
|
);
|
|
958
980
|
}
|
|
959
981
|
return (impl as (...a: unknown[]) => unknown)(...args);
|
|
@@ -969,9 +991,8 @@ function buildGadgetPackageProbeShim(packageName: string): ProbeModuleNamespace
|
|
|
969
991
|
},
|
|
970
992
|
// `key in ns` checks (esbuild interop may probe) report true so
|
|
971
993
|
// the named import binds to the thunk rather than `undefined`.
|
|
972
|
-
has: (_t, key) =>
|
|
973
|
-
|
|
974
|
-
},
|
|
994
|
+
has: (_t, key) => typeof key !== "symbol" && key !== "then" && key !== "default",
|
|
995
|
+
}
|
|
975
996
|
) as ProbeModuleNamespace;
|
|
976
997
|
}
|
|
977
998
|
|
|
@@ -1039,12 +1060,16 @@ async function setupHappyDom(): Promise<() => void> {
|
|
|
1039
1060
|
return;
|
|
1040
1061
|
}
|
|
1041
1062
|
// Not ours — re-throw on next tick so the default handler runs.
|
|
1042
|
-
process.nextTick(() => {
|
|
1063
|
+
process.nextTick(() => {
|
|
1064
|
+
throw err;
|
|
1065
|
+
});
|
|
1043
1066
|
};
|
|
1044
1067
|
const unhandledHandler = (reason: unknown): void => {
|
|
1045
1068
|
if (isTeardownArtifact(reason)) return;
|
|
1046
1069
|
const err = reason instanceof Error ? reason : new Error(String(reason));
|
|
1047
|
-
process.nextTick(() => {
|
|
1070
|
+
process.nextTick(() => {
|
|
1071
|
+
throw err;
|
|
1072
|
+
});
|
|
1048
1073
|
};
|
|
1049
1074
|
process.on("uncaughtException", uncaughtHandler);
|
|
1050
1075
|
process.on("unhandledRejection", unhandledHandler);
|
|
@@ -1144,9 +1169,9 @@ function finalize(
|
|
|
1144
1169
|
issues: RenderCheckIssue[],
|
|
1145
1170
|
t0: number,
|
|
1146
1171
|
actionsChecked: number,
|
|
1147
|
-
streamsChecked: number
|
|
1172
|
+
streamsChecked: number
|
|
1148
1173
|
): RenderCheckResult {
|
|
1149
|
-
const ok = !issues.some(i => i.outcome === "failed");
|
|
1174
|
+
const ok = !issues.some((i) => i.outcome === "failed");
|
|
1150
1175
|
return {
|
|
1151
1176
|
ok,
|
|
1152
1177
|
issues,
|
|
@@ -1186,7 +1211,9 @@ async function checkActionWiring(input: CheckActionInput): Promise<RenderCheckIs
|
|
|
1186
1211
|
check: "action-wiring",
|
|
1187
1212
|
outcome: "failed",
|
|
1188
1213
|
subject: actionName,
|
|
1189
|
-
reason:
|
|
1214
|
+
reason:
|
|
1215
|
+
wiring.reason ??
|
|
1216
|
+
`Action '${actionName}' is declared in contract but useAction('${actionName}') is not wired to any UI element`,
|
|
1190
1217
|
diagnostics: baseDiagnostics,
|
|
1191
1218
|
};
|
|
1192
1219
|
}
|
|
@@ -1197,7 +1224,9 @@ async function checkActionWiring(input: CheckActionInput): Promise<RenderCheckIs
|
|
|
1197
1224
|
check: "action-wiring",
|
|
1198
1225
|
outcome: "unverified",
|
|
1199
1226
|
subject: actionName,
|
|
1200
|
-
reason:
|
|
1227
|
+
reason:
|
|
1228
|
+
wiring.reason ??
|
|
1229
|
+
"Source indicates non-click or non-native wiring; static probe did not verify execution deterministically.",
|
|
1201
1230
|
diagnostics: baseDiagnostics,
|
|
1202
1231
|
};
|
|
1203
1232
|
}
|
|
@@ -1248,24 +1277,20 @@ async function checkActionWiring(input: CheckActionInput): Promise<RenderCheckIs
|
|
|
1248
1277
|
* `id`/`Id`/`ID` and whose declared type is string — the shape the
|
|
1249
1278
|
* matcher's booking/selection contracts carry (`bookSlot { slotId }`).
|
|
1250
1279
|
*/
|
|
1251
|
-
function idLikeRequiredStringField(
|
|
1252
|
-
schema: Record<string, unknown> | undefined,
|
|
1253
|
-
): string | null {
|
|
1280
|
+
function idLikeRequiredStringField(schema: Record<string, unknown> | undefined): string | null {
|
|
1254
1281
|
if (schema === undefined) return null;
|
|
1255
|
-
const properties = schema[
|
|
1256
|
-
const required = schema[
|
|
1257
|
-
if (properties === null || typeof properties !==
|
|
1282
|
+
const properties = schema["properties"];
|
|
1283
|
+
const required = schema["required"];
|
|
1284
|
+
if (properties === null || typeof properties !== "object") return null;
|
|
1258
1285
|
if (!Array.isArray(required)) return null;
|
|
1259
|
-
for (const [propName, propSchema] of Object.entries(
|
|
1260
|
-
properties as Record<string, unknown>,
|
|
1261
|
-
)) {
|
|
1286
|
+
for (const [propName, propSchema] of Object.entries(properties as Record<string, unknown>)) {
|
|
1262
1287
|
if (!/id$/i.test(propName)) continue;
|
|
1263
1288
|
if (!required.includes(propName)) continue;
|
|
1264
1289
|
const t =
|
|
1265
|
-
propSchema !== null && typeof propSchema ===
|
|
1266
|
-
? (propSchema as Record<string, unknown>)[
|
|
1290
|
+
propSchema !== null && typeof propSchema === "object"
|
|
1291
|
+
? (propSchema as Record<string, unknown>)["type"]
|
|
1267
1292
|
: undefined;
|
|
1268
|
-
if (t ===
|
|
1293
|
+
if (t === "string") return propName;
|
|
1269
1294
|
}
|
|
1270
1295
|
return null;
|
|
1271
1296
|
}
|
|
@@ -1288,16 +1313,16 @@ interface CheckSelectionIdentityInput {
|
|
|
1288
1313
|
* null (covered by other checks or genuinely unjudgeable here).
|
|
1289
1314
|
*/
|
|
1290
1315
|
async function checkSelectionIdentity(
|
|
1291
|
-
input: CheckSelectionIdentityInput
|
|
1316
|
+
input: CheckSelectionIdentityInput
|
|
1292
1317
|
): Promise<RenderCheckIssue | null> {
|
|
1293
1318
|
const { container, probe, user, actionName, idField } = input;
|
|
1294
1319
|
|
|
1295
1320
|
const clickables = Array.from(
|
|
1296
|
-
container.querySelectorAll<MinimalElement>('button, [role="button"]')
|
|
1321
|
+
container.querySelectorAll<MinimalElement>('button, [role="button"]')
|
|
1297
1322
|
);
|
|
1298
1323
|
const byLabel = new Map<string, MinimalElement[]>();
|
|
1299
1324
|
for (const el of clickables) {
|
|
1300
|
-
const label = (el.textContent ??
|
|
1325
|
+
const label = (el.textContent ?? "").replace(/\s+/g, " ").trim();
|
|
1301
1326
|
if (label.length === 0) continue;
|
|
1302
1327
|
const group = byLabel.get(label);
|
|
1303
1328
|
if (group) group.push(el);
|
|
@@ -1307,9 +1332,7 @@ async function checkSelectionIdentity(
|
|
|
1307
1332
|
for (const [label, group] of byLabel) {
|
|
1308
1333
|
if (group.length < 2) continue;
|
|
1309
1334
|
|
|
1310
|
-
const payloadFor = async (
|
|
1311
|
-
el: MinimalElement,
|
|
1312
|
-
): Promise<unknown | undefined> => {
|
|
1335
|
+
const payloadFor = async (el: MinimalElement): Promise<unknown | undefined> => {
|
|
1313
1336
|
const before = probe.getFireLog().length;
|
|
1314
1337
|
try {
|
|
1315
1338
|
await user.click(el);
|
|
@@ -1320,10 +1343,7 @@ async function checkSelectionIdentity(
|
|
|
1320
1343
|
const events = probe
|
|
1321
1344
|
.getFireLog()
|
|
1322
1345
|
.slice(before)
|
|
1323
|
-
.filter(
|
|
1324
|
-
(e): e is ActionFiredEvent =>
|
|
1325
|
-
e.kind === 'action.fired' && e.name === actionName,
|
|
1326
|
-
);
|
|
1346
|
+
.filter((e): e is ActionFiredEvent => e.kind === "action.fired" && e.name === actionName);
|
|
1327
1347
|
return events.length > 0 ? events[events.length - 1]!.payload : undefined;
|
|
1328
1348
|
};
|
|
1329
1349
|
|
|
@@ -1334,9 +1354,9 @@ async function checkSelectionIdentity(
|
|
|
1334
1354
|
if (first === undefined || second === undefined) continue;
|
|
1335
1355
|
|
|
1336
1356
|
const idOf = (payload: unknown): string | undefined => {
|
|
1337
|
-
if (payload === null || typeof payload !==
|
|
1357
|
+
if (payload === null || typeof payload !== "object") return undefined;
|
|
1338
1358
|
const v = (payload as Record<string, unknown>)[idField];
|
|
1339
|
-
return typeof v ===
|
|
1359
|
+
return typeof v === "string" ? v : undefined;
|
|
1340
1360
|
};
|
|
1341
1361
|
const firstId = idOf(first);
|
|
1342
1362
|
const secondId = idOf(second);
|
|
@@ -1345,11 +1365,10 @@ async function checkSelectionIdentity(
|
|
|
1345
1365
|
|
|
1346
1366
|
if (firstId === secondId) {
|
|
1347
1367
|
return {
|
|
1348
|
-
check:
|
|
1349
|
-
outcome:
|
|
1368
|
+
check: "selection-identity",
|
|
1369
|
+
outcome: "failed",
|
|
1350
1370
|
subject: actionName,
|
|
1351
|
-
reason:
|
|
1352
|
-
`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}".`,
|
|
1353
1372
|
elementHint: describeElement(group[0]!),
|
|
1354
1373
|
};
|
|
1355
1374
|
}
|
|
@@ -1396,16 +1415,16 @@ async function simulateAndCheck(input: SimulateAndCheckInput): Promise<SimulateR
|
|
|
1396
1415
|
}
|
|
1397
1416
|
await flushPromises();
|
|
1398
1417
|
const newEvents = probe.getFireLog().slice(before);
|
|
1399
|
-
const matched = newEvents.some(e => e.kind === eventKind && e.name === actionName);
|
|
1418
|
+
const matched = newEvents.some((e) => e.kind === eventKind && e.name === actionName);
|
|
1400
1419
|
if (matched) return { fired: true };
|
|
1401
1420
|
}
|
|
1402
1421
|
|
|
1403
1422
|
// Capture what DID fire — useful diagnostic.
|
|
1404
1423
|
const allFired = probe
|
|
1405
1424
|
.getFireLog()
|
|
1406
|
-
.filter(e => e.kind === "action.fired")
|
|
1407
|
-
.map(e => e.name);
|
|
1408
|
-
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);
|
|
1409
1428
|
|
|
1410
1429
|
return {
|
|
1411
1430
|
fired: false,
|
|
@@ -1417,7 +1436,7 @@ async function simulateAndCheck(input: SimulateAndCheckInput): Promise<SimulateR
|
|
|
1417
1436
|
async function dispatchTrigger(
|
|
1418
1437
|
el: MinimalElement,
|
|
1419
1438
|
kind: "click" | "submit" | "change" | "keyboard-enter",
|
|
1420
|
-
user: { click: (el: MinimalElement) => Promise<void> }
|
|
1439
|
+
user: { click: (el: MinimalElement) => Promise<void> }
|
|
1421
1440
|
): Promise<void> {
|
|
1422
1441
|
switch (kind) {
|
|
1423
1442
|
case "click":
|
|
@@ -1428,7 +1447,10 @@ async function dispatchTrigger(
|
|
|
1428
1447
|
// dispatch a 'submit' event on it. happy-dom respects this.
|
|
1429
1448
|
const form = closestForm(el);
|
|
1430
1449
|
if (!form) return;
|
|
1431
|
-
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
|
+
});
|
|
1432
1454
|
toEventDispatcher(form).dispatchEvent(ev);
|
|
1433
1455
|
return;
|
|
1434
1456
|
}
|
|
@@ -1444,8 +1466,14 @@ async function dispatchTrigger(
|
|
|
1444
1466
|
}
|
|
1445
1467
|
case "keyboard-enter": {
|
|
1446
1468
|
// Click first to focus, then dispatch an Enter keydown.
|
|
1447
|
-
try {
|
|
1448
|
-
|
|
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;
|
|
1449
1477
|
if (KeyboardEventCtor) {
|
|
1450
1478
|
const ev = new KeyboardEventCtor("keydown", { key: "Enter", bubbles: true });
|
|
1451
1479
|
toEventDispatcher(el).dispatchEvent(ev);
|
|
@@ -1482,7 +1510,10 @@ function setSyntheticValue(el: MinimalElement): void {
|
|
|
1482
1510
|
const current = node.value ?? "";
|
|
1483
1511
|
let pick = opts[0]!.value;
|
|
1484
1512
|
for (let i = 0; i < opts.length; i++) {
|
|
1485
|
-
if (opts[i]!.value !== current) {
|
|
1513
|
+
if (opts[i]!.value !== current) {
|
|
1514
|
+
pick = opts[i]!.value;
|
|
1515
|
+
break;
|
|
1516
|
+
}
|
|
1486
1517
|
}
|
|
1487
1518
|
node.value = pick;
|
|
1488
1519
|
}
|
|
@@ -1533,7 +1564,7 @@ function findCandidateElements(
|
|
|
1533
1564
|
container: MinimalElement,
|
|
1534
1565
|
kind: "click" | "submit" | "change" | "keyboard-enter",
|
|
1535
1566
|
name: string,
|
|
1536
|
-
label: string
|
|
1567
|
+
label: string
|
|
1537
1568
|
): MinimalElement[] {
|
|
1538
1569
|
switch (kind) {
|
|
1539
1570
|
case "click":
|
|
@@ -1541,7 +1572,7 @@ function findCandidateElements(
|
|
|
1541
1572
|
case "submit": {
|
|
1542
1573
|
// submit-eligible: <form>, button[type=submit], input[type=submit]
|
|
1543
1574
|
const all = container.querySelectorAll<MinimalElement>(
|
|
1544
|
-
'form, button[type="submit"], input[type="submit"]'
|
|
1575
|
+
'form, button[type="submit"], input[type="submit"]'
|
|
1545
1576
|
);
|
|
1546
1577
|
return Array.from(all);
|
|
1547
1578
|
}
|
|
@@ -1594,7 +1625,9 @@ interface CheckStreamRerenderInput {
|
|
|
1594
1625
|
React: typeof import("react");
|
|
1595
1626
|
}
|
|
1596
1627
|
|
|
1597
|
-
async function checkStreamRerender(
|
|
1628
|
+
async function checkStreamRerender(
|
|
1629
|
+
input: CheckStreamRerenderInput
|
|
1630
|
+
): Promise<RenderCheckIssue | null> {
|
|
1598
1631
|
const { container, eventName, probe } = input;
|
|
1599
1632
|
|
|
1600
1633
|
if (!probe.getRegistered().streams.includes(eventName)) {
|
|
@@ -1613,7 +1646,7 @@ async function checkStreamRerender(input: CheckStreamRerenderInput): Promise<Ren
|
|
|
1613
1646
|
// in multiple common field names so downstream consumers find it.
|
|
1614
1647
|
const payload = { id: marker, text: marker, value: marker, message: marker, name: marker };
|
|
1615
1648
|
|
|
1616
|
-
await new Promise<void>(resolve => {
|
|
1649
|
+
await new Promise<void>((resolve) => {
|
|
1617
1650
|
setTimeout(() => {
|
|
1618
1651
|
probe.emitStream(eventName, payload);
|
|
1619
1652
|
resolve();
|
|
@@ -1643,7 +1676,11 @@ async function checkStreamRerender(input: CheckStreamRerenderInput): Promise<Ren
|
|
|
1643
1676
|
// Helpers
|
|
1644
1677
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
1645
1678
|
|
|
1646
|
-
function findActionElements(
|
|
1679
|
+
function findActionElements(
|
|
1680
|
+
container: MinimalElement,
|
|
1681
|
+
name: string,
|
|
1682
|
+
label: string
|
|
1683
|
+
): MinimalElement[] {
|
|
1647
1684
|
const candidates: MinimalElement[] = [];
|
|
1648
1685
|
const seen = new Set<MinimalElement>();
|
|
1649
1686
|
|
|
@@ -1652,7 +1689,7 @@ function findActionElements(container: MinimalElement, name: string, label: stri
|
|
|
1652
1689
|
const labelKey = norm(label);
|
|
1653
1690
|
|
|
1654
1691
|
const allClickable = container.querySelectorAll<MinimalElement>(
|
|
1655
|
-
'button, [role="button"], input[type="submit"], input[type="button"], a[href]'
|
|
1692
|
+
'button, [role="button"], input[type="submit"], input[type="button"], a[href]'
|
|
1656
1693
|
);
|
|
1657
1694
|
|
|
1658
1695
|
// Pass 1: text/aria-label/data-action match
|
|
@@ -1715,5 +1752,5 @@ function pickScalarMarker(value: unknown): string | null {
|
|
|
1715
1752
|
}
|
|
1716
1753
|
|
|
1717
1754
|
function flushPromises(): Promise<void> {
|
|
1718
|
-
return new Promise(resolve => setTimeout(resolve, 0));
|
|
1755
|
+
return new Promise((resolve) => setTimeout(resolve, 0));
|
|
1719
1756
|
}
|