@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,169 @@ 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
880
+ var PENDING_PHASE_RESERVE_MS = 5e3;
881
+ var WALK_MAX_PRESSES = 60;
882
+ var WALK_ACTION_CAP_MS = 3e3;
883
+ var WALK_DEADLINE_MARGIN_MS = 2e3;
701
884
  async function runRenderCheck(input, options = {}) {
702
885
  const g = hostGlobals();
703
886
  if ("window" in g && "document" in g) {
@@ -763,7 +946,9 @@ async function runRenderCheckInProcess(input) {
763
946
  const wireConfig = createProbeWireConfig(probe);
764
947
  uninstallSpy = probe.installPostMessageSpy();
765
948
  installProductionActShim();
766
- const { render, cleanup } = await import('@testing-library/react');
949
+ const { render, cleanup, act } = await import('@testing-library/react');
950
+ const { ActionPendingContext } = await import('@ggui-ai/wire/internal');
951
+ const pendingSource = createProbePendingSource();
767
952
  const userEventModule = await import('@testing-library/user-event');
768
953
  const userEvent = userEventModule.default ?? userEventModule;
769
954
  const originalConsoleError = console.error;
@@ -800,7 +985,8 @@ async function runRenderCheckInProcess(input) {
800
985
  };
801
986
  const boundaryRef = {
802
987
  stack: null,
803
- error: null
988
+ error: null,
989
+ catches: 0
804
990
  };
805
991
  class ProbeErrorBoundary extends React.Component {
806
992
  state = { caught: false };
@@ -808,6 +994,7 @@ async function runRenderCheckInProcess(input) {
808
994
  return { caught: true };
809
995
  }
810
996
  componentDidCatch(error, errorInfo) {
997
+ boundaryRef.catches += 1;
811
998
  if (errorInfo.componentStack && !boundaryRef.stack) {
812
999
  boundaryRef.stack = errorInfo.componentStack;
813
1000
  }
@@ -820,19 +1007,19 @@ async function runRenderCheckInProcess(input) {
820
1007
  return this.props.children;
821
1008
  }
822
1009
  }
1010
+ const cardTree = () => React.createElement(GguiWireProvider, {
1011
+ config: wireConfig,
1012
+ children: React.createElement(ProbeErrorBoundary, {
1013
+ children: React.createElement(ActionPendingContext.Provider, {
1014
+ value: pendingSource,
1015
+ children: React.createElement(Component, input.mockupProps)
1016
+ })
1017
+ })
1018
+ });
823
1019
  let renderResult;
824
1020
  const RENDER_TIMEOUT_MS = 5e3;
825
1021
  try {
826
- const renderPromise = Promise.resolve().then(
827
- () => render(
828
- React.createElement(GguiWireProvider, {
829
- config: wireConfig,
830
- children: React.createElement(ProbeErrorBoundary, {
831
- children: React.createElement(Component, input.mockupProps)
832
- })
833
- })
834
- )
835
- );
1022
+ const renderPromise = Promise.resolve().then(() => render(cardTree()));
836
1023
  const timeoutPromise = new Promise((_, reject) => {
837
1024
  setTimeout(
838
1025
  () => reject(
@@ -884,6 +1071,9 @@ async function runRenderCheckInProcess(input) {
884
1071
  const user = buildClickUser(userEvent, hostGlobals().document);
885
1072
  let actionsChecked = 0;
886
1073
  let streamsChecked = 0;
1074
+ let pendingTally;
1075
+ const pendingTargets = [];
1076
+ const walkedTexts = [];
887
1077
  try {
888
1078
  let inputPriming;
889
1079
  try {
@@ -900,15 +1090,42 @@ async function runRenderCheckInProcess(input) {
900
1090
  hookName: "useAction",
901
1091
  hookArg: name
902
1092
  });
1093
+ const walkPriming = {};
1094
+ const mount = async () => {
1095
+ let mounted;
1096
+ try {
1097
+ mounted = render(cardTree());
1098
+ } catch {
1099
+ return void 0;
1100
+ }
1101
+ await flushPromises();
1102
+ const root = mounted.container;
1103
+ return {
1104
+ container: toMinimalElement(root),
1105
+ prime: () => {
1106
+ try {
1107
+ primeInputs(root);
1108
+ } catch (err) {
1109
+ walkPriming.error ??= err instanceof Error ? err.message : String(err);
1110
+ }
1111
+ }
1112
+ };
1113
+ };
903
1114
  const issue = await checkActionWiring({
904
- container,
1115
+ mount,
905
1116
  actionName: name,
906
1117
  actionLabel: entry.label,
907
1118
  wiring,
908
1119
  probe,
909
- user
1120
+ user,
1121
+ pendingTargets,
1122
+ stopAt: walkStopAt(input.deadlineAt),
1123
+ walkedTexts
910
1124
  });
911
- if (issue) issues.push({ ...issue, diagnostics: { ...issue.diagnostics, inputPriming } });
1125
+ if (issue) {
1126
+ const priming = "primed" in inputPriming && walkPriming.error !== void 0 ? { error: `on a screen the action walk reached: ${walkPriming.error}` } : inputPriming;
1127
+ issues.push({ ...issue, diagnostics: { ...issue.diagnostics, inputPriming: priming } });
1128
+ }
912
1129
  }
913
1130
  }
914
1131
  if (input.contract?.actionSpec) {
@@ -930,6 +1147,7 @@ async function runRenderCheckInProcess(input) {
930
1147
  if (input.contract?.propsSpec) {
931
1148
  for (const issue of checkPropCoverage({
932
1149
  container,
1150
+ walkedTexts,
933
1151
  propsSpec: input.contract.propsSpec,
934
1152
  mockupProps: input.mockupProps
935
1153
  })) {
@@ -1035,10 +1253,21 @@ async function runRenderCheckInProcess(input) {
1035
1253
  }
1036
1254
  }
1037
1255
  }
1256
+ const timeLeft = input.deadlineAt === void 0 ? Number.POSITIVE_INFINITY : input.deadlineAt - Date.now();
1257
+ if (input.contract?.actionSpec && timeLeft >= PENDING_PHASE_RESERVE_MS) {
1258
+ pendingTally = await runPendingPhase({
1259
+ targets: pendingTargets,
1260
+ source: pendingSource,
1261
+ act,
1262
+ catches: () => boundaryRef.catches,
1263
+ actions: Object.entries(input.contract.actionSpec).map(([name, entry]) => ({ name, label: entry.label })),
1264
+ ...input.deadlineAt !== void 0 ? { deadlineAt: input.deadlineAt } : {}
1265
+ });
1266
+ }
1038
1267
  } finally {
1039
1268
  cleanup();
1040
1269
  }
1041
- return finalize(issues, t0, actionsChecked, streamsChecked);
1270
+ return finalize(issues, t0, actionsChecked, streamsChecked, pendingTally);
1042
1271
  } finally {
1043
1272
  try {
1044
1273
  uninstallSpy?.();
@@ -1250,7 +1479,7 @@ async function setupHappyDom() {
1250
1479
  delete g.IS_REACT_ACT_ENVIRONMENT;
1251
1480
  };
1252
1481
  }
1253
- function finalize(issues, t0, actionsChecked, streamsChecked) {
1482
+ function finalize(issues, t0, actionsChecked, streamsChecked, pendingTally) {
1254
1483
  const ok = !issues.some((i) => i.outcome === "failed");
1255
1484
  return {
1256
1485
  ok,
@@ -1258,12 +1487,86 @@ function finalize(issues, t0, actionsChecked, streamsChecked) {
1258
1487
  stats: {
1259
1488
  actionsChecked,
1260
1489
  streamsChecked,
1261
- renderMs: Date.now() - t0
1490
+ renderMs: Date.now() - t0,
1491
+ ...pendingTally !== void 0 ? {
1492
+ pendingAffordance: {
1493
+ dispatched: pendingTally.dispatched,
1494
+ visible: pendingTally.visible,
1495
+ missing: [...pendingTally.missing],
1496
+ gone: [...pendingTally.gone]
1497
+ }
1498
+ } : {}
1262
1499
  }
1263
1500
  };
1264
1501
  }
1502
+ var PENDING_TARGET_CAP_MS = 2e3;
1503
+ var PENDING_PHASE_TAIL_MS = 1e3;
1504
+ async function runPendingPhase(input) {
1505
+ const tally = { dispatched: 0, visible: 0, missing: [], gone: [] };
1506
+ let stopped = false;
1507
+ for (const target of input.targets) {
1508
+ const budget = Math.min(
1509
+ PENDING_TARGET_CAP_MS,
1510
+ input.deadlineAt === void 0 ? Number.POSITIVE_INFINITY : input.deadlineAt - Date.now() - PENDING_PHASE_TAIL_MS
1511
+ );
1512
+ const found = stopped || budget <= 0 ? void 0 : pendingControlOf(target, input.actions);
1513
+ if (found === void 0) {
1514
+ tally.gone.push(target.actionName);
1515
+ if (budget <= 0) stopped = true;
1516
+ continue;
1517
+ }
1518
+ const el = controlToRead(found, target);
1519
+ const before = lookOf(el);
1520
+ const caughtBefore = input.catches();
1521
+ const settled = await settlesWithin(
1522
+ budget,
1523
+ (async () => {
1524
+ await input.act(async () => {
1525
+ input.source.mark(target.actionName);
1526
+ });
1527
+ await flushPromises();
1528
+ })()
1529
+ );
1530
+ const crashed = input.catches() !== caughtBefore;
1531
+ tally.dispatched += 1;
1532
+ if (settled && !crashed && lookChanged(before, lookOf(el))) tally.visible += 1;
1533
+ else tally.missing.push(target.actionName);
1534
+ if (!settled) {
1535
+ stopped = true;
1536
+ continue;
1537
+ }
1538
+ await input.act(async () => {
1539
+ input.source.clear();
1540
+ });
1541
+ await flushPromises();
1542
+ if (crashed) stopped = true;
1543
+ }
1544
+ return tally;
1545
+ }
1546
+ function pendingControlOf(target, actions) {
1547
+ const movedToAnother = !matchesActionIdentity(target.element, target.actionName, target.actionLabel) && actions.some((a) => a.name !== target.actionName && matchesActionIdentity(target.element, a.name, a.label));
1548
+ if (lookOf(target.element).connected && !movedToAnother) return target.element;
1549
+ if (target.wiringKind !== "click") return void 0;
1550
+ return findActionElements(target.container, target.actionName, target.actionLabel, { identityOnly: true })[0];
1551
+ }
1552
+ function controlToRead(el, target) {
1553
+ if (el.tagName.toLowerCase() !== "form") return el;
1554
+ for (const c of el.querySelectorAll('button, input[type="submit"], input[type="button"], [role="button"]')) {
1555
+ if (matchesActionIdentity(c, target.actionName, target.actionLabel)) return c;
1556
+ }
1557
+ for (const submitter of el.querySelectorAll(
1558
+ 'button[type="submit"], input[type="submit"], button:not([type])'
1559
+ )) {
1560
+ return submitter;
1561
+ }
1562
+ return el;
1563
+ }
1564
+ function walkStopAt(deadlineAt) {
1565
+ const cap = Date.now() + WALK_ACTION_CAP_MS;
1566
+ return deadlineAt === void 0 ? cap : Math.min(cap, deadlineAt - PENDING_PHASE_RESERVE_MS - WALK_DEADLINE_MARGIN_MS);
1567
+ }
1265
1568
  async function checkActionWiring(input) {
1266
- const { container, actionName, actionLabel, wiring, probe, user } = input;
1569
+ const { actionName, actionLabel, wiring, probe, user } = input;
1267
1570
  const baseDiagnostics = {
1268
1571
  observedJsxElements: wiring.observedJsxElements,
1269
1572
  observedNativeProps: wiring.observedNativeProps,
@@ -1287,25 +1590,56 @@ async function checkActionWiring(input) {
1287
1590
  diagnostics: baseDiagnostics
1288
1591
  };
1289
1592
  }
1290
- const fired = await simulateAndCheck({
1291
- container,
1292
- user,
1293
- probe,
1294
- wiringKind: wiring.kind,
1295
- actionName,
1296
- actionLabel,
1297
- eventKind: "action.fired"
1593
+ const kind = wiring.kind;
1594
+ const walk = await walkToAction({
1595
+ mount: input.mount,
1596
+ controls: (container) => actionControls(container, kind, actionName, actionLabel),
1597
+ press: async (control) => {
1598
+ const before = probe.getFireLog().length;
1599
+ try {
1600
+ await dispatchTrigger(control.element, control.how, user);
1601
+ } catch {
1602
+ return false;
1603
+ }
1604
+ await flushPromises();
1605
+ return probe.getFireLog().slice(before).some((e) => e.kind === "action.fired" && e.name === actionName);
1606
+ },
1607
+ describe: describeElement,
1608
+ stopAt: input.stopAt,
1609
+ maxPresses: WALK_MAX_PRESSES
1298
1610
  });
1299
- if (fired.fired) return null;
1611
+ input.walkedTexts.push(...walk.screenTexts);
1612
+ if (walk.fired) {
1613
+ if (input.pendingTargets && walk.element && walk.container && (kind === "click" || kind === "submit")) {
1614
+ input.pendingTargets.push({
1615
+ actionName,
1616
+ actionLabel,
1617
+ wiringKind: kind,
1618
+ element: walk.element,
1619
+ container: walk.container
1620
+ });
1621
+ }
1622
+ return null;
1623
+ }
1624
+ const allFired = probe.getFireLog().filter((e) => e.kind === "action.fired").map((e) => e.name);
1625
+ const otherActionsFired = Array.from(new Set(allFired)).filter((n) => n !== actionName);
1626
+ const stoppedBy = walk.stoppedBy === "dispatched" ? "explored" : walk.stoppedBy;
1300
1627
  return {
1301
1628
  check: "action-wiring",
1302
1629
  outcome: "unverified",
1303
1630
  subject: actionName,
1304
- 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.`,
1305
- elementHint: fired.attemptedHint,
1631
+ 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.`,
1632
+ ...walk.presses === 0 && stoppedBy === "explored" ? { elementHint: `No ${kind}-eligible element found in DOM` } : {},
1306
1633
  diagnostics: {
1307
1634
  ...baseDiagnostics,
1308
- actionsFiredFromClicks: fired.otherActionsFired
1635
+ actionsFiredFromClicks: otherActionsFired,
1636
+ walk: {
1637
+ presses: walk.presses,
1638
+ screens: walk.screens,
1639
+ stoppedBy,
1640
+ namedControlSeen: walk.namedControlSeen,
1641
+ lastPressed: walk.lastPressed
1642
+ }
1309
1643
  }
1310
1644
  };
1311
1645
  }
@@ -1373,32 +1707,6 @@ async function checkSelectionIdentity(input) {
1373
1707
  }
1374
1708
  return null;
1375
1709
  }
1376
- async function simulateAndCheck(input) {
1377
- const { container, user, probe, wiringKind, actionName, actionLabel, eventKind } = input;
1378
- const candidates = findCandidateElements(container, wiringKind, actionName, actionLabel);
1379
- if (candidates.length === 0) {
1380
- return { fired: false, attemptedHint: `No ${wiringKind}-eligible element found in DOM` };
1381
- }
1382
- for (const el of candidates) {
1383
- const before = probe.getFireLog().length;
1384
- try {
1385
- await dispatchTrigger(el, wiringKind, user);
1386
- } catch {
1387
- continue;
1388
- }
1389
- await flushPromises();
1390
- const newEvents = probe.getFireLog().slice(before);
1391
- const matched = newEvents.some((e) => e.kind === eventKind && e.name === actionName);
1392
- if (matched) return { fired: true };
1393
- }
1394
- const allFired = probe.getFireLog().filter((e) => e.kind === "action.fired").map((e) => e.name);
1395
- const otherActionsFired = Array.from(new Set(allFired)).filter((n) => n !== actionName);
1396
- return {
1397
- fired: false,
1398
- attemptedHint: candidates.length ? describeElement(candidates[0]) : void 0,
1399
- otherActionsFired
1400
- };
1401
- }
1402
1710
  async function dispatchTrigger(el, kind, user) {
1403
1711
  switch (kind) {
1404
1712
  case "click":
@@ -1484,40 +1792,40 @@ function closestForm(el) {
1484
1792
  }
1485
1793
  return null;
1486
1794
  }
1487
- function findCandidateElements(container, kind, name, label) {
1488
- switch (kind) {
1489
- case "click":
1490
- return findActionElements(container, name, label);
1491
- case "submit": {
1492
- const all = container.querySelectorAll(
1493
- 'form, button[type="submit"], input[type="submit"]'
1494
- );
1495
- return Array.from(all);
1496
- }
1497
- case "change": {
1498
- const all = container.querySelectorAll("select, input, textarea");
1499
- return Array.from(all);
1500
- }
1501
- case "keyboard-enter":
1502
- return findActionElements(container, name, label);
1795
+ function actionControls(container, kind, name, label) {
1796
+ const clickables = findActionElements(container, name, label);
1797
+ const named = (el) => matchesActionIdentity(el, name, label);
1798
+ if (kind === "click" || kind === "keyboard-enter") {
1799
+ return clickables.map((element) => ({ element, namesAction: named(element), how: kind }));
1503
1800
  }
1801
+ const eligible = Array.from(
1802
+ container.querySelectorAll(
1803
+ kind === "submit" ? 'form, button[type="submit"], input[type="submit"]' : "select, input, textarea"
1804
+ )
1805
+ );
1806
+ const seen = new Set(eligible);
1807
+ return [
1808
+ ...eligible.map((element) => ({ element, namesAction: named(element), how: kind })),
1809
+ ...clickables.filter((el) => !seen.has(el)).map((element) => ({ element, namesAction: named(element), how: "click" }))
1810
+ ];
1504
1811
  }
1505
1812
  function checkPropCoverage(input) {
1506
- const { container, propsSpec, mockupProps } = input;
1813
+ const { container, walkedTexts, propsSpec, mockupProps } = input;
1507
1814
  const issues = [];
1508
- const text = container.textContent ?? "";
1815
+ const texts = [container.textContent ?? "", ...walkedTexts];
1509
1816
  for (const [propName, entry] of Object.entries(propsSpec.properties)) {
1510
1817
  if (!entry.required) continue;
1511
1818
  const value = mockupProps[propName];
1512
1819
  if (value === void 0 || value === null) continue;
1513
1820
  const marker = pickScalarMarker(value);
1514
1821
  if (marker === null) continue;
1515
- if (!text.includes(marker)) {
1822
+ if (!texts.some((text) => text.includes(marker))) {
1823
+ const where = walkedTexts.length > 0 ? ", on the first screen or any screen the action walk reached" : "";
1516
1824
  issues.push({
1517
1825
  check: "prop-coverage",
1518
1826
  outcome: "unverified",
1519
1827
  subject: propName,
1520
- reason: `Required prop '${propName}' value (${JSON.stringify(marker).slice(0, 60)}) not visible in rendered DOM`
1828
+ reason: `Required prop '${propName}' value (${JSON.stringify(marker).slice(0, 60)}) not visible in rendered DOM${where}`
1521
1829
  });
1522
1830
  }
1523
1831
  }
@@ -1557,26 +1865,28 @@ async function checkStreamRerender(input) {
1557
1865
  }
1558
1866
  return null;
1559
1867
  }
1560
- function findActionElements(container, name, label) {
1868
+ function matchesActionIdentity(el, name, label) {
1869
+ const norm = (s) => s.toLowerCase().replace(/[\s_-]+/g, "");
1870
+ const aria = (el.getAttribute("aria-label") ?? "").toLowerCase();
1871
+ const txt = (el.textContent ?? "").toLowerCase();
1872
+ const dataAction = (el.getAttribute("data-action") ?? "").toLowerCase();
1873
+ 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));
1874
+ }
1875
+ function findActionElements(container, name, label, options = {}) {
1561
1876
  const candidates = [];
1562
1877
  const seen = /* @__PURE__ */ new Set();
1563
- const norm = (s) => s.toLowerCase().replace(/[\s_-]+/g, "");
1564
- const nameKey = norm(name);
1565
- const labelKey = norm(label);
1566
1878
  const allClickable = container.querySelectorAll(
1567
1879
  'button, [role="button"], input[type="submit"], input[type="button"], a[href]'
1568
1880
  );
1569
1881
  for (const el of allClickable) {
1570
- const aria = (el.getAttribute("aria-label") ?? "").toLowerCase();
1571
- const txt = (el.textContent ?? "").toLowerCase();
1572
- const dataAction = (el.getAttribute("data-action") ?? "").toLowerCase();
1573
- if (aria.includes(label.toLowerCase()) || txt.includes(label.toLowerCase()) || norm(aria).includes(nameKey) || norm(txt).includes(nameKey) || dataAction === name.toLowerCase() || norm(txt).includes(labelKey)) {
1882
+ if (matchesActionIdentity(el, name, label)) {
1574
1883
  if (!seen.has(el)) {
1575
1884
  seen.add(el);
1576
1885
  candidates.push(el);
1577
1886
  }
1578
1887
  }
1579
1888
  }
1889
+ if (options.identityOnly) return candidates;
1580
1890
  for (const el of allClickable) {
1581
1891
  if (!seen.has(el)) {
1582
1892
  seen.add(el);
@@ -1847,7 +2157,9 @@ function createRuntimeRenderCheck(config = {}) {
1847
2157
  elapsedMs: Date.now() - t0,
1848
2158
  ...queuedField(queuedMs),
1849
2159
  renderMs: result.stats.renderMs,
1850
- ...load
2160
+ ...load,
2161
+ // ggui#1398: report-only — carried to the probe's meta for the bench, never an issue.
2162
+ ...result.stats.pendingAffordance !== void 0 ? { pendingAffordance: result.stats.pendingAffordance } : {}
1851
2163
  };
1852
2164
  }
1853
2165
  };
@@ -1901,6 +2213,18 @@ function toEvalIssue(issue) {
1901
2213
  "primed" in diag.inputPriming ? `input priming: ${diag.inputPriming.primed} control(s) filled before the click` : `input priming failed: ${diag.inputPriming.error}`
1902
2214
  );
1903
2215
  }
2216
+ if (diag?.walk) {
2217
+ const w = diag.walk;
2218
+ const stopped = {
2219
+ explored: "every reachable control pressed",
2220
+ "press-cap": "stopped at the press cap",
2221
+ deadline: "stopped at the time budget",
2222
+ "no-mount": "the card could not be mounted for the walk"
2223
+ }[w.stoppedBy];
2224
+ 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";
2225
+ const last = w.lastPressed.length ? `; last pressed: ${w.lastPressed.join(", ")}` : "";
2226
+ diagParts.push(`walk: ${w.presses} press(es) across ${w.screens} screen(s), ${stopped}; ${named}${last}`);
2227
+ }
1904
2228
  const diagSuffix = diagParts.length ? ` [observed: ${diagParts.join("; ")}]` : "";
1905
2229
  switch (issue.check) {
1906
2230
  case "render-no-throw":