@ggui-ai/ui-gen 0.22.0 → 0.24.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 (90) hide show
  1. package/dist/adapter-Du7P_5Fx.d.ts +201 -0
  2. package/dist/adapters/generation-dispatch.d.ts +33 -12
  3. package/dist/adapters/generation-dispatch.js +549 -129
  4. package/dist/adapters/generation-dispatch.js.map +1 -1
  5. package/dist/adapters/index.js +1 -1
  6. package/dist/adapters/index.js.map +1 -1
  7. package/dist/advanced/index.d.ts +5 -3
  8. package/dist/advanced/index.js +601 -144
  9. package/dist/advanced/index.js.map +1 -1
  10. package/dist/blueprint-validator.js +178 -54
  11. package/dist/blueprint-validator.js.map +1 -1
  12. package/dist/boilerplate.js +1 -0
  13. package/dist/boilerplate.js.map +1 -1
  14. package/dist/check/index.d.ts +2 -1
  15. package/dist/check/index.js +47 -1
  16. package/dist/check/index.js.map +1 -1
  17. package/dist/{create-ui-generator-BEkzEtNz.d.ts → create-ui-generator-GIhzx08x.d.ts} +34 -10
  18. package/dist/{dispatch-DJNpQ7DI.d.ts → dispatch-BnAH56p4.d.ts} +1 -1
  19. package/dist/evaluation/axis-checks/index.d.ts +4 -3
  20. package/dist/evaluation/axis-checks/index.js +19 -4
  21. package/dist/evaluation/axis-checks/index.js.map +1 -1
  22. package/dist/evaluation/axis-checks/registry.d.ts +2 -1
  23. package/dist/evaluation/axis-checks/registry.js +19 -4
  24. package/dist/evaluation/axis-checks/registry.js.map +1 -1
  25. package/dist/evaluation/index.d.ts +4 -3
  26. package/dist/evaluation/index.js +21 -6
  27. package/dist/evaluation/index.js.map +1 -1
  28. package/dist/harness/check/runtime-render/index.d.ts +27 -8
  29. package/dist/harness/check/runtime-render/index.js +217 -54
  30. package/dist/harness/check/runtime-render/index.js.map +1 -1
  31. package/dist/harness/check/runtime-render/render-check-worker.d.ts +8 -1
  32. package/dist/harness/check/runtime-render/render-check-worker.js +72 -32
  33. package/dist/harness/check/runtime-render/render-check-worker.js.map +1 -1
  34. package/dist/harness/index.d.ts +2 -1
  35. package/dist/harness/index.js +3 -2
  36. package/dist/harness/index.js.map +1 -1
  37. package/dist/harness/prompts.js.map +1 -1
  38. package/dist/harness/result-types.d.ts +2 -1
  39. package/dist/harness/runtime.d.ts +2 -1
  40. package/dist/harness/runtime.js +2 -1
  41. package/dist/harness/runtime.js.map +1 -1
  42. package/dist/harness/types-public.d.ts +18 -5
  43. package/dist/harness/types-public.js +1 -0
  44. package/dist/harness/types-public.js.map +1 -1
  45. package/dist/harness/validator-trace-sink.d.ts +2 -1
  46. package/dist/index.d.ts +38 -3
  47. package/dist/index.js +592 -144
  48. package/dist/index.js.map +1 -1
  49. package/dist/patch.d.ts +2 -1
  50. package/dist/policy.d.ts +7 -4
  51. package/dist/policy.js.map +1 -1
  52. package/dist/tools/get-primitives-ts.d.ts +1 -1
  53. package/dist/tools/get-primitives-ts.js +1 -1
  54. package/dist/tools/get-primitives-ts.js.map +1 -1
  55. package/dist/tools/render-check-worker.d.ts +7 -1
  56. package/dist/tools/render-check-worker.js +55 -23
  57. package/dist/tools/render-check-worker.js.map +1 -1
  58. package/dist/{types-public-mM_-1Yk3.d.ts → types-public-Bk6d1WLN.d.ts} +98 -9
  59. package/dist/validation/index.d.ts +1 -1
  60. package/dist/validation/index.js +1 -1
  61. package/dist/validation/index.js.map +1 -1
  62. package/dist/workflows.d.ts +2 -1
  63. package/package.json +12 -12
  64. package/src/adapters/generation-dispatch.ts +137 -96
  65. package/src/advanced/generator.ts +16 -1
  66. package/src/blueprint-validator.ts +21 -6
  67. package/src/boilerplate/hard-sections.ts +1 -0
  68. package/src/check/run-tier0.ts +80 -1
  69. package/src/coding-agent/tools.ts +6 -1
  70. package/src/create-ui-generator.ts +185 -95
  71. package/src/evaluation/axis-checks/checks/state-payload.ts +29 -4
  72. package/src/evaluation/types-public.ts +94 -8
  73. package/src/harness/check/runtime-render/adapter.ts +223 -58
  74. package/src/harness/check/runtime-render/index.ts +75 -2
  75. package/src/harness/check/runtime-render/render-check-host.ts +111 -26
  76. package/src/harness/check/runtime-render/render-check-worker.ts +83 -29
  77. package/src/harness/check/runtime-render/render-check.ts +173 -117
  78. package/src/harness/coding/generate-task-runner.ts +57 -4
  79. package/src/harness/coding/init-session.ts +13 -0
  80. package/src/harness/coding/run-eval-round.ts +238 -13
  81. package/src/harness/llm-router.ts +3 -3
  82. package/src/harness/policy.ts +11 -0
  83. package/src/harness/primitive-slice.ts +104 -0
  84. package/src/harness/types-public.ts +17 -5
  85. package/src/index.ts +4 -0
  86. package/src/policy.ts +7 -4
  87. package/src/tools/get-primitives-ts.ts +1 -1
  88. package/src/tools/render-check-worker.ts +74 -23
  89. package/src/validation/primitives.ts +1 -1
  90. package/dist/render-check-C0CqVceF.d.ts +0 -87
@@ -11,13 +11,24 @@
11
11
  * `unverified`, and nothing throws):
12
12
  *
13
13
  * - worker exit 0 + parseable verdict → the verdict, verbatim.
14
- * - timeout → `failed` (runaway component: the
15
- * in-process check has no comparable wall-clock bound, the
16
- * subprocess finally gives us one).
17
- * - spawn-error / overflow / non-zero exit / unparseable stdout
14
+ * - timeout → `incomplete` (ggui#1299): the check
15
+ * did not finish, which is NOT a verdict on the component. The bound
16
+ * is wall-clock, and on a contended host an ordinary component
17
+ * crosses it; mapping it to `failed` reported crashes that never
18
+ * happened. It carries its elapsed ms, and the host load (below)
19
+ * says how busy the host was. React's own runaway guards ("Too many
20
+ * re-renders", "Maximum update depth exceeded") throw, so they still
21
+ * reach `render-no-throw` as `failed`; what now reads `incomplete`
22
+ * is a check that ran out of wall-clock time, whatever the cause.
23
+ * - overflow → `failed` (a pathological logging or
24
+ * error loop; output volume does not depend on host load).
25
+ * - spawn-error / non-zero exit / unparseable stdout
18
26
  * → `unverified` (the harness could not
19
27
  * run the check; never blame the component for our plumbing).
20
28
  *
29
+ * Every isolated check records the host's 1-minute load average at its
30
+ * start and end, and the CPUs available, on `stats.hostLoad`.
31
+ *
21
32
  * Spawn resolution copies the proven `src/tools/render-check.ts`
22
33
  * pattern: prefer the built `dist/.../render-check-worker.js`; in
23
34
  * dev/test (vitest imports `src/`), fall back to `node --import tsx`
@@ -29,18 +40,78 @@
29
40
  */
30
41
  import { existsSync } from 'node:fs';
31
42
  import { createRequire } from 'node:module';
43
+ import { availableParallelism, loadavg } from 'node:os';
32
44
  import { fileURLToPath, pathToFileURL } from 'node:url';
33
45
  import { runSandboxed } from '@ggui-ai/sandbox';
34
46
  import type { SandboxResult } from '@ggui-ai/sandbox';
47
+ import type { ProbeHostLoad } from '../../../evaluation/types-public.js';
35
48
  import type { RenderCheckResult, RunRenderCheckInput } from './render-check.js';
36
49
 
37
50
  /**
38
- * Wall-clock bound for one isolated check. The rich probe renders,
39
- * clicks every declared action, and re-renders streams — slower than
40
- * the smoke worker's 12s budget; 30s bounds runaway components while
41
- * staying far under any generation-turn budget.
51
+ * The subprocess bounds of one isolated check (ggui#1380). They are
52
+ * INPUTS of the host, not constants: a serving deployment runs the probe
53
+ * under its own memory and latency budget and sets them per check
54
+ * instance (`createRuntimeRenderCheck`); the evaluation lane runs on the
55
+ * defaults below, which are the values the host always used.
56
+ */
57
+ export interface RenderCheckHostBounds {
58
+ /**
59
+ * Wall-clock bound for one isolated check. The rich probe renders,
60
+ * clicks every declared action, and re-renders streams — slower than
61
+ * the smoke worker's 12s budget; 30s bounds runaway components while
62
+ * staying far under any generation-turn budget.
63
+ */
64
+ readonly timeoutMs: number;
65
+ /**
66
+ * V8 heap cap. The worker realm loads react-dom, happy-dom and
67
+ * testing-library on top of the component itself; 512 MB leaves
68
+ * headroom while still bounding pathological allocation.
69
+ */
70
+ readonly heapMb: number;
71
+ /**
72
+ * Grace between SIGTERM and SIGKILL once the bound is crossed. The
73
+ * effective value is `min(gracePeriodMs, floor(timeoutMs / 2))` —
74
+ * `runSandboxed` requires the grace to be strictly below the bound, so
75
+ * a short bound never trips its RangeError.
76
+ */
77
+ readonly gracePeriodMs: number;
78
+ }
79
+
80
+ export const DEFAULT_RENDER_CHECK_HOST_BOUNDS: RenderCheckHostBounds = {
81
+ timeoutMs: 30_000,
82
+ heapMb: 512,
83
+ gracePeriodMs: 2_000,
84
+ };
85
+
86
+ /**
87
+ * Fill the caller's partial bounds from the defaults and clamp the grace
88
+ * below the bound. Exported so the kill-path pin can run a fixture under
89
+ * exactly the numbers the host would pass for the same input.
42
90
  */
43
- const CHECK_TIMEOUT_MS = 30_000;
91
+ export function resolveRenderCheckHostBounds(
92
+ bounds: Partial<RenderCheckHostBounds> = {},
93
+ ): RenderCheckHostBounds {
94
+ const timeoutMs = bounds.timeoutMs ?? DEFAULT_RENDER_CHECK_HOST_BOUNDS.timeoutMs;
95
+ const heapMb = bounds.heapMb ?? DEFAULT_RENDER_CHECK_HOST_BOUNDS.heapMb;
96
+ // Refused at construction, like `maxConcurrent`: a 0 / NaN / fractional
97
+ // bound would otherwise flow into the sandbox's flags as a nonsense number.
98
+ // `timeoutMs` needs room for a grace of at least 1 ms below it.
99
+ if (!Number.isInteger(timeoutMs) || timeoutMs < 2) {
100
+ throw new RangeError(`render-check: timeoutMs must be an integer ≥ 2, got ${timeoutMs}`);
101
+ }
102
+ if (!Number.isInteger(heapMb) || heapMb < 1) {
103
+ throw new RangeError(`render-check: heapMb must be a positive integer, got ${heapMb}`);
104
+ }
105
+ const requestedGrace = bounds.gracePeriodMs ?? DEFAULT_RENDER_CHECK_HOST_BOUNDS.gracePeriodMs;
106
+ const gracePeriodMs = Math.max(1, Math.min(requestedGrace, Math.floor(timeoutMs / 2)));
107
+ return { timeoutMs, heapMb, gracePeriodMs };
108
+ }
109
+
110
+ /** Options of {@link runRenderCheckViaWorker}. */
111
+ export interface RenderCheckHostOptions {
112
+ /** Subprocess bounds; every field omitted falls to {@link DEFAULT_RENDER_CHECK_HOST_BOUNDS}. */
113
+ readonly bounds?: Partial<RenderCheckHostBounds>;
114
+ }
44
115
 
45
116
  /**
46
117
  * stdout cap. The verdict JSON carries issue lists with diagnostics
@@ -49,16 +120,14 @@ const CHECK_TIMEOUT_MS = 30_000;
49
120
  */
50
121
  const CHECK_STDOUT_CAP = 2 * 1024 * 1024;
51
122
 
52
- /**
53
- * V8 heap cap. The worker realm loads react-dom, happy-dom and
54
- * testing-library on top of the component itself; 512 MB leaves
55
- * headroom while still bounding pathological allocation.
56
- */
57
- const CHECK_NODE_HEAP_MB = 512;
58
-
59
123
  const WORKER_BASENAME = 'render-check-worker';
60
124
 
61
- function resolveWorkerSpawn(): { command: string; args: string[] } {
125
+ /**
126
+ * How the host spawns the worker from THIS location: the built dist sibling
127
+ * when one exists, else the source `.ts` through tsx. Exported so the entry
128
+ * guard can be pinned with the same spawn the host makes (no build needed).
129
+ */
130
+ export function resolveWorkerSpawn(): { command: string; args: string[] } {
62
131
  const jsCandidates = [
63
132
  // Sibling — when import.meta.url is already .../harness/check/runtime-render/.
64
133
  new URL(`./${WORKER_BASENAME}.js`, import.meta.url),
@@ -113,18 +182,22 @@ function unverified(reason: string, t0: number): RenderCheckResult {
113
182
  }
114
183
 
115
184
  /**
116
- * Map one sandbox result onto a `RenderCheckResult`. Exported for the
117
- * unit pins — the spawn itself is exercised by the integration test.
185
+ * Map one sandbox result onto a `RenderCheckResult`. `boundMs` is the
186
+ * wall-clock bound the run was given, reported on a timeout. Exported for
187
+ * the unit pins — the spawn itself is exercised by the integration test.
118
188
  */
119
189
  export function mapSandboxResultToCheckResult(
120
190
  result: SandboxResult,
121
191
  t0: number,
192
+ boundMs: number = DEFAULT_RENDER_CHECK_HOST_BOUNDS.timeoutMs,
122
193
  ): RenderCheckResult {
123
194
  if (result.outcome === 'timeout') {
124
- return failed(
125
- `render check timed out after ${CHECK_TIMEOUT_MS}ms in the isolated worker (likely infinite loop or runaway effect in the component).`,
126
- t0,
127
- );
195
+ return {
196
+ ok: false,
197
+ issues: [],
198
+ incomplete: { kind: 'timeout', elapsedMs: result.durationMs, boundMs },
199
+ stats: { actionsChecked: 0, streamsChecked: 0, renderMs: Date.now() - t0 },
200
+ };
128
201
  }
129
202
  if (result.outcome === 'overflow-stdout' || result.outcome === 'overflow-stderr') {
130
203
  return failed(
@@ -172,11 +245,15 @@ export function mapSandboxResultToCheckResult(
172
245
 
173
246
  /**
174
247
  * Run the render check in an isolated subprocess. See module doc.
248
+ * `options.bounds` sets the subprocess bounds for this run; omitted
249
+ * fields are the evaluation lane's defaults.
175
250
  */
176
251
  export async function runRenderCheckViaWorker(
177
252
  input: RunRenderCheckInput,
253
+ options: RenderCheckHostOptions = {},
178
254
  ): Promise<RenderCheckResult> {
179
255
  const t0 = Date.now();
256
+ const bounds = resolveRenderCheckHostBounds(options.bounds);
180
257
  let spawn: { command: string; args: string[] };
181
258
  try {
182
259
  spawn = resolveWorkerSpawn();
@@ -184,12 +261,14 @@ export async function runRenderCheckViaWorker(
184
261
  return unverified(err instanceof Error ? err.message : String(err), t0);
185
262
  }
186
263
 
264
+ const loadAtStart = loadavg()[0] ?? 0;
187
265
  const result = await runSandboxed({
188
266
  command: spawn.command,
189
267
  args: spawn.args,
190
- timeoutMs: CHECK_TIMEOUT_MS,
268
+ timeoutMs: bounds.timeoutMs,
269
+ gracePeriodMs: bounds.gracePeriodMs,
191
270
  maxStdoutBytes: CHECK_STDOUT_CAP,
192
- nodeHeapMb: CHECK_NODE_HEAP_MB,
271
+ nodeHeapMb: bounds.heapMb,
193
272
  stdin: JSON.stringify(input),
194
273
  // NODE_ENV steers React's production vs development build — keep
195
274
  // parity with the caller; everything else stays on the sandbox's
@@ -197,5 +276,11 @@ export async function runRenderCheckViaWorker(
197
276
  env: { NODE_ENV: process.env.NODE_ENV ?? 'production' },
198
277
  });
199
278
 
200
- return mapSandboxResultToCheckResult(result, t0);
279
+ const hostLoad: ProbeHostLoad = {
280
+ start: loadAtStart,
281
+ end: loadavg()[0] ?? 0,
282
+ cores: availableParallelism(),
283
+ };
284
+ const mapped = mapSandboxResultToCheckResult(result, t0, bounds.timeoutMs);
285
+ return { ...mapped, stats: { ...mapped.stats, hostLoad } };
201
286
  }
@@ -28,7 +28,20 @@
28
28
  * (the render SMOKE worker): reached by spawn path, never by import
29
29
  * specifier; registered as a tsup entry so `dist/harness/check/
30
30
  * runtime-render/render-check-worker.js` ships.
31
+ *
32
+ * Shutdown (ggui#1380): the compile runs on esbuild's service, a child
33
+ * process esbuild keeps alive for the life of this worker. After the
34
+ * verdict is written the worker calls `esbuild.stop()` so that child is
35
+ * gone before the worker is; a worker stopped at its wall-clock bound is
36
+ * killed by the host, and the service's stdin closing with it is what
37
+ * ends the service in that case. `workerMain` is exported for the
38
+ * process-shape pins; `main()` runs only when this file is the process
39
+ * entry, so importing it never reads stdin.
31
40
  */
41
+ import { realpathSync } from 'node:fs';
42
+ import { resolve as resolvePath } from 'node:path';
43
+ import { fileURLToPath } from 'node:url';
44
+ import * as esbuild from 'esbuild';
32
45
  import { runRenderCheckInProcess } from './render-check.js';
33
46
  import type { RunRenderCheckInput } from './render-check.js';
34
47
 
@@ -44,37 +57,78 @@ function emit(value: unknown): void {
44
57
  process.stdout.write(`${JSON.stringify(value)}\n`);
45
58
  }
46
59
 
47
- async function main(): Promise<void> {
48
- const raw = await readAllStdin();
49
- let input: RunRenderCheckInput;
60
+ /**
61
+ * Parse one input document, run the check, write the verdict. Whatever
62
+ * happens after parsing, esbuild's service is stopped before this
63
+ * returns (or rejects). If the service was never started, `stop()` is a
64
+ * no-op.
65
+ */
66
+ export async function workerMain(raw: string): Promise<void> {
50
67
  try {
51
- input = JSON.parse(raw) as RunRenderCheckInput;
52
- } catch (err) {
53
- process.stderr.write(
54
- `render-check worker: malformed input JSON — ${
55
- err instanceof Error ? err.message : String(err)
56
- }\n`,
57
- );
58
- process.exitCode = 1;
59
- return;
60
- }
61
- if (typeof input.sourceCode !== 'string' || input.sourceCode.length === 0) {
62
- process.stderr.write(
63
- 'render-check worker: input.sourceCode must be a non-empty string\n',
64
- );
65
- process.exitCode = 1;
66
- return;
68
+ let input: RunRenderCheckInput;
69
+ try {
70
+ input = JSON.parse(raw) as RunRenderCheckInput;
71
+ } catch (err) {
72
+ process.stderr.write(
73
+ `render-check worker: malformed input JSON — ${
74
+ err instanceof Error ? err.message : String(err)
75
+ }\n`,
76
+ );
77
+ process.exitCode = 1;
78
+ return;
79
+ }
80
+ if (typeof input.sourceCode !== 'string' || input.sourceCode.length === 0) {
81
+ process.stderr.write(
82
+ 'render-check worker: input.sourceCode must be a non-empty string\n',
83
+ );
84
+ process.exitCode = 1;
85
+ return;
86
+ }
87
+
88
+ // The in-process check never throws for component failures (they are
89
+ // reported as issues); an escape here is a harness defect — let it
90
+ // crash the worker so the host maps it to `unverified` with the
91
+ // stderr tail as the reason.
92
+ const result = await runRenderCheckInProcess(input);
93
+ emit(result);
94
+ // The check's async-infra grace period (React scheduler drain) holds
95
+ // handlers for ~100ms; exit through the event loop naturally so that
96
+ // drain completes inside the worker instead of leaking crashes.
97
+ } finally {
98
+ // Post-verdict work must never turn a written verdict into a failure:
99
+ // a rejecting `stop()` would otherwise become an unhandled rejection,
100
+ // exit code 1, and the host discarding the verdict on stdout.
101
+ try {
102
+ await esbuild.stop();
103
+ } catch (err) {
104
+ process.stderr.write(
105
+ `render-check worker: esbuild.stop() failed — ${err instanceof Error ? err.message : String(err)}\n`,
106
+ );
107
+ }
67
108
  }
109
+ }
68
110
 
69
- // The in-process check never throws for component failures (they are
70
- // reported as issues); an escape here is a harness defect — let it
71
- // crash the worker so the host maps it to `unverified` with the
72
- // stderr tail as the reason.
73
- const result = await runRenderCheckInProcess(input);
74
- emit(result);
75
- // The check's async-infra grace period (React scheduler drain) holds
76
- // handlers for ~100ms; exit through the event loop naturally so that
77
- // drain completes inside the worker instead of leaking crashes.
111
+ async function main(): Promise<void> {
112
+ await workerMain(await readAllStdin());
113
+ }
114
+
115
+ /** True when this file is the process entry (`node <this file>`), not an import. */
116
+ function isProcessEntry(): boolean {
117
+ const entry = process.argv[1];
118
+ if (entry === undefined) return false;
119
+ // Resolve BOTH sides through the filesystem: `import.meta.url` is this
120
+ // file's realpath while argv[1] is whatever the spawner passed — a
121
+ // symlinked spawn path must still read as the entry, or the worker would
122
+ // exit with no verdict and the host would read a silent `unverified`.
123
+ let entryReal: string;
124
+ try {
125
+ entryReal = realpathSync(resolvePath(entry));
126
+ } catch {
127
+ return false;
128
+ }
129
+ return entryReal === realpathSync(fileURLToPath(import.meta.url));
78
130
  }
79
131
 
80
- void main();
132
+ if (isProcessEntry()) {
133
+ void main();
134
+ }