@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
@@ -20,6 +20,7 @@
20
20
 
21
21
  export type Priority = "P0" | "P1" | "P2";
22
22
 
23
+ import type { GenerationRuntimeProbeOutcome, GenerationRuntimeProbeStatus } from "@ggui-ai/mcp-server-core";
23
24
  import type { CanvasClass, DesignMode } from "../design-mode.js";
24
25
 
25
26
  // ─── Issue shape ───────────────────────────────────────────────────────────
@@ -63,8 +64,10 @@ export interface EvalIssue {
63
64
  }
64
65
 
65
66
  /**
66
- * Runtime-render probe execution status — three-valued so "the probe
67
- * never ran" is never conflated with "the probe ran clean".
67
+ * Runtime-render probe execution status — four-valued so "the probe
68
+ * never ran" is never conflated with "the probe ran clean". ONE union: the
69
+ * same values a generation's metadata reports (`GenerationRuntimeProbeStatus`
70
+ * in `@ggui-ai/mcp-server-core`), declared there and named here.
68
71
  *
69
72
  * - `ran` — probe executed; its findings (possibly none) are
70
73
  * in the issue stream under `runtime:*` subcategories.
@@ -74,20 +77,96 @@ export interface EvalIssue {
74
77
  * consumers MUST NOT count it as a pass.
75
78
  * - `not-applicable`— nothing to probe (no compiled code, no contract
76
79
  * surface, or no runtimeRender check configured).
80
+ * - `timed-out` — probe started but did not finish inside its
81
+ * wall-clock bound (ggui#1299). Carries ZERO
82
+ * evidence about the component: on a contended
83
+ * host an ordinary component crosses the bound.
84
+ * Consumers MUST NOT count it as a pass, and MUST
85
+ * NOT count it as a crash either. `elapsedMs` and
86
+ * `hostLoad` say how long it ran and how busy the
87
+ * host was, so a reader can tell the two cases apart.
77
88
  */
78
- export type RuntimeProbeStatus = "ran" | "infra-skipped" | "not-applicable";
89
+ export type RuntimeProbeStatus = GenerationRuntimeProbeStatus;
90
+
91
+ /**
92
+ * The host's 1-minute load average at a probe's start and end, beside the
93
+ * number of CPUs the process can use (ggui#1299). A load well above
94
+ * `cores` means the probe competed for CPU; a wall-clock bound crossed
95
+ * there says more about the host than about the component.
96
+ */
97
+ export interface ProbeHostLoad {
98
+ readonly start: number;
99
+ readonly end: number;
100
+ readonly cores: number;
101
+ }
102
+
103
+ /**
104
+ * The engine's own fields on a probe's meta, beside the outcome a
105
+ * generation's metadata reports: why a probe did not finish, its render
106
+ * time, the host's load around it. Present on every arm of
107
+ * {@link RuntimeProbeMeta}; each is absent when the probe had nothing to
108
+ * say about it.
109
+ */
110
+ export interface RuntimeProbeMetaDetail {
111
+ /** Populated for `infra-skipped` / `not-applicable` / `timed-out` — why the probe didn't finish. */
112
+ readonly reason?: string;
113
+ /** `ran` only: the check's own render time, inside `elapsedMs`. */
114
+ readonly renderMs?: number;
115
+ /** Host load around the probe, when it ran isolated (absent for an in-process probe). */
116
+ readonly hostLoad?: ProbeHostLoad;
117
+ /** Time the check waited for a probe slot before it started, ms; present only when > 0 (ggui#1380 C2b). */
118
+ readonly queuedMs?: number;
119
+ }
79
120
 
80
121
  /**
81
122
  * Probe execution meta stamped onto an `EvalResult` at the exit-decision
82
123
  * points that invoke the probe. Absence of this field means the eval
83
124
  * path never reached a probe invocation at all — consumers treat that
84
125
  * the same as not-run, never as a pass.
126
+ *
127
+ * ONE union with the generation metadata's probe outcome
128
+ * (`GenerationRuntimeProbeOutcome` in `@ggui-ai/mcp-server-core`, declared
129
+ * there and carried here with {@link RuntimeProbeMetaDetail} on every arm),
130
+ * so the round's stamp and the metadata's record cannot say different
131
+ * things (ggui#1380):
132
+ *
133
+ * - `ran` + `verdict: 'pass'` — the probe executed and no check failed;
134
+ * - `ran` + `verdict: 'fail'` — ANY check failed; `failChecks` lists the
135
+ * distinct check kinds that did (`RenderCheckKind`, the engine's name
136
+ * for the same union), never empty, in the order `RENDER_CHECK_KINDS`
137
+ * declares them, parsed from the probe's own
138
+ * `runtime:<check>[:<subject>]` issues;
139
+ * - `timed-out` / `infra-skipped` / `not-applicable` — no verdict, and the
140
+ * type carries none. `elapsedMs` (on every status that reached the
141
+ * check) and `hostLoad` say how long it ran and how busy the host was.
142
+ *
143
+ * The verdict and the repair trigger are two different questions. The
144
+ * verdict is `fail` on any failing check. The one repair turn a probe-only
145
+ * round buys fires on a `render-no-throw` fail the engine recognises
146
+ * (`isRecoverableRenderCrash`) and on nothing else: a `fail` verdict whose
147
+ * checks are `prop-sensitivity`, `action-wiring` or any other kind is
148
+ * recorded here and never fed back.
85
149
  */
86
- export interface RuntimeProbeMeta {
87
- readonly status: RuntimeProbeStatus;
88
- /** Populated for `infra-skipped` / `not-applicable` — why the probe didn't run. */
89
- readonly reason?: string;
90
- }
150
+ export type RuntimeProbeMeta = GenerationRuntimeProbeOutcome & RuntimeProbeMetaDetail;
151
+
152
+ /**
153
+ * The one repair turn a probe-only round buys (ggui#1380). A serving
154
+ * deployment that wires the runtime probe and configures no evaluator runs
155
+ * the probe once after the coding turns; a render crash the engine
156
+ * recognises buys exactly one repair turn, then one re-probe. This records
157
+ * what came of it:
158
+ * - `compiled: false` — the repair turn's code did not pass self-check, so
159
+ * no re-probe ran (there is no `after`) and the pre-repair card is the
160
+ * one served;
161
+ * - `compiled: true` — the re-probe ran and `after` is its whole meta:
162
+ * its status, its verdict when it ran, and the checks that still fail.
163
+ * Whether the crash class the turn was bought for is still there is
164
+ * `after.verdict === 'fail' && after.failChecks.includes('render-no-throw')`.
165
+ * The two arms are exclusive by type.
166
+ */
167
+ export type RuntimeProbeRepair =
168
+ | { readonly attempted: true; readonly compiled: false; readonly trigger?: never }
169
+ | { readonly attempted: true; readonly compiled: true; readonly trigger: RuntimeProbeMeta };
91
170
 
92
171
  /**
93
172
  * Per-criterion execution status — three-valued so "the criterion
@@ -202,6 +281,13 @@ export interface EvalResult {
202
281
  * probe runner; absent on eval paths that never invoke the probe.
203
282
  */
204
283
  runtimeProbe?: RuntimeProbeMeta;
284
+ /**
285
+ * The probe-only round's repair record (ggui#1380) — present only when a
286
+ * recoverable render crash bought the one repair turn. See
287
+ * {@link RuntimeProbeRepair}. `runtimeProbe` beside it is the re-probe's
288
+ * verdict when the repair compiled, else the pre-repair probe's.
289
+ */
290
+ runtimeProbeRepair?: RuntimeProbeRepair;
205
291
  /**
206
292
  * The contract-feedback round, when one fired this generation (ggui#1261):
207
293
  * which exit-probe findings bought it and the source as it stood when the
@@ -6,62 +6,205 @@
6
6
 
7
7
  import type { EvalIssue } from "../../../evaluation/types-public.js";
8
8
  import type { RuntimeRenderCheck } from "../../types-public.js";
9
- import { runRenderCheck, type RenderCheckIssue } from "./render-check.js";
9
+ import {
10
+ RENDER_CHECK_KINDS,
11
+ runRenderCheck,
12
+ type RenderCheckIssue,
13
+ type RenderCheckKind,
14
+ type RunRenderCheckOptions,
15
+ } from "./render-check.js";
16
+ import { resolveRenderCheckHostBounds } from "./render-check-host.js";
10
17
  import { prepareMockupProps } from "./prepare-mockup.js";
11
18
 
12
- export const DEFAULT_RUNTIME_RENDER_CHECK: RuntimeRenderCheck = {
13
- id: "runtime-render",
14
- run: async input => {
15
- const { sourceCode, compiledCode, contract, fixtureProps } = input;
19
+ /**
20
+ * How one runtime-render check instance runs its isolated worker
21
+ * (ggui#1380). A serving deployment sets the probe's wall-clock bound,
22
+ * worker heap and concurrency here; every field omitted is the evaluation
23
+ * lane's behaviour — the host's default bounds and no concurrency cap.
24
+ */
25
+ export interface RuntimeRenderProbeConfig {
26
+ /** Wall-clock bound of one isolated check, ms. Default: the host's (30 000). */
27
+ readonly timeoutMs?: number;
28
+ /** V8 heap cap of the worker, MB. Default: the host's (512). */
29
+ readonly heapMb?: number;
30
+ /**
31
+ * Live workers this instance may have at once. The (K+1)th check waits
32
+ * in FIFO order and spawns only when a slot frees; a check that throws
33
+ * releases its slot. Default: unbounded. Must be a positive integer.
34
+ */
35
+ readonly maxConcurrent?: number;
36
+ }
16
37
 
17
- // Nothing to render / no contract surface to verify — the probe has
18
- // no subject, which is different from the probe failing to run.
19
- if (compiledCode === null) {
20
- return { status: "not-applicable", issues: [], reason: "no compiled code" };
38
+ /**
39
+ * K-slot FIFO limiter. `undefined` slots = no limiter (the task runs at
40
+ * once). A slot is released in `finally`, so a rejected task never holds
41
+ * one.
42
+ */
43
+ function createSlotLimiter(
44
+ maxConcurrent: number | undefined
45
+ ): <T>(task: (waitedMs: number) => Promise<T>) => Promise<T> {
46
+ if (maxConcurrent === undefined) return (task) => task(0);
47
+ if (!Number.isInteger(maxConcurrent) || maxConcurrent < 1) {
48
+ throw new RangeError(
49
+ `createRuntimeRenderCheck: maxConcurrent must be a positive integer, got ${maxConcurrent}`
50
+ );
51
+ }
52
+ let active = 0;
53
+ const waiting: Array<() => void> = [];
54
+ /** Resolves with the time the caller actually WAITED for a slot: 0 when one was free. */
55
+ const acquire = (): Promise<number> => {
56
+ if (active < maxConcurrent) {
57
+ active += 1;
58
+ return Promise.resolve(0);
21
59
  }
22
- if (!contract) {
23
- return { status: "not-applicable", issues: [], reason: "no contract surface" };
60
+ const enqueuedAt = Date.now();
61
+ return new Promise<number>((resolve) => {
62
+ waiting.push(() => {
63
+ active += 1;
64
+ resolve(Date.now() - enqueuedAt);
65
+ });
66
+ });
67
+ };
68
+ const release = (): void => {
69
+ active -= 1;
70
+ const next = waiting.shift();
71
+ if (next !== undefined) next();
72
+ };
73
+ return async (task) => {
74
+ const waitedMs = await acquire();
75
+ try {
76
+ return await task(waitedMs);
77
+ } finally {
78
+ release();
24
79
  }
80
+ };
81
+ }
25
82
 
26
- const mockup = prepareMockupProps({ contract, fixtureProps });
83
+ /** `{ queuedMs }` when the check actually waited for a slot, `{}` when one was free. */
84
+ function queuedField(queuedMs: number): { readonly queuedMs?: number } {
85
+ return queuedMs > 0 ? { queuedMs } : {};
86
+ }
27
87
 
28
- let result;
29
- try {
30
- result = await runRenderCheck({
31
- sourceCode,
32
- mockupProps: mockup.props,
33
- contract,
34
- });
35
- } catch (e) {
36
- // Triad audit (2026-04-27): every error that escapes `runRenderCheck`
37
- // is an INFRA problem — happy-dom import failure, ESM/CJS interop
38
- // (`Dynamic require of "events"`), bundler name collision (`Window2
39
- // is not a constructor`), missing `@testing-library/react`, etc.
40
- // Component-level failures are caught and emitted as `RenderCheckIssue`
41
- // entries from inside `runRenderCheck`; they don't propagate out.
42
- //
43
- // An infra failure must never become an eval issue (the coding
44
- // agent can't fix the environment — pre-2026-04-27 that phantom
45
- // issue dragged every score-below-80 cell's eval-fix loop), but it
46
- // must also never be silent to scoring: ggui#403 found the bench
47
- // reporting `probe_pass 3/3` on cells where this branch fired on
48
- // every invocation. The `infra-skipped` status is the No-Silent-
49
- // Block channel — consumers surface it as did-not-run, never pass.
50
- const message = e instanceof Error ? e.message : String(e);
51
- console.warn(
52
- `[runtime-render] probe skipped — infra failure: ${message}`,
53
- );
54
- return { status: "infra-skipped", issues: [], reason: message };
55
- }
88
+ /** The `runRenderCheck` options a config maps to — `undefined` when no bound is set. */
89
+ function toRenderCheckOptions(config: RuntimeRenderProbeConfig): RunRenderCheckOptions | undefined {
90
+ if (config.timeoutMs === undefined && config.heapMb === undefined) return undefined;
91
+ return {
92
+ bounds: {
93
+ ...(config.timeoutMs !== undefined ? { timeoutMs: config.timeoutMs } : {}),
94
+ ...(config.heapMb !== undefined ? { heapMb: config.heapMb } : {}),
95
+ },
96
+ };
97
+ }
98
+
99
+ /**
100
+ * Build a runtime-render check instance. One FIFO limiter per instance;
101
+ * the bounds reach `runRenderCheck` on every run. `createRuntimeRenderCheck()`
102
+ * with no config is {@link DEFAULT_RUNTIME_RENDER_CHECK}'s behaviour.
103
+ */
104
+ export function createRuntimeRenderCheck(
105
+ config: RuntimeRenderProbeConfig = {}
106
+ ): RuntimeRenderCheck {
107
+ const limit = createSlotLimiter(config.maxConcurrent);
108
+ const options = toRenderCheckOptions(config);
109
+ // A bad bound is refused here, at construction, never at the first probe.
110
+ if (options !== undefined) resolveRenderCheckHostBounds(options.bounds);
111
+ return {
112
+ id: "runtime-render",
113
+ run: async (input) => {
114
+ const { sourceCode, compiledCode, contract, fixtureProps } = input;
56
115
 
57
- return {
58
- status: "ran",
59
- issues: result.issues
60
- .map(toEvalIssue)
61
- .filter((x): x is EvalIssue => x !== null),
62
- };
63
- },
64
- };
116
+ // Nothing to render / no contract surface to verify — the probe has
117
+ // no subject, which is different from the probe failing to run.
118
+ if (compiledCode === null) {
119
+ return { status: "not-applicable", issues: [], reason: "no compiled code" };
120
+ }
121
+ if (!contract) {
122
+ return { status: "not-applicable", issues: [], reason: "no contract surface" };
123
+ }
124
+
125
+ const mockup = prepareMockupProps({ contract, fixtureProps });
126
+
127
+ // ggui#1380 — the adapter's own clock around the check: every status
128
+ // that reached the check (`ran`, `timed-out`, `infra-skipped`) reports
129
+ // how long it took, so a reader of the probe meta can tell a 2 s probe
130
+ // from a 30 s one on the same status. The two not-applicable returns
131
+ // above carry nothing: nothing ran.
132
+ // The clock starts when the slot is acquired, so `elapsedMs` is the
133
+ // check's own wall-clock; the time the check actually WAITED for a slot
134
+ // is `queuedMs` (present only when it queued) — a latency reader needs
135
+ // the two apart exactly when a serving deployment queues more probes
136
+ // than it has slots. A check that found a slot free carries no key.
137
+ let queuedMs = 0;
138
+ let t0 = Date.now();
139
+ let result;
140
+ try {
141
+ result = await limit((waitedMs) => {
142
+ queuedMs = waitedMs;
143
+ t0 = Date.now();
144
+ return runRenderCheck(
145
+ {
146
+ sourceCode,
147
+ mockupProps: mockup.props,
148
+ contract,
149
+ },
150
+ options
151
+ );
152
+ });
153
+ } catch (e) {
154
+ // Triad audit (2026-04-27): every error that escapes `runRenderCheck`
155
+ // is an INFRA problem — happy-dom import failure, ESM/CJS interop
156
+ // (`Dynamic require of "events"`), bundler name collision (`Window2
157
+ // is not a constructor`), missing `@testing-library/react`, etc.
158
+ // Component-level failures are caught and emitted as `RenderCheckIssue`
159
+ // entries from inside `runRenderCheck`; they don't propagate out.
160
+ //
161
+ // An infra failure must never become an eval issue (the coding
162
+ // agent can't fix the environment — pre-2026-04-27 that phantom
163
+ // issue dragged every score-below-80 cell's eval-fix loop), but it
164
+ // must also never be silent to scoring: ggui#403 found the bench
165
+ // reporting `probe_pass 3/3` on cells where this branch fired on
166
+ // every invocation. The `infra-skipped` status is the No-Silent-
167
+ // Block channel — consumers surface it as did-not-run, never pass.
168
+ const message = e instanceof Error ? e.message : String(e);
169
+ console.warn(`[runtime-render] probe skipped — infra failure: ${message}`);
170
+ return { status: "infra-skipped", issues: [], reason: message, elapsedMs: Date.now() - t0, ...queuedField(queuedMs) };
171
+ }
172
+
173
+ const hostLoad = result.stats.hostLoad;
174
+ const load = hostLoad !== undefined ? { hostLoad } : {};
175
+
176
+ // ggui#1299: a check that ran out of wall-clock time is not a verdict on
177
+ // the component. It becomes the `timed-out` status (did-not-run, never a
178
+ // pass, never a crash), never an eval issue — so the coding agent is never
179
+ // told to fix a crash that did not happen.
180
+ if (result.incomplete !== undefined) {
181
+ const { elapsedMs, boundMs } = result.incomplete;
182
+ const loadNote =
183
+ hostLoad !== undefined
184
+ ? `; host load ${hostLoad.start.toFixed(1)} → ${hostLoad.end.toFixed(1)} on ${hostLoad.cores} CPUs`
185
+ : "";
186
+ const reason = `render check did not finish within ${boundMs} ms (stopped at ${elapsedMs} ms)${loadNote}`;
187
+ console.warn(`[runtime-render] probe timed out — ${reason}`);
188
+ // One clock for every status that reached the worker: the adapter's
189
+ // wall-clock around the check. The worker's own reading survives in
190
+ // `reason` ("stopped at N ms").
191
+ return { status: "timed-out", issues: [], reason, elapsedMs: Date.now() - t0, ...queuedField(queuedMs), ...load };
192
+ }
193
+
194
+ return {
195
+ status: "ran",
196
+ issues: result.issues.map(toEvalIssue).filter((x): x is EvalIssue => x !== null),
197
+ elapsedMs: Date.now() - t0,
198
+ ...queuedField(queuedMs),
199
+ renderMs: result.stats.renderMs,
200
+ ...load,
201
+ };
202
+ },
203
+ };
204
+ }
205
+
206
+ /** The evaluation lane's instance: the host's default bounds, no concurrency cap. */
207
+ export const DEFAULT_RUNTIME_RENDER_CHECK: RuntimeRenderCheck = createRuntimeRenderCheck();
65
208
 
66
209
  /**
67
210
  * Map a runtime-render crash reason to a class-specific fix string.
@@ -158,11 +301,7 @@ export function classifyRenderCrashFix(reason: string): string {
158
301
  "(b) check it's an array before iterating (`Array.isArray(x) && ...`)."
159
302
  );
160
303
  }
161
- if (
162
- r.includes("cannot read") ||
163
- r.includes("undefined is not") ||
164
- r.includes("null is not")
165
- ) {
304
+ if (r.includes("cannot read") || r.includes("undefined is not") || r.includes("null is not")) {
166
305
  return (
167
306
  "Null/undefined access. Add optional chaining (`obj?.field`) and " +
168
307
  "default values for optional props/state before reading nested fields. " +
@@ -176,7 +315,32 @@ export function classifyRenderCrashFix(reason: string): string {
176
315
  );
177
316
  }
178
317
 
179
- function toEvalIssue(issue: RenderCheckIssue): EvalIssue | null {
318
+ /** The prefix every probe issue's subcategory carries: `runtime:<check>[:<subject>]`. */
319
+ const RUNTIME_SUBCATEGORY_PREFIX = "runtime:";
320
+
321
+ /**
322
+ * The check a probe issue's subcategory names (ggui#1380) — the inverse of
323
+ * the `runtime:<check>[:<subject>]` form {@link toEvalIssue} writes, so a
324
+ * round can list WHICH checks failed from the issues alone. `undefined` for
325
+ * anything that is not a probe subcategory naming a member of
326
+ * {@link RENDER_CHECK_KINDS} (an axis check's subcategory, a did-not-run
327
+ * code such as `runtime:probe-timeout`, a bare check name).
328
+ */
329
+ export function parseRuntimeSubcategory(subcategory: string): RenderCheckKind | undefined {
330
+ if (!subcategory.startsWith(RUNTIME_SUBCATEGORY_PREFIX)) return undefined;
331
+ const rest = subcategory.slice(RUNTIME_SUBCATEGORY_PREFIX.length);
332
+ const separator = rest.indexOf(":");
333
+ const check = separator === -1 ? rest : rest.slice(0, separator);
334
+ return RENDER_CHECK_KINDS.find((kind) => kind === check);
335
+ }
336
+
337
+ /**
338
+ * One probe finding → the eval issue the loop feeds back and the metadata
339
+ * counts. Exported for the pin that every switch case below is a member of
340
+ * {@link RENDER_CHECK_KINDS} and round-trips through
341
+ * {@link parseRuntimeSubcategory}.
342
+ */
343
+ export function toEvalIssue(issue: RenderCheckIssue): EvalIssue | null {
180
344
  // verified → no issue (silent pass)
181
345
  if (issue.outcome === "verified" || issue.outcome === "skipped") return null;
182
346
 
@@ -209,7 +373,7 @@ function toEvalIssue(issue: RenderCheckIssue): EvalIssue | null {
209
373
  diagParts.push(
210
374
  "primed" in diag.inputPriming
211
375
  ? `input priming: ${diag.inputPriming.primed} control(s) filled before the click`
212
- : `input priming failed: ${diag.inputPriming.error}`,
376
+ : `input priming failed: ${diag.inputPriming.error}`
213
377
  );
214
378
  }
215
379
  const diagSuffix = diagParts.length ? ` [observed: ${diagParts.join("; ")}]` : "";
@@ -245,9 +409,10 @@ function toEvalIssue(issue: RenderCheckIssue): EvalIssue | null {
245
409
  };
246
410
 
247
411
  case "action-wiring": {
248
- const fix = issue.outcome === "unverified"
249
- ? `Source shows the action callback flowing into a non-native or custom-component prop. If wiring is intentional (e.g., Dropdown.onChange, drag-drop), this warn is informational — manual/browser verification is required to confirm. Otherwise wire to a native onClick={() => ${subject}(payload)} on <button> or design-system <Button>.`
250
- : `Wire ${subject}() to a native event prop. Common fix: <Button onClick={() => ${subject}(payload)}>Label</Button>. Source-AST analysis didn't find this wiring in your JSX.`;
412
+ const fix =
413
+ issue.outcome === "unverified"
414
+ ? `Source shows the action callback flowing into a non-native or custom-component prop. If wiring is intentional (e.g., Dropdown.onChange, drag-drop), this warn is informational — manual/browser verification is required to confirm. Otherwise wire to a native onClick={() => ${subject}(payload)} on <button> or design-system <Button>.`
415
+ : `Wire ${subject}() to a native event prop. Common fix: <Button onClick={() => ${subject}(payload)}>Label</Button>. Source-AST analysis didn't find this wiring in your JSX.`;
251
416
  return {
252
417
  tier: 0,
253
418
  result,
@@ -10,15 +10,26 @@
10
10
  // - declared stream events update the DOM when emitted (warn)
11
11
 
12
12
  import { installProductionActShim } from "./production-act-shim.js";
13
+ import type { RenderCheckHostBounds } from "./render-check-host.js";
13
14
 
14
- export { runRenderCheck, type RenderCheckResult, type RenderCheckIssue } from "./render-check.js";
15
+ export {
16
+ RENDER_CHECK_KINDS,
17
+ runRenderCheck,
18
+ type RenderCheckKind,
19
+ type RenderCheckResult,
20
+ type RenderCheckIssue,
21
+ } from "./render-check.js";
15
22
  export { createProbe, createProbeWireConfig, type Probe } from "./probe.js";
16
23
  export { prepareMockupProps, type MockupPropsResult } from "./prepare-mockup.js";
17
24
  export {
18
25
  DEFAULT_RUNTIME_RENDER_CHECK,
19
26
  classifyRenderCrashFix,
27
+ createRuntimeRenderCheck,
20
28
  isRecoverableRenderCrash,
29
+ parseRuntimeSubcategory,
30
+ type RuntimeRenderProbeConfig,
21
31
  } from "./adapter.js";
32
+ export type { RenderCheckHostBounds, RenderCheckHostOptions } from "./render-check-host.js";
22
33
 
23
34
  /**
24
35
  * Pre-warm the runtime-render probe's runtime dependencies so the first
@@ -41,7 +52,11 @@ export {
41
52
  *
42
53
  * Returns the wall-clock spent loading.
43
54
  */
44
- export async function warmupRuntimeRenderProbe(): Promise<{ ms: number; loaded: number; missing: number }> {
55
+ export async function warmupRuntimeRenderProbe(): Promise<{
56
+ ms: number;
57
+ loaded: number;
58
+ missing: number;
59
+ }> {
45
60
  const start = Date.now();
46
61
  let loaded = 0;
47
62
  let missing = 0;
@@ -67,3 +82,61 @@ export async function warmupRuntimeRenderProbe(): Promise<{ ms: number; loaded:
67
82
  ]);
68
83
  return { ms: Date.now() - start, loaded, missing };
69
84
  }
85
+
86
+ /**
87
+ * The component the boot warm renders: a constant element, an empty
88
+ * contract — nothing to click, nothing to cover, one compile and one
89
+ * render in the worker.
90
+ */
91
+ const WARMUP_COMPONENT = "export default function Warmup() { return <div>warm</div>; }";
92
+
93
+ /**
94
+ * Warm the PROCESS that runs every probe (ggui#1380): spawn one real check
95
+ * worker on a trivial component through the same host every probe uses.
96
+ * Afterwards the node binary, the worker's module tree and esbuild's
97
+ * service have all run once on this machine (OS page cache warm), and the
98
+ * result says whether that process can run a check at all — which
99
+ * `warmupRuntimeRenderProbe` (an import in THIS process) cannot tell.
100
+ *
101
+ * `bounds` are the subprocess bounds for the one run; a serving deployment
102
+ * passes the same bounds it will probe with. Never throws: a worker that
103
+ * cannot run is `{ ok: false, reason }`, so a boot sequence can log it and
104
+ * decide, instead of dying in a fire-and-forget.
105
+ */
106
+ export async function warmupRuntimeRenderWorker(
107
+ bounds?: Partial<RenderCheckHostBounds>
108
+ ): Promise<{ ok: true; ms: number } | { ok: false; ms: number; reason: string }> {
109
+ const start = Date.now();
110
+ try {
111
+ // Lazy: the host pulls `@ggui-ai/sandbox` (spawn machinery); keep it off
112
+ // the import graph of everything that only needs the in-process check.
113
+ const { runRenderCheckViaWorker } = await import("./render-check-host.js");
114
+ const result = await runRenderCheckViaWorker(
115
+ { sourceCode: WARMUP_COMPONENT, mockupProps: {}, contract: {} },
116
+ bounds !== undefined ? { bounds } : {}
117
+ );
118
+ const ms = Date.now() - start;
119
+ if (result.incomplete !== undefined) {
120
+ return {
121
+ ok: false,
122
+ ms,
123
+ reason: `the warm-up check did not finish within ${result.incomplete.boundMs} ms (stopped at ${result.incomplete.elapsedMs} ms)`,
124
+ };
125
+ }
126
+ const blocking = result.issues.find(
127
+ (i) => i.outcome === "failed" || i.outcome === "unverified"
128
+ );
129
+ if (blocking !== undefined) {
130
+ return { ok: false, ms, reason: `${blocking.check} ${blocking.outcome}: ${blocking.reason}` };
131
+ }
132
+ return { ok: true, ms };
133
+ } catch (err) {
134
+ // The host itself never throws for a worker fault; what reaches here is
135
+ // the spawn layer refusing to run at all (an invalid bound, no binary).
136
+ return {
137
+ ok: false,
138
+ ms: Date.now() - start,
139
+ reason: err instanceof Error ? err.message : String(err),
140
+ };
141
+ }
142
+ }