@ggui-ai/ui-gen 0.10.0 → 0.12.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 (35) hide show
  1. package/dist/adapters/generation-dispatch.js +328 -24
  2. package/dist/adapters/generation-dispatch.js.map +1 -1
  3. package/dist/advanced/index.d.ts +1 -1
  4. package/dist/advanced/index.js +333 -29
  5. package/dist/advanced/index.js.map +1 -1
  6. package/dist/blueprint-validator.js +307 -3
  7. package/dist/blueprint-validator.js.map +1 -1
  8. package/dist/boilerplate.js +7 -7
  9. package/dist/boilerplate.js.map +1 -1
  10. package/dist/check/index.js +27 -11
  11. package/dist/check/index.js.map +1 -1
  12. package/dist/harness/check/runtime-render/index.d.ts +1 -1
  13. package/dist/harness/check/runtime-render/index.js +307 -3
  14. package/dist/harness/check/runtime-render/index.js.map +1 -1
  15. package/dist/harness/check/runtime-render/render-check-worker.d.ts +2 -0
  16. package/dist/harness/check/runtime-render/render-check-worker.js +1458 -0
  17. package/dist/harness/check/runtime-render/render-check-worker.js.map +1 -0
  18. package/dist/harness/index.js +7 -7
  19. package/dist/harness/index.js.map +1 -1
  20. package/dist/harness/runtime.js +7 -7
  21. package/dist/harness/runtime.js.map +1 -1
  22. package/dist/harness/types-public.js +7 -7
  23. package/dist/harness/types-public.js.map +1 -1
  24. package/dist/index.js +327 -23
  25. package/dist/index.js.map +1 -1
  26. package/dist/provider-adapter-contract.js +1161 -387
  27. package/dist/provider-adapter-contract.js.map +1 -1
  28. package/dist/{render-check-BNYGVDJd.d.ts → render-check-WfOVO9kU.d.ts} +21 -1
  29. package/package.json +12 -12
  30. package/src/boilerplate/system-prompt.ts +7 -7
  31. package/src/check/run-tier0.ts +35 -16
  32. package/src/harness/check/runtime-render/adapter.ts +28 -0
  33. package/src/harness/check/runtime-render/render-check-host.ts +201 -0
  34. package/src/harness/check/runtime-render/render-check-worker.ts +80 -0
  35. package/src/harness/check/runtime-render/render-check.ts +264 -1
@@ -0,0 +1,1458 @@
1
+ import { listContractGadgets, HOOK_NAME_RE } from '@ggui-ai/protocol';
2
+ import ts from 'typescript';
3
+ import { createRequire } from 'module';
4
+
5
+ // src/harness/check/runtime-render/render-check.ts
6
+
7
+ // src/harness/check/runtime-render/probe.ts
8
+ function makeInternals() {
9
+ return {
10
+ fireLog: [],
11
+ streamHandlers: /* @__PURE__ */ new Map(),
12
+ registeredStreams: /* @__PURE__ */ new Set()
13
+ };
14
+ }
15
+ function createProbe() {
16
+ let internals = makeInternals();
17
+ const probe = {
18
+ emitStream: (eventName, payload) => {
19
+ const handlers = internals.streamHandlers.get(eventName);
20
+ if (!handlers) return;
21
+ for (const handler of handlers) handler(payload);
22
+ },
23
+ getFireLog: () => internals.fireLog.slice(),
24
+ getRegistered: () => ({
25
+ streams: Array.from(internals.registeredStreams)
26
+ }),
27
+ fired: (actionName) => internals.fireLog.some((e) => e.kind === "action.fired" && e.name === actionName),
28
+ installPostMessageSpy: () => installPostMessageSpy(probe),
29
+ reset: () => {
30
+ internals = makeInternals();
31
+ probe.__internals = internals;
32
+ },
33
+ __internals: internals
34
+ };
35
+ return probe;
36
+ }
37
+ function installPostMessageSpy(probe) {
38
+ const parent = globalThis.window?.parent;
39
+ if (!parent || typeof parent.postMessage !== "function") {
40
+ return () => {
41
+ };
42
+ }
43
+ const original = parent.postMessage;
44
+ const spy = (...args) => {
45
+ const envelope = args[0];
46
+ recordEnvelope(probe.__internals, envelope);
47
+ return original.apply(parent, args);
48
+ };
49
+ try {
50
+ Object.defineProperty(parent, "postMessage", {
51
+ value: spy,
52
+ configurable: true,
53
+ writable: true
54
+ });
55
+ } catch {
56
+ return () => {
57
+ };
58
+ }
59
+ return () => {
60
+ try {
61
+ Object.defineProperty(parent, "postMessage", {
62
+ value: original,
63
+ configurable: true,
64
+ writable: true
65
+ });
66
+ } catch {
67
+ }
68
+ };
69
+ }
70
+ function recordEnvelope(internals, envelope) {
71
+ if (!envelope || typeof envelope !== "object") return;
72
+ const e = envelope;
73
+ if (typeof e.method !== "string") return;
74
+ const params = e.params ?? {};
75
+ switch (e.method) {
76
+ case "ui/open-link": {
77
+ const url = typeof params.url === "string" ? params.url : "";
78
+ internals.fireLog.push({
79
+ kind: "link.opened",
80
+ url,
81
+ ts: Date.now()
82
+ });
83
+ return;
84
+ }
85
+ case "ui/request-display-mode": {
86
+ const mode = typeof params.mode === "string" ? params.mode : "";
87
+ internals.fireLog.push({
88
+ kind: "displayMode.requested",
89
+ mode,
90
+ ts: Date.now()
91
+ });
92
+ return;
93
+ }
94
+ case "tools/call": {
95
+ return;
96
+ }
97
+ default:
98
+ return;
99
+ }
100
+ }
101
+ function createProbeWireConfig(probe) {
102
+ return {
103
+ app: {
104
+ appId: "probe-app",
105
+ appName: "Probe",
106
+ appDescription: "Eval-time probe wire config"
107
+ },
108
+ render: {
109
+ sessionId: "probe-render",
110
+ isConnected: true
111
+ },
112
+ auth: {
113
+ isAuthenticated: false
114
+ },
115
+ dispatch: (actionName, data) => {
116
+ probe.__internals.fireLog.push({
117
+ kind: "action.fired",
118
+ name: actionName,
119
+ payload: data,
120
+ ts: Date.now()
121
+ });
122
+ },
123
+ subscribe: (eventType, handler) => {
124
+ probe.__internals.registeredStreams.add(eventType);
125
+ let set = probe.__internals.streamHandlers.get(eventType);
126
+ if (!set) {
127
+ set = /* @__PURE__ */ new Set();
128
+ probe.__internals.streamHandlers.set(eventType, set);
129
+ }
130
+ const wrapped = handler;
131
+ set.add(wrapped);
132
+ return () => {
133
+ set.delete(wrapped);
134
+ };
135
+ }
136
+ // The WireConfig surface is dispatch + subscribe only. Capability
137
+ // hooks live in `@ggui-ai/gadgets` and own their own lifecycle —
138
+ // they never route through WireConfig.
139
+ };
140
+ }
141
+
142
+ // src/internal/evaluate-component-source.ts
143
+ async function evaluateComponentSource(sourceCode, sandboxRequire) {
144
+ const esbuild = await import('esbuild');
145
+ const { Script } = await import('vm');
146
+ const cjsResult = await esbuild.transform(sourceCode, {
147
+ loader: "tsx",
148
+ target: "es2020",
149
+ format: "cjs",
150
+ jsx: "automatic",
151
+ jsxImportSource: "react",
152
+ sourcefile: "Component.tsx"
153
+ });
154
+ const sandboxModule = { exports: {} };
155
+ const wrappedCode = `(function(require, exports, module) {
156
+ ${cjsResult.code}
157
+ })`;
158
+ const script = new Script(wrappedCode, { filename: "Component.cjs" });
159
+ const fn = script.runInThisContext();
160
+ fn(sandboxRequire, sandboxModule.exports, sandboxModule);
161
+ return sandboxModule.exports;
162
+ }
163
+
164
+ // src/harness/check/runtime-render/load-component.ts
165
+ async function loadComponent(input) {
166
+ const { sourceCode, moduleResolutions = {} } = input;
167
+ const { createRequire: createRequire2 } = await import('module');
168
+ const require_ = createRequire2(import.meta.url);
169
+ const sandboxRequire = (id) => {
170
+ if (id in moduleResolutions) return moduleResolutions[id];
171
+ if (id === "react/jsx-runtime" || id === "react/jsx-dev-runtime") {
172
+ return require_("react/jsx-runtime");
173
+ }
174
+ if (id === "react") return require_("react");
175
+ if (id === "react-dom" || id.startsWith("react-dom/")) {
176
+ return require_(id);
177
+ }
178
+ if (id === "@ggui-ai/wire") return require_("@ggui-ai/wire");
179
+ if (id.startsWith("@ggui-ai/design")) {
180
+ try {
181
+ return require_(id);
182
+ } catch {
183
+ throw new Error(
184
+ `Required dependency not available in render-check sandbox: ${id}. Install @ggui-ai/design or run render-check from an environment that has it.`
185
+ );
186
+ }
187
+ }
188
+ if (id === "@ggui-ai/gadgets") {
189
+ try {
190
+ return require_("@ggui-ai/gadgets");
191
+ } catch {
192
+ throw new Error(
193
+ `Required dependency not available in render-check sandbox: ${id}. Install @ggui-ai/gadgets or pass it through moduleResolutions.`
194
+ );
195
+ }
196
+ }
197
+ throw new Error(`Import not allowed in render-check sandbox: ${id}`);
198
+ };
199
+ const exportsRecord = await evaluateComponentSource(sourceCode, sandboxRequire);
200
+ const candidate = exportsRecord.default;
201
+ if (typeof candidate !== "function") {
202
+ throw new Error("Compiled component has no default-exported function");
203
+ }
204
+ return { Component: candidate };
205
+ }
206
+ var NATIVE_CLICK_PROPS = /* @__PURE__ */ new Set(["onClick"]);
207
+ var NATIVE_SUBMIT_PROPS = /* @__PURE__ */ new Set(["onSubmit"]);
208
+ var NATIVE_CHANGE_PROPS = /* @__PURE__ */ new Set(["onChange"]);
209
+ var NATIVE_KEY_PROPS = /* @__PURE__ */ new Set(["onKeyDown", "onKeyUp", "onKeyPress"]);
210
+ var HOST_CLICK_TAGS = /* @__PURE__ */ new Set(["button", "a", "div", "span", "li", "input"]);
211
+ var HOST_CHANGE_TAGS = /* @__PURE__ */ new Set(["select", "input", "textarea"]);
212
+ function findWiring(input) {
213
+ const { sourceCode, hookName, hookArg } = input;
214
+ const sf = ts.createSourceFile("Component.tsx", sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
215
+ const hookBindings = findHookBindings(sf, hookName, hookArg);
216
+ if (hookBindings.length === 0) {
217
+ return {
218
+ kind: "missing",
219
+ reason: `Hook ${hookName}('${hookArg}') is not destructured in the component`,
220
+ observedJsxElements: [],
221
+ observedNativeProps: [],
222
+ observedCustomProps: []
223
+ };
224
+ }
225
+ const callableNames = expandAliases(sf, hookBindings);
226
+ const observedJsxElements = [];
227
+ const observedNativeProps = [];
228
+ const observedCustomProps = [];
229
+ let sawClickOnHost = false;
230
+ let sawSubmitOnForm = false;
231
+ let sawChangeOnNativeInput = false;
232
+ let sawKeyOnAnything = false;
233
+ let sawNonNativeProp = false;
234
+ const customPropElements = [];
235
+ const submitButton = { found: false };
236
+ function visit(node) {
237
+ if (ts.isJsxAttribute(node) && node.initializer) {
238
+ const attrName = node.name.getText(sf);
239
+ const initRefs = findReferencedNames(node.initializer, callableNames);
240
+ if (initRefs.size > 0) {
241
+ const parent = node.parent.parent;
242
+ const tagName = getTagName(parent);
243
+ if (tagName) observedJsxElements.push(tagName);
244
+ const isHostTag = isHostElementTag(tagName ?? "");
245
+ if (NATIVE_CLICK_PROPS.has(attrName)) {
246
+ observedNativeProps.push(attrName);
247
+ if (isHostTag && (HOST_CLICK_TAGS.has((tagName ?? "").toLowerCase()) || tagName === "button")) {
248
+ sawClickOnHost = true;
249
+ } else {
250
+ sawClickOnHost = true;
251
+ }
252
+ } else if (NATIVE_SUBMIT_PROPS.has(attrName)) {
253
+ observedNativeProps.push(attrName);
254
+ if ((tagName ?? "").toLowerCase() === "form") sawSubmitOnForm = true;
255
+ else sawSubmitOnForm = true;
256
+ } else if (NATIVE_CHANGE_PROPS.has(attrName)) {
257
+ observedNativeProps.push(attrName);
258
+ if (HOST_CHANGE_TAGS.has((tagName ?? "").toLowerCase())) {
259
+ sawChangeOnNativeInput = true;
260
+ } else {
261
+ observedCustomProps.push(attrName);
262
+ sawNonNativeProp = true;
263
+ if (tagName) customPropElements.push(tagName);
264
+ }
265
+ } else if (NATIVE_KEY_PROPS.has(attrName)) {
266
+ observedNativeProps.push(attrName);
267
+ sawKeyOnAnything = true;
268
+ } else {
269
+ observedCustomProps.push(attrName);
270
+ sawNonNativeProp = true;
271
+ if (tagName) customPropElements.push(tagName);
272
+ }
273
+ }
274
+ }
275
+ if (ts.isJsxOpeningLikeElement(node)) {
276
+ const tag = getTagName(node);
277
+ if ((tag ?? "").toLowerCase() === "button") {
278
+ const typeAttr = node.attributes.properties.find(
279
+ (p) => ts.isJsxAttribute(p) && p.name.getText(sf) === "type"
280
+ );
281
+ if (typeAttr && ts.isJsxAttribute(typeAttr) && typeAttr.initializer) {
282
+ const v = typeAttr.initializer.getText(sf).toLowerCase();
283
+ if (v.includes("submit")) submitButton.found = true;
284
+ }
285
+ }
286
+ }
287
+ ts.forEachChild(node, visit);
288
+ }
289
+ visit(sf);
290
+ if (sawClickOnHost) {
291
+ return wrapDetection("click", observedJsxElements, observedNativeProps, observedCustomProps);
292
+ }
293
+ if (sawSubmitOnForm || submitButton.found) {
294
+ return wrapDetection("submit", observedJsxElements, observedNativeProps, observedCustomProps);
295
+ }
296
+ if (sawChangeOnNativeInput) {
297
+ return wrapDetection("change", observedJsxElements, observedNativeProps, observedCustomProps);
298
+ }
299
+ if (sawKeyOnAnything) {
300
+ return wrapDetection("keyboard-enter", observedJsxElements, observedNativeProps, observedCustomProps);
301
+ }
302
+ if (sawNonNativeProp) {
303
+ const props = Array.from(new Set(observedCustomProps)).join(", ");
304
+ const tags = Array.from(new Set(customPropElements)).join(", ");
305
+ return {
306
+ kind: "unverified",
307
+ reason: `Source indicates non-click or non-native wiring; static probe did not verify execution deterministically. Callback flows into ${props} on <${tags}>.`,
308
+ observedJsxElements: dedupe(observedJsxElements),
309
+ observedNativeProps: dedupe(observedNativeProps),
310
+ observedCustomProps: dedupe(observedCustomProps)
311
+ };
312
+ }
313
+ return {
314
+ kind: "missing",
315
+ reason: `Hook ${hookName}('${hookArg}') is destructured but never referenced in any JSX attribute`,
316
+ observedJsxElements: [],
317
+ observedNativeProps: [],
318
+ observedCustomProps: []
319
+ };
320
+ }
321
+ function findHookBindings(sf, hookName, hookArg) {
322
+ const bindings = [];
323
+ function visit(node) {
324
+ if (ts.isVariableDeclaration(node) && node.initializer && ts.isCallExpression(node.initializer)) {
325
+ const call = node.initializer;
326
+ const callee = call.expression.getText(sf);
327
+ if (callee !== hookName) {
328
+ ts.forEachChild(node, visit);
329
+ return;
330
+ }
331
+ const firstArg = call.arguments[0];
332
+ if (!firstArg || !ts.isStringLiteral(firstArg)) {
333
+ ts.forEachChild(node, visit);
334
+ return;
335
+ }
336
+ if (firstArg.text !== hookArg) {
337
+ ts.forEachChild(node, visit);
338
+ return;
339
+ }
340
+ if (ts.isIdentifier(node.name)) {
341
+ bindings.push(node.name.text);
342
+ } else if (ts.isObjectBindingPattern(node.name)) {
343
+ for (const elem of node.name.elements) {
344
+ const target = elem.name;
345
+ if (ts.isIdentifier(target)) bindings.push(target.text);
346
+ }
347
+ }
348
+ }
349
+ ts.forEachChild(node, visit);
350
+ }
351
+ visit(sf);
352
+ return bindings;
353
+ }
354
+ function expandAliases(sf, bindings) {
355
+ const all = new Set(bindings);
356
+ function visit(node) {
357
+ if (ts.isVariableDeclaration(node) && node.initializer && ts.isIdentifier(node.name)) {
358
+ const newName = node.name.text;
359
+ const init = node.initializer;
360
+ if (ts.isIdentifier(init) && all.has(init.text)) {
361
+ all.add(newName);
362
+ }
363
+ if (ts.isArrowFunction(init) || ts.isFunctionExpression(init)) {
364
+ if (bodyReferences(init.body, all)) {
365
+ all.add(newName);
366
+ }
367
+ }
368
+ if (ts.isCallExpression(init)) {
369
+ if (bodyReferences(init, all)) {
370
+ all.add(newName);
371
+ }
372
+ }
373
+ }
374
+ ts.forEachChild(node, visit);
375
+ }
376
+ visit(sf);
377
+ return all;
378
+ }
379
+ function bodyReferences(body, names) {
380
+ let found = false;
381
+ function walk(n) {
382
+ if (found) return;
383
+ if (ts.isIdentifier(n) && names.has(n.text)) {
384
+ found = true;
385
+ return;
386
+ }
387
+ ts.forEachChild(n, walk);
388
+ }
389
+ walk(body);
390
+ return found;
391
+ }
392
+ function findReferencedNames(node, names) {
393
+ const found = /* @__PURE__ */ new Set();
394
+ function walk(n) {
395
+ if (ts.isIdentifier(n) && names.has(n.text)) {
396
+ found.add(n.text);
397
+ }
398
+ ts.forEachChild(n, walk);
399
+ }
400
+ walk(node);
401
+ return found;
402
+ }
403
+ function getTagName(el) {
404
+ if (ts.isJsxOpeningLikeElement(el)) {
405
+ return el.tagName.getText();
406
+ }
407
+ return null;
408
+ }
409
+ function isHostElementTag(name) {
410
+ if (!name) return false;
411
+ return name[0] === name[0].toLowerCase();
412
+ }
413
+ function wrapDetection(kind, jsx, native, custom) {
414
+ return {
415
+ kind,
416
+ observedJsxElements: dedupe(jsx),
417
+ observedNativeProps: dedupe(native),
418
+ observedCustomProps: dedupe(custom)
419
+ };
420
+ }
421
+ function dedupe(arr) {
422
+ return Array.from(new Set(arr));
423
+ }
424
+ var installed = false;
425
+ function installProductionActShim() {
426
+ if (installed) return;
427
+ installed = true;
428
+ const require_ = createRequire(import.meta.url);
429
+ let reactCjs;
430
+ try {
431
+ reactCjs = require_("react");
432
+ } catch {
433
+ return;
434
+ }
435
+ if (typeof reactCjs.act === "function") {
436
+ return;
437
+ }
438
+ try {
439
+ const testUtils = require_("react-dom/test-utils");
440
+ const reactDom = require_("react-dom");
441
+ testUtils.act = createFlushSyncAct(reactDom.flushSync);
442
+ } catch {
443
+ }
444
+ }
445
+ function createFlushSyncAct(flushSync) {
446
+ return (callback) => {
447
+ let callbackResult;
448
+ flushSync(() => {
449
+ callbackResult = callback();
450
+ });
451
+ if (isThenable(callbackResult)) {
452
+ const pending = callbackResult;
453
+ return new Promise((resolve, reject) => {
454
+ pending.then(() => resolve(), reject);
455
+ });
456
+ }
457
+ return new Promise((resolve) => {
458
+ queueMicrotask(resolve);
459
+ });
460
+ };
461
+ }
462
+ function isThenable(value) {
463
+ return typeof value === "object" && value !== null && typeof value.then === "function";
464
+ }
465
+
466
+ // src/internal/open-record.ts
467
+ function openRecord(o) {
468
+ return o;
469
+ }
470
+ function hostGlobals() {
471
+ return openRecord(globalThis);
472
+ }
473
+
474
+ // src/harness/check/runtime-render/host-boundary.ts
475
+ function getMethod(o, name) {
476
+ if (typeof o !== "object" && typeof o !== "function") return void 0;
477
+ if (o === null) return void 0;
478
+ const v = openRecord(o)[name];
479
+ if (typeof v !== "function") return void 0;
480
+ return v.bind(o);
481
+ }
482
+ function toMinimalElement(v) {
483
+ if (typeof v === "object" && v !== null && typeof openRecord(v).tagName === "string" && typeof openRecord(v).getAttribute === "function" && typeof openRecord(v).querySelectorAll === "function") {
484
+ return v;
485
+ }
486
+ throw new Error(
487
+ "[runtime-render] host value lacks the element surface (tagName/getAttribute/querySelectorAll)"
488
+ );
489
+ }
490
+ function buildClickUser(userEventModule, doc) {
491
+ const setup = getMethod(userEventModule, "setup");
492
+ const instance = setup ? setup({ document: doc }) : userEventModule;
493
+ const click = getMethod(instance, "click");
494
+ if (!click) {
495
+ throw new Error(
496
+ "[runtime-render] user-event module exposes neither setup().click nor click()"
497
+ );
498
+ }
499
+ return {
500
+ click: async (el) => {
501
+ await click(el);
502
+ }
503
+ };
504
+ }
505
+ function toEventDispatcher(el) {
506
+ const dispatch = getMethod(el, "dispatchEvent");
507
+ if (!dispatch) {
508
+ throw new Error(
509
+ `[runtime-render] element <${el.tagName.toLowerCase()}> has no dispatchEvent`
510
+ );
511
+ }
512
+ return {
513
+ dispatchEvent: (ev) => {
514
+ dispatch(ev);
515
+ }
516
+ };
517
+ }
518
+ function editableSurface(el) {
519
+ return el;
520
+ }
521
+ function parentLink(node) {
522
+ return node;
523
+ }
524
+
525
+ // src/harness/check/runtime-render/render-check.ts
526
+ async function runRenderCheckInProcess(input) {
527
+ const t0 = Date.now();
528
+ const issues = [];
529
+ const teardown = await setupHappyDom();
530
+ let uninstallSpy;
531
+ let uninstallGadgetRegistry;
532
+ try {
533
+ const React = await import('react');
534
+ const ReactJsxRuntime = await import('react/jsx-runtime');
535
+ const Wire = await import('@ggui-ai/wire');
536
+ const { GguiWireProvider } = Wire;
537
+ const moduleResolutions = {
538
+ "react": React,
539
+ "react/jsx-runtime": ReactJsxRuntime,
540
+ "@ggui-ai/wire": Wire
541
+ };
542
+ const designSpecifiers = [
543
+ "@ggui-ai/design",
544
+ "@ggui-ai/design/primitives",
545
+ "@ggui-ai/design/components",
546
+ "@ggui-ai/design/compositions",
547
+ "@ggui-ai/design/interact"
548
+ ];
549
+ for (const id of designSpecifiers) {
550
+ try {
551
+ moduleResolutions[id] = await import(id);
552
+ } catch {
553
+ }
554
+ }
555
+ try {
556
+ moduleResolutions["@ggui-ai/gadgets"] = await import('@ggui-ai/gadgets');
557
+ } catch {
558
+ }
559
+ uninstallGadgetRegistry = installGadgetStubRegistry(input.contract);
560
+ for (const pkg of collectThirdPartyGadgetPackages(input.contract)) {
561
+ if (pkg in moduleResolutions) continue;
562
+ moduleResolutions[pkg] = buildGadgetPackageProbeShim(pkg);
563
+ }
564
+ let Component;
565
+ try {
566
+ const loaded = await loadComponent({
567
+ sourceCode: input.sourceCode,
568
+ moduleResolutions
569
+ });
570
+ Component = loaded.Component;
571
+ } catch (e) {
572
+ issues.push({
573
+ check: "render-no-throw",
574
+ outcome: "failed",
575
+ reason: `Failed to load component: ${e instanceof Error ? e.message : String(e)}`
576
+ });
577
+ return finalize(issues, t0, 0, 0);
578
+ }
579
+ const probe = createProbe();
580
+ const wireConfig = createProbeWireConfig(probe);
581
+ uninstallSpy = probe.installPostMessageSpy();
582
+ installProductionActShim();
583
+ const { render, cleanup } = await import('@testing-library/react');
584
+ const userEventModule = await import('@testing-library/user-event');
585
+ const userEvent = userEventModule.default ?? userEventModule;
586
+ const originalConsoleError = console.error;
587
+ let loopSignature = null;
588
+ let capturedComponentStack = null;
589
+ const loopPatterns = [
590
+ [/Maximum update depth exceeded/i, "max-update-depth"],
591
+ [/Too many re-renders/i, "too-many-renders"],
592
+ [/Rendered more hooks than during the previous render/i, "hook-count-drift"]
593
+ ];
594
+ console.error = (...args) => {
595
+ const text = args.map((a) => typeof a === "string" ? a : "").join(" ");
596
+ if (!capturedComponentStack) {
597
+ for (const arg of args) {
598
+ if (typeof arg === "string" && /^\s*at\s+\S/m.test(arg) && arg.length > 20) {
599
+ capturedComponentStack = arg;
600
+ break;
601
+ }
602
+ }
603
+ if (!capturedComponentStack) {
604
+ const stackMatch = text.match(/(?:^|\n)((?:\s*at\s+\S[^\n]*\n?){2,})/);
605
+ if (stackMatch) {
606
+ capturedComponentStack = stackMatch[1].trim();
607
+ }
608
+ }
609
+ }
610
+ for (const [pattern, tag] of loopPatterns) {
611
+ if (pattern.test(text)) {
612
+ loopSignature = tag;
613
+ throw new Error(`[runtime-render] infinite render loop detected (${tag})`);
614
+ }
615
+ }
616
+ originalConsoleError.apply(console, args);
617
+ };
618
+ const boundaryRef = {
619
+ stack: null,
620
+ error: null
621
+ };
622
+ class ProbeErrorBoundary extends React.Component {
623
+ state = { caught: false };
624
+ static getDerivedStateFromError() {
625
+ return { caught: true };
626
+ }
627
+ componentDidCatch(error, errorInfo) {
628
+ if (errorInfo.componentStack && !boundaryRef.stack) {
629
+ boundaryRef.stack = errorInfo.componentStack;
630
+ }
631
+ if (!boundaryRef.error) {
632
+ boundaryRef.error = error;
633
+ }
634
+ }
635
+ render() {
636
+ if (this.state.caught) return null;
637
+ return this.props.children;
638
+ }
639
+ }
640
+ let renderResult;
641
+ const RENDER_TIMEOUT_MS = 5e3;
642
+ try {
643
+ const renderPromise = Promise.resolve().then(
644
+ () => render(
645
+ React.createElement(GguiWireProvider, {
646
+ config: wireConfig,
647
+ children: React.createElement(ProbeErrorBoundary, {
648
+ children: React.createElement(Component, input.mockupProps)
649
+ })
650
+ })
651
+ )
652
+ );
653
+ const timeoutPromise = new Promise((_, reject) => {
654
+ setTimeout(() => reject(new Error(`[runtime-render] render exceeded ${RENDER_TIMEOUT_MS}ms \u2014 classified as runtime-hang`)), RENDER_TIMEOUT_MS).unref?.();
655
+ });
656
+ renderResult = await Promise.race([renderPromise, timeoutPromise]);
657
+ } catch (e) {
658
+ const msg = e instanceof Error ? e.message : String(e);
659
+ const errStack = e instanceof Error ? e.stack ?? "" : "";
660
+ const baseReason = loopSignature ? `Infinite render loop (${loopSignature}) \u2014 likely setState/dispatch inside useEffect with missing or unstable dependency` : /runtime-hang/.test(msg) ? `Render wall-clock exceeded ${RENDER_TIMEOUT_MS}ms \u2014 likely async effect loop or unresolved promise` : `Render threw: ${msg}`;
661
+ const userFrames = errStack.split("\n").map((l) => l.trim()).filter((l) => l.startsWith("at ")).filter((l) => !/(node_modules|happy-dom|@testing-library|react-dom|node:internal|node:async)/.test(l)).slice(0, 5);
662
+ const stackSource = boundaryRef.stack ?? capturedComponentStack ?? "";
663
+ const componentFrames = stackSource.split("\n").map((l) => l.trim()).filter((l) => l.startsWith("at ")).slice(0, 4);
664
+ const stackBlock = userFrames.length ? `
665
+ Stack (user frames): ${userFrames.join(" | ")}` : "";
666
+ const componentBlock = componentFrames.length ? `
667
+ Component stack: ${componentFrames.join(" > ")}` : "";
668
+ const loopReason = `${baseReason}${stackBlock}${componentBlock}`;
669
+ issues.push({
670
+ check: "render-no-throw",
671
+ outcome: "failed",
672
+ reason: loopReason
673
+ });
674
+ console.error = originalConsoleError;
675
+ return finalize(issues, t0, 0, 0);
676
+ }
677
+ console.error = originalConsoleError;
678
+ if (boundaryRef.stack || boundaryRef.error) {
679
+ const componentFrames = (boundaryRef.stack ?? "").split("\n").map((l) => l.trim()).filter((l) => l.startsWith("at ")).slice(0, 4);
680
+ const compBlock = componentFrames.length ? `
681
+ Component stack: ${componentFrames.join(" > ")}` : "";
682
+ const errMsg = boundaryRef.error ? boundaryRef.error.message : "(no error message)";
683
+ issues.push({
684
+ check: "render-no-throw",
685
+ outcome: "failed",
686
+ reason: `Render threw: ${errMsg}${compBlock}`
687
+ });
688
+ cleanup();
689
+ return finalize(issues, t0, 0, 0);
690
+ }
691
+ const container = toMinimalElement(renderResult.container);
692
+ const user = buildClickUser(userEvent, hostGlobals().document);
693
+ let actionsChecked = 0;
694
+ let streamsChecked = 0;
695
+ try {
696
+ if (input.contract?.actionSpec) {
697
+ const actionSpec = input.contract.actionSpec;
698
+ for (const [name, entry] of Object.entries(actionSpec)) {
699
+ actionsChecked++;
700
+ const wiring = findWiring({
701
+ sourceCode: input.sourceCode,
702
+ hookName: "useAction",
703
+ hookArg: name
704
+ });
705
+ const issue = await checkActionWiring({
706
+ container,
707
+ actionName: name,
708
+ actionLabel: entry.label,
709
+ wiring,
710
+ probe,
711
+ user
712
+ });
713
+ if (issue) issues.push(issue);
714
+ }
715
+ }
716
+ if (input.contract?.actionSpec) {
717
+ for (const [name, entry] of Object.entries(input.contract.actionSpec)) {
718
+ const idField = idLikeRequiredStringField(
719
+ entry.schema
720
+ );
721
+ if (idField === null) continue;
722
+ const issue = await checkSelectionIdentity({
723
+ container,
724
+ probe,
725
+ user,
726
+ actionName: name,
727
+ idField
728
+ });
729
+ if (issue) issues.push(issue);
730
+ }
731
+ }
732
+ if (input.contract?.propsSpec) {
733
+ for (const issue of checkPropCoverage({
734
+ container,
735
+ propsSpec: input.contract.propsSpec,
736
+ mockupProps: input.mockupProps
737
+ })) {
738
+ issues.push(issue);
739
+ }
740
+ }
741
+ if (input.contract?.propsSpec) {
742
+ const propsSpec5c = input.contract.propsSpec;
743
+ const normalize = (s) => (s ?? "").replace(/\s+/g, " ").trim();
744
+ const baseText = normalize(container.textContent);
745
+ for (const [propName, entry] of Object.entries(propsSpec5c.properties)) {
746
+ if (!entry.required) continue;
747
+ const orig = input.mockupProps[propName];
748
+ if (typeof orig !== "string" && typeof orig !== "number") continue;
749
+ const marker = pickScalarMarker(orig);
750
+ if (marker === null || !baseText.includes(marker)) continue;
751
+ const varied = typeof orig === "string" ? "Zzyzx Qw" : orig + 137731;
752
+ const variedProbe = createProbe();
753
+ const variedConfig = createProbeWireConfig(variedProbe);
754
+ let variedResult;
755
+ try {
756
+ variedResult = render(
757
+ React.createElement(GguiWireProvider, {
758
+ config: variedConfig,
759
+ children: React.createElement(ProbeErrorBoundary, {
760
+ children: React.createElement(Component, {
761
+ ...input.mockupProps,
762
+ [propName]: varied
763
+ })
764
+ })
765
+ })
766
+ );
767
+ } catch {
768
+ continue;
769
+ }
770
+ await flushPromises();
771
+ const variedText = normalize(variedResult.container.textContent);
772
+ variedResult.unmount();
773
+ if (variedText === baseText) {
774
+ issues.push({
775
+ check: "prop-sensitivity",
776
+ outcome: "failed",
777
+ subject: propName,
778
+ reason: `props.${propName} is displayed ('${marker}' appears in the DOM) but changing its value to '${String(varied)}' left the rendered output BYTE-IDENTICAL \u2014 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.`
779
+ });
780
+ }
781
+ }
782
+ }
783
+ if (input.contract?.streamSpec) {
784
+ const streamSpec = input.contract.streamSpec;
785
+ for (const [name] of Object.entries(streamSpec)) {
786
+ streamsChecked++;
787
+ const issue = await checkStreamRerender({
788
+ container,
789
+ eventName: name,
790
+ probe,
791
+ React
792
+ });
793
+ if (issue) issues.push(issue);
794
+ }
795
+ }
796
+ const declaredProps = input.contract?.propsSpec?.properties;
797
+ if (declaredProps) {
798
+ const optionalFilled = Object.entries(declaredProps).filter(
799
+ ([name, entry]) => entry.required !== true && input.mockupProps[name] !== void 0
800
+ ).map(([name]) => name);
801
+ if (optionalFilled.length > 0) {
802
+ const requiredOnlyProps = { ...input.mockupProps };
803
+ for (const name of optionalFilled) delete requiredOnlyProps[name];
804
+ const omitBoundaryRef = {};
805
+ class OmitErrorBoundary extends React.Component {
806
+ state = { caught: false };
807
+ static getDerivedStateFromError() {
808
+ return { caught: true };
809
+ }
810
+ componentDidCatch(error) {
811
+ omitBoundaryRef.error ??= error;
812
+ }
813
+ render() {
814
+ if (this.state.caught) return null;
815
+ return this.props.children;
816
+ }
817
+ }
818
+ try {
819
+ render(
820
+ React.createElement(GguiWireProvider, {
821
+ config: wireConfig,
822
+ children: React.createElement(OmitErrorBoundary, {
823
+ children: React.createElement(Component, requiredOnlyProps)
824
+ })
825
+ })
826
+ );
827
+ } catch (e) {
828
+ omitBoundaryRef.error ??= e instanceof Error ? e : new Error(String(e));
829
+ }
830
+ if (omitBoundaryRef.error) {
831
+ issues.push({
832
+ check: "optional-props-omitted",
833
+ outcome: "failed",
834
+ subject: optionalFilled.join(", "),
835
+ reason: `Render crashed when the optional prop(s) ${optionalFilled.map((n) => `'${n}'`).join(", ")} were omitted: ${omitBoundaryRef.error.message}. The contract marks them optional (no \`required: true\`), so a legitimate render may leave them out.`
836
+ });
837
+ }
838
+ }
839
+ }
840
+ } finally {
841
+ cleanup();
842
+ }
843
+ return finalize(issues, t0, actionsChecked, streamsChecked);
844
+ } finally {
845
+ try {
846
+ uninstallSpy?.();
847
+ } catch {
848
+ }
849
+ try {
850
+ uninstallGadgetRegistry?.();
851
+ } catch {
852
+ }
853
+ teardown();
854
+ }
855
+ }
856
+ function installGadgetStubRegistry(contract) {
857
+ const declared = contract?.clientCapabilities?.gadgets;
858
+ if (!declared || Object.keys(declared).length === 0) {
859
+ return () => {
860
+ };
861
+ }
862
+ const noop = () => {
863
+ };
864
+ const valueStub = new Proxy(
865
+ {},
866
+ {
867
+ get: (_t, key) => key === "then" || typeof key === "symbol" ? void 0 : noop
868
+ }
869
+ );
870
+ const result = { status: "idle", value: valueStub, start: noop };
871
+ const resultStub = new Proxy(result, {
872
+ get: (t, key) => {
873
+ if (key === "status" || key === "value" || key === "start") {
874
+ return t[key];
875
+ }
876
+ return key === "then" || typeof key === "symbol" ? void 0 : noop;
877
+ }
878
+ });
879
+ const gadgets = {};
880
+ for (const use of contract ? listContractGadgets(contract) : []) {
881
+ const pkgSlot = gadgets[use.package] ??= {};
882
+ if (HOOK_NAME_RE.test(use.name)) {
883
+ pkgSlot[use.name] = () => resultStub;
884
+ } else {
885
+ pkgSlot[use.name] = () => null;
886
+ }
887
+ }
888
+ const root = globalThis;
889
+ const prior = root.__ggui__;
890
+ root.__ggui__ = { gadgets, publicEnv: {} };
891
+ return () => {
892
+ if (prior === void 0) {
893
+ delete root.__ggui__;
894
+ } else {
895
+ root.__ggui__ = prior;
896
+ }
897
+ };
898
+ }
899
+ function collectThirdPartyGadgetPackages(contract) {
900
+ if (!contract) return [];
901
+ const packages = /* @__PURE__ */ new Set();
902
+ for (const use of listContractGadgets(contract)) {
903
+ if (use.package !== "@ggui-ai/gadgets") packages.add(use.package);
904
+ }
905
+ return [...packages];
906
+ }
907
+ function buildGadgetPackageProbeShim(packageName) {
908
+ const resolveExport = (name) => {
909
+ const root = globalThis;
910
+ return root.__ggui__?.gadgets?.[packageName]?.[name];
911
+ };
912
+ const makeThunk = (name) => (...args) => {
913
+ const impl = resolveExport(name);
914
+ if (typeof impl !== "function") {
915
+ throw new Error(
916
+ `[gadget] export '${name}' from '${packageName}' is not loaded in the render-check probe \u2014 the component imports it but the contract's clientCapabilities.gadgets never declared it, so installGadgetStubRegistry planted no stub.`
917
+ );
918
+ }
919
+ return impl(...args);
920
+ };
921
+ return new Proxy(
922
+ {},
923
+ {
924
+ get: (_t, key) => {
925
+ if (typeof key === "symbol" || key === "default" || key === "then") {
926
+ return void 0;
927
+ }
928
+ return makeThunk(key);
929
+ },
930
+ // `key in ns` checks (esbuild interop may probe) report true so
931
+ // the named import binds to the thunk rather than `undefined`.
932
+ has: (_t, key) => typeof key !== "symbol" && key !== "then" && key !== "default"
933
+ }
934
+ );
935
+ }
936
+ async function setupHappyDom() {
937
+ const g = hostGlobals();
938
+ const pendingTimeouts = /* @__PURE__ */ new Set();
939
+ const pendingIntervals = /* @__PURE__ */ new Set();
940
+ const origSetTimeout = globalThis.setTimeout;
941
+ const origSetInterval = globalThis.setInterval;
942
+ const origClearTimeout = globalThis.clearTimeout;
943
+ const origClearInterval = globalThis.clearInterval;
944
+ globalThis.setTimeout = ((...args) => {
945
+ const id = origSetTimeout(...args);
946
+ pendingTimeouts.add(id);
947
+ return id;
948
+ });
949
+ globalThis.setInterval = ((...args) => {
950
+ const id = origSetInterval(...args);
951
+ pendingIntervals.add(id);
952
+ return id;
953
+ });
954
+ globalThis.clearTimeout = (id) => {
955
+ if (id !== void 0) pendingTimeouts.delete(id);
956
+ return origClearTimeout(id);
957
+ };
958
+ globalThis.clearInterval = (id) => {
959
+ if (id !== void 0) pendingIntervals.delete(id);
960
+ return origClearInterval(id);
961
+ };
962
+ const isTeardownArtifact = (err) => {
963
+ const msg = err instanceof Error ? err.message : String(err);
964
+ return /window is not defined/i.test(msg) || /document is not defined/i.test(msg) || /Cannot read propert(?:y|ies) of undefined \(reading 'event'\)/i.test(msg) || /requestAnimationFrame is not defined/i.test(msg);
965
+ };
966
+ const uncaughtHandler = (err) => {
967
+ if (isTeardownArtifact(err)) {
968
+ return;
969
+ }
970
+ process.nextTick(() => {
971
+ throw err;
972
+ });
973
+ };
974
+ const unhandledHandler = (reason) => {
975
+ if (isTeardownArtifact(reason)) return;
976
+ const err = reason instanceof Error ? reason : new Error(String(reason));
977
+ process.nextTick(() => {
978
+ throw err;
979
+ });
980
+ };
981
+ process.on("uncaughtException", uncaughtHandler);
982
+ process.on("unhandledRejection", unhandledHandler);
983
+ const cleanupAsyncInfra = () => {
984
+ for (const id of pendingTimeouts) origClearTimeout(id);
985
+ for (const id of pendingIntervals) origClearInterval(id);
986
+ pendingTimeouts.clear();
987
+ pendingIntervals.clear();
988
+ globalThis.setTimeout = origSetTimeout;
989
+ globalThis.setInterval = origSetInterval;
990
+ globalThis.clearTimeout = origClearTimeout;
991
+ globalThis.clearInterval = origClearInterval;
992
+ origSetTimeout(() => {
993
+ process.off("uncaughtException", uncaughtHandler);
994
+ process.off("unhandledRejection", unhandledHandler);
995
+ }, 100).unref?.();
996
+ };
997
+ if ("window" in g && "document" in g) {
998
+ const priorActFlag = g.IS_REACT_ACT_ENVIRONMENT;
999
+ g.IS_REACT_ACT_ENVIRONMENT = true;
1000
+ return () => {
1001
+ cleanupAsyncInfra();
1002
+ if (priorActFlag === void 0) delete g.IS_REACT_ACT_ENVIRONMENT;
1003
+ else g.IS_REACT_ACT_ENVIRONMENT = priorActFlag;
1004
+ };
1005
+ }
1006
+ const { Window } = await import('happy-dom');
1007
+ const window = new Window({ url: "https://render-check.local" });
1008
+ const windowAny = openRecord(window);
1009
+ const keys = [
1010
+ "window",
1011
+ "document",
1012
+ "navigator",
1013
+ "HTMLElement",
1014
+ "Node",
1015
+ "Element",
1016
+ "Event",
1017
+ "MouseEvent",
1018
+ "KeyboardEvent",
1019
+ "getComputedStyle",
1020
+ "requestAnimationFrame",
1021
+ "cancelAnimationFrame"
1022
+ ];
1023
+ const prior = {};
1024
+ for (const k of keys) {
1025
+ prior[k] = { value: g[k], existed: k in g };
1026
+ try {
1027
+ Object.defineProperty(g, k, {
1028
+ value: windowAny[k],
1029
+ writable: true,
1030
+ configurable: true
1031
+ });
1032
+ } catch {
1033
+ }
1034
+ }
1035
+ g.IS_REACT_ACT_ENVIRONMENT = true;
1036
+ return () => {
1037
+ cleanupAsyncInfra();
1038
+ for (const k of keys) {
1039
+ try {
1040
+ if (prior[k].existed) {
1041
+ Object.defineProperty(g, k, {
1042
+ value: prior[k].value,
1043
+ writable: true,
1044
+ configurable: true
1045
+ });
1046
+ } else {
1047
+ delete g[k];
1048
+ }
1049
+ } catch {
1050
+ }
1051
+ }
1052
+ delete g.IS_REACT_ACT_ENVIRONMENT;
1053
+ };
1054
+ }
1055
+ function finalize(issues, t0, actionsChecked, streamsChecked) {
1056
+ const ok = !issues.some((i) => i.outcome === "failed");
1057
+ return {
1058
+ ok,
1059
+ issues,
1060
+ stats: {
1061
+ actionsChecked,
1062
+ streamsChecked,
1063
+ renderMs: Date.now() - t0
1064
+ }
1065
+ };
1066
+ }
1067
+ async function checkActionWiring(input) {
1068
+ const { container, actionName, actionLabel, wiring, probe, user } = input;
1069
+ const baseDiagnostics = {
1070
+ observedJsxElements: wiring.observedJsxElements,
1071
+ observedNativeProps: wiring.observedNativeProps,
1072
+ observedCustomProps: wiring.observedCustomProps
1073
+ };
1074
+ if (wiring.kind === "missing") {
1075
+ return {
1076
+ check: "action-wiring",
1077
+ outcome: "failed",
1078
+ subject: actionName,
1079
+ reason: wiring.reason ?? `Action '${actionName}' is declared in contract but useAction('${actionName}') is not wired to any UI element`,
1080
+ diagnostics: baseDiagnostics
1081
+ };
1082
+ }
1083
+ if (wiring.kind === "unverified") {
1084
+ return {
1085
+ check: "action-wiring",
1086
+ outcome: "unverified",
1087
+ subject: actionName,
1088
+ reason: wiring.reason ?? "Source indicates non-click or non-native wiring; static probe did not verify execution deterministically.",
1089
+ diagnostics: baseDiagnostics
1090
+ };
1091
+ }
1092
+ const fired = await simulateAndCheck({
1093
+ container,
1094
+ user,
1095
+ probe,
1096
+ wiringKind: wiring.kind,
1097
+ actionName,
1098
+ actionLabel,
1099
+ eventKind: "action.fired"
1100
+ });
1101
+ if (fired.fired) return null;
1102
+ return {
1103
+ check: "action-wiring",
1104
+ outcome: "unverified",
1105
+ subject: actionName,
1106
+ 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.`,
1107
+ elementHint: fired.attemptedHint,
1108
+ diagnostics: {
1109
+ ...baseDiagnostics,
1110
+ actionsFiredFromClicks: fired.otherActionsFired
1111
+ }
1112
+ };
1113
+ }
1114
+ function idLikeRequiredStringField(schema) {
1115
+ if (schema === void 0) return null;
1116
+ const properties = schema["properties"];
1117
+ const required = schema["required"];
1118
+ if (properties === null || typeof properties !== "object") return null;
1119
+ if (!Array.isArray(required)) return null;
1120
+ for (const [propName, propSchema] of Object.entries(
1121
+ properties
1122
+ )) {
1123
+ if (!/id$/i.test(propName)) continue;
1124
+ if (!required.includes(propName)) continue;
1125
+ const t = propSchema !== null && typeof propSchema === "object" ? propSchema["type"] : void 0;
1126
+ if (t === "string") return propName;
1127
+ }
1128
+ return null;
1129
+ }
1130
+ async function checkSelectionIdentity(input) {
1131
+ const { container, probe, user, actionName, idField } = input;
1132
+ const clickables = Array.from(
1133
+ container.querySelectorAll('button, [role="button"]')
1134
+ );
1135
+ const byLabel = /* @__PURE__ */ new Map();
1136
+ for (const el of clickables) {
1137
+ const label = (el.textContent ?? "").replace(/\s+/g, " ").trim();
1138
+ if (label.length === 0) continue;
1139
+ const group = byLabel.get(label);
1140
+ if (group) group.push(el);
1141
+ else byLabel.set(label, [el]);
1142
+ }
1143
+ for (const [label, group] of byLabel) {
1144
+ if (group.length < 2) continue;
1145
+ const payloadFor = async (el) => {
1146
+ const before = probe.getFireLog().length;
1147
+ try {
1148
+ await user.click(el);
1149
+ } catch {
1150
+ return void 0;
1151
+ }
1152
+ await flushPromises();
1153
+ const events = probe.getFireLog().slice(before).filter(
1154
+ (e) => e.kind === "action.fired" && e.name === actionName
1155
+ );
1156
+ return events.length > 0 ? events[events.length - 1].payload : void 0;
1157
+ };
1158
+ const first = await payloadFor(group[0]);
1159
+ const second = await payloadFor(group[1]);
1160
+ if (first === void 0 || second === void 0) continue;
1161
+ const idOf = (payload) => {
1162
+ if (payload === null || typeof payload !== "object") return void 0;
1163
+ const v = payload[idField];
1164
+ return typeof v === "string" ? v : void 0;
1165
+ };
1166
+ const firstId = idOf(first);
1167
+ const secondId = idOf(second);
1168
+ if (firstId === void 0 || secondId === void 0) continue;
1169
+ if (firstId === secondId) {
1170
+ return {
1171
+ check: "selection-identity",
1172
+ outcome: "failed",
1173
+ subject: actionName,
1174
+ reason: `Two clickable elements share the visible label '${label}', and clicking EACH fired '${actionName}' with the same ${idField} ('${firstId}') \u2014 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}".`,
1175
+ elementHint: describeElement(group[0])
1176
+ };
1177
+ }
1178
+ return null;
1179
+ }
1180
+ return null;
1181
+ }
1182
+ async function simulateAndCheck(input) {
1183
+ const { container, user, probe, wiringKind, actionName, actionLabel, eventKind } = input;
1184
+ const candidates = findCandidateElements(container, wiringKind, actionName, actionLabel);
1185
+ if (candidates.length === 0) {
1186
+ return { fired: false, attemptedHint: `No ${wiringKind}-eligible element found in DOM` };
1187
+ }
1188
+ for (const el of candidates) {
1189
+ const before = probe.getFireLog().length;
1190
+ try {
1191
+ await dispatchTrigger(el, wiringKind, user);
1192
+ } catch {
1193
+ continue;
1194
+ }
1195
+ await flushPromises();
1196
+ const newEvents = probe.getFireLog().slice(before);
1197
+ const matched = newEvents.some((e) => e.kind === eventKind && e.name === actionName);
1198
+ if (matched) return { fired: true };
1199
+ }
1200
+ const allFired = probe.getFireLog().filter((e) => e.kind === "action.fired").map((e) => e.name);
1201
+ const otherActionsFired = Array.from(new Set(allFired)).filter((n) => n !== actionName);
1202
+ return {
1203
+ fired: false,
1204
+ attemptedHint: candidates.length ? describeElement(candidates[0]) : void 0,
1205
+ otherActionsFired
1206
+ };
1207
+ }
1208
+ async function dispatchTrigger(el, kind, user) {
1209
+ switch (kind) {
1210
+ case "click":
1211
+ await user.click(el);
1212
+ return;
1213
+ case "submit": {
1214
+ const form = closestForm(el);
1215
+ if (!form) return;
1216
+ const ev = new globalThis.Event("submit", { bubbles: true, cancelable: true });
1217
+ toEventDispatcher(form).dispatchEvent(ev);
1218
+ return;
1219
+ }
1220
+ case "change": {
1221
+ setSyntheticValue(el);
1222
+ const ev = new globalThis.Event("change", { bubbles: true });
1223
+ toEventDispatcher(el).dispatchEvent(ev);
1224
+ return;
1225
+ }
1226
+ case "keyboard-enter": {
1227
+ try {
1228
+ await user.click(el);
1229
+ } catch {
1230
+ }
1231
+ const KeyboardEventCtor = globalThis.KeyboardEvent;
1232
+ if (KeyboardEventCtor) {
1233
+ const ev = new KeyboardEventCtor("keydown", { key: "Enter", bubbles: true });
1234
+ toEventDispatcher(el).dispatchEvent(ev);
1235
+ }
1236
+ return;
1237
+ }
1238
+ }
1239
+ }
1240
+ function setSyntheticValue(el) {
1241
+ try {
1242
+ const tag = el.tagName.toLowerCase();
1243
+ const node = editableSurface(el);
1244
+ if (tag === "select") {
1245
+ const opts = node.options;
1246
+ if (opts && opts.length > 0) {
1247
+ const current = node.value ?? "";
1248
+ let pick = opts[0].value;
1249
+ for (let i = 0; i < opts.length; i++) {
1250
+ if (opts[i].value !== current) {
1251
+ pick = opts[i].value;
1252
+ break;
1253
+ }
1254
+ }
1255
+ node.value = pick;
1256
+ }
1257
+ return;
1258
+ }
1259
+ if (tag === "input") {
1260
+ const type = (node.type ?? "text").toLowerCase();
1261
+ if (type === "checkbox" || type === "radio") {
1262
+ node.checked = !node.checked;
1263
+ return;
1264
+ }
1265
+ if (type === "number" || type === "range") {
1266
+ const cur = Number(node.value ?? "");
1267
+ node.value = String(Number.isFinite(cur) ? cur + 1 : 1);
1268
+ return;
1269
+ }
1270
+ node.value = node.value && node.value.length > 0 ? `${node.value}x` : "test";
1271
+ return;
1272
+ }
1273
+ if (tag === "textarea") {
1274
+ node.value = node.value && node.value.length > 0 ? `${node.value}x` : "test";
1275
+ return;
1276
+ }
1277
+ } catch {
1278
+ }
1279
+ }
1280
+ function closestForm(el) {
1281
+ let cur = parentLink(el);
1282
+ while (cur) {
1283
+ if (cur.tagName && cur.tagName.toLowerCase() === "form") {
1284
+ return toMinimalElement(cur);
1285
+ }
1286
+ cur = typeof cur.parentNode === "object" && cur.parentNode !== null ? parentLink(cur.parentNode) : null;
1287
+ }
1288
+ return null;
1289
+ }
1290
+ function findCandidateElements(container, kind, name, label) {
1291
+ switch (kind) {
1292
+ case "click":
1293
+ return findActionElements(container, name, label);
1294
+ case "submit": {
1295
+ const all = container.querySelectorAll(
1296
+ 'form, button[type="submit"], input[type="submit"]'
1297
+ );
1298
+ return Array.from(all);
1299
+ }
1300
+ case "change": {
1301
+ const all = container.querySelectorAll("select, input, textarea");
1302
+ return Array.from(all);
1303
+ }
1304
+ case "keyboard-enter":
1305
+ return findActionElements(container, name, label);
1306
+ }
1307
+ }
1308
+ function checkPropCoverage(input) {
1309
+ const { container, propsSpec, mockupProps } = input;
1310
+ const issues = [];
1311
+ const text = container.textContent ?? "";
1312
+ for (const [propName, entry] of Object.entries(propsSpec.properties)) {
1313
+ if (!entry.required) continue;
1314
+ const value = mockupProps[propName];
1315
+ if (value === void 0 || value === null) continue;
1316
+ const marker = pickScalarMarker(value);
1317
+ if (marker === null) continue;
1318
+ if (!text.includes(marker)) {
1319
+ issues.push({
1320
+ check: "prop-coverage",
1321
+ outcome: "unverified",
1322
+ subject: propName,
1323
+ reason: `Required prop '${propName}' value (${JSON.stringify(marker).slice(0, 60)}) not visible in rendered DOM`
1324
+ });
1325
+ }
1326
+ }
1327
+ return issues;
1328
+ }
1329
+ async function checkStreamRerender(input) {
1330
+ const { container, eventName, probe } = input;
1331
+ if (!probe.getRegistered().streams.includes(eventName)) {
1332
+ return {
1333
+ check: "stream-rerender",
1334
+ outcome: "unverified",
1335
+ subject: eventName,
1336
+ reason: `useStream('${eventName}') was never called \u2014 the stream event is declared in the contract but the component does not subscribe to it`
1337
+ };
1338
+ }
1339
+ const before = container.textContent ?? "";
1340
+ const marker = `__probe_marker_${Date.now()}__`;
1341
+ const payload = { id: marker, text: marker, value: marker, message: marker, name: marker };
1342
+ await new Promise((resolve) => {
1343
+ setTimeout(() => {
1344
+ probe.emitStream(eventName, payload);
1345
+ resolve();
1346
+ }, 0);
1347
+ });
1348
+ await flushPromises();
1349
+ const after = container.textContent ?? "";
1350
+ if (after === before) {
1351
+ return {
1352
+ check: "stream-rerender",
1353
+ outcome: "unverified",
1354
+ subject: eventName,
1355
+ reason: `useStream('${eventName}') is subscribed but emitting a payload did not change the DOM`
1356
+ };
1357
+ }
1358
+ if (!after.includes(marker)) {
1359
+ return null;
1360
+ }
1361
+ return null;
1362
+ }
1363
+ function findActionElements(container, name, label) {
1364
+ const candidates = [];
1365
+ const seen = /* @__PURE__ */ new Set();
1366
+ const norm = (s) => s.toLowerCase().replace(/[\s_-]+/g, "");
1367
+ const nameKey = norm(name);
1368
+ const labelKey = norm(label);
1369
+ const allClickable = container.querySelectorAll(
1370
+ 'button, [role="button"], input[type="submit"], input[type="button"], a[href]'
1371
+ );
1372
+ for (const el of allClickable) {
1373
+ const aria = (el.getAttribute("aria-label") ?? "").toLowerCase();
1374
+ const txt = (el.textContent ?? "").toLowerCase();
1375
+ const dataAction = (el.getAttribute("data-action") ?? "").toLowerCase();
1376
+ if (aria.includes(label.toLowerCase()) || txt.includes(label.toLowerCase()) || norm(aria).includes(nameKey) || norm(txt).includes(nameKey) || dataAction === name.toLowerCase() || norm(txt).includes(labelKey)) {
1377
+ if (!seen.has(el)) {
1378
+ seen.add(el);
1379
+ candidates.push(el);
1380
+ }
1381
+ }
1382
+ }
1383
+ for (const el of allClickable) {
1384
+ if (!seen.has(el)) {
1385
+ seen.add(el);
1386
+ candidates.push(el);
1387
+ }
1388
+ }
1389
+ return candidates;
1390
+ }
1391
+ function describeElement(el) {
1392
+ const tag = el.tagName.toLowerCase();
1393
+ const aria = el.getAttribute("aria-label");
1394
+ const txt = (el.textContent ?? "").trim().slice(0, 30);
1395
+ if (aria) return `<${tag} aria-label="${aria}">`;
1396
+ if (txt) return `<${tag}>${txt}</${tag}>`;
1397
+ return `<${tag}>`;
1398
+ }
1399
+ function pickScalarMarker(value) {
1400
+ if (typeof value === "string" && value.length > 0) return value;
1401
+ if (typeof value === "number") return String(value);
1402
+ if (Array.isArray(value)) {
1403
+ for (const item of value) {
1404
+ const m = pickScalarMarker(item);
1405
+ if (m) return m;
1406
+ }
1407
+ return null;
1408
+ }
1409
+ if (value && typeof value === "object") {
1410
+ for (const v of Object.values(value)) {
1411
+ const m = pickScalarMarker(v);
1412
+ if (m) return m;
1413
+ }
1414
+ }
1415
+ return null;
1416
+ }
1417
+ function flushPromises() {
1418
+ return new Promise((resolve) => setTimeout(resolve, 0));
1419
+ }
1420
+
1421
+ // src/harness/check/runtime-render/render-check-worker.ts
1422
+ async function readAllStdin() {
1423
+ const chunks = [];
1424
+ for await (const chunk of process.stdin) {
1425
+ chunks.push(chunk);
1426
+ }
1427
+ return Buffer.concat(chunks).toString("utf8");
1428
+ }
1429
+ function emit(value) {
1430
+ process.stdout.write(`${JSON.stringify(value)}
1431
+ `);
1432
+ }
1433
+ async function main() {
1434
+ const raw = await readAllStdin();
1435
+ let input;
1436
+ try {
1437
+ input = JSON.parse(raw);
1438
+ } catch (err) {
1439
+ process.stderr.write(
1440
+ `render-check worker: malformed input JSON \u2014 ${err instanceof Error ? err.message : String(err)}
1441
+ `
1442
+ );
1443
+ process.exitCode = 1;
1444
+ return;
1445
+ }
1446
+ if (typeof input.sourceCode !== "string" || input.sourceCode.length === 0) {
1447
+ process.stderr.write(
1448
+ "render-check worker: input.sourceCode must be a non-empty string\n"
1449
+ );
1450
+ process.exitCode = 1;
1451
+ return;
1452
+ }
1453
+ const result = await runRenderCheckInProcess(input);
1454
+ emit(result);
1455
+ }
1456
+ void main();
1457
+ //# sourceMappingURL=render-check-worker.js.map
1458
+ //# sourceMappingURL=render-check-worker.js.map