@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.
Files changed (90) hide show
  1. package/dist/adapter-Du7P_5Fx.d.ts +201 -0
  2. package/dist/adapters/generation-dispatch.d.ts +33 -12
  3. package/dist/adapters/generation-dispatch.js +549 -129
  4. package/dist/adapters/generation-dispatch.js.map +1 -1
  5. package/dist/adapters/index.js +1 -1
  6. package/dist/adapters/index.js.map +1 -1
  7. package/dist/advanced/index.d.ts +5 -3
  8. package/dist/advanced/index.js +601 -144
  9. package/dist/advanced/index.js.map +1 -1
  10. package/dist/blueprint-validator.js +178 -54
  11. package/dist/blueprint-validator.js.map +1 -1
  12. package/dist/boilerplate.js +1 -0
  13. package/dist/boilerplate.js.map +1 -1
  14. package/dist/check/index.d.ts +2 -1
  15. package/dist/check/index.js +47 -1
  16. package/dist/check/index.js.map +1 -1
  17. package/dist/{create-ui-generator-BEkzEtNz.d.ts → create-ui-generator-GIhzx08x.d.ts} +34 -10
  18. package/dist/{dispatch-DJNpQ7DI.d.ts → dispatch-BnAH56p4.d.ts} +1 -1
  19. package/dist/evaluation/axis-checks/index.d.ts +4 -3
  20. package/dist/evaluation/axis-checks/index.js +19 -4
  21. package/dist/evaluation/axis-checks/index.js.map +1 -1
  22. package/dist/evaluation/axis-checks/registry.d.ts +2 -1
  23. package/dist/evaluation/axis-checks/registry.js +19 -4
  24. package/dist/evaluation/axis-checks/registry.js.map +1 -1
  25. package/dist/evaluation/index.d.ts +4 -3
  26. package/dist/evaluation/index.js +21 -6
  27. package/dist/evaluation/index.js.map +1 -1
  28. package/dist/harness/check/runtime-render/index.d.ts +27 -8
  29. package/dist/harness/check/runtime-render/index.js +217 -54
  30. package/dist/harness/check/runtime-render/index.js.map +1 -1
  31. package/dist/harness/check/runtime-render/render-check-worker.d.ts +8 -1
  32. package/dist/harness/check/runtime-render/render-check-worker.js +72 -32
  33. package/dist/harness/check/runtime-render/render-check-worker.js.map +1 -1
  34. package/dist/harness/index.d.ts +2 -1
  35. package/dist/harness/index.js +3 -2
  36. package/dist/harness/index.js.map +1 -1
  37. package/dist/harness/prompts.js.map +1 -1
  38. package/dist/harness/result-types.d.ts +2 -1
  39. package/dist/harness/runtime.d.ts +2 -1
  40. package/dist/harness/runtime.js +2 -1
  41. package/dist/harness/runtime.js.map +1 -1
  42. package/dist/harness/types-public.d.ts +18 -5
  43. package/dist/harness/types-public.js +1 -0
  44. package/dist/harness/types-public.js.map +1 -1
  45. package/dist/harness/validator-trace-sink.d.ts +2 -1
  46. package/dist/index.d.ts +38 -3
  47. package/dist/index.js +592 -144
  48. package/dist/index.js.map +1 -1
  49. package/dist/patch.d.ts +2 -1
  50. package/dist/policy.d.ts +7 -4
  51. package/dist/policy.js.map +1 -1
  52. package/dist/tools/get-primitives-ts.d.ts +1 -1
  53. package/dist/tools/get-primitives-ts.js +1 -1
  54. package/dist/tools/get-primitives-ts.js.map +1 -1
  55. package/dist/tools/render-check-worker.d.ts +7 -1
  56. package/dist/tools/render-check-worker.js +55 -23
  57. package/dist/tools/render-check-worker.js.map +1 -1
  58. package/dist/{types-public-mM_-1Yk3.d.ts → types-public-Bk6d1WLN.d.ts} +98 -9
  59. package/dist/validation/index.d.ts +1 -1
  60. package/dist/validation/index.js +1 -1
  61. package/dist/validation/index.js.map +1 -1
  62. package/dist/workflows.d.ts +2 -1
  63. package/package.json +12 -12
  64. package/src/adapters/generation-dispatch.ts +137 -96
  65. package/src/advanced/generator.ts +16 -1
  66. package/src/blueprint-validator.ts +21 -6
  67. package/src/boilerplate/hard-sections.ts +1 -0
  68. package/src/check/run-tier0.ts +80 -1
  69. package/src/coding-agent/tools.ts +6 -1
  70. package/src/create-ui-generator.ts +185 -95
  71. package/src/evaluation/axis-checks/checks/state-payload.ts +29 -4
  72. package/src/evaluation/types-public.ts +94 -8
  73. package/src/harness/check/runtime-render/adapter.ts +223 -58
  74. package/src/harness/check/runtime-render/index.ts +75 -2
  75. package/src/harness/check/runtime-render/render-check-host.ts +111 -26
  76. package/src/harness/check/runtime-render/render-check-worker.ts +83 -29
  77. package/src/harness/check/runtime-render/render-check.ts +173 -117
  78. package/src/harness/coding/generate-task-runner.ts +57 -4
  79. package/src/harness/coding/init-session.ts +13 -0
  80. package/src/harness/coding/run-eval-round.ts +238 -13
  81. package/src/harness/llm-router.ts +3 -3
  82. package/src/harness/policy.ts +11 -0
  83. package/src/harness/primitive-slice.ts +104 -0
  84. package/src/harness/types-public.ts +17 -5
  85. package/src/index.ts +4 -0
  86. package/src/policy.ts +7 -4
  87. package/src/tools/get-primitives-ts.ts +1 -1
  88. package/src/tools/render-check-worker.ts +74 -23
  89. package/src/validation/primitives.ts +1 -1
  90. 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
- DataContract,
20
- JsonObject,
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
- | { readonly primed: number }
119
- | { readonly error: string };
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
- "react": React,
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(() => reject(new Error(`[runtime-render] render exceeded ${RENDER_TIMEOUT_MS}ms — classified as runtime-hang`)), RENDER_TIMEOUT_MS).unref?.();
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((l) => !/(node_modules|happy-dom|@testing-library|react-dom|node:internal|node:async)/.test(l))
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(([name, entry]) =>
715
- (entry as { required?: boolean }).required !== true &&
716
- input.mockupProps[name] !== undefined,
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 { uninstallSpy?.(); } catch { /* ignore */ }
773
- try { uninstallGadgetRegistry?.(); } catch { /* ignore */ }
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
- key === "then" || typeof key === "symbol" ? undefined : noop,
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
- typeof key !== "symbol" && key !== "then" && key !== "default",
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(() => { throw err; });
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(() => { throw err; });
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: wiring.reason ?? `Action '${actionName}' is declared in contract but useAction('${actionName}') is not wired to any UI element`,
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: wiring.reason ?? "Source indicates non-click or non-native wiring; static probe did not verify execution deterministically.",
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['properties'];
1237
- const required = schema['required'];
1238
- if (properties === null || typeof properties !== 'object') return null;
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 === 'object'
1247
- ? (propSchema as Record<string, unknown>)['type']
1290
+ propSchema !== null && typeof propSchema === "object"
1291
+ ? (propSchema as Record<string, unknown>)["type"]
1248
1292
  : undefined;
1249
- if (t === 'string') return propName;
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 ?? '').replace(/\s+/g, ' ').trim();
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 !== 'object') return undefined;
1357
+ if (payload === null || typeof payload !== "object") return undefined;
1319
1358
  const v = (payload as Record<string, unknown>)[idField];
1320
- return typeof v === 'string' ? v : undefined;
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: 'selection-identity',
1330
- outcome: 'failed',
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", { bubbles: true, cancelable: true });
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 { await user.click(el); } catch { /* ignore */ }
1429
- const KeyboardEventCtor = (globalThis as { KeyboardEvent?: new (type: string, init: object) => Event }).KeyboardEvent;
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) { pick = opts[i]!.value; break; }
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(input: CheckStreamRerenderInput): Promise<RenderCheckIssue | null> {
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(container: MinimalElement, name: string, label: string): MinimalElement[] {
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
  }