@ggui-ai/ui-gen 0.25.0 → 0.26.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 (106) hide show
  1. package/README.md +2 -2
  2. package/dist/{adapter-Cg_rkpGq.d.ts → adapter-BJwn74rx.d.ts} +49 -1
  3. package/dist/adapters/generation-dispatch.d.ts +8 -5
  4. package/dist/adapters/generation-dispatch.js +1118 -193
  5. package/dist/adapters/generation-dispatch.js.map +1 -1
  6. package/dist/adapters/index.js +27 -6
  7. package/dist/adapters/index.js.map +1 -1
  8. package/dist/advanced/index.d.ts +10 -9
  9. package/dist/advanced/index.js +1126 -202
  10. package/dist/advanced/index.js.map +1 -1
  11. package/dist/blueprint-validator.js +416 -92
  12. package/dist/blueprint-validator.js.map +1 -1
  13. package/dist/boilerplate.d.ts +2 -2
  14. package/dist/boilerplate.js +10 -3
  15. package/dist/boilerplate.js.map +1 -1
  16. package/dist/check/index.d.ts +3 -2
  17. package/dist/check/index.js +9 -2
  18. package/dist/check/index.js.map +1 -1
  19. package/dist/{create-ui-generator-DFnnArrv.d.ts → create-ui-generator-N4gRXpcL.d.ts} +3 -4
  20. package/dist/{design-mode-DLufhrNp.d.ts → design-mode-DGWN_Gkm.d.ts} +1 -1
  21. package/dist/{dispatch-CStl9eAw.d.ts → dispatch-BUoXRj_r.d.ts} +2 -2
  22. package/dist/evaluation/axis-checks/index.d.ts +5 -4
  23. package/dist/evaluation/axis-checks/index.js.map +1 -1
  24. package/dist/evaluation/axis-checks/registry.d.ts +4 -3
  25. package/dist/evaluation/index.d.ts +107 -539
  26. package/dist/evaluation/index.js +552 -55
  27. package/dist/evaluation/index.js.map +1 -1
  28. package/dist/evaluation/types.d.ts +17 -0
  29. package/dist/evaluation/types.js.map +1 -1
  30. package/dist/{generate-9IeNT4ZY.d.ts → generate-tSO_shQ0.d.ts} +1 -1
  31. package/dist/harness/check/runtime-render/index.d.ts +5 -5
  32. package/dist/harness/check/runtime-render/index.js +416 -92
  33. package/dist/harness/check/runtime-render/index.js.map +1 -1
  34. package/dist/harness/check/runtime-render/render-check-worker.js +393 -88
  35. package/dist/harness/check/runtime-render/render-check-worker.js.map +1 -1
  36. package/dist/harness/index.d.ts +4 -5
  37. package/dist/harness/index.js +36 -5
  38. package/dist/harness/index.js.map +1 -1
  39. package/dist/harness/prompts.d.ts +2 -2
  40. package/dist/harness/prompts.js +10 -1
  41. package/dist/harness/prompts.js.map +1 -1
  42. package/dist/harness/result-types.d.ts +17 -3
  43. package/dist/harness/result-types.js.map +1 -1
  44. package/dist/harness/runtime.d.ts +14 -6
  45. package/dist/harness/runtime.js +11 -4
  46. package/dist/harness/runtime.js.map +1 -1
  47. package/dist/harness/types-public.d.ts +4 -2
  48. package/dist/harness/types-public.js +10 -3
  49. package/dist/harness/types-public.js.map +1 -1
  50. package/dist/harness/validator-trace-sink.d.ts +4 -3
  51. package/dist/index.d.ts +46 -11
  52. package/dist/index.js +1128 -200
  53. package/dist/index.js.map +1 -1
  54. package/dist/patch.d.ts +3 -3
  55. package/dist/{prompts-YU9piOgs.d.ts → prompts-BhNUuRoZ.d.ts} +1 -1
  56. package/dist/tools/render-check-worker.js +8 -1
  57. package/dist/tools/render-check-worker.js.map +1 -1
  58. package/dist/{types-public-BJTLpzWn.d.ts → types-public-B7wn4ce5.d.ts} +384 -3
  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/visual-evaluator-C9L-o6ap.d.ts +639 -0
  63. package/dist/workflows.d.ts +3 -3
  64. package/package.json +12 -12
  65. package/src/adapters/generation-dispatch.ts +2 -0
  66. package/src/boilerplate/free-design-prompt.ts +3 -3
  67. package/src/boilerplate/hard-sections.ts +1 -1
  68. package/src/boilerplate/templates/layouts/chat-mobile.tsx.tmpl +3 -1
  69. package/src/boilerplate/templates/layouts/chat-universal.tsx.tmpl +3 -1
  70. package/src/boilerplate/templates/layouts/fullscreen-desktop.tsx.tmpl +3 -1
  71. package/src/boilerplate/templates/layouts/fullscreen-universal.tsx.tmpl +3 -1
  72. package/src/check/run-tier0.ts +6 -3
  73. package/src/contract-context.ts +1 -1
  74. package/src/evaluation/cost-tracker.ts +54 -6
  75. package/src/evaluation/criteria/edge-probe.ts +286 -0
  76. package/src/evaluation/criteria/edge.ts +202 -0
  77. package/src/evaluation/criteria/resolve.ts +55 -7
  78. package/src/evaluation/index.ts +5 -1
  79. package/src/evaluation/judge-spend.ts +48 -0
  80. package/src/evaluation/types-public.ts +91 -0
  81. package/src/evaluation/types.ts +10 -0
  82. package/src/evaluation/visual-evaluator.ts +236 -40
  83. package/src/harness/check/runtime-render/adapter.ts +18 -0
  84. package/src/harness/check/runtime-render/probe-pending.ts +111 -0
  85. package/src/harness/check/runtime-render/render-check-host.ts +10 -6
  86. package/src/harness/check/runtime-render/render-check-worker.ts +6 -3
  87. package/src/harness/check/runtime-render/render-check.ts +417 -133
  88. package/src/harness/check/runtime-render/walk.ts +200 -0
  89. package/src/harness/coding/assemble-result.ts +2 -0
  90. package/src/harness/coding/generate-task-runner.ts +27 -0
  91. package/src/harness/coding/init-session.ts +5 -3
  92. package/src/harness/coding/run-coding-turn.ts +5 -1
  93. package/src/harness/coding/run-eval-round.ts +63 -3
  94. package/src/harness/in-loop-evaluation.ts +39 -0
  95. package/src/harness/llm-router.ts +58 -1
  96. package/src/harness/result-types.ts +9 -0
  97. package/src/harness/runtime.ts +9 -0
  98. package/src/harness/types-public.ts +10 -1
  99. package/src/index.ts +5 -0
  100. package/src/internal/verdict-line.ts +33 -0
  101. package/src/taught-tokens.ts +32 -0
  102. package/src/tools/render-check-worker.ts +2 -1
  103. package/src/tools/render-check.ts +8 -7
  104. package/src/validation/primitives.ts +1 -1
  105. package/dist/llm-router-nfO7wPec.d.ts +0 -267
  106. package/dist/{bank-CT4eEkta.d.ts → bank-Ctdp3FUA.d.ts} +3 -3
@@ -17,6 +17,22 @@ var __export = (target, all) => {
17
17
  __defProp(target, name, { get: all[name], enumerable: true });
18
18
  };
19
19
 
20
+ // src/internal/verdict-line.ts
21
+ function parseVerdictLine(stdout) {
22
+ const lines = stdout.split("\n");
23
+ for (let i = lines.length - 1; i >= 0; i -= 1) {
24
+ const line = lines[i];
25
+ if (line.startsWith(`${VERDICT_PREFIX} `)) return JSON.parse(line.slice(VERDICT_PREFIX.length + 1));
26
+ }
27
+ return void 0;
28
+ }
29
+ var VERDICT_PREFIX;
30
+ var init_verdict_line = __esm({
31
+ "src/internal/verdict-line.ts"() {
32
+ VERDICT_PREFIX = "@@ggui-render-check-verdict@@";
33
+ }
34
+ });
35
+
20
36
  // src/harness/check/runtime-render/render-check-host.ts
21
37
  var render_check_host_exports = {};
22
38
  __export(render_check_host_exports, {
@@ -124,18 +140,19 @@ function mapSandboxResultToCheckResult(result, t0, boundMs = DEFAULT_RENDER_CHEC
124
140
  t0
125
141
  );
126
142
  }
127
- const stdout = result.stdout.trim();
128
- if (stdout.length === 0) {
129
- return unverified("render check worker exited without producing a verdict.", t0);
130
- }
143
+ let verdict;
131
144
  try {
132
- return JSON.parse(stdout);
145
+ verdict = parseVerdictLine(result.stdout);
133
146
  } catch (err) {
134
147
  return unverified(
135
148
  `render check worker verdict was not valid JSON \u2014 ${err instanceof Error ? err.message : String(err)}`,
136
149
  t0
137
150
  );
138
151
  }
152
+ if (verdict === void 0) {
153
+ return unverified("render check worker exited without producing a verdict.", t0);
154
+ }
155
+ return verdict;
139
156
  }
140
157
  async function runRenderCheckViaWorker(input, options = {}) {
141
158
  const t0 = Date.now();
@@ -154,7 +171,9 @@ async function runRenderCheckViaWorker(input, options = {}) {
154
171
  gracePeriodMs: bounds.gracePeriodMs,
155
172
  maxStdoutBytes: CHECK_STDOUT_CAP,
156
173
  nodeHeapMb: bounds.heapMb,
157
- stdin: JSON.stringify(input),
174
+ // ggui#1398: the check learns its own deadline, so its report-only
175
+ // pending phase can step aside instead of pushing it past the bound.
176
+ stdin: JSON.stringify({ ...input, deadlineAt: t0 + bounds.timeoutMs }),
158
177
  // NODE_ENV steers React's production vs development build — keep
159
178
  // parity with the caller; everything else stays on the sandbox's
160
179
  // default allowlist.
@@ -171,6 +190,7 @@ async function runRenderCheckViaWorker(input, options = {}) {
171
190
  var DEFAULT_RENDER_CHECK_HOST_BOUNDS, CHECK_STDOUT_CAP, WORKER_BASENAME;
172
191
  var init_render_check_host = __esm({
173
192
  "src/harness/check/runtime-render/render-check-host.ts"() {
193
+ init_verdict_line();
174
194
  DEFAULT_RENDER_CHECK_HOST_BOUNDS = {
175
195
  timeoutMs: 3e4,
176
196
  heapMb: 512,
@@ -698,6 +718,165 @@ function editableSurface(el) {
698
718
  function parentLink(node) {
699
719
  return node;
700
720
  }
721
+
722
+ // src/harness/check/runtime-render/probe-pending.ts
723
+ function createProbePendingSource() {
724
+ const pending = /* @__PURE__ */ new Set();
725
+ const listeners = /* @__PURE__ */ new Set();
726
+ const notify = () => {
727
+ for (const listener of [...listeners]) listener();
728
+ };
729
+ return {
730
+ isPending: (actionName) => pending.has(actionName),
731
+ subscribe: (listener) => {
732
+ listeners.add(listener);
733
+ return () => {
734
+ listeners.delete(listener);
735
+ };
736
+ },
737
+ mark: (actionName) => {
738
+ pending.add(actionName);
739
+ notify();
740
+ },
741
+ clear: () => {
742
+ if (pending.size === 0) return;
743
+ pending.clear();
744
+ notify();
745
+ }
746
+ };
747
+ }
748
+ function lookOf(el) {
749
+ return {
750
+ connected: el.isConnected !== false,
751
+ // React renders `disabled={true}` as `disabled=""`, so presence is the test.
752
+ disabled: el.getAttribute("disabled") !== null || el.getAttribute("aria-disabled") === "true",
753
+ ariaBusy: el.getAttribute("aria-busy") === "true",
754
+ text: el.tagName?.toLowerCase() === "input" ? String(typeof el.value === "string" ? el.value : el.getAttribute("value") ?? "") : el.textContent ?? ""
755
+ };
756
+ }
757
+ async function settlesWithin(ms, work) {
758
+ let timer;
759
+ const expired = new Promise((resolve) => {
760
+ timer = setTimeout(() => resolve(false), Math.max(0, ms));
761
+ });
762
+ try {
763
+ return await Promise.race([work.then(() => true), expired]);
764
+ } finally {
765
+ if (timer !== void 0) clearTimeout(timer);
766
+ }
767
+ }
768
+ function lookChanged(before, after) {
769
+ if (before.connected && !after.connected) return true;
770
+ if (!before.disabled && after.disabled) return true;
771
+ if (!before.ariaBusy && after.ariaBusy) return true;
772
+ return before.text.trim() !== after.text.trim();
773
+ }
774
+
775
+ // src/harness/check/runtime-render/walk.ts
776
+ var LAST_PRESSED_KEPT = 4;
777
+ function isDisabled(el) {
778
+ return el.getAttribute("disabled") !== null || el.getAttribute("aria-disabled") === "true";
779
+ }
780
+ function labelOf(el) {
781
+ const named = el.getAttribute("aria-label") ?? el.getAttribute("placeholder");
782
+ return (named ?? el.textContent ?? "").replace(/\s+/g, " ").trim().slice(0, 60);
783
+ }
784
+ function screenFingerprint(container) {
785
+ const parts = [];
786
+ for (const el of container.querySelectorAll(
787
+ 'button, [role="button"], a[href], input, select, textarea, form'
788
+ )) {
789
+ parts.push(
790
+ [el.tagName.toLowerCase(), el.getAttribute("type") ?? "", labelOf(el), isDisabled(el) ? "disabled" : ""].join("|")
791
+ );
792
+ }
793
+ return parts.join("\n");
794
+ }
795
+ function keyed(controls, describe) {
796
+ const seen = /* @__PURE__ */ new Map();
797
+ const out = /* @__PURE__ */ new Map();
798
+ for (const c of controls) {
799
+ const d = describe(c.element);
800
+ const n = seen.get(d) ?? 0;
801
+ seen.set(d, n + 1);
802
+ out.set(`${d}#${n}`, c);
803
+ }
804
+ return out;
805
+ }
806
+ function firstStepToUnpressed(screens, from) {
807
+ const firstStep = /* @__PURE__ */ new Map([[from, void 0]]);
808
+ const queue = [from];
809
+ while (queue.length > 0) {
810
+ const at = queue.shift();
811
+ const screen = screens.get(at);
812
+ if (screen === void 0) continue;
813
+ if (at !== from && screen.order.some((k) => !screen.led.has(k))) return firstStep.get(at);
814
+ for (const [key, to] of screen.led) {
815
+ if (to === at || firstStep.has(to)) continue;
816
+ firstStep.set(to, firstStep.get(at) ?? key);
817
+ queue.push(to);
818
+ }
819
+ }
820
+ return void 0;
821
+ }
822
+ async function walkToAction(input) {
823
+ const lastPressed = [];
824
+ const screenTexts = [];
825
+ const screens = /* @__PURE__ */ new Map();
826
+ let presses = 0;
827
+ let namedControlSeen = false;
828
+ const outcome = (stoppedBy, extra = {}) => ({
829
+ fired: false,
830
+ presses,
831
+ screens: screens.size,
832
+ stoppedBy,
833
+ namedControlSeen,
834
+ lastPressed: [...lastPressed],
835
+ screenTexts: [...screenTexts],
836
+ ...extra
837
+ });
838
+ const mounted = await input.mount();
839
+ if (mounted === void 0) return outcome("no-mount");
840
+ const { container } = mounted;
841
+ const arrive = () => {
842
+ mounted.prime();
843
+ const controls = keyed(input.controls(container), input.describe);
844
+ if ([...controls.values()].some((c) => c.namesAction)) namedControlSeen = true;
845
+ const id = screenFingerprint(container);
846
+ if (!screens.has(id)) {
847
+ screens.set(id, { order: [...controls.keys()], led: /* @__PURE__ */ new Map() });
848
+ screenTexts.push(container.textContent ?? "");
849
+ }
850
+ return { id, controls };
851
+ };
852
+ let here = arrive();
853
+ const first = here.id;
854
+ for (; ; ) {
855
+ const screen = screens.get(here.id);
856
+ const onOpenFirstScreen = here.id === first && screen.order.some((k) => !screen.led.has(k));
857
+ if (!onOpenFirstScreen) {
858
+ if (presses >= input.maxPresses) return outcome("press-cap");
859
+ if (Date.now() >= input.stopAt) return outcome("deadline");
860
+ }
861
+ const key = screen.order.find((k) => !screen.led.has(k)) ?? firstStepToUnpressed(screens, here.id);
862
+ if (key === void 0) return outcome("explored");
863
+ const control = here.controls.get(key);
864
+ if (control === void 0) {
865
+ screen.led.set(key, here.id);
866
+ continue;
867
+ }
868
+ presses += 1;
869
+ lastPressed.push(input.describe(control.element));
870
+ if (lastPressed.length > LAST_PRESSED_KEPT) lastPressed.shift();
871
+ if (await input.press(control)) {
872
+ return outcome("dispatched", { fired: true, element: control.element, container });
873
+ }
874
+ here = arrive();
875
+ if (!screen.led.has(key)) screen.led.set(key, here.id);
876
+ }
877
+ }
878
+
879
+ // src/harness/check/runtime-render/render-check.ts
701
880
  var RENDER_CHECK_KINDS = [
702
881
  "render-no-throw",
703
882
  "prop-sensitivity",
@@ -707,6 +886,10 @@ var RENDER_CHECK_KINDS = [
707
886
  "optional-props-omitted",
708
887
  "stream-rerender"
709
888
  ];
889
+ var PENDING_PHASE_RESERVE_MS = 5e3;
890
+ var WALK_MAX_PRESSES = 60;
891
+ var WALK_ACTION_CAP_MS = 3e3;
892
+ var WALK_DEADLINE_MARGIN_MS = 2e3;
710
893
  async function runRenderCheck(input, options = {}) {
711
894
  const g = hostGlobals();
712
895
  if ("window" in g && "document" in g) {
@@ -772,7 +955,9 @@ async function runRenderCheckInProcess(input) {
772
955
  const wireConfig = createProbeWireConfig(probe);
773
956
  uninstallSpy = probe.installPostMessageSpy();
774
957
  installProductionActShim();
775
- const { render, cleanup } = await import('@testing-library/react');
958
+ const { render, cleanup, act } = await import('@testing-library/react');
959
+ const { ActionPendingContext } = await import('@ggui-ai/wire/internal');
960
+ const pendingSource = createProbePendingSource();
776
961
  const userEventModule = await import('@testing-library/user-event');
777
962
  const userEvent = userEventModule.default ?? userEventModule;
778
963
  const originalConsoleError = console.error;
@@ -809,7 +994,8 @@ async function runRenderCheckInProcess(input) {
809
994
  };
810
995
  const boundaryRef = {
811
996
  stack: null,
812
- error: null
997
+ error: null,
998
+ catches: 0
813
999
  };
814
1000
  class ProbeErrorBoundary extends React.Component {
815
1001
  state = { caught: false };
@@ -817,6 +1003,7 @@ async function runRenderCheckInProcess(input) {
817
1003
  return { caught: true };
818
1004
  }
819
1005
  componentDidCatch(error, errorInfo) {
1006
+ boundaryRef.catches += 1;
820
1007
  if (errorInfo.componentStack && !boundaryRef.stack) {
821
1008
  boundaryRef.stack = errorInfo.componentStack;
822
1009
  }
@@ -829,19 +1016,19 @@ async function runRenderCheckInProcess(input) {
829
1016
  return this.props.children;
830
1017
  }
831
1018
  }
1019
+ const cardTree = () => React.createElement(GguiWireProvider, {
1020
+ config: wireConfig,
1021
+ children: React.createElement(ProbeErrorBoundary, {
1022
+ children: React.createElement(ActionPendingContext.Provider, {
1023
+ value: pendingSource,
1024
+ children: React.createElement(Component, input.mockupProps)
1025
+ })
1026
+ })
1027
+ });
832
1028
  let renderResult;
833
1029
  const RENDER_TIMEOUT_MS = 5e3;
834
1030
  try {
835
- const renderPromise = Promise.resolve().then(
836
- () => render(
837
- React.createElement(GguiWireProvider, {
838
- config: wireConfig,
839
- children: React.createElement(ProbeErrorBoundary, {
840
- children: React.createElement(Component, input.mockupProps)
841
- })
842
- })
843
- )
844
- );
1031
+ const renderPromise = Promise.resolve().then(() => render(cardTree()));
845
1032
  const timeoutPromise = new Promise((_, reject) => {
846
1033
  setTimeout(
847
1034
  () => reject(
@@ -893,6 +1080,9 @@ async function runRenderCheckInProcess(input) {
893
1080
  const user = buildClickUser(userEvent, hostGlobals().document);
894
1081
  let actionsChecked = 0;
895
1082
  let streamsChecked = 0;
1083
+ let pendingTally;
1084
+ const pendingTargets = [];
1085
+ const walkedTexts = [];
896
1086
  try {
897
1087
  let inputPriming;
898
1088
  try {
@@ -909,15 +1099,42 @@ async function runRenderCheckInProcess(input) {
909
1099
  hookName: "useAction",
910
1100
  hookArg: name
911
1101
  });
1102
+ const walkPriming = {};
1103
+ const mount = async () => {
1104
+ let mounted;
1105
+ try {
1106
+ mounted = render(cardTree());
1107
+ } catch {
1108
+ return void 0;
1109
+ }
1110
+ await flushPromises();
1111
+ const root = mounted.container;
1112
+ return {
1113
+ container: toMinimalElement(root),
1114
+ prime: () => {
1115
+ try {
1116
+ primeInputs(root);
1117
+ } catch (err) {
1118
+ walkPriming.error ??= err instanceof Error ? err.message : String(err);
1119
+ }
1120
+ }
1121
+ };
1122
+ };
912
1123
  const issue = await checkActionWiring({
913
- container,
1124
+ mount,
914
1125
  actionName: name,
915
1126
  actionLabel: entry.label,
916
1127
  wiring,
917
1128
  probe,
918
- user
1129
+ user,
1130
+ pendingTargets,
1131
+ stopAt: walkStopAt(input.deadlineAt),
1132
+ walkedTexts
919
1133
  });
920
- if (issue) issues.push({ ...issue, diagnostics: { ...issue.diagnostics, inputPriming } });
1134
+ if (issue) {
1135
+ const priming = "primed" in inputPriming && walkPriming.error !== void 0 ? { error: `on a screen the action walk reached: ${walkPriming.error}` } : inputPriming;
1136
+ issues.push({ ...issue, diagnostics: { ...issue.diagnostics, inputPriming: priming } });
1137
+ }
921
1138
  }
922
1139
  }
923
1140
  if (input.contract?.actionSpec) {
@@ -939,6 +1156,7 @@ async function runRenderCheckInProcess(input) {
939
1156
  if (input.contract?.propsSpec) {
940
1157
  for (const issue of checkPropCoverage({
941
1158
  container,
1159
+ walkedTexts,
942
1160
  propsSpec: input.contract.propsSpec,
943
1161
  mockupProps: input.mockupProps
944
1162
  })) {
@@ -1044,10 +1262,21 @@ async function runRenderCheckInProcess(input) {
1044
1262
  }
1045
1263
  }
1046
1264
  }
1265
+ const timeLeft = input.deadlineAt === void 0 ? Number.POSITIVE_INFINITY : input.deadlineAt - Date.now();
1266
+ if (input.contract?.actionSpec && timeLeft >= PENDING_PHASE_RESERVE_MS) {
1267
+ pendingTally = await runPendingPhase({
1268
+ targets: pendingTargets,
1269
+ source: pendingSource,
1270
+ act,
1271
+ catches: () => boundaryRef.catches,
1272
+ actions: Object.entries(input.contract.actionSpec).map(([name, entry]) => ({ name, label: entry.label })),
1273
+ ...input.deadlineAt !== void 0 ? { deadlineAt: input.deadlineAt } : {}
1274
+ });
1275
+ }
1047
1276
  } finally {
1048
1277
  cleanup();
1049
1278
  }
1050
- return finalize(issues, t0, actionsChecked, streamsChecked);
1279
+ return finalize(issues, t0, actionsChecked, streamsChecked, pendingTally);
1051
1280
  } finally {
1052
1281
  try {
1053
1282
  uninstallSpy?.();
@@ -1259,7 +1488,7 @@ async function setupHappyDom() {
1259
1488
  delete g.IS_REACT_ACT_ENVIRONMENT;
1260
1489
  };
1261
1490
  }
1262
- function finalize(issues, t0, actionsChecked, streamsChecked) {
1491
+ function finalize(issues, t0, actionsChecked, streamsChecked, pendingTally) {
1263
1492
  const ok = !issues.some((i) => i.outcome === "failed");
1264
1493
  return {
1265
1494
  ok,
@@ -1267,12 +1496,86 @@ function finalize(issues, t0, actionsChecked, streamsChecked) {
1267
1496
  stats: {
1268
1497
  actionsChecked,
1269
1498
  streamsChecked,
1270
- renderMs: Date.now() - t0
1499
+ renderMs: Date.now() - t0,
1500
+ ...pendingTally !== void 0 ? {
1501
+ pendingAffordance: {
1502
+ dispatched: pendingTally.dispatched,
1503
+ visible: pendingTally.visible,
1504
+ missing: [...pendingTally.missing],
1505
+ gone: [...pendingTally.gone]
1506
+ }
1507
+ } : {}
1271
1508
  }
1272
1509
  };
1273
1510
  }
1511
+ var PENDING_TARGET_CAP_MS = 2e3;
1512
+ var PENDING_PHASE_TAIL_MS = 1e3;
1513
+ async function runPendingPhase(input) {
1514
+ const tally = { dispatched: 0, visible: 0, missing: [], gone: [] };
1515
+ let stopped = false;
1516
+ for (const target of input.targets) {
1517
+ const budget = Math.min(
1518
+ PENDING_TARGET_CAP_MS,
1519
+ input.deadlineAt === void 0 ? Number.POSITIVE_INFINITY : input.deadlineAt - Date.now() - PENDING_PHASE_TAIL_MS
1520
+ );
1521
+ const found = stopped || budget <= 0 ? void 0 : pendingControlOf(target, input.actions);
1522
+ if (found === void 0) {
1523
+ tally.gone.push(target.actionName);
1524
+ if (budget <= 0) stopped = true;
1525
+ continue;
1526
+ }
1527
+ const el = controlToRead(found, target);
1528
+ const before = lookOf(el);
1529
+ const caughtBefore = input.catches();
1530
+ const settled = await settlesWithin(
1531
+ budget,
1532
+ (async () => {
1533
+ await input.act(async () => {
1534
+ input.source.mark(target.actionName);
1535
+ });
1536
+ await flushPromises();
1537
+ })()
1538
+ );
1539
+ const crashed = input.catches() !== caughtBefore;
1540
+ tally.dispatched += 1;
1541
+ if (settled && !crashed && lookChanged(before, lookOf(el))) tally.visible += 1;
1542
+ else tally.missing.push(target.actionName);
1543
+ if (!settled) {
1544
+ stopped = true;
1545
+ continue;
1546
+ }
1547
+ await input.act(async () => {
1548
+ input.source.clear();
1549
+ });
1550
+ await flushPromises();
1551
+ if (crashed) stopped = true;
1552
+ }
1553
+ return tally;
1554
+ }
1555
+ function pendingControlOf(target, actions) {
1556
+ const movedToAnother = !matchesActionIdentity(target.element, target.actionName, target.actionLabel) && actions.some((a) => a.name !== target.actionName && matchesActionIdentity(target.element, a.name, a.label));
1557
+ if (lookOf(target.element).connected && !movedToAnother) return target.element;
1558
+ if (target.wiringKind !== "click") return void 0;
1559
+ return findActionElements(target.container, target.actionName, target.actionLabel, { identityOnly: true })[0];
1560
+ }
1561
+ function controlToRead(el, target) {
1562
+ if (el.tagName.toLowerCase() !== "form") return el;
1563
+ for (const c of el.querySelectorAll('button, input[type="submit"], input[type="button"], [role="button"]')) {
1564
+ if (matchesActionIdentity(c, target.actionName, target.actionLabel)) return c;
1565
+ }
1566
+ for (const submitter of el.querySelectorAll(
1567
+ 'button[type="submit"], input[type="submit"], button:not([type])'
1568
+ )) {
1569
+ return submitter;
1570
+ }
1571
+ return el;
1572
+ }
1573
+ function walkStopAt(deadlineAt) {
1574
+ const cap = Date.now() + WALK_ACTION_CAP_MS;
1575
+ return deadlineAt === void 0 ? cap : Math.min(cap, deadlineAt - PENDING_PHASE_RESERVE_MS - WALK_DEADLINE_MARGIN_MS);
1576
+ }
1274
1577
  async function checkActionWiring(input) {
1275
- const { container, actionName, actionLabel, wiring, probe, user } = input;
1578
+ const { actionName, actionLabel, wiring, probe, user } = input;
1276
1579
  const baseDiagnostics = {
1277
1580
  observedJsxElements: wiring.observedJsxElements,
1278
1581
  observedNativeProps: wiring.observedNativeProps,
@@ -1296,25 +1599,56 @@ async function checkActionWiring(input) {
1296
1599
  diagnostics: baseDiagnostics
1297
1600
  };
1298
1601
  }
1299
- const fired = await simulateAndCheck({
1300
- container,
1301
- user,
1302
- probe,
1303
- wiringKind: wiring.kind,
1304
- actionName,
1305
- actionLabel,
1306
- eventKind: "action.fired"
1602
+ const kind = wiring.kind;
1603
+ const walk = await walkToAction({
1604
+ mount: input.mount,
1605
+ controls: (container) => actionControls(container, kind, actionName, actionLabel),
1606
+ press: async (control) => {
1607
+ const before = probe.getFireLog().length;
1608
+ try {
1609
+ await dispatchTrigger(control.element, control.how, user);
1610
+ } catch {
1611
+ return false;
1612
+ }
1613
+ await flushPromises();
1614
+ return probe.getFireLog().slice(before).some((e) => e.kind === "action.fired" && e.name === actionName);
1615
+ },
1616
+ describe: describeElement,
1617
+ stopAt: input.stopAt,
1618
+ maxPresses: WALK_MAX_PRESSES
1307
1619
  });
1308
- if (fired.fired) return null;
1620
+ input.walkedTexts.push(...walk.screenTexts);
1621
+ if (walk.fired) {
1622
+ if (input.pendingTargets && walk.element && walk.container && (kind === "click" || kind === "submit")) {
1623
+ input.pendingTargets.push({
1624
+ actionName,
1625
+ actionLabel,
1626
+ wiringKind: kind,
1627
+ element: walk.element,
1628
+ container: walk.container
1629
+ });
1630
+ }
1631
+ return null;
1632
+ }
1633
+ const allFired = probe.getFireLog().filter((e) => e.kind === "action.fired").map((e) => e.name);
1634
+ const otherActionsFired = Array.from(new Set(allFired)).filter((n) => n !== actionName);
1635
+ const stoppedBy = walk.stoppedBy === "dispatched" ? "explored" : walk.stoppedBy;
1309
1636
  return {
1310
1637
  check: "action-wiring",
1311
1638
  outcome: "unverified",
1312
1639
  subject: actionName,
1313
- reason: `Source confirms ${wiring.kind} wiring exists for action '${actionName}', but synthetic ${wiring.kind} did not dispatch it. Likely cause: conditional rendering, required input/form fill, or a multi-step interaction the static probe cannot complete.`,
1314
- elementHint: fired.attemptedHint,
1640
+ reason: `Source confirms ${kind} wiring exists for action '${actionName}', but synthetic ${kind} did not dispatch it. Likely cause: conditional rendering, required input/form fill, or a multi-step interaction the static probe cannot complete.`,
1641
+ ...walk.presses === 0 && stoppedBy === "explored" ? { elementHint: `No ${kind}-eligible element found in DOM` } : {},
1315
1642
  diagnostics: {
1316
1643
  ...baseDiagnostics,
1317
- actionsFiredFromClicks: fired.otherActionsFired
1644
+ actionsFiredFromClicks: otherActionsFired,
1645
+ walk: {
1646
+ presses: walk.presses,
1647
+ screens: walk.screens,
1648
+ stoppedBy,
1649
+ namedControlSeen: walk.namedControlSeen,
1650
+ lastPressed: walk.lastPressed
1651
+ }
1318
1652
  }
1319
1653
  };
1320
1654
  }
@@ -1382,32 +1716,6 @@ async function checkSelectionIdentity(input) {
1382
1716
  }
1383
1717
  return null;
1384
1718
  }
1385
- async function simulateAndCheck(input) {
1386
- const { container, user, probe, wiringKind, actionName, actionLabel, eventKind } = input;
1387
- const candidates = findCandidateElements(container, wiringKind, actionName, actionLabel);
1388
- if (candidates.length === 0) {
1389
- return { fired: false, attemptedHint: `No ${wiringKind}-eligible element found in DOM` };
1390
- }
1391
- for (const el of candidates) {
1392
- const before = probe.getFireLog().length;
1393
- try {
1394
- await dispatchTrigger(el, wiringKind, user);
1395
- } catch {
1396
- continue;
1397
- }
1398
- await flushPromises();
1399
- const newEvents = probe.getFireLog().slice(before);
1400
- const matched = newEvents.some((e) => e.kind === eventKind && e.name === actionName);
1401
- if (matched) return { fired: true };
1402
- }
1403
- const allFired = probe.getFireLog().filter((e) => e.kind === "action.fired").map((e) => e.name);
1404
- const otherActionsFired = Array.from(new Set(allFired)).filter((n) => n !== actionName);
1405
- return {
1406
- fired: false,
1407
- attemptedHint: candidates.length ? describeElement(candidates[0]) : void 0,
1408
- otherActionsFired
1409
- };
1410
- }
1411
1719
  async function dispatchTrigger(el, kind, user) {
1412
1720
  switch (kind) {
1413
1721
  case "click":
@@ -1493,40 +1801,40 @@ function closestForm(el) {
1493
1801
  }
1494
1802
  return null;
1495
1803
  }
1496
- function findCandidateElements(container, kind, name, label) {
1497
- switch (kind) {
1498
- case "click":
1499
- return findActionElements(container, name, label);
1500
- case "submit": {
1501
- const all = container.querySelectorAll(
1502
- 'form, button[type="submit"], input[type="submit"]'
1503
- );
1504
- return Array.from(all);
1505
- }
1506
- case "change": {
1507
- const all = container.querySelectorAll("select, input, textarea");
1508
- return Array.from(all);
1509
- }
1510
- case "keyboard-enter":
1511
- return findActionElements(container, name, label);
1804
+ function actionControls(container, kind, name, label) {
1805
+ const clickables = findActionElements(container, name, label);
1806
+ const named = (el) => matchesActionIdentity(el, name, label);
1807
+ if (kind === "click" || kind === "keyboard-enter") {
1808
+ return clickables.map((element) => ({ element, namesAction: named(element), how: kind }));
1512
1809
  }
1810
+ const eligible = Array.from(
1811
+ container.querySelectorAll(
1812
+ kind === "submit" ? 'form, button[type="submit"], input[type="submit"]' : "select, input, textarea"
1813
+ )
1814
+ );
1815
+ const seen = new Set(eligible);
1816
+ return [
1817
+ ...eligible.map((element) => ({ element, namesAction: named(element), how: kind })),
1818
+ ...clickables.filter((el) => !seen.has(el)).map((element) => ({ element, namesAction: named(element), how: "click" }))
1819
+ ];
1513
1820
  }
1514
1821
  function checkPropCoverage(input) {
1515
- const { container, propsSpec, mockupProps } = input;
1822
+ const { container, walkedTexts, propsSpec, mockupProps } = input;
1516
1823
  const issues = [];
1517
- const text = container.textContent ?? "";
1824
+ const texts = [container.textContent ?? "", ...walkedTexts];
1518
1825
  for (const [propName, entry] of Object.entries(propsSpec.properties)) {
1519
1826
  if (!entry.required) continue;
1520
1827
  const value = mockupProps[propName];
1521
1828
  if (value === void 0 || value === null) continue;
1522
1829
  const marker = pickScalarMarker(value);
1523
1830
  if (marker === null) continue;
1524
- if (!text.includes(marker)) {
1831
+ if (!texts.some((text) => text.includes(marker))) {
1832
+ const where = walkedTexts.length > 0 ? ", on the first screen or any screen the action walk reached" : "";
1525
1833
  issues.push({
1526
1834
  check: "prop-coverage",
1527
1835
  outcome: "unverified",
1528
1836
  subject: propName,
1529
- reason: `Required prop '${propName}' value (${JSON.stringify(marker).slice(0, 60)}) not visible in rendered DOM`
1837
+ reason: `Required prop '${propName}' value (${JSON.stringify(marker).slice(0, 60)}) not visible in rendered DOM${where}`
1530
1838
  });
1531
1839
  }
1532
1840
  }
@@ -1566,26 +1874,28 @@ async function checkStreamRerender(input) {
1566
1874
  }
1567
1875
  return null;
1568
1876
  }
1569
- function findActionElements(container, name, label) {
1877
+ function matchesActionIdentity(el, name, label) {
1878
+ const norm = (s) => s.toLowerCase().replace(/[\s_-]+/g, "");
1879
+ const aria = (el.getAttribute("aria-label") ?? "").toLowerCase();
1880
+ const txt = (el.textContent ?? "").toLowerCase();
1881
+ const dataAction = (el.getAttribute("data-action") ?? "").toLowerCase();
1882
+ return aria.includes(label.toLowerCase()) || txt.includes(label.toLowerCase()) || norm(aria).includes(norm(name)) || norm(txt).includes(norm(name)) || dataAction === name.toLowerCase() || norm(txt).includes(norm(label));
1883
+ }
1884
+ function findActionElements(container, name, label, options = {}) {
1570
1885
  const candidates = [];
1571
1886
  const seen = /* @__PURE__ */ new Set();
1572
- const norm = (s) => s.toLowerCase().replace(/[\s_-]+/g, "");
1573
- const nameKey = norm(name);
1574
- const labelKey = norm(label);
1575
1887
  const allClickable = container.querySelectorAll(
1576
1888
  'button, [role="button"], input[type="submit"], input[type="button"], a[href]'
1577
1889
  );
1578
1890
  for (const el of allClickable) {
1579
- const aria = (el.getAttribute("aria-label") ?? "").toLowerCase();
1580
- const txt = (el.textContent ?? "").toLowerCase();
1581
- const dataAction = (el.getAttribute("data-action") ?? "").toLowerCase();
1582
- if (aria.includes(label.toLowerCase()) || txt.includes(label.toLowerCase()) || norm(aria).includes(nameKey) || norm(txt).includes(nameKey) || dataAction === name.toLowerCase() || norm(txt).includes(labelKey)) {
1891
+ if (matchesActionIdentity(el, name, label)) {
1583
1892
  if (!seen.has(el)) {
1584
1893
  seen.add(el);
1585
1894
  candidates.push(el);
1586
1895
  }
1587
1896
  }
1588
1897
  }
1898
+ if (options.identityOnly) return candidates;
1589
1899
  for (const el of allClickable) {
1590
1900
  if (!seen.has(el)) {
1591
1901
  seen.add(el);
@@ -1856,7 +2166,9 @@ function createRuntimeRenderCheck(config = {}) {
1856
2166
  elapsedMs: Date.now() - t0,
1857
2167
  ...queuedField(queuedMs),
1858
2168
  renderMs: result.stats.renderMs,
1859
- ...load
2169
+ ...load,
2170
+ // ggui#1398: report-only — carried to the probe's meta for the bench, never an issue.
2171
+ ...result.stats.pendingAffordance !== void 0 ? { pendingAffordance: result.stats.pendingAffordance } : {}
1860
2172
  };
1861
2173
  }
1862
2174
  };
@@ -1933,6 +2245,18 @@ function toEvalIssue(issue) {
1933
2245
  "primed" in diag.inputPriming ? `input priming: ${diag.inputPriming.primed} control(s) filled before the click` : `input priming failed: ${diag.inputPriming.error}`
1934
2246
  );
1935
2247
  }
2248
+ if (diag?.walk) {
2249
+ const w = diag.walk;
2250
+ const stopped = {
2251
+ explored: "every reachable control pressed",
2252
+ "press-cap": "stopped at the press cap",
2253
+ deadline: "stopped at the time budget",
2254
+ "no-mount": "the card could not be mounted for the walk"
2255
+ }[w.stoppedBy];
2256
+ const named = w.namedControlSeen ? "a control naming the action rendered, but pressing it did not dispatch the action" : "no screen rendered a control naming the action";
2257
+ const last = w.lastPressed.length ? `; last pressed: ${w.lastPressed.join(", ")}` : "";
2258
+ diagParts.push(`walk: ${w.presses} press(es) across ${w.screens} screen(s), ${stopped}; ${named}${last}`);
2259
+ }
1936
2260
  const diagSuffix = diagParts.length ? ` [observed: ${diagParts.join("; ")}]` : "";
1937
2261
  switch (issue.check) {
1938
2262
  case "render-no-throw":