@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.
- package/dist/adapters/generation-dispatch.js +328 -24
- package/dist/adapters/generation-dispatch.js.map +1 -1
- package/dist/advanced/index.d.ts +1 -1
- package/dist/advanced/index.js +333 -29
- package/dist/advanced/index.js.map +1 -1
- package/dist/blueprint-validator.js +307 -3
- package/dist/blueprint-validator.js.map +1 -1
- package/dist/boilerplate.js +7 -7
- package/dist/boilerplate.js.map +1 -1
- package/dist/check/index.js +27 -11
- package/dist/check/index.js.map +1 -1
- package/dist/harness/check/runtime-render/index.d.ts +1 -1
- package/dist/harness/check/runtime-render/index.js +307 -3
- package/dist/harness/check/runtime-render/index.js.map +1 -1
- package/dist/harness/check/runtime-render/render-check-worker.d.ts +2 -0
- package/dist/harness/check/runtime-render/render-check-worker.js +1458 -0
- package/dist/harness/check/runtime-render/render-check-worker.js.map +1 -0
- package/dist/harness/index.js +7 -7
- package/dist/harness/index.js.map +1 -1
- package/dist/harness/runtime.js +7 -7
- package/dist/harness/runtime.js.map +1 -1
- package/dist/harness/types-public.js +7 -7
- package/dist/harness/types-public.js.map +1 -1
- package/dist/index.js +327 -23
- package/dist/index.js.map +1 -1
- package/dist/provider-adapter-contract.js +1161 -387
- package/dist/provider-adapter-contract.js.map +1 -1
- package/dist/{render-check-BNYGVDJd.d.ts → render-check-WfOVO9kU.d.ts} +21 -1
- package/package.json +12 -12
- package/src/boilerplate/system-prompt.ts +7 -7
- package/src/check/run-tier0.ts +35 -16
- package/src/harness/check/runtime-render/adapter.ts +28 -0
- package/src/harness/check/runtime-render/render-check-host.ts +201 -0
- package/src/harness/check/runtime-render/render-check-worker.ts +80 -0
- package/src/harness/check/runtime-render/render-check.ts +264 -1
package/dist/advanced/index.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { GeneratorTier, UiGenerator } from '@ggui-ai/mcp-server-core';
|
|
|
2
2
|
import { JsonObject } from '@ggui-ai/protocol';
|
|
3
3
|
import { BehaviorFailure, PlaywrightModule } from '@ggui-ai/ui-visual-tester';
|
|
4
4
|
import { C as CreateUiGeneratorOptions } from '../create-ui-generator-CF0EL0Li.js';
|
|
5
|
-
import { R as RenderCheckIssue } from '../render-check-
|
|
5
|
+
import { R as RenderCheckIssue } from '../render-check-WfOVO9kU.js';
|
|
6
6
|
import '@ggui-ai/gadgets';
|
|
7
7
|
import '../types-public-DIpjC-OA.js';
|
|
8
8
|
import '../axes-CzLEMDeB.js';
|
package/dist/advanced/index.js
CHANGED
|
@@ -3,22 +3,22 @@ import * as esbuild2 from 'esbuild';
|
|
|
3
3
|
import { build } from 'esbuild';
|
|
4
4
|
import Anthropic from '@anthropic-ai/sdk';
|
|
5
5
|
import { classifyUi, MODEL_REGISTRY, DANGEROUS_PATTERNS as DANGEROUS_PATTERNS$1, listContractGadgets, HOOK_NAME_RE, STDLIB_GADGETS_PACKAGE, resolveAppGadgets } from '@ggui-ai/protocol';
|
|
6
|
-
import fs, { mkdirSync, writeFileSync, unlinkSync,
|
|
6
|
+
import fs, { mkdirSync, writeFileSync, unlinkSync, existsSync, readFileSync, rm, createReadStream, createWriteStream } from 'fs';
|
|
7
7
|
import { contentHash } from '@ggui-ai/protocol/content-hash';
|
|
8
8
|
import ts7 from 'typescript';
|
|
9
9
|
import path, { dirname, resolve, join, basename } from 'path';
|
|
10
10
|
import { createRequire } from 'module';
|
|
11
11
|
import { Linter } from 'eslint';
|
|
12
|
+
import { fileURLToPath, pathToFileURL } from 'url';
|
|
13
|
+
import { runSandboxed } from '@ggui-ai/sandbox';
|
|
12
14
|
import { createHash } from 'crypto';
|
|
13
15
|
import 'fs/promises';
|
|
14
16
|
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, pathToFileURL } from 'url';
|
|
19
20
|
import { PlaywrightNotAvailableError, validateContractBehavior } from '@ggui-ai/ui-visual-tester';
|
|
20
21
|
import { parseGeneratorSlug, formatGeneratorSlug, isValidGeneratorSlug } from '@ggui-ai/mcp-server-core';
|
|
21
|
-
import { runSandboxed } from '@ggui-ai/sandbox';
|
|
22
22
|
import git from 'isomorphic-git';
|
|
23
23
|
import { Volume, createFsFromVolume } from 'memfs';
|
|
24
24
|
import { createPatch, applyPatch } from 'diff';
|
|
@@ -3270,17 +3270,33 @@ ${errorDetail}`,
|
|
|
3270
3270
|
}
|
|
3271
3271
|
}
|
|
3272
3272
|
const hexMatch = line.match(/#[0-9a-fA-F]{3,8}\b/);
|
|
3273
|
-
if (hexMatch && !
|
|
3274
|
-
|
|
3275
|
-
|
|
3276
|
-
|
|
3277
|
-
|
|
3278
|
-
|
|
3279
|
-
|
|
3280
|
-
|
|
3281
|
-
|
|
3282
|
-
|
|
3283
|
-
|
|
3273
|
+
if (hexMatch && !assetEscapedLines.has(lineNum)) {
|
|
3274
|
+
const fallbackVarMatch = line.match(
|
|
3275
|
+
/var\((--ggui-[a-zA-Z0-9-]+)\s*,[^)]*#[0-9a-fA-F]{3,8}/
|
|
3276
|
+
);
|
|
3277
|
+
if (fallbackVarMatch) {
|
|
3278
|
+
issues.push({
|
|
3279
|
+
tier: 0,
|
|
3280
|
+
result: "fail",
|
|
3281
|
+
category: "tokens",
|
|
3282
|
+
subcategory: "token-fallback",
|
|
3283
|
+
severity: "critical",
|
|
3284
|
+
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.`,
|
|
3285
|
+
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.`,
|
|
3286
|
+
line: lineNum
|
|
3287
|
+
});
|
|
3288
|
+
} else if (!line.includes("// fallback")) {
|
|
3289
|
+
issues.push({
|
|
3290
|
+
tier: 0,
|
|
3291
|
+
result: "fail",
|
|
3292
|
+
category: "tokens",
|
|
3293
|
+
subcategory: "hex-color",
|
|
3294
|
+
severity: "critical",
|
|
3295
|
+
description: `Hardcoded color "${hexMatch[0]}" breaks theme switching \u2014 use design tokens.`,
|
|
3296
|
+
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.`,
|
|
3297
|
+
line: lineNum
|
|
3298
|
+
});
|
|
3299
|
+
}
|
|
3284
3300
|
}
|
|
3285
3301
|
const colorFnMatch = line.match(/\b(rgba?|hsla?)\s*\(/);
|
|
3286
3302
|
if (colorFnMatch && !line.includes("var(--ggui-") && !line.includes("// fallback") && !line.includes("$value") && !assetEscapedLines.has(lineNum)) {
|
|
@@ -5442,6 +5458,140 @@ var init_policy2 = __esm({
|
|
|
5442
5458
|
}
|
|
5443
5459
|
});
|
|
5444
5460
|
|
|
5461
|
+
// src/harness/check/runtime-render/render-check-host.ts
|
|
5462
|
+
var render_check_host_exports = {};
|
|
5463
|
+
__export(render_check_host_exports, {
|
|
5464
|
+
mapSandboxResultToCheckResult: () => mapSandboxResultToCheckResult,
|
|
5465
|
+
runRenderCheckViaWorker: () => runRenderCheckViaWorker
|
|
5466
|
+
});
|
|
5467
|
+
function resolveWorkerSpawn2() {
|
|
5468
|
+
const jsCandidates = [
|
|
5469
|
+
// Sibling — when import.meta.url is already .../harness/check/runtime-render/.
|
|
5470
|
+
new URL(`./${WORKER_BASENAME}.js`, import.meta.url),
|
|
5471
|
+
// From dist/index.js.
|
|
5472
|
+
new URL(`./harness/check/runtime-render/${WORKER_BASENAME}.js`, import.meta.url),
|
|
5473
|
+
// From dist/harness/index.js.
|
|
5474
|
+
new URL(`./check/runtime-render/${WORKER_BASENAME}.js`, import.meta.url),
|
|
5475
|
+
// From dist/check/index.js, dist/advanced/index.js, dist/<flat>.js.
|
|
5476
|
+
new URL(`../harness/check/runtime-render/${WORKER_BASENAME}.js`, import.meta.url)
|
|
5477
|
+
];
|
|
5478
|
+
for (const jsUrl of jsCandidates) {
|
|
5479
|
+
const jsPath = fileURLToPath(jsUrl);
|
|
5480
|
+
if (existsSync(jsPath)) {
|
|
5481
|
+
return { command: process.execPath, args: [jsPath] };
|
|
5482
|
+
}
|
|
5483
|
+
}
|
|
5484
|
+
const tsCandidates = [
|
|
5485
|
+
new URL(`./${WORKER_BASENAME}.ts`, import.meta.url),
|
|
5486
|
+
new URL(`../harness/check/runtime-render/${WORKER_BASENAME}.ts`, import.meta.url)
|
|
5487
|
+
];
|
|
5488
|
+
for (const tsUrl of tsCandidates) {
|
|
5489
|
+
const tsPath = fileURLToPath(tsUrl);
|
|
5490
|
+
if (existsSync(tsPath)) {
|
|
5491
|
+
const require_ = createRequire(import.meta.url);
|
|
5492
|
+
const tsxLoader = pathToFileURL(require_.resolve("tsx")).href;
|
|
5493
|
+
return { command: process.execPath, args: ["--import", tsxLoader, tsPath] };
|
|
5494
|
+
}
|
|
5495
|
+
}
|
|
5496
|
+
const tried = [...jsCandidates, ...tsCandidates].map((u) => fileURLToPath(u)).join(", ");
|
|
5497
|
+
throw new Error(
|
|
5498
|
+
`render-check: isolated worker not found at any of: ${tried}. Did \`pnpm build\` run?`
|
|
5499
|
+
);
|
|
5500
|
+
}
|
|
5501
|
+
function failed(reason, t0) {
|
|
5502
|
+
return {
|
|
5503
|
+
ok: false,
|
|
5504
|
+
issues: [{ check: "render-no-throw", outcome: "failed", reason }],
|
|
5505
|
+
stats: { actionsChecked: 0, streamsChecked: 0, renderMs: Date.now() - t0 }
|
|
5506
|
+
};
|
|
5507
|
+
}
|
|
5508
|
+
function unverified(reason, t0) {
|
|
5509
|
+
return {
|
|
5510
|
+
ok: false,
|
|
5511
|
+
issues: [{ check: "render-no-throw", outcome: "unverified", reason }],
|
|
5512
|
+
stats: { actionsChecked: 0, streamsChecked: 0, renderMs: Date.now() - t0 }
|
|
5513
|
+
};
|
|
5514
|
+
}
|
|
5515
|
+
function mapSandboxResultToCheckResult(result, t0) {
|
|
5516
|
+
if (result.outcome === "timeout") {
|
|
5517
|
+
return failed(
|
|
5518
|
+
`render check timed out after ${CHECK_TIMEOUT_MS}ms in the isolated worker (likely infinite loop or runaway effect in the component).`,
|
|
5519
|
+
t0
|
|
5520
|
+
);
|
|
5521
|
+
}
|
|
5522
|
+
if (result.outcome === "overflow-stdout" || result.outcome === "overflow-stderr") {
|
|
5523
|
+
return failed(
|
|
5524
|
+
`render check worker produced excessive output (${result.outcome}) \u2014 the component is likely in a pathological logging or error loop.`,
|
|
5525
|
+
t0
|
|
5526
|
+
);
|
|
5527
|
+
}
|
|
5528
|
+
if (result.outcome === "spawn-error") {
|
|
5529
|
+
return unverified(
|
|
5530
|
+
`render check worker failed to start \u2014 ${result.errorMessage}`,
|
|
5531
|
+
t0
|
|
5532
|
+
);
|
|
5533
|
+
}
|
|
5534
|
+
if (result.outcome === "canceled") {
|
|
5535
|
+
return unverified("render check worker was canceled before completing.", t0);
|
|
5536
|
+
}
|
|
5537
|
+
if (result.outcome !== "exit") {
|
|
5538
|
+
return unverified(
|
|
5539
|
+
`render check worker ended with unexpected sandbox outcome '${result.outcome}'.`,
|
|
5540
|
+
t0
|
|
5541
|
+
);
|
|
5542
|
+
}
|
|
5543
|
+
if (result.exitCode !== 0) {
|
|
5544
|
+
const tail = result.stderr.trim() || result.stdout.trim();
|
|
5545
|
+
return unverified(
|
|
5546
|
+
`render check worker exited ${result.exitCode}${tail ? ` \u2014 ${tail.slice(0, 600)}` : ""}`,
|
|
5547
|
+
t0
|
|
5548
|
+
);
|
|
5549
|
+
}
|
|
5550
|
+
const stdout = result.stdout.trim();
|
|
5551
|
+
if (stdout.length === 0) {
|
|
5552
|
+
return unverified("render check worker exited without producing a verdict.", t0);
|
|
5553
|
+
}
|
|
5554
|
+
try {
|
|
5555
|
+
return JSON.parse(stdout);
|
|
5556
|
+
} catch (err) {
|
|
5557
|
+
return unverified(
|
|
5558
|
+
`render check worker verdict was not valid JSON \u2014 ${err instanceof Error ? err.message : String(err)}`,
|
|
5559
|
+
t0
|
|
5560
|
+
);
|
|
5561
|
+
}
|
|
5562
|
+
}
|
|
5563
|
+
async function runRenderCheckViaWorker(input) {
|
|
5564
|
+
const t0 = Date.now();
|
|
5565
|
+
let spawn;
|
|
5566
|
+
try {
|
|
5567
|
+
spawn = resolveWorkerSpawn2();
|
|
5568
|
+
} catch (err) {
|
|
5569
|
+
return unverified(err instanceof Error ? err.message : String(err), t0);
|
|
5570
|
+
}
|
|
5571
|
+
const result = await runSandboxed({
|
|
5572
|
+
command: spawn.command,
|
|
5573
|
+
args: spawn.args,
|
|
5574
|
+
timeoutMs: CHECK_TIMEOUT_MS,
|
|
5575
|
+
maxStdoutBytes: CHECK_STDOUT_CAP,
|
|
5576
|
+
nodeHeapMb: CHECK_NODE_HEAP_MB,
|
|
5577
|
+
stdin: JSON.stringify(input),
|
|
5578
|
+
// NODE_ENV steers React's production vs development build — keep
|
|
5579
|
+
// parity with the caller; everything else stays on the sandbox's
|
|
5580
|
+
// default allowlist.
|
|
5581
|
+
env: { NODE_ENV: process.env.NODE_ENV ?? "production" }
|
|
5582
|
+
});
|
|
5583
|
+
return mapSandboxResultToCheckResult(result, t0);
|
|
5584
|
+
}
|
|
5585
|
+
var CHECK_TIMEOUT_MS, CHECK_STDOUT_CAP, CHECK_NODE_HEAP_MB, WORKER_BASENAME;
|
|
5586
|
+
var init_render_check_host = __esm({
|
|
5587
|
+
"src/harness/check/runtime-render/render-check-host.ts"() {
|
|
5588
|
+
CHECK_TIMEOUT_MS = 3e4;
|
|
5589
|
+
CHECK_STDOUT_CAP = 2 * 1024 * 1024;
|
|
5590
|
+
CHECK_NODE_HEAP_MB = 512;
|
|
5591
|
+
WORKER_BASENAME = "render-check-worker";
|
|
5592
|
+
}
|
|
5593
|
+
});
|
|
5594
|
+
|
|
5445
5595
|
// src/harness/hashline.ts
|
|
5446
5596
|
var hashline_exports = {};
|
|
5447
5597
|
__export(hashline_exports, {
|
|
@@ -13364,15 +13514,15 @@ EVERY color, spacing, typography, shadow, and radius value MUST come from design
|
|
|
13364
13514
|
|
|
13365
13515
|
MANDATORY:
|
|
13366
13516
|
1. NEVER use hardcoded hex colors like \`#7c3aed\` \u2014 ONLY \`var(--ggui-color-*)\` tokens.
|
|
13367
|
-
2. NEVER use CSS gradients with custom colors. If you need a gradient: \`linear-gradient(to bottom, var(--ggui-color-primary-500
|
|
13517
|
+
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))\`.
|
|
13368
13518
|
3. NEVER invent your own palette. The system provides primary, neutral, success, warning, error, and info \u2014 use ONLY these.
|
|
13369
|
-
4.
|
|
13519
|
+
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.
|
|
13370
13520
|
|
|
13371
13521
|
Token categories:
|
|
13372
|
-
- Brand: \`var(--ggui-color-primary-600
|
|
13373
|
-
- Text: \`var(--ggui-color-onSurface
|
|
13374
|
-
- Backgrounds: \`var(--ggui-color-surface
|
|
13375
|
-
- Borders: \`var(--ggui-color-outline
|
|
13522
|
+
- Brand: \`var(--ggui-color-primary-600)\`, \`var(--ggui-color-primary-50)\`
|
|
13523
|
+
- Text: \`var(--ggui-color-onSurface)\`, \`var(--ggui-color-onSurfaceVariant)\`
|
|
13524
|
+
- Backgrounds: \`var(--ggui-color-surface)\`, \`var(--ggui-color-surfaceVariant)\`
|
|
13525
|
+
- Borders: \`var(--ggui-color-outline)\`
|
|
13376
13526
|
- Spacing: \`var(--ggui-spacing-4, 16px)\`, \`var(--ggui-spacing-6, 24px)\`
|
|
13377
13527
|
- Typography: \`var(--ggui-font-size-sm, 14px)\`, \`var(--ggui-font-weight-semibold, 600)\`
|
|
13378
13528
|
- Shadows: \`var(--ggui-shape-shadow-sm)\`, \`var(--ggui-shape-shadow-md)\`, \`var(--ggui-shape-shadow-lg)\`
|
|
@@ -13557,7 +13707,7 @@ Hero sections should feel airy. Use \`padding="xl"\` (32px) on the main card, no
|
|
|
13557
13707
|
|
|
13558
13708
|
- [ ] Imports ONLY from: react, @ggui-ai/design, @ggui-ai/wire
|
|
13559
13709
|
- [ ] No raw HTML elements (\`<button>\`, \`<input>\`, \`<div>\` for layout) \u2014 uses primitives
|
|
13560
|
-
- [ ] ZERO hardcoded hex colors \u2014 every color is \`var(--ggui-color
|
|
13710
|
+
- [ ] ZERO hardcoded hex colors \u2014 every color is a bare \`var(--ggui-color-*)\`, no literal fallback
|
|
13561
13711
|
- [ ] No raw pixel values for spacing \u2014 all via \`var(--ggui-spacing-*)\` tokens
|
|
13562
13712
|
- [ ] Primary palette used throughout (headers, borders, icons) \u2014 not just buttons
|
|
13563
13713
|
- [ ] Typed Props interface exported; request-specific data is a prop with default
|
|
@@ -15523,8 +15673,8 @@ ${cjsResult.code}
|
|
|
15523
15673
|
// src/harness/check/runtime-render/load-component.ts
|
|
15524
15674
|
async function loadComponent(input) {
|
|
15525
15675
|
const { sourceCode, moduleResolutions = {} } = input;
|
|
15526
|
-
const { createRequire:
|
|
15527
|
-
const require_ =
|
|
15676
|
+
const { createRequire: createRequire5 } = await import('module');
|
|
15677
|
+
const require_ = createRequire5(import.meta.url);
|
|
15528
15678
|
const sandboxRequire = (id) => {
|
|
15529
15679
|
if (id in moduleResolutions) return moduleResolutions[id];
|
|
15530
15680
|
if (id === "react/jsx-runtime" || id === "react/jsx-dev-runtime") {
|
|
@@ -15883,6 +16033,14 @@ function parentLink(node) {
|
|
|
15883
16033
|
|
|
15884
16034
|
// src/harness/check/runtime-render/render-check.ts
|
|
15885
16035
|
async function runRenderCheck(input) {
|
|
16036
|
+
const g = hostGlobals();
|
|
16037
|
+
if ("window" in g && "document" in g) {
|
|
16038
|
+
return runRenderCheckInProcess(input);
|
|
16039
|
+
}
|
|
16040
|
+
const { runRenderCheckViaWorker: runRenderCheckViaWorker2 } = await Promise.resolve().then(() => (init_render_check_host(), render_check_host_exports));
|
|
16041
|
+
return runRenderCheckViaWorker2(input);
|
|
16042
|
+
}
|
|
16043
|
+
async function runRenderCheckInProcess(input) {
|
|
15886
16044
|
const t0 = Date.now();
|
|
15887
16045
|
const issues = [];
|
|
15888
16046
|
const teardown = await setupHappyDom();
|
|
@@ -16072,6 +16230,22 @@ async function runRenderCheck(input) {
|
|
|
16072
16230
|
if (issue) issues.push(issue);
|
|
16073
16231
|
}
|
|
16074
16232
|
}
|
|
16233
|
+
if (input.contract?.actionSpec) {
|
|
16234
|
+
for (const [name, entry] of Object.entries(input.contract.actionSpec)) {
|
|
16235
|
+
const idField = idLikeRequiredStringField(
|
|
16236
|
+
entry.schema
|
|
16237
|
+
);
|
|
16238
|
+
if (idField === null) continue;
|
|
16239
|
+
const issue = await checkSelectionIdentity({
|
|
16240
|
+
container,
|
|
16241
|
+
probe,
|
|
16242
|
+
user,
|
|
16243
|
+
actionName: name,
|
|
16244
|
+
idField
|
|
16245
|
+
});
|
|
16246
|
+
if (issue) issues.push(issue);
|
|
16247
|
+
}
|
|
16248
|
+
}
|
|
16075
16249
|
if (input.contract?.propsSpec) {
|
|
16076
16250
|
for (const issue of checkPropCoverage({
|
|
16077
16251
|
container,
|
|
@@ -16081,6 +16255,48 @@ async function runRenderCheck(input) {
|
|
|
16081
16255
|
issues.push(issue);
|
|
16082
16256
|
}
|
|
16083
16257
|
}
|
|
16258
|
+
if (input.contract?.propsSpec) {
|
|
16259
|
+
const propsSpec5c = input.contract.propsSpec;
|
|
16260
|
+
const normalize = (s) => (s ?? "").replace(/\s+/g, " ").trim();
|
|
16261
|
+
const baseText = normalize(container.textContent);
|
|
16262
|
+
for (const [propName, entry] of Object.entries(propsSpec5c.properties)) {
|
|
16263
|
+
if (!entry.required) continue;
|
|
16264
|
+
const orig = input.mockupProps[propName];
|
|
16265
|
+
if (typeof orig !== "string" && typeof orig !== "number") continue;
|
|
16266
|
+
const marker = pickScalarMarker(orig);
|
|
16267
|
+
if (marker === null || !baseText.includes(marker)) continue;
|
|
16268
|
+
const varied = typeof orig === "string" ? "Zzyzx Qw" : orig + 137731;
|
|
16269
|
+
const variedProbe = createProbe();
|
|
16270
|
+
const variedConfig = createProbeWireConfig(variedProbe);
|
|
16271
|
+
let variedResult;
|
|
16272
|
+
try {
|
|
16273
|
+
variedResult = render(
|
|
16274
|
+
React.createElement(GguiWireProvider, {
|
|
16275
|
+
config: variedConfig,
|
|
16276
|
+
children: React.createElement(ProbeErrorBoundary, {
|
|
16277
|
+
children: React.createElement(Component, {
|
|
16278
|
+
...input.mockupProps,
|
|
16279
|
+
[propName]: varied
|
|
16280
|
+
})
|
|
16281
|
+
})
|
|
16282
|
+
})
|
|
16283
|
+
);
|
|
16284
|
+
} catch {
|
|
16285
|
+
continue;
|
|
16286
|
+
}
|
|
16287
|
+
await flushPromises();
|
|
16288
|
+
const variedText = normalize(variedResult.container.textContent);
|
|
16289
|
+
variedResult.unmount();
|
|
16290
|
+
if (variedText === baseText) {
|
|
16291
|
+
issues.push({
|
|
16292
|
+
check: "prop-sensitivity",
|
|
16293
|
+
outcome: "failed",
|
|
16294
|
+
subject: propName,
|
|
16295
|
+
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.`
|
|
16296
|
+
});
|
|
16297
|
+
}
|
|
16298
|
+
}
|
|
16299
|
+
}
|
|
16084
16300
|
if (input.contract?.streamSpec) {
|
|
16085
16301
|
const streamSpec = input.contract.streamSpec;
|
|
16086
16302
|
for (const [name] of Object.entries(streamSpec)) {
|
|
@@ -16412,6 +16628,74 @@ async function checkActionWiring(input) {
|
|
|
16412
16628
|
}
|
|
16413
16629
|
};
|
|
16414
16630
|
}
|
|
16631
|
+
function idLikeRequiredStringField(schema) {
|
|
16632
|
+
if (schema === void 0) return null;
|
|
16633
|
+
const properties = schema["properties"];
|
|
16634
|
+
const required = schema["required"];
|
|
16635
|
+
if (properties === null || typeof properties !== "object") return null;
|
|
16636
|
+
if (!Array.isArray(required)) return null;
|
|
16637
|
+
for (const [propName, propSchema] of Object.entries(
|
|
16638
|
+
properties
|
|
16639
|
+
)) {
|
|
16640
|
+
if (!/id$/i.test(propName)) continue;
|
|
16641
|
+
if (!required.includes(propName)) continue;
|
|
16642
|
+
const t = propSchema !== null && typeof propSchema === "object" ? propSchema["type"] : void 0;
|
|
16643
|
+
if (t === "string") return propName;
|
|
16644
|
+
}
|
|
16645
|
+
return null;
|
|
16646
|
+
}
|
|
16647
|
+
async function checkSelectionIdentity(input) {
|
|
16648
|
+
const { container, probe, user, actionName, idField } = input;
|
|
16649
|
+
const clickables = Array.from(
|
|
16650
|
+
container.querySelectorAll('button, [role="button"]')
|
|
16651
|
+
);
|
|
16652
|
+
const byLabel = /* @__PURE__ */ new Map();
|
|
16653
|
+
for (const el of clickables) {
|
|
16654
|
+
const label = (el.textContent ?? "").replace(/\s+/g, " ").trim();
|
|
16655
|
+
if (label.length === 0) continue;
|
|
16656
|
+
const group = byLabel.get(label);
|
|
16657
|
+
if (group) group.push(el);
|
|
16658
|
+
else byLabel.set(label, [el]);
|
|
16659
|
+
}
|
|
16660
|
+
for (const [label, group] of byLabel) {
|
|
16661
|
+
if (group.length < 2) continue;
|
|
16662
|
+
const payloadFor = async (el) => {
|
|
16663
|
+
const before = probe.getFireLog().length;
|
|
16664
|
+
try {
|
|
16665
|
+
await user.click(el);
|
|
16666
|
+
} catch {
|
|
16667
|
+
return void 0;
|
|
16668
|
+
}
|
|
16669
|
+
await flushPromises();
|
|
16670
|
+
const events = probe.getFireLog().slice(before).filter(
|
|
16671
|
+
(e) => e.kind === "action.fired" && e.name === actionName
|
|
16672
|
+
);
|
|
16673
|
+
return events.length > 0 ? events[events.length - 1].payload : void 0;
|
|
16674
|
+
};
|
|
16675
|
+
const first = await payloadFor(group[0]);
|
|
16676
|
+
const second = await payloadFor(group[1]);
|
|
16677
|
+
if (first === void 0 || second === void 0) continue;
|
|
16678
|
+
const idOf = (payload) => {
|
|
16679
|
+
if (payload === null || typeof payload !== "object") return void 0;
|
|
16680
|
+
const v = payload[idField];
|
|
16681
|
+
return typeof v === "string" ? v : void 0;
|
|
16682
|
+
};
|
|
16683
|
+
const firstId = idOf(first);
|
|
16684
|
+
const secondId = idOf(second);
|
|
16685
|
+
if (firstId === void 0 || secondId === void 0) continue;
|
|
16686
|
+
if (firstId === secondId) {
|
|
16687
|
+
return {
|
|
16688
|
+
check: "selection-identity",
|
|
16689
|
+
outcome: "failed",
|
|
16690
|
+
subject: actionName,
|
|
16691
|
+
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}".`,
|
|
16692
|
+
elementHint: describeElement(group[0])
|
|
16693
|
+
};
|
|
16694
|
+
}
|
|
16695
|
+
return null;
|
|
16696
|
+
}
|
|
16697
|
+
return null;
|
|
16698
|
+
}
|
|
16415
16699
|
async function simulateAndCheck(input) {
|
|
16416
16700
|
const { container, user, probe, wiringKind, actionName, actionLabel, eventKind } = input;
|
|
16417
16701
|
const candidates = findCandidateElements(container, wiringKind, actionName, actionLabel);
|
|
@@ -16910,6 +17194,26 @@ function toEvalIssue(issue) {
|
|
|
16910
17194
|
fix
|
|
16911
17195
|
};
|
|
16912
17196
|
}
|
|
17197
|
+
case "selection-identity":
|
|
17198
|
+
return {
|
|
17199
|
+
tier: 0,
|
|
17200
|
+
result,
|
|
17201
|
+
category: "contract",
|
|
17202
|
+
subcategory,
|
|
17203
|
+
severity: "critical",
|
|
17204
|
+
description: `${issue.reason}${elementHint}${diagSuffix}`,
|
|
17205
|
+
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.`
|
|
17206
|
+
};
|
|
17207
|
+
case "prop-sensitivity":
|
|
17208
|
+
return {
|
|
17209
|
+
tier: 0,
|
|
17210
|
+
result,
|
|
17211
|
+
category: "contract",
|
|
17212
|
+
subcategory,
|
|
17213
|
+
severity: "critical",
|
|
17214
|
+
description: `${issue.reason}${diagSuffix}`,
|
|
17215
|
+
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.`
|
|
17216
|
+
};
|
|
16913
17217
|
case "prop-coverage":
|
|
16914
17218
|
return {
|
|
16915
17219
|
tier: 0,
|
|
@@ -19826,12 +20130,12 @@ async function runSlowStage(input) {
|
|
|
19826
20130
|
};
|
|
19827
20131
|
}
|
|
19828
20132
|
function scoreFromRenderCheck(issues) {
|
|
19829
|
-
const
|
|
19830
|
-
const
|
|
19831
|
-
if (
|
|
19832
|
-
if (
|
|
19833
|
-
if (
|
|
19834
|
-
if (
|
|
20133
|
+
const failed2 = issues.filter((i) => i.outcome === "failed").length;
|
|
20134
|
+
const unverified2 = issues.filter((i) => i.outcome === "unverified").length;
|
|
20135
|
+
if (failed2 === 0 && unverified2 === 0) return 1;
|
|
20136
|
+
if (failed2 === 0) return 0.85;
|
|
20137
|
+
if (failed2 === 1) return 0.4;
|
|
20138
|
+
if (failed2 === 2) return 0.3;
|
|
19835
20139
|
return 0.2;
|
|
19836
20140
|
}
|
|
19837
20141
|
function scoreFromSlowStage(contract, failures) {
|