@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
|
@@ -1,8 +1,158 @@
|
|
|
1
|
+
import { existsSync } from 'fs';
|
|
1
2
|
import { createRequire } from 'module';
|
|
3
|
+
import { fileURLToPath, pathToFileURL } from 'url';
|
|
4
|
+
import { runSandboxed } from '@ggui-ai/sandbox';
|
|
2
5
|
import { listContractGadgets, HOOK_NAME_RE } from '@ggui-ai/protocol';
|
|
3
6
|
import ts from 'typescript';
|
|
4
7
|
|
|
5
|
-
|
|
8
|
+
var __defProp = Object.defineProperty;
|
|
9
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
10
|
+
var __esm = (fn, res, err) => function __init() {
|
|
11
|
+
if (err) throw err[0];
|
|
12
|
+
try {
|
|
13
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
14
|
+
} catch (e) {
|
|
15
|
+
throw err = [e], e;
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
var __export = (target, all) => {
|
|
19
|
+
for (var name in all)
|
|
20
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
// src/harness/check/runtime-render/render-check-host.ts
|
|
24
|
+
var render_check_host_exports = {};
|
|
25
|
+
__export(render_check_host_exports, {
|
|
26
|
+
mapSandboxResultToCheckResult: () => mapSandboxResultToCheckResult,
|
|
27
|
+
runRenderCheckViaWorker: () => runRenderCheckViaWorker
|
|
28
|
+
});
|
|
29
|
+
function resolveWorkerSpawn() {
|
|
30
|
+
const jsCandidates = [
|
|
31
|
+
// Sibling — when import.meta.url is already .../harness/check/runtime-render/.
|
|
32
|
+
new URL(`./${WORKER_BASENAME}.js`, import.meta.url),
|
|
33
|
+
// From dist/index.js.
|
|
34
|
+
new URL(`./harness/check/runtime-render/${WORKER_BASENAME}.js`, import.meta.url),
|
|
35
|
+
// From dist/harness/index.js.
|
|
36
|
+
new URL(`./check/runtime-render/${WORKER_BASENAME}.js`, import.meta.url),
|
|
37
|
+
// From dist/check/index.js, dist/advanced/index.js, dist/<flat>.js.
|
|
38
|
+
new URL(`../harness/check/runtime-render/${WORKER_BASENAME}.js`, import.meta.url)
|
|
39
|
+
];
|
|
40
|
+
for (const jsUrl of jsCandidates) {
|
|
41
|
+
const jsPath = fileURLToPath(jsUrl);
|
|
42
|
+
if (existsSync(jsPath)) {
|
|
43
|
+
return { command: process.execPath, args: [jsPath] };
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
const tsCandidates = [
|
|
47
|
+
new URL(`./${WORKER_BASENAME}.ts`, import.meta.url),
|
|
48
|
+
new URL(`../harness/check/runtime-render/${WORKER_BASENAME}.ts`, import.meta.url)
|
|
49
|
+
];
|
|
50
|
+
for (const tsUrl of tsCandidates) {
|
|
51
|
+
const tsPath = fileURLToPath(tsUrl);
|
|
52
|
+
if (existsSync(tsPath)) {
|
|
53
|
+
const require_ = createRequire(import.meta.url);
|
|
54
|
+
const tsxLoader = pathToFileURL(require_.resolve("tsx")).href;
|
|
55
|
+
return { command: process.execPath, args: ["--import", tsxLoader, tsPath] };
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
const tried = [...jsCandidates, ...tsCandidates].map((u) => fileURLToPath(u)).join(", ");
|
|
59
|
+
throw new Error(
|
|
60
|
+
`render-check: isolated worker not found at any of: ${tried}. Did \`pnpm build\` run?`
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
function failed(reason, t0) {
|
|
64
|
+
return {
|
|
65
|
+
ok: false,
|
|
66
|
+
issues: [{ check: "render-no-throw", outcome: "failed", reason }],
|
|
67
|
+
stats: { actionsChecked: 0, streamsChecked: 0, renderMs: Date.now() - t0 }
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
function unverified(reason, t0) {
|
|
71
|
+
return {
|
|
72
|
+
ok: false,
|
|
73
|
+
issues: [{ check: "render-no-throw", outcome: "unverified", reason }],
|
|
74
|
+
stats: { actionsChecked: 0, streamsChecked: 0, renderMs: Date.now() - t0 }
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
function mapSandboxResultToCheckResult(result, t0) {
|
|
78
|
+
if (result.outcome === "timeout") {
|
|
79
|
+
return failed(
|
|
80
|
+
`render check timed out after ${CHECK_TIMEOUT_MS}ms in the isolated worker (likely infinite loop or runaway effect in the component).`,
|
|
81
|
+
t0
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
if (result.outcome === "overflow-stdout" || result.outcome === "overflow-stderr") {
|
|
85
|
+
return failed(
|
|
86
|
+
`render check worker produced excessive output (${result.outcome}) \u2014 the component is likely in a pathological logging or error loop.`,
|
|
87
|
+
t0
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
if (result.outcome === "spawn-error") {
|
|
91
|
+
return unverified(
|
|
92
|
+
`render check worker failed to start \u2014 ${result.errorMessage}`,
|
|
93
|
+
t0
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
if (result.outcome === "canceled") {
|
|
97
|
+
return unverified("render check worker was canceled before completing.", t0);
|
|
98
|
+
}
|
|
99
|
+
if (result.outcome !== "exit") {
|
|
100
|
+
return unverified(
|
|
101
|
+
`render check worker ended with unexpected sandbox outcome '${result.outcome}'.`,
|
|
102
|
+
t0
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
if (result.exitCode !== 0) {
|
|
106
|
+
const tail = result.stderr.trim() || result.stdout.trim();
|
|
107
|
+
return unverified(
|
|
108
|
+
`render check worker exited ${result.exitCode}${tail ? ` \u2014 ${tail.slice(0, 600)}` : ""}`,
|
|
109
|
+
t0
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
const stdout = result.stdout.trim();
|
|
113
|
+
if (stdout.length === 0) {
|
|
114
|
+
return unverified("render check worker exited without producing a verdict.", t0);
|
|
115
|
+
}
|
|
116
|
+
try {
|
|
117
|
+
return JSON.parse(stdout);
|
|
118
|
+
} catch (err) {
|
|
119
|
+
return unverified(
|
|
120
|
+
`render check worker verdict was not valid JSON \u2014 ${err instanceof Error ? err.message : String(err)}`,
|
|
121
|
+
t0
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
async function runRenderCheckViaWorker(input) {
|
|
126
|
+
const t0 = Date.now();
|
|
127
|
+
let spawn;
|
|
128
|
+
try {
|
|
129
|
+
spawn = resolveWorkerSpawn();
|
|
130
|
+
} catch (err) {
|
|
131
|
+
return unverified(err instanceof Error ? err.message : String(err), t0);
|
|
132
|
+
}
|
|
133
|
+
const result = await runSandboxed({
|
|
134
|
+
command: spawn.command,
|
|
135
|
+
args: spawn.args,
|
|
136
|
+
timeoutMs: CHECK_TIMEOUT_MS,
|
|
137
|
+
maxStdoutBytes: CHECK_STDOUT_CAP,
|
|
138
|
+
nodeHeapMb: CHECK_NODE_HEAP_MB,
|
|
139
|
+
stdin: JSON.stringify(input),
|
|
140
|
+
// NODE_ENV steers React's production vs development build — keep
|
|
141
|
+
// parity with the caller; everything else stays on the sandbox's
|
|
142
|
+
// default allowlist.
|
|
143
|
+
env: { NODE_ENV: process.env.NODE_ENV ?? "production" }
|
|
144
|
+
});
|
|
145
|
+
return mapSandboxResultToCheckResult(result, t0);
|
|
146
|
+
}
|
|
147
|
+
var CHECK_TIMEOUT_MS, CHECK_STDOUT_CAP, CHECK_NODE_HEAP_MB, WORKER_BASENAME;
|
|
148
|
+
var init_render_check_host = __esm({
|
|
149
|
+
"src/harness/check/runtime-render/render-check-host.ts"() {
|
|
150
|
+
CHECK_TIMEOUT_MS = 3e4;
|
|
151
|
+
CHECK_STDOUT_CAP = 2 * 1024 * 1024;
|
|
152
|
+
CHECK_NODE_HEAP_MB = 512;
|
|
153
|
+
WORKER_BASENAME = "render-check-worker";
|
|
154
|
+
}
|
|
155
|
+
});
|
|
6
156
|
var installed = false;
|
|
7
157
|
function installProductionActShim() {
|
|
8
158
|
if (installed) return;
|
|
@@ -205,8 +355,8 @@ ${cjsResult.code}
|
|
|
205
355
|
// src/harness/check/runtime-render/load-component.ts
|
|
206
356
|
async function loadComponent(input) {
|
|
207
357
|
const { sourceCode, moduleResolutions = {} } = input;
|
|
208
|
-
const { createRequire:
|
|
209
|
-
const require_ =
|
|
358
|
+
const { createRequire: createRequire3 } = await import('module');
|
|
359
|
+
const require_ = createRequire3(import.meta.url);
|
|
210
360
|
const sandboxRequire = (id) => {
|
|
211
361
|
if (id in moduleResolutions) return moduleResolutions[id];
|
|
212
362
|
if (id === "react/jsx-runtime" || id === "react/jsx-dev-runtime") {
|
|
@@ -524,6 +674,14 @@ function parentLink(node) {
|
|
|
524
674
|
|
|
525
675
|
// src/harness/check/runtime-render/render-check.ts
|
|
526
676
|
async function runRenderCheck(input) {
|
|
677
|
+
const g = hostGlobals();
|
|
678
|
+
if ("window" in g && "document" in g) {
|
|
679
|
+
return runRenderCheckInProcess(input);
|
|
680
|
+
}
|
|
681
|
+
const { runRenderCheckViaWorker: runRenderCheckViaWorker2 } = await Promise.resolve().then(() => (init_render_check_host(), render_check_host_exports));
|
|
682
|
+
return runRenderCheckViaWorker2(input);
|
|
683
|
+
}
|
|
684
|
+
async function runRenderCheckInProcess(input) {
|
|
527
685
|
const t0 = Date.now();
|
|
528
686
|
const issues = [];
|
|
529
687
|
const teardown = await setupHappyDom();
|
|
@@ -713,6 +871,22 @@ async function runRenderCheck(input) {
|
|
|
713
871
|
if (issue) issues.push(issue);
|
|
714
872
|
}
|
|
715
873
|
}
|
|
874
|
+
if (input.contract?.actionSpec) {
|
|
875
|
+
for (const [name, entry] of Object.entries(input.contract.actionSpec)) {
|
|
876
|
+
const idField = idLikeRequiredStringField(
|
|
877
|
+
entry.schema
|
|
878
|
+
);
|
|
879
|
+
if (idField === null) continue;
|
|
880
|
+
const issue = await checkSelectionIdentity({
|
|
881
|
+
container,
|
|
882
|
+
probe,
|
|
883
|
+
user,
|
|
884
|
+
actionName: name,
|
|
885
|
+
idField
|
|
886
|
+
});
|
|
887
|
+
if (issue) issues.push(issue);
|
|
888
|
+
}
|
|
889
|
+
}
|
|
716
890
|
if (input.contract?.propsSpec) {
|
|
717
891
|
for (const issue of checkPropCoverage({
|
|
718
892
|
container,
|
|
@@ -722,6 +896,48 @@ async function runRenderCheck(input) {
|
|
|
722
896
|
issues.push(issue);
|
|
723
897
|
}
|
|
724
898
|
}
|
|
899
|
+
if (input.contract?.propsSpec) {
|
|
900
|
+
const propsSpec5c = input.contract.propsSpec;
|
|
901
|
+
const normalize = (s) => (s ?? "").replace(/\s+/g, " ").trim();
|
|
902
|
+
const baseText = normalize(container.textContent);
|
|
903
|
+
for (const [propName, entry] of Object.entries(propsSpec5c.properties)) {
|
|
904
|
+
if (!entry.required) continue;
|
|
905
|
+
const orig = input.mockupProps[propName];
|
|
906
|
+
if (typeof orig !== "string" && typeof orig !== "number") continue;
|
|
907
|
+
const marker = pickScalarMarker(orig);
|
|
908
|
+
if (marker === null || !baseText.includes(marker)) continue;
|
|
909
|
+
const varied = typeof orig === "string" ? "Zzyzx Qw" : orig + 137731;
|
|
910
|
+
const variedProbe = createProbe();
|
|
911
|
+
const variedConfig = createProbeWireConfig(variedProbe);
|
|
912
|
+
let variedResult;
|
|
913
|
+
try {
|
|
914
|
+
variedResult = render(
|
|
915
|
+
React.createElement(GguiWireProvider, {
|
|
916
|
+
config: variedConfig,
|
|
917
|
+
children: React.createElement(ProbeErrorBoundary, {
|
|
918
|
+
children: React.createElement(Component, {
|
|
919
|
+
...input.mockupProps,
|
|
920
|
+
[propName]: varied
|
|
921
|
+
})
|
|
922
|
+
})
|
|
923
|
+
})
|
|
924
|
+
);
|
|
925
|
+
} catch {
|
|
926
|
+
continue;
|
|
927
|
+
}
|
|
928
|
+
await flushPromises();
|
|
929
|
+
const variedText = normalize(variedResult.container.textContent);
|
|
930
|
+
variedResult.unmount();
|
|
931
|
+
if (variedText === baseText) {
|
|
932
|
+
issues.push({
|
|
933
|
+
check: "prop-sensitivity",
|
|
934
|
+
outcome: "failed",
|
|
935
|
+
subject: propName,
|
|
936
|
+
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.`
|
|
937
|
+
});
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
}
|
|
725
941
|
if (input.contract?.streamSpec) {
|
|
726
942
|
const streamSpec = input.contract.streamSpec;
|
|
727
943
|
for (const [name] of Object.entries(streamSpec)) {
|
|
@@ -1053,6 +1269,74 @@ async function checkActionWiring(input) {
|
|
|
1053
1269
|
}
|
|
1054
1270
|
};
|
|
1055
1271
|
}
|
|
1272
|
+
function idLikeRequiredStringField(schema) {
|
|
1273
|
+
if (schema === void 0) return null;
|
|
1274
|
+
const properties = schema["properties"];
|
|
1275
|
+
const required = schema["required"];
|
|
1276
|
+
if (properties === null || typeof properties !== "object") return null;
|
|
1277
|
+
if (!Array.isArray(required)) return null;
|
|
1278
|
+
for (const [propName, propSchema] of Object.entries(
|
|
1279
|
+
properties
|
|
1280
|
+
)) {
|
|
1281
|
+
if (!/id$/i.test(propName)) continue;
|
|
1282
|
+
if (!required.includes(propName)) continue;
|
|
1283
|
+
const t = propSchema !== null && typeof propSchema === "object" ? propSchema["type"] : void 0;
|
|
1284
|
+
if (t === "string") return propName;
|
|
1285
|
+
}
|
|
1286
|
+
return null;
|
|
1287
|
+
}
|
|
1288
|
+
async function checkSelectionIdentity(input) {
|
|
1289
|
+
const { container, probe, user, actionName, idField } = input;
|
|
1290
|
+
const clickables = Array.from(
|
|
1291
|
+
container.querySelectorAll('button, [role="button"]')
|
|
1292
|
+
);
|
|
1293
|
+
const byLabel = /* @__PURE__ */ new Map();
|
|
1294
|
+
for (const el of clickables) {
|
|
1295
|
+
const label = (el.textContent ?? "").replace(/\s+/g, " ").trim();
|
|
1296
|
+
if (label.length === 0) continue;
|
|
1297
|
+
const group = byLabel.get(label);
|
|
1298
|
+
if (group) group.push(el);
|
|
1299
|
+
else byLabel.set(label, [el]);
|
|
1300
|
+
}
|
|
1301
|
+
for (const [label, group] of byLabel) {
|
|
1302
|
+
if (group.length < 2) continue;
|
|
1303
|
+
const payloadFor = async (el) => {
|
|
1304
|
+
const before = probe.getFireLog().length;
|
|
1305
|
+
try {
|
|
1306
|
+
await user.click(el);
|
|
1307
|
+
} catch {
|
|
1308
|
+
return void 0;
|
|
1309
|
+
}
|
|
1310
|
+
await flushPromises();
|
|
1311
|
+
const events = probe.getFireLog().slice(before).filter(
|
|
1312
|
+
(e) => e.kind === "action.fired" && e.name === actionName
|
|
1313
|
+
);
|
|
1314
|
+
return events.length > 0 ? events[events.length - 1].payload : void 0;
|
|
1315
|
+
};
|
|
1316
|
+
const first = await payloadFor(group[0]);
|
|
1317
|
+
const second = await payloadFor(group[1]);
|
|
1318
|
+
if (first === void 0 || second === void 0) continue;
|
|
1319
|
+
const idOf = (payload) => {
|
|
1320
|
+
if (payload === null || typeof payload !== "object") return void 0;
|
|
1321
|
+
const v = payload[idField];
|
|
1322
|
+
return typeof v === "string" ? v : void 0;
|
|
1323
|
+
};
|
|
1324
|
+
const firstId = idOf(first);
|
|
1325
|
+
const secondId = idOf(second);
|
|
1326
|
+
if (firstId === void 0 || secondId === void 0) continue;
|
|
1327
|
+
if (firstId === secondId) {
|
|
1328
|
+
return {
|
|
1329
|
+
check: "selection-identity",
|
|
1330
|
+
outcome: "failed",
|
|
1331
|
+
subject: actionName,
|
|
1332
|
+
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}".`,
|
|
1333
|
+
elementHint: describeElement(group[0])
|
|
1334
|
+
};
|
|
1335
|
+
}
|
|
1336
|
+
return null;
|
|
1337
|
+
}
|
|
1338
|
+
return null;
|
|
1339
|
+
}
|
|
1056
1340
|
async function simulateAndCheck(input) {
|
|
1057
1341
|
const { container, user, probe, wiringKind, actionName, actionLabel, eventKind } = input;
|
|
1058
1342
|
const candidates = findCandidateElements(container, wiringKind, actionName, actionLabel);
|
|
@@ -1536,6 +1820,26 @@ function toEvalIssue(issue) {
|
|
|
1536
1820
|
fix
|
|
1537
1821
|
};
|
|
1538
1822
|
}
|
|
1823
|
+
case "selection-identity":
|
|
1824
|
+
return {
|
|
1825
|
+
tier: 0,
|
|
1826
|
+
result,
|
|
1827
|
+
category: "contract",
|
|
1828
|
+
subcategory,
|
|
1829
|
+
severity: "critical",
|
|
1830
|
+
description: `${issue.reason}${elementHint}${diagSuffix}`,
|
|
1831
|
+
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.`
|
|
1832
|
+
};
|
|
1833
|
+
case "prop-sensitivity":
|
|
1834
|
+
return {
|
|
1835
|
+
tier: 0,
|
|
1836
|
+
result,
|
|
1837
|
+
category: "contract",
|
|
1838
|
+
subcategory,
|
|
1839
|
+
severity: "critical",
|
|
1840
|
+
description: `${issue.reason}${diagSuffix}`,
|
|
1841
|
+
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.`
|
|
1842
|
+
};
|
|
1539
1843
|
case "prop-coverage":
|
|
1540
1844
|
return {
|
|
1541
1845
|
tier: 0,
|