@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.
Files changed (123) hide show
  1. package/dist/adapter-Cg_rkpGq.d.ts +201 -0
  2. package/dist/adapters/generation-dispatch.d.ts +58 -15
  3. package/dist/adapters/generation-dispatch.js +1418 -180
  4. package/dist/adapters/generation-dispatch.js.map +1 -1
  5. package/dist/adapters/index.js +3 -1
  6. package/dist/adapters/index.js.map +1 -1
  7. package/dist/advanced/index.d.ts +12 -8
  8. package/dist/advanced/index.js +1467 -239
  9. package/dist/advanced/index.js.map +1 -1
  10. package/dist/{axes-CzLEMDeB.d.ts → axes-CKVCzR_x.d.ts} +1 -1
  11. package/dist/bank-CT4eEkta.d.ts +118 -0
  12. package/dist/blueprint-validator.js +153 -59
  13. package/dist/blueprint-validator.js.map +1 -1
  14. package/dist/boilerplate.js +24 -12
  15. package/dist/boilerplate.js.map +1 -1
  16. package/dist/check/index.d.ts +12 -4
  17. package/dist/check/index.js +94 -2
  18. package/dist/check/index.js.map +1 -1
  19. package/dist/classifier/index.d.ts +2 -2
  20. package/dist/compose.d.ts +2 -2
  21. package/dist/{create-ui-generator-D0kbWwln.d.ts → create-ui-generator-DFnnArrv.d.ts} +43 -11
  22. package/dist/{dispatch-wxaIMwZt.d.ts → dispatch-CStl9eAw.d.ts} +5 -3
  23. package/dist/evaluation/axis-checks/index.d.ts +6 -5
  24. package/dist/evaluation/axis-checks/index.js +90 -2
  25. package/dist/evaluation/axis-checks/index.js.map +1 -1
  26. package/dist/evaluation/axis-checks/registry.d.ts +4 -3
  27. package/dist/evaluation/axis-checks/registry.js +88 -1
  28. package/dist/evaluation/axis-checks/registry.js.map +1 -1
  29. package/dist/evaluation/index.d.ts +301 -23
  30. package/dist/evaluation/index.js +746 -30
  31. package/dist/evaluation/index.js.map +1 -1
  32. package/dist/evaluation/types.d.ts +9 -1
  33. package/dist/evaluation/types.js.map +1 -1
  34. package/dist/fragments/index.d.ts +3 -3
  35. package/dist/harness/check/runtime-render/index.d.ts +30 -11
  36. package/dist/harness/check/runtime-render/index.js +203 -60
  37. package/dist/harness/check/runtime-render/index.js.map +1 -1
  38. package/dist/harness/check/runtime-render/render-check-worker.d.ts +8 -1
  39. package/dist/harness/check/runtime-render/render-check-worker.js +72 -32
  40. package/dist/harness/check/runtime-render/render-check-worker.js.map +1 -1
  41. package/dist/harness/index.d.ts +5 -4
  42. package/dist/harness/index.js +33 -15
  43. package/dist/harness/index.js.map +1 -1
  44. package/dist/harness/prompts.js.map +1 -1
  45. package/dist/harness/result-types.d.ts +4 -3
  46. package/dist/harness/runtime.d.ts +11 -4
  47. package/dist/harness/runtime.js +26 -14
  48. package/dist/harness/runtime.js.map +1 -1
  49. package/dist/harness/types-public.d.ts +13 -4
  50. package/dist/harness/types-public.js +33 -15
  51. package/dist/harness/types-public.js.map +1 -1
  52. package/dist/harness/validator-trace-sink.d.ts +4 -3
  53. package/dist/hash.d.ts +1 -1
  54. package/dist/index.d.ts +59 -8
  55. package/dist/index.js +1476 -241
  56. package/dist/index.js.map +1 -1
  57. package/dist/{llm-router-DwwUK1bz.d.ts → llm-router-nfO7wPec.d.ts} +3 -1
  58. package/dist/patch.d.ts +4 -3
  59. package/dist/policy.d.ts +8 -5
  60. package/dist/policy.js.map +1 -1
  61. package/dist/tools/get-primitives-ts.d.ts +1 -1
  62. package/dist/tools/get-primitives-ts.js +1 -1
  63. package/dist/tools/get-primitives-ts.js.map +1 -1
  64. package/dist/tools/render-check-worker.d.ts +7 -1
  65. package/dist/tools/render-check-worker.js +55 -23
  66. package/dist/tools/render-check-worker.js.map +1 -1
  67. package/dist/{types-BOvHNG7K.d.ts → types-Dh1s4K9j.d.ts} +1 -1
  68. package/dist/{types-public-Ow-tNtFE.d.ts → types-public-BJTLpzWn.d.ts} +208 -15
  69. package/dist/validation/index.d.ts +1 -1
  70. package/dist/validation/index.js +1 -1
  71. package/dist/validation/index.js.map +1 -1
  72. package/dist/workflows.d.ts +4 -3
  73. package/package.json +12 -12
  74. package/src/adapters/generation-dispatch.ts +173 -98
  75. package/src/adapters/tools.ts +2 -1
  76. package/src/boilerplate/free-design-prompt.ts +1 -1
  77. package/src/boilerplate/generate.ts +26 -1
  78. package/src/boilerplate/hard-sections.ts +13 -13
  79. package/src/boilerplate/system-prompt.ts +1 -1
  80. package/src/check/index.ts +1 -0
  81. package/src/check/run-tier0.ts +80 -1
  82. package/src/check/sample-props.ts +63 -0
  83. package/src/coding-agent/tools.ts +4 -1
  84. package/src/create-harness.ts +3 -3
  85. package/src/create-ui-generator.ts +204 -95
  86. package/src/evaluation/__fixtures__/png.ts +100 -0
  87. package/src/evaluation/axis-checks/checks/state-merge.ts +1 -1
  88. package/src/evaluation/axis-checks/checks/universal.ts +111 -0
  89. package/src/evaluation/axis-checks/dispatch.ts +4 -1
  90. package/src/evaluation/criteria/bank.ts +85 -0
  91. package/src/evaluation/criteria/context.ts +52 -0
  92. package/src/evaluation/criteria/resolve.ts +159 -0
  93. package/src/evaluation/criteria/select.ts +79 -0
  94. package/src/evaluation/index.ts +23 -0
  95. package/src/evaluation/ink-extent.ts +200 -0
  96. package/src/evaluation/theme-css.ts +12 -5
  97. package/src/evaluation/types-public.ts +200 -11
  98. package/src/evaluation/types.ts +9 -0
  99. package/src/evaluation/visual-evaluator.ts +636 -30
  100. package/src/harness/check/runtime-render/adapter.ts +221 -75
  101. package/src/harness/check/runtime-render/index.ts +75 -2
  102. package/src/harness/check/runtime-render/render-check-host.ts +82 -19
  103. package/src/harness/check/runtime-render/render-check-worker.ts +83 -29
  104. package/src/harness/check/runtime-render/render-check.ts +154 -117
  105. package/src/harness/coding/generate-task-runner.ts +60 -4
  106. package/src/harness/coding/init-session.ts +13 -0
  107. package/src/harness/coding/run-coding-turn.ts +7 -3
  108. package/src/harness/coding/run-eval-round.ts +257 -15
  109. package/src/harness/policy.ts +11 -0
  110. package/src/harness/primitive-slice.ts +104 -0
  111. package/src/harness/runtime.ts +5 -0
  112. package/src/harness/types-public.ts +9 -1
  113. package/src/harness-tool-names.ts +38 -0
  114. package/src/index.ts +16 -0
  115. package/src/policy.ts +7 -4
  116. package/src/run-check.ts +2 -0
  117. package/src/tools/get-primitives-ts.ts +1 -1
  118. package/src/tools/get-wire.ts +1 -1
  119. package/src/tools/render-check-worker.ts +74 -23
  120. package/src/tools/render-check.ts +0 -60
  121. package/src/tools.ts +11 -1
  122. package/src/validation/primitives.ts +1 -1
  123. 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
- 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";
@@ -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
- "react": React,
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(() => 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?.();
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((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
+ )
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(([name, entry]) =>
734
- (entry as { required?: boolean }).required !== true &&
735
- input.mockupProps[name] !== undefined,
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 { uninstallSpy?.(); } catch { /* ignore */ }
792
- try { uninstallGadgetRegistry?.(); } catch { /* ignore */ }
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
- key === "then" || typeof key === "symbol" ? undefined : noop,
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
- typeof key !== "symbol" && key !== "then" && key !== "default",
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(() => { throw err; });
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(() => { throw err; });
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: 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`,
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: 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.",
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['properties'];
1256
- const required = schema['required'];
1257
- 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;
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 === 'object'
1266
- ? (propSchema as Record<string, unknown>)['type']
1290
+ propSchema !== null && typeof propSchema === "object"
1291
+ ? (propSchema as Record<string, unknown>)["type"]
1267
1292
  : undefined;
1268
- if (t === 'string') return propName;
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 ?? '').replace(/\s+/g, ' ').trim();
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 !== 'object') return undefined;
1357
+ if (payload === null || typeof payload !== "object") return undefined;
1338
1358
  const v = (payload as Record<string, unknown>)[idField];
1339
- return typeof v === 'string' ? v : undefined;
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: 'selection-identity',
1349
- outcome: 'failed',
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", { bubbles: true, cancelable: true });
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 { await user.click(el); } catch { /* ignore */ }
1448
- 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;
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) { pick = opts[i]!.value; break; }
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(input: CheckStreamRerenderInput): Promise<RenderCheckIssue | null> {
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(container: MinimalElement, name: string, label: string): MinimalElement[] {
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
  }