@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
@@ -3,11 +3,13 @@ import 'zod';
3
3
  import { classifyUi, MODEL_REGISTRY, DANGEROUS_PATTERNS as DANGEROUS_PATTERNS$1, listContractGadgets, HOOK_NAME_RE, STDLIB_GADGETS_PACKAGE, resolveAppGadgets } from '@ggui-ai/protocol';
4
4
  import * as esbuild from 'esbuild';
5
5
  import { build } from 'esbuild';
6
- import fs, { mkdirSync, writeFileSync, unlinkSync, readFileSync, existsSync, rm, createReadStream, createWriteStream } from 'fs';
6
+ import fs, { mkdirSync, writeFileSync, unlinkSync, existsSync, readFileSync, rm, createReadStream, createWriteStream } from 'fs';
7
7
  import { contentHash } from '@ggui-ai/protocol/content-hash';
8
+ import { createRequire } from 'module';
9
+ import { fileURLToPath, pathToFileURL } from 'url';
10
+ import { runSandboxed } from '@ggui-ai/sandbox';
8
11
  import ts2 from 'typescript';
9
12
  import path, { dirname, resolve, join, basename } from 'path';
10
- import { createRequire } from 'module';
11
13
  import { Linter } from 'eslint';
12
14
  import { createHash } from 'crypto';
13
15
  import 'fs/promises';
@@ -15,8 +17,6 @@ import { tmpdir } from 'os';
15
17
  import { Readable } from 'stream';
16
18
  import { pipeline } from 'stream/promises';
17
19
  import { createBrotliDecompress, createUnzip } from 'zlib';
18
- import { fileURLToPath } from 'url';
19
- import '@ggui-ai/sandbox';
20
20
  import git from 'isomorphic-git';
21
21
  import { Volume, createFsFromVolume } from 'memfs';
22
22
  import { createPatch, applyPatch } from 'diff';
@@ -2555,6 +2555,140 @@ var init_validation = __esm({
2555
2555
  init_ui_compiler();
2556
2556
  }
2557
2557
  });
2558
+
2559
+ // src/harness/check/runtime-render/render-check-host.ts
2560
+ var render_check_host_exports = {};
2561
+ __export(render_check_host_exports, {
2562
+ mapSandboxResultToCheckResult: () => mapSandboxResultToCheckResult,
2563
+ runRenderCheckViaWorker: () => runRenderCheckViaWorker
2564
+ });
2565
+ function resolveWorkerSpawn() {
2566
+ const jsCandidates = [
2567
+ // Sibling — when import.meta.url is already .../harness/check/runtime-render/.
2568
+ new URL(`./${WORKER_BASENAME}.js`, import.meta.url),
2569
+ // From dist/index.js.
2570
+ new URL(`./harness/check/runtime-render/${WORKER_BASENAME}.js`, import.meta.url),
2571
+ // From dist/harness/index.js.
2572
+ new URL(`./check/runtime-render/${WORKER_BASENAME}.js`, import.meta.url),
2573
+ // From dist/check/index.js, dist/advanced/index.js, dist/<flat>.js.
2574
+ new URL(`../harness/check/runtime-render/${WORKER_BASENAME}.js`, import.meta.url)
2575
+ ];
2576
+ for (const jsUrl of jsCandidates) {
2577
+ const jsPath = fileURLToPath(jsUrl);
2578
+ if (existsSync(jsPath)) {
2579
+ return { command: process.execPath, args: [jsPath] };
2580
+ }
2581
+ }
2582
+ const tsCandidates = [
2583
+ new URL(`./${WORKER_BASENAME}.ts`, import.meta.url),
2584
+ new URL(`../harness/check/runtime-render/${WORKER_BASENAME}.ts`, import.meta.url)
2585
+ ];
2586
+ for (const tsUrl of tsCandidates) {
2587
+ const tsPath = fileURLToPath(tsUrl);
2588
+ if (existsSync(tsPath)) {
2589
+ const require_ = createRequire(import.meta.url);
2590
+ const tsxLoader = pathToFileURL(require_.resolve("tsx")).href;
2591
+ return { command: process.execPath, args: ["--import", tsxLoader, tsPath] };
2592
+ }
2593
+ }
2594
+ const tried = [...jsCandidates, ...tsCandidates].map((u) => fileURLToPath(u)).join(", ");
2595
+ throw new Error(
2596
+ `render-check: isolated worker not found at any of: ${tried}. Did \`pnpm build\` run?`
2597
+ );
2598
+ }
2599
+ function failed(reason, t0) {
2600
+ return {
2601
+ ok: false,
2602
+ issues: [{ check: "render-no-throw", outcome: "failed", reason }],
2603
+ stats: { actionsChecked: 0, streamsChecked: 0, renderMs: Date.now() - t0 }
2604
+ };
2605
+ }
2606
+ function unverified(reason, t0) {
2607
+ return {
2608
+ ok: false,
2609
+ issues: [{ check: "render-no-throw", outcome: "unverified", reason }],
2610
+ stats: { actionsChecked: 0, streamsChecked: 0, renderMs: Date.now() - t0 }
2611
+ };
2612
+ }
2613
+ function mapSandboxResultToCheckResult(result, t0) {
2614
+ if (result.outcome === "timeout") {
2615
+ return failed(
2616
+ `render check timed out after ${CHECK_TIMEOUT_MS}ms in the isolated worker (likely infinite loop or runaway effect in the component).`,
2617
+ t0
2618
+ );
2619
+ }
2620
+ if (result.outcome === "overflow-stdout" || result.outcome === "overflow-stderr") {
2621
+ return failed(
2622
+ `render check worker produced excessive output (${result.outcome}) \u2014 the component is likely in a pathological logging or error loop.`,
2623
+ t0
2624
+ );
2625
+ }
2626
+ if (result.outcome === "spawn-error") {
2627
+ return unverified(
2628
+ `render check worker failed to start \u2014 ${result.errorMessage}`,
2629
+ t0
2630
+ );
2631
+ }
2632
+ if (result.outcome === "canceled") {
2633
+ return unverified("render check worker was canceled before completing.", t0);
2634
+ }
2635
+ if (result.outcome !== "exit") {
2636
+ return unverified(
2637
+ `render check worker ended with unexpected sandbox outcome '${result.outcome}'.`,
2638
+ t0
2639
+ );
2640
+ }
2641
+ if (result.exitCode !== 0) {
2642
+ const tail = result.stderr.trim() || result.stdout.trim();
2643
+ return unverified(
2644
+ `render check worker exited ${result.exitCode}${tail ? ` \u2014 ${tail.slice(0, 600)}` : ""}`,
2645
+ t0
2646
+ );
2647
+ }
2648
+ const stdout = result.stdout.trim();
2649
+ if (stdout.length === 0) {
2650
+ return unverified("render check worker exited without producing a verdict.", t0);
2651
+ }
2652
+ try {
2653
+ return JSON.parse(stdout);
2654
+ } catch (err) {
2655
+ return unverified(
2656
+ `render check worker verdict was not valid JSON \u2014 ${err instanceof Error ? err.message : String(err)}`,
2657
+ t0
2658
+ );
2659
+ }
2660
+ }
2661
+ async function runRenderCheckViaWorker(input) {
2662
+ const t0 = Date.now();
2663
+ let spawn;
2664
+ try {
2665
+ spawn = resolveWorkerSpawn();
2666
+ } catch (err) {
2667
+ return unverified(err instanceof Error ? err.message : String(err), t0);
2668
+ }
2669
+ const result = await runSandboxed({
2670
+ command: spawn.command,
2671
+ args: spawn.args,
2672
+ timeoutMs: CHECK_TIMEOUT_MS,
2673
+ maxStdoutBytes: CHECK_STDOUT_CAP,
2674
+ nodeHeapMb: CHECK_NODE_HEAP_MB,
2675
+ stdin: JSON.stringify(input),
2676
+ // NODE_ENV steers React's production vs development build — keep
2677
+ // parity with the caller; everything else stays on the sandbox's
2678
+ // default allowlist.
2679
+ env: { NODE_ENV: process.env.NODE_ENV ?? "production" }
2680
+ });
2681
+ return mapSandboxResultToCheckResult(result, t0);
2682
+ }
2683
+ var CHECK_TIMEOUT_MS, CHECK_STDOUT_CAP, CHECK_NODE_HEAP_MB, WORKER_BASENAME;
2684
+ var init_render_check_host = __esm({
2685
+ "src/harness/check/runtime-render/render-check-host.ts"() {
2686
+ CHECK_TIMEOUT_MS = 3e4;
2687
+ CHECK_STDOUT_CAP = 2 * 1024 * 1024;
2688
+ CHECK_NODE_HEAP_MB = 512;
2689
+ WORKER_BASENAME = "render-check-worker";
2690
+ }
2691
+ });
2558
2692
  function extractWireCallSites(code) {
2559
2693
  const sf = ts2.createSourceFile(
2560
2694
  "component.tsx",
@@ -3586,17 +3720,33 @@ ${errorDetail}`,
3586
3720
  }
3587
3721
  }
3588
3722
  const hexMatch = line.match(/#[0-9a-fA-F]{3,8}\b/);
3589
- if (hexMatch && !line.includes("var(--ggui-") && !line.includes("// fallback") && !assetEscapedLines.has(lineNum)) {
3590
- issues.push({
3591
- tier: 0,
3592
- result: "fail",
3593
- category: "tokens",
3594
- subcategory: "hex-color",
3595
- severity: "critical",
3596
- description: `Hardcoded color "${hexMatch[0]}" breaks theme switching \u2014 use design tokens.`,
3597
- fix: `Replace with a primitive variant (Button variant="primary"|Badge variant="success"|...) OR a token reference like var(--ggui-color-primary-500, ${hexMatch[0]}). Hardcoded colors mean the operator's theme has no effect on this surface.`,
3598
- line: lineNum
3599
- });
3723
+ if (hexMatch && !assetEscapedLines.has(lineNum)) {
3724
+ const fallbackVarMatch = line.match(
3725
+ /var\((--ggui-[a-zA-Z0-9-]+)\s*,[^)]*#[0-9a-fA-F]{3,8}/
3726
+ );
3727
+ if (fallbackVarMatch) {
3728
+ issues.push({
3729
+ tier: 0,
3730
+ result: "fail",
3731
+ category: "tokens",
3732
+ subcategory: "token-fallback",
3733
+ severity: "critical",
3734
+ description: `Literal fallback "${hexMatch[0]}" inside a token reference \u2014 the runtime injects every token, and the fallback paints the wrong brand exactly when the theme matters most.`,
3735
+ fix: `Drop the fallback: write var(${fallbackVarMatch[1]}) bare. If the token might not exist, that is a vocabulary problem to fix by choosing a token from the design-system docs, never by a literal.`,
3736
+ line: lineNum
3737
+ });
3738
+ } else if (!line.includes("// fallback")) {
3739
+ issues.push({
3740
+ tier: 0,
3741
+ result: "fail",
3742
+ category: "tokens",
3743
+ subcategory: "hex-color",
3744
+ severity: "critical",
3745
+ description: `Hardcoded color "${hexMatch[0]}" breaks theme switching \u2014 use design tokens.`,
3746
+ fix: `Replace with a primitive variant (Button variant="primary"|Badge variant="success"|...) OR a bare token reference like var(--ggui-color-primary-500) \u2014 never a literal fallback. Hardcoded colors mean the operator's theme has no effect on this surface.`,
3747
+ line: lineNum
3748
+ });
3749
+ }
3600
3750
  }
3601
3751
  const colorFnMatch = line.match(/\b(rgba?|hsla?)\s*\(/);
3602
3752
  if (colorFnMatch && !line.includes("var(--ggui-") && !line.includes("// fallback") && !line.includes("$value") && !assetEscapedLines.has(lineNum)) {
@@ -11061,15 +11211,15 @@ EVERY color, spacing, typography, shadow, and radius value MUST come from design
11061
11211
 
11062
11212
  MANDATORY:
11063
11213
  1. NEVER use hardcoded hex colors like \`#7c3aed\` \u2014 ONLY \`var(--ggui-color-*)\` tokens.
11064
- 2. NEVER use CSS gradients with custom colors. If you need a gradient: \`linear-gradient(to bottom, var(--ggui-color-primary-500, #0ea5e9), var(--ggui-color-primary-700, #0369a1))\`.
11214
+ 2. NEVER use CSS gradients with custom colors. If you need a gradient: \`linear-gradient(to bottom, var(--ggui-color-primary-500), var(--ggui-color-primary-700))\`.
11065
11215
  3. NEVER invent your own palette. The system provides primary, neutral, success, warning, error, and info \u2014 use ONLY these.
11066
- 4. ALWAYS include fallback values: \`var(--ggui-color-primary-600, #0284c7)\`.
11216
+ 4. NEVER include literal fallback values in color tokens \u2014 write \`var(--ggui-color-primary-600)\` bare, never \`var(--ggui-color-primary-600, #0284c7)\`. The runtime injects every token; a literal fallback paints the wrong brand exactly when the operator's theme matters most.
11067
11217
 
11068
11218
  Token categories:
11069
- - Brand: \`var(--ggui-color-primary-600, #0284c7)\`, \`var(--ggui-color-primary-50, #f0f9ff)\`
11070
- - Text: \`var(--ggui-color-onSurface, #18181b)\`, \`var(--ggui-color-onSurfaceVariant, #52525b)\`
11071
- - Backgrounds: \`var(--ggui-color-surface, #fafafa)\`, \`var(--ggui-color-surfaceVariant, #f4f4f5)\`
11072
- - Borders: \`var(--ggui-color-outline, #d4d4d8)\`
11219
+ - Brand: \`var(--ggui-color-primary-600)\`, \`var(--ggui-color-primary-50)\`
11220
+ - Text: \`var(--ggui-color-onSurface)\`, \`var(--ggui-color-onSurfaceVariant)\`
11221
+ - Backgrounds: \`var(--ggui-color-surface)\`, \`var(--ggui-color-surfaceVariant)\`
11222
+ - Borders: \`var(--ggui-color-outline)\`
11073
11223
  - Spacing: \`var(--ggui-spacing-4, 16px)\`, \`var(--ggui-spacing-6, 24px)\`
11074
11224
  - Typography: \`var(--ggui-font-size-sm, 14px)\`, \`var(--ggui-font-weight-semibold, 600)\`
11075
11225
  - Shadows: \`var(--ggui-shape-shadow-sm)\`, \`var(--ggui-shape-shadow-md)\`, \`var(--ggui-shape-shadow-lg)\`
@@ -11254,7 +11404,7 @@ Hero sections should feel airy. Use \`padding="xl"\` (32px) on the main card, no
11254
11404
 
11255
11405
  - [ ] Imports ONLY from: react, @ggui-ai/design, @ggui-ai/wire
11256
11406
  - [ ] No raw HTML elements (\`<button>\`, \`<input>\`, \`<div>\` for layout) \u2014 uses primitives
11257
- - [ ] ZERO hardcoded hex colors \u2014 every color is \`var(--ggui-color-*, fallback)\`
11407
+ - [ ] ZERO hardcoded hex colors \u2014 every color is a bare \`var(--ggui-color-*)\`, no literal fallback
11258
11408
  - [ ] No raw pixel values for spacing \u2014 all via \`var(--ggui-spacing-*)\` tokens
11259
11409
  - [ ] Primary palette used throughout (headers, borders, icons) \u2014 not just buttons
11260
11410
  - [ ] Typed Props interface exported; request-specific data is a prop with default
@@ -13546,8 +13696,8 @@ ${cjsResult.code}
13546
13696
  // src/harness/check/runtime-render/load-component.ts
13547
13697
  async function loadComponent(input) {
13548
13698
  const { sourceCode, moduleResolutions = {} } = input;
13549
- const { createRequire: createRequire4 } = await import('module');
13550
- const require_ = createRequire4(import.meta.url);
13699
+ const { createRequire: createRequire5 } = await import('module');
13700
+ const require_ = createRequire5(import.meta.url);
13551
13701
  const sandboxRequire = (id) => {
13552
13702
  if (id in moduleResolutions) return moduleResolutions[id];
13553
13703
  if (id === "react/jsx-runtime" || id === "react/jsx-dev-runtime") {
@@ -13906,6 +14056,14 @@ function parentLink(node) {
13906
14056
 
13907
14057
  // src/harness/check/runtime-render/render-check.ts
13908
14058
  async function runRenderCheck(input) {
14059
+ const g = hostGlobals();
14060
+ if ("window" in g && "document" in g) {
14061
+ return runRenderCheckInProcess(input);
14062
+ }
14063
+ const { runRenderCheckViaWorker: runRenderCheckViaWorker2 } = await Promise.resolve().then(() => (init_render_check_host(), render_check_host_exports));
14064
+ return runRenderCheckViaWorker2(input);
14065
+ }
14066
+ async function runRenderCheckInProcess(input) {
13909
14067
  const t0 = Date.now();
13910
14068
  const issues = [];
13911
14069
  const teardown = await setupHappyDom();
@@ -14095,6 +14253,22 @@ async function runRenderCheck(input) {
14095
14253
  if (issue) issues.push(issue);
14096
14254
  }
14097
14255
  }
14256
+ if (input.contract?.actionSpec) {
14257
+ for (const [name, entry] of Object.entries(input.contract.actionSpec)) {
14258
+ const idField = idLikeRequiredStringField(
14259
+ entry.schema
14260
+ );
14261
+ if (idField === null) continue;
14262
+ const issue = await checkSelectionIdentity({
14263
+ container,
14264
+ probe,
14265
+ user,
14266
+ actionName: name,
14267
+ idField
14268
+ });
14269
+ if (issue) issues.push(issue);
14270
+ }
14271
+ }
14098
14272
  if (input.contract?.propsSpec) {
14099
14273
  for (const issue of checkPropCoverage({
14100
14274
  container,
@@ -14104,6 +14278,48 @@ async function runRenderCheck(input) {
14104
14278
  issues.push(issue);
14105
14279
  }
14106
14280
  }
14281
+ if (input.contract?.propsSpec) {
14282
+ const propsSpec5c = input.contract.propsSpec;
14283
+ const normalize = (s) => (s ?? "").replace(/\s+/g, " ").trim();
14284
+ const baseText = normalize(container.textContent);
14285
+ for (const [propName, entry] of Object.entries(propsSpec5c.properties)) {
14286
+ if (!entry.required) continue;
14287
+ const orig = input.mockupProps[propName];
14288
+ if (typeof orig !== "string" && typeof orig !== "number") continue;
14289
+ const marker = pickScalarMarker(orig);
14290
+ if (marker === null || !baseText.includes(marker)) continue;
14291
+ const varied = typeof orig === "string" ? "Zzyzx Qw" : orig + 137731;
14292
+ const variedProbe = createProbe();
14293
+ const variedConfig = createProbeWireConfig(variedProbe);
14294
+ let variedResult;
14295
+ try {
14296
+ variedResult = render(
14297
+ React.createElement(GguiWireProvider, {
14298
+ config: variedConfig,
14299
+ children: React.createElement(ProbeErrorBoundary, {
14300
+ children: React.createElement(Component, {
14301
+ ...input.mockupProps,
14302
+ [propName]: varied
14303
+ })
14304
+ })
14305
+ })
14306
+ );
14307
+ } catch {
14308
+ continue;
14309
+ }
14310
+ await flushPromises();
14311
+ const variedText = normalize(variedResult.container.textContent);
14312
+ variedResult.unmount();
14313
+ if (variedText === baseText) {
14314
+ issues.push({
14315
+ check: "prop-sensitivity",
14316
+ outcome: "failed",
14317
+ subject: propName,
14318
+ 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.`
14319
+ });
14320
+ }
14321
+ }
14322
+ }
14107
14323
  if (input.contract?.streamSpec) {
14108
14324
  const streamSpec = input.contract.streamSpec;
14109
14325
  for (const [name] of Object.entries(streamSpec)) {
@@ -14435,6 +14651,74 @@ async function checkActionWiring(input) {
14435
14651
  }
14436
14652
  };
14437
14653
  }
14654
+ function idLikeRequiredStringField(schema) {
14655
+ if (schema === void 0) return null;
14656
+ const properties = schema["properties"];
14657
+ const required = schema["required"];
14658
+ if (properties === null || typeof properties !== "object") return null;
14659
+ if (!Array.isArray(required)) return null;
14660
+ for (const [propName, propSchema] of Object.entries(
14661
+ properties
14662
+ )) {
14663
+ if (!/id$/i.test(propName)) continue;
14664
+ if (!required.includes(propName)) continue;
14665
+ const t = propSchema !== null && typeof propSchema === "object" ? propSchema["type"] : void 0;
14666
+ if (t === "string") return propName;
14667
+ }
14668
+ return null;
14669
+ }
14670
+ async function checkSelectionIdentity(input) {
14671
+ const { container, probe, user, actionName, idField } = input;
14672
+ const clickables = Array.from(
14673
+ container.querySelectorAll('button, [role="button"]')
14674
+ );
14675
+ const byLabel = /* @__PURE__ */ new Map();
14676
+ for (const el of clickables) {
14677
+ const label = (el.textContent ?? "").replace(/\s+/g, " ").trim();
14678
+ if (label.length === 0) continue;
14679
+ const group = byLabel.get(label);
14680
+ if (group) group.push(el);
14681
+ else byLabel.set(label, [el]);
14682
+ }
14683
+ for (const [label, group] of byLabel) {
14684
+ if (group.length < 2) continue;
14685
+ const payloadFor = async (el) => {
14686
+ const before = probe.getFireLog().length;
14687
+ try {
14688
+ await user.click(el);
14689
+ } catch {
14690
+ return void 0;
14691
+ }
14692
+ await flushPromises();
14693
+ const events = probe.getFireLog().slice(before).filter(
14694
+ (e) => e.kind === "action.fired" && e.name === actionName
14695
+ );
14696
+ return events.length > 0 ? events[events.length - 1].payload : void 0;
14697
+ };
14698
+ const first = await payloadFor(group[0]);
14699
+ const second = await payloadFor(group[1]);
14700
+ if (first === void 0 || second === void 0) continue;
14701
+ const idOf = (payload) => {
14702
+ if (payload === null || typeof payload !== "object") return void 0;
14703
+ const v = payload[idField];
14704
+ return typeof v === "string" ? v : void 0;
14705
+ };
14706
+ const firstId = idOf(first);
14707
+ const secondId = idOf(second);
14708
+ if (firstId === void 0 || secondId === void 0) continue;
14709
+ if (firstId === secondId) {
14710
+ return {
14711
+ check: "selection-identity",
14712
+ outcome: "failed",
14713
+ subject: actionName,
14714
+ 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}".`,
14715
+ elementHint: describeElement(group[0])
14716
+ };
14717
+ }
14718
+ return null;
14719
+ }
14720
+ return null;
14721
+ }
14438
14722
  async function simulateAndCheck(input) {
14439
14723
  const { container, user, probe, wiringKind, actionName, actionLabel, eventKind } = input;
14440
14724
  const candidates = findCandidateElements(container, wiringKind, actionName, actionLabel);
@@ -14933,6 +15217,26 @@ function toEvalIssue(issue) {
14933
15217
  fix
14934
15218
  };
14935
15219
  }
15220
+ case "selection-identity":
15221
+ return {
15222
+ tier: 0,
15223
+ result,
15224
+ category: "contract",
15225
+ subcategory,
15226
+ severity: "critical",
15227
+ description: `${issue.reason}${elementHint}${diagSuffix}`,
15228
+ fix: `Key the action payload (and any selection state) on the item's ID from the data, never on its display text. Common fix: map over items and pass the item's id into the handler \u2014 onClick={() => ${subject}({ ...payload, id: item.id })} \u2014 and store selection as the id (setSelected(item.id)), not the label.`
15229
+ };
15230
+ case "prop-sensitivity":
15231
+ return {
15232
+ tier: 0,
15233
+ result,
15234
+ category: "contract",
15235
+ subcategory,
15236
+ severity: "critical",
15237
+ description: `${issue.reason}${diagSuffix}`,
15238
+ fix: `Derive the display AND any logic from props.${subject} \u2014 render {props.${subject}} and branch on the prop's value, never on the literal value the request happened to mention (e.g. props.${subject} ?? 'January' followed by January-special-cased logic bakes the request into the component). The contract delivers the value; trust it.`
15239
+ };
14936
15240
  case "prop-coverage":
14937
15241
  return {
14938
15242
  tier: 0,