@ggui-ai/ui-gen 0.9.0 → 0.11.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 (92) hide show
  1. package/dist/adapters/generation-dispatch.d.ts +9 -2
  2. package/dist/adapters/generation-dispatch.js +693 -89
  3. package/dist/adapters/generation-dispatch.js.map +1 -1
  4. package/dist/adapters/index.js +28 -11
  5. package/dist/adapters/index.js.map +1 -1
  6. package/dist/advanced/index.d.ts +5 -3
  7. package/dist/advanced/index.js +714 -101
  8. package/dist/advanced/index.js.map +1 -1
  9. package/dist/blueprint-validator.js +267 -3
  10. package/dist/blueprint-validator.js.map +1 -1
  11. package/dist/boilerplate.js +14 -2
  12. package/dist/boilerplate.js.map +1 -1
  13. package/dist/check/index.d.ts +1 -1
  14. package/dist/check/index.js +11 -1
  15. package/dist/check/index.js.map +1 -1
  16. package/dist/{create-ui-generator-B9kV4U9h.d.ts → create-ui-generator-CF0EL0Li.d.ts} +13 -1
  17. package/dist/{dispatch-Deo9qzrG.d.ts → dispatch-D75M-OfZ.d.ts} +1 -1
  18. package/dist/evaluation/axis-checks/index.d.ts +3 -3
  19. package/dist/evaluation/axis-checks/index.js +101 -0
  20. package/dist/evaluation/axis-checks/index.js.map +1 -1
  21. package/dist/evaluation/axis-checks/registry.d.ts +1 -1
  22. package/dist/evaluation/axis-checks/registry.js +101 -0
  23. package/dist/evaluation/axis-checks/registry.js.map +1 -1
  24. package/dist/evaluation/index.d.ts +46 -3
  25. package/dist/evaluation/index.js +358 -17
  26. package/dist/evaluation/index.js.map +1 -1
  27. package/dist/harness/check/runtime-render/index.d.ts +2 -2
  28. package/dist/harness/check/runtime-render/index.js +268 -3
  29. package/dist/harness/check/runtime-render/index.js.map +1 -1
  30. package/dist/harness/check/runtime-render/render-check-worker.d.ts +2 -0
  31. package/dist/harness/check/runtime-render/render-check-worker.js +1332 -0
  32. package/dist/harness/check/runtime-render/render-check-worker.js.map +1 -0
  33. package/dist/harness/index.d.ts +2 -2
  34. package/dist/harness/index.js +263 -18
  35. package/dist/harness/index.js.map +1 -1
  36. package/dist/harness/llm-trace-sink.d.ts +1 -1
  37. package/dist/harness/llm-trace-sink.js.map +1 -1
  38. package/dist/harness/prompts.js +17 -3
  39. package/dist/harness/prompts.js.map +1 -1
  40. package/dist/harness/result-types.d.ts +1 -1
  41. package/dist/harness/runtime.d.ts +9 -2
  42. package/dist/harness/runtime.js +20 -6
  43. package/dist/harness/runtime.js.map +1 -1
  44. package/dist/harness/types-public.d.ts +1 -1
  45. package/dist/harness/types-public.js +14 -2
  46. package/dist/harness/types-public.js.map +1 -1
  47. package/dist/harness/validator-trace-sink.d.ts +1 -1
  48. package/dist/index.d.ts +5 -3
  49. package/dist/index.js +708 -95
  50. package/dist/index.js.map +1 -1
  51. package/dist/{llm-router-1TyI0_TV.d.ts → llm-router-BEVtbAb9.d.ts} +61 -4
  52. package/dist/patch.d.ts +1 -1
  53. package/dist/provider-adapter.d.ts +16 -6
  54. package/dist/provider-adapter.js.map +1 -1
  55. package/dist/providers/index.d.ts +19 -6
  56. package/dist/providers/index.js.map +1 -1
  57. package/dist/{render-check-CNZjHD5g.d.ts → render-check-BZoIE8-u.d.ts} +21 -1
  58. package/dist/{types-public-C36k1q6s.d.ts → types-public-DIpjC-OA.d.ts} +1 -1
  59. package/dist/workflows.d.ts +1 -1
  60. package/package.json +12 -12
  61. package/src/adapters/generation-dispatch.ts +9 -0
  62. package/src/adapters/openrouter/client.ts +2 -2
  63. package/src/adapters/openrouter/types.ts +10 -0
  64. package/src/boilerplate/generate.ts +10 -2
  65. package/src/boilerplate/json-schema-ts.ts +22 -2
  66. package/src/boilerplate/system-prompt.ts +1 -0
  67. package/src/compile.ts +1 -1
  68. package/src/contract-context.ts +10 -2
  69. package/src/create-ui-generator.ts +14 -0
  70. package/src/design-system-docs.ts +6 -4
  71. package/src/evaluation/axis-checks/checks/state-ui-affordance.ts +162 -0
  72. package/src/evaluation/axis-checks/registry.ts +2 -0
  73. package/src/evaluation/index.ts +11 -0
  74. package/src/evaluation/llm-evaluator.ts +14 -1
  75. package/src/evaluation/visual-evaluator.ts +44 -65
  76. package/src/harness/check/runtime-render/adapter.ts +18 -0
  77. package/src/harness/check/runtime-render/index.ts +8 -0
  78. package/src/harness/check/runtime-render/prepare-mockup.ts +14 -0
  79. package/src/harness/check/runtime-render/production-act-shim.ts +158 -0
  80. package/src/harness/check/runtime-render/render-check-host.ts +201 -0
  81. package/src/harness/check/runtime-render/render-check-worker.ts +80 -0
  82. package/src/harness/check/runtime-render/render-check.ts +112 -1
  83. package/src/harness/coding/run-eval-round.ts +61 -5
  84. package/src/harness/index.ts +1 -1
  85. package/src/harness/llm-router.ts +397 -18
  86. package/src/harness/llm-trace-sink.ts +2 -1
  87. package/src/harness/runtime.ts +7 -0
  88. package/src/index.ts +5 -5
  89. package/src/provider-adapter.ts +16 -6
  90. package/src/providers/bedrock.ts +5 -4
  91. package/src/providers/index.ts +16 -4
  92. package/src/tools/render-check.ts +19 -7
package/dist/index.js CHANGED
@@ -3,21 +3,21 @@ 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, listContractGadgets, HOOK_NAME_RE, DANGEROUS_PATTERNS as DANGEROUS_PATTERNS$1, STDLIB_GADGETS_PACKAGE, resolveAppGadgets } from '@ggui-ai/protocol';
6
- import fs, { mkdirSync, writeFileSync, unlinkSync, readFileSync, existsSync, rm, createReadStream, createWriteStream } from 'fs';
6
+ import fs, { mkdirSync, writeFileSync, unlinkSync, existsSync, readFileSync, rm, createReadStream, createWriteStream } from 'fs';
7
7
  import { contentHash } from '@ggui-ai/protocol/content-hash';
8
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 { parseGeneratorSlug, formatGeneratorSlug, isValidGeneratorSlug } from '@ggui-ai/mcp-server-core';
20
- import { runSandboxed } from '@ggui-ai/sandbox';
21
21
  import git from 'isomorphic-git';
22
22
  import { Volume, createFsFromVolume } from 'memfs';
23
23
  import { createPatch, applyPatch } from 'diff';
@@ -1154,14 +1154,16 @@ var OpenRouterError;
1154
1154
  var init_types = __esm({
1155
1155
  "src/adapters/openrouter/types.ts"() {
1156
1156
  OpenRouterError = class extends Error {
1157
- constructor(message, status, code) {
1157
+ constructor(message, status, code, headers) {
1158
1158
  super(message);
1159
1159
  this.status = status;
1160
1160
  this.code = code;
1161
+ this.headers = headers;
1161
1162
  this.name = "OpenRouterError";
1162
1163
  }
1163
1164
  status;
1164
1165
  code;
1166
+ headers;
1165
1167
  get isTransient() {
1166
1168
  return this.status === 408 || this.status === 429 || this.status === 502 || this.status === 503;
1167
1169
  }
@@ -1206,7 +1208,7 @@ var init_client2 = __esm({
1206
1208
  if (!response.ok) {
1207
1209
  const errorBody = await response.json().catch(() => ({ error: { message: response.statusText } }));
1208
1210
  const message = errorBody.error?.message ?? response.statusText;
1209
- throw new OpenRouterError(message, response.status);
1211
+ throw new OpenRouterError(message, response.status, void 0, response.headers);
1210
1212
  }
1211
1213
  return response.json();
1212
1214
  }
@@ -1224,7 +1226,7 @@ var init_client2 = __esm({
1224
1226
  if (!response.ok) {
1225
1227
  const errorBody = await response.json().catch(() => ({ error: { message: response.statusText } }));
1226
1228
  const message = errorBody.error?.message ?? response.statusText;
1227
- throw new OpenRouterError(message, response.status);
1229
+ throw new OpenRouterError(message, response.status, void 0, response.headers);
1228
1230
  }
1229
1231
  if (!response.body) {
1230
1232
  throw new OpenRouterError("No response body for streaming request", 500);
@@ -2077,6 +2079,15 @@ function jsonSchemaTypeToTs(schema) {
2077
2079
  if (schema.enum?.length) {
2078
2080
  return schema.enum.map((v) => typeof v === "string" ? `'${v}'` : String(v)).join(" | ");
2079
2081
  }
2082
+ if (Array.isArray(schema.type)) {
2083
+ const { nullable: _nullable, ...rest } = schema;
2084
+ const members = [...new Set(schema.type)].map(
2085
+ (member) => jsonSchemaTypeToTs({ ...rest, type: member })
2086
+ );
2087
+ const unique = [...new Set(members)];
2088
+ if (schema.nullable && !unique.includes("null")) unique.push("null");
2089
+ return unique.join(" | ");
2090
+ }
2080
2091
  let result;
2081
2092
  switch (schema.type) {
2082
2093
  case "string":
@@ -2094,7 +2105,8 @@ function jsonSchemaTypeToTs(schema) {
2094
2105
  case "array": {
2095
2106
  if (schema.items) {
2096
2107
  const itemType = jsonSchemaTypeToTs(schema.items);
2097
- result = schema.items.type === "object" && schema.items.properties ? `Array<${itemType}>` : itemType.includes("|") ? `(${itemType})[]` : `${itemType}[]`;
2108
+ const itemsDeclareObject = Array.isArray(schema.items.type) ? schema.items.type.includes("object") : schema.items.type === "object";
2109
+ result = itemsDeclareObject && schema.items.properties ? `Array<${itemType}>` : itemType.includes("|") ? `(${itemType})[]` : `${itemType}[]`;
2098
2110
  } else {
2099
2111
  result = "unknown[]";
2100
2112
  }
@@ -4112,6 +4124,29 @@ function errorSummary(e) {
4112
4124
  }
4113
4125
  return `${e.constructor.name}${status}${code}: ${e.message.slice(0, 120)}${body}`;
4114
4126
  }
4127
+ function isRateLimited(e) {
4128
+ if (!(e instanceof Error) || !("status" in e)) return false;
4129
+ return e.status === 429;
4130
+ }
4131
+ function extractStatus(e) {
4132
+ if (!(e instanceof Error) || !("status" in e)) return void 0;
4133
+ const status = e.status;
4134
+ return typeof status === "number" ? status : void 0;
4135
+ }
4136
+ function extractRetryAfterSec(e) {
4137
+ if (!(e instanceof Error) || !("headers" in e)) return void 0;
4138
+ const headers = e.headers;
4139
+ if (headers === null || typeof headers !== "object" || !("get" in headers) || typeof headers.get !== "function") {
4140
+ return void 0;
4141
+ }
4142
+ const raw = headers.get("retry-after");
4143
+ if (raw === null) return void 0;
4144
+ const parsed = Number(raw);
4145
+ return Number.isFinite(parsed) && parsed >= 0 ? Math.floor(parsed) : void 0;
4146
+ }
4147
+ function sleep(ms) {
4148
+ return new Promise((resolve3) => setTimeout(resolve3, ms));
4149
+ }
4115
4150
  function interactionText(interaction) {
4116
4151
  let text = "";
4117
4152
  for (const step of interaction.steps ?? []) {
@@ -4136,24 +4171,38 @@ function toUserInputStep(text) {
4136
4171
  function createAgent(providerOrConfig) {
4137
4172
  const provider = typeof providerOrConfig === "string" ? providerOrConfig : providerOrConfig.provider;
4138
4173
  const routeOverride = typeof providerOrConfig === "string" ? void 0 : providerOrConfig.routeOverride;
4174
+ const onRetry = typeof providerOrConfig === "string" ? void 0 : providerOrConfig.onRetry;
4139
4175
  switch (provider) {
4140
4176
  case "anthropic":
4141
- return new AnthropicAgent(routeOverride);
4177
+ return new AnthropicAgent(routeOverride, onRetry);
4142
4178
  case "openai":
4143
- return new OpenAIAgent(routeOverride);
4179
+ return new OpenAIAgent(routeOverride, onRetry);
4144
4180
  case "google":
4145
- return new GoogleAgent(routeOverride);
4181
+ return new GoogleAgent(routeOverride, onRetry);
4146
4182
  case "openrouter":
4147
- return new OpenRouterAgent(routeOverride);
4183
+ return new OpenRouterAgent(routeOverride, onRetry);
4148
4184
  }
4149
4185
  }
4150
- var LLMAgent, AnthropicAgent, OpenAIAgent, GoogleAgent, OpenRouterAgent;
4186
+ function createVisionAgent(config) {
4187
+ switch (config.provider) {
4188
+ case "anthropic":
4189
+ return new AnthropicAgent(config.routeOverride, config.onRetry);
4190
+ case "google":
4191
+ return new GoogleAgent(config.routeOverride, config.onRetry);
4192
+ }
4193
+ }
4194
+ var MAX_RETRY_ATTEMPTS, RETRY_AFTER_CAP_SEC, DEFAULT_BACKOFF_BASE_MS, DEFAULT_BACKOFF_MAX_MS, MAX_TOTAL_RETRY_DELAY_MS, LLMAgent, AnthropicAgent, OpenAIAgent, GoogleAgent, OpenRouterAgent;
4151
4195
  var init_llm_router = __esm({
4152
4196
  "src/harness/llm-router.ts"() {
4153
4197
  init_client();
4154
4198
  init_provider_router();
4155
4199
  init_tool_bridge();
4156
4200
  init_llm_trace_sink();
4201
+ MAX_RETRY_ATTEMPTS = 2;
4202
+ RETRY_AFTER_CAP_SEC = 15;
4203
+ DEFAULT_BACKOFF_BASE_MS = 1e3;
4204
+ DEFAULT_BACKOFF_MAX_MS = 8e3;
4205
+ MAX_TOTAL_RETRY_DELAY_MS = 2e4;
4157
4206
  LLMAgent = class {
4158
4207
  client = null;
4159
4208
  // ── Session state for server-side chaining ──────────────
@@ -4168,8 +4217,11 @@ var init_llm_router = __esm({
4168
4217
  * back to `process.env` exactly as before.
4169
4218
  */
4170
4219
  routeOverride;
4171
- constructor(routeOverride) {
4220
+ /** See `AgentConfig.onRetry` (#489). Absent is a no-op. */
4221
+ onRetry;
4222
+ constructor(routeOverride, onRetry) {
4172
4223
  this.routeOverride = routeOverride;
4224
+ this.onRetry = onRetry;
4173
4225
  }
4174
4226
  async getClient() {
4175
4227
  if (!this.client) {
@@ -4204,17 +4256,83 @@ var init_llm_router = __esm({
4204
4256
  async sendToolResult(_results) {
4205
4257
  }
4206
4258
  /**
4207
- * Execute an API call. No retry — if it fails, it fails.
4208
- * Logs the error with provider context and re-throws.
4259
+ * Execute an API call, retrying once or twice on HTTP 429
4260
+ * (rate-limited) before giving up (#489).
4261
+ *
4262
+ * Policy: on a 429, honor the provider's `Retry-After` header when
4263
+ * present (capped at `RETRY_AFTER_CAP_SEC` — a provider asking for a
4264
+ * longer wait is treated as "don't retry", not "wait longer": making
4265
+ * the caller sit through 15+ more seconds on a single failed call is
4266
+ * a bad experience regardless of what's driving the request);
4267
+ * otherwise fall back to exponential backoff with jitter, capped at
4268
+ * `DEFAULT_BACKOFF_MAX_MS` per attempt. Stops after
4269
+ * `MAX_RETRY_ATTEMPTS` retries OR once the cumulative delay would
4270
+ * exceed `MAX_TOTAL_RETRY_DELAY_MS`, whichever comes first. Any
4271
+ * non-429 error, or a 429 with no attempts left, is logged and
4272
+ * re-thrown immediately — unchanged from the pre-#489 behavior.
4273
+ *
4274
+ * **Budget scope (final-review correction):** `MAX_TOTAL_RETRY_DELAY_MS`
4275
+ * bounds a single `apiCall()` invocation, not a whole generation. A
4276
+ * generation issues many sequential `apiCall()` calls (coding turns,
4277
+ * tool-result round-trips, eval rounds); under sustained throttling
4278
+ * each one can independently add up to `MAX_TOTAL_RETRY_DELAY_MS`, so
4279
+ * a generation with k rate-limited calls can add up to roughly `k *
4280
+ * MAX_TOTAL_RETRY_DELAY_MS` to how long it holds its caller's
4281
+ * resources (e.g. the caller's concurrency slot) — not a flat 20s
4282
+ * ceiling. No shared per-generation budget is threaded here by
4283
+ * design (accepted ruling, not an oversight): the caller's own
4284
+ * queue/admission backstop is what bounds how long other
4285
+ * queued work waits, independent of any one generation's retry
4286
+ * total.
4209
4287
  */
4210
4288
  async apiCall(fn) {
4211
4289
  const start = Date.now();
4212
- try {
4213
- return await fn();
4214
- } catch (e) {
4215
- const ms = Date.now() - start;
4216
- console.error(`[${this.provider}] API error after ${ms}ms: ${errorSummary(e)}`);
4217
- throw e;
4290
+ let totalDelayMs = 0;
4291
+ for (let attempt = 0; ; attempt++) {
4292
+ try {
4293
+ return await fn();
4294
+ } catch (e) {
4295
+ const ms = Date.now() - start;
4296
+ if (attempt >= MAX_RETRY_ATTEMPTS || !isRateLimited(e)) {
4297
+ console.error(`[${this.provider}] API error after ${ms}ms: ${errorSummary(e)}`);
4298
+ throw e;
4299
+ }
4300
+ const retryAfterSec = extractRetryAfterSec(e);
4301
+ if (retryAfterSec !== void 0 && retryAfterSec > RETRY_AFTER_CAP_SEC) {
4302
+ console.error(
4303
+ `[${this.provider}] API error after ${ms}ms (retry-after ${retryAfterSec}s exceeds ${RETRY_AFTER_CAP_SEC}s cap, not retrying): ${errorSummary(e)}`
4304
+ );
4305
+ throw e;
4306
+ }
4307
+ const delayMs = retryAfterSec !== void 0 ? retryAfterSec * 1e3 : Math.min(DEFAULT_BACKOFF_BASE_MS * 2 ** attempt, DEFAULT_BACKOFF_MAX_MS) + Math.floor(Math.random() * 250);
4308
+ if (totalDelayMs + delayMs > MAX_TOTAL_RETRY_DELAY_MS) {
4309
+ console.error(
4310
+ `[${this.provider}] API error after ${ms}ms (retry budget exhausted, not retrying): ${errorSummary(e)}`
4311
+ );
4312
+ throw e;
4313
+ }
4314
+ totalDelayMs += delayMs;
4315
+ const info = {
4316
+ provider: this.provider,
4317
+ attempt: attempt + 1,
4318
+ maxAttempts: MAX_RETRY_ATTEMPTS,
4319
+ status: extractStatus(e),
4320
+ retryAfterSec,
4321
+ delayMs,
4322
+ message: errorSummary(e)
4323
+ };
4324
+ console.warn(
4325
+ `[${this.provider}] rate-limited (attempt ${info.attempt}/${info.maxAttempts}), retrying in ${delayMs}ms`
4326
+ );
4327
+ try {
4328
+ this.onRetry?.(info);
4329
+ } catch (observerError) {
4330
+ console.warn(
4331
+ `[${this.provider}] onRetry observer threw (ignored, retry proceeds): ${errorSummary(observerError)}`
4332
+ );
4333
+ }
4334
+ await sleep(delayMs);
4335
+ }
4218
4336
  }
4219
4337
  }
4220
4338
  };
@@ -4307,6 +4425,73 @@ var init_llm_router = __esm({
4307
4425
  throw e;
4308
4426
  }
4309
4427
  }
4428
+ async callVision(model, systemPrompt, userPrompt, image, maxTokens) {
4429
+ const client = await this.getClient();
4430
+ const traceId = newLlmTraceId();
4431
+ const startedAt = Date.now();
4432
+ const resolvedModel = this.resolveModel(model);
4433
+ const imageBlock = {
4434
+ type: "image",
4435
+ source: {
4436
+ type: "base64",
4437
+ media_type: image.mediaType,
4438
+ data: image.base64
4439
+ }
4440
+ };
4441
+ try {
4442
+ const response = await this.apiCall(
4443
+ () => client.messages.create({
4444
+ model: resolvedModel,
4445
+ max_tokens: maxTokens ?? 1500,
4446
+ system: systemPrompt,
4447
+ messages: [
4448
+ {
4449
+ role: "user",
4450
+ content: [imageBlock, { type: "text", text: userPrompt }]
4451
+ }
4452
+ ]
4453
+ })
4454
+ );
4455
+ const text = response.content.filter((block) => block.type === "text").map((block) => block.text).join("");
4456
+ const endedAt = Date.now();
4457
+ emitLlmTraceEvent({
4458
+ id: traceId,
4459
+ at: startedAt,
4460
+ endedAt,
4461
+ durationMs: endedAt - startedAt,
4462
+ provider: "anthropic",
4463
+ model: resolvedModel,
4464
+ kind: "callVision",
4465
+ systemPrompt,
4466
+ userPrompt,
4467
+ result: {
4468
+ inputTokens: response.usage.input_tokens,
4469
+ outputTokens: response.usage.output_tokens,
4470
+ text
4471
+ }
4472
+ });
4473
+ return {
4474
+ text,
4475
+ inputTokens: response.usage.input_tokens,
4476
+ outputTokens: response.usage.output_tokens
4477
+ };
4478
+ } catch (e) {
4479
+ const endedAt = Date.now();
4480
+ emitLlmTraceEvent({
4481
+ id: traceId,
4482
+ at: startedAt,
4483
+ endedAt,
4484
+ durationMs: endedAt - startedAt,
4485
+ provider: "anthropic",
4486
+ model: resolvedModel,
4487
+ kind: "callVision",
4488
+ systemPrompt,
4489
+ userPrompt,
4490
+ error: { message: e instanceof Error ? e.message : String(e) }
4491
+ });
4492
+ throw e;
4493
+ }
4494
+ }
4310
4495
  async callTools(model, systemPrompt, userPrompt, tools, toolChoice = "required", _scopedTools) {
4311
4496
  const client = await this.getClient();
4312
4497
  const traceId = newLlmTraceId();
@@ -4681,6 +4866,72 @@ var init_llm_router = __esm({
4681
4866
  outputTokens: interaction.usage?.total_output_tokens ?? 0
4682
4867
  };
4683
4868
  }
4869
+ async callVision(model, systemPrompt, userPrompt, image, maxTokens) {
4870
+ const client = await this.getClient();
4871
+ const traceId = newLlmTraceId();
4872
+ const startedAt = Date.now();
4873
+ const resolvedModel = this.resolveModel(model);
4874
+ try {
4875
+ const response = await this.apiCall(
4876
+ () => client.models.generateContent({
4877
+ model: resolvedModel,
4878
+ contents: [
4879
+ {
4880
+ role: "user",
4881
+ parts: [
4882
+ { inlineData: { mimeType: image.mediaType, data: image.base64 } },
4883
+ { text: userPrompt }
4884
+ ]
4885
+ }
4886
+ ],
4887
+ config: {
4888
+ systemInstruction: systemPrompt,
4889
+ ...maxTokens ? { maxOutputTokens: maxTokens } : {}
4890
+ }
4891
+ })
4892
+ );
4893
+ const text = response.candidates?.[0]?.content?.parts?.map((p) => p.text || "").join("") || "";
4894
+ const usage = response.usageMetadata;
4895
+ const result = {
4896
+ text,
4897
+ inputTokens: usage?.promptTokenCount ?? 0,
4898
+ outputTokens: usage?.candidatesTokenCount ?? 0
4899
+ };
4900
+ const endedAt = Date.now();
4901
+ emitLlmTraceEvent({
4902
+ id: traceId,
4903
+ at: startedAt,
4904
+ endedAt,
4905
+ durationMs: endedAt - startedAt,
4906
+ provider: "google",
4907
+ model: resolvedModel,
4908
+ kind: "callVision",
4909
+ systemPrompt,
4910
+ userPrompt,
4911
+ result: {
4912
+ inputTokens: result.inputTokens,
4913
+ outputTokens: result.outputTokens,
4914
+ text
4915
+ }
4916
+ });
4917
+ return result;
4918
+ } catch (e) {
4919
+ const endedAt = Date.now();
4920
+ emitLlmTraceEvent({
4921
+ id: traceId,
4922
+ at: startedAt,
4923
+ endedAt,
4924
+ durationMs: endedAt - startedAt,
4925
+ provider: "google",
4926
+ model: resolvedModel,
4927
+ kind: "callVision",
4928
+ systemPrompt,
4929
+ userPrompt,
4930
+ error: { message: e instanceof Error ? e.message : String(e) }
4931
+ });
4932
+ throw e;
4933
+ }
4934
+ }
4684
4935
  async callTools(model, systemPrompt, userPrompt, tools, _toolChoice = "required", scopedTools) {
4685
4936
  const client = await this.getClient();
4686
4937
  const interactionTools = tools.map((t) => ({
@@ -5191,6 +5442,140 @@ var init_policy2 = __esm({
5191
5442
  }
5192
5443
  });
5193
5444
 
5445
+ // src/harness/check/runtime-render/render-check-host.ts
5446
+ var render_check_host_exports = {};
5447
+ __export(render_check_host_exports, {
5448
+ mapSandboxResultToCheckResult: () => mapSandboxResultToCheckResult,
5449
+ runRenderCheckViaWorker: () => runRenderCheckViaWorker
5450
+ });
5451
+ function resolveWorkerSpawn2() {
5452
+ const jsCandidates = [
5453
+ // Sibling — when import.meta.url is already .../harness/check/runtime-render/.
5454
+ new URL(`./${WORKER_BASENAME}.js`, import.meta.url),
5455
+ // From dist/index.js.
5456
+ new URL(`./harness/check/runtime-render/${WORKER_BASENAME}.js`, import.meta.url),
5457
+ // From dist/harness/index.js.
5458
+ new URL(`./check/runtime-render/${WORKER_BASENAME}.js`, import.meta.url),
5459
+ // From dist/check/index.js, dist/advanced/index.js, dist/<flat>.js.
5460
+ new URL(`../harness/check/runtime-render/${WORKER_BASENAME}.js`, import.meta.url)
5461
+ ];
5462
+ for (const jsUrl of jsCandidates) {
5463
+ const jsPath = fileURLToPath(jsUrl);
5464
+ if (existsSync(jsPath)) {
5465
+ return { command: process.execPath, args: [jsPath] };
5466
+ }
5467
+ }
5468
+ const tsCandidates = [
5469
+ new URL(`./${WORKER_BASENAME}.ts`, import.meta.url),
5470
+ new URL(`../harness/check/runtime-render/${WORKER_BASENAME}.ts`, import.meta.url)
5471
+ ];
5472
+ for (const tsUrl of tsCandidates) {
5473
+ const tsPath = fileURLToPath(tsUrl);
5474
+ if (existsSync(tsPath)) {
5475
+ const require_ = createRequire(import.meta.url);
5476
+ const tsxLoader = pathToFileURL(require_.resolve("tsx")).href;
5477
+ return { command: process.execPath, args: ["--import", tsxLoader, tsPath] };
5478
+ }
5479
+ }
5480
+ const tried = [...jsCandidates, ...tsCandidates].map((u) => fileURLToPath(u)).join(", ");
5481
+ throw new Error(
5482
+ `render-check: isolated worker not found at any of: ${tried}. Did \`pnpm build\` run?`
5483
+ );
5484
+ }
5485
+ function failed(reason, t0) {
5486
+ return {
5487
+ ok: false,
5488
+ issues: [{ check: "render-no-throw", outcome: "failed", reason }],
5489
+ stats: { actionsChecked: 0, streamsChecked: 0, renderMs: Date.now() - t0 }
5490
+ };
5491
+ }
5492
+ function unverified(reason, t0) {
5493
+ return {
5494
+ ok: false,
5495
+ issues: [{ check: "render-no-throw", outcome: "unverified", reason }],
5496
+ stats: { actionsChecked: 0, streamsChecked: 0, renderMs: Date.now() - t0 }
5497
+ };
5498
+ }
5499
+ function mapSandboxResultToCheckResult(result, t0) {
5500
+ if (result.outcome === "timeout") {
5501
+ return failed(
5502
+ `render check timed out after ${CHECK_TIMEOUT_MS}ms in the isolated worker (likely infinite loop or runaway effect in the component).`,
5503
+ t0
5504
+ );
5505
+ }
5506
+ if (result.outcome === "overflow-stdout" || result.outcome === "overflow-stderr") {
5507
+ return failed(
5508
+ `render check worker produced excessive output (${result.outcome}) \u2014 the component is likely in a pathological logging or error loop.`,
5509
+ t0
5510
+ );
5511
+ }
5512
+ if (result.outcome === "spawn-error") {
5513
+ return unverified(
5514
+ `render check worker failed to start \u2014 ${result.errorMessage}`,
5515
+ t0
5516
+ );
5517
+ }
5518
+ if (result.outcome === "canceled") {
5519
+ return unverified("render check worker was canceled before completing.", t0);
5520
+ }
5521
+ if (result.outcome !== "exit") {
5522
+ return unverified(
5523
+ `render check worker ended with unexpected sandbox outcome '${result.outcome}'.`,
5524
+ t0
5525
+ );
5526
+ }
5527
+ if (result.exitCode !== 0) {
5528
+ const tail = result.stderr.trim() || result.stdout.trim();
5529
+ return unverified(
5530
+ `render check worker exited ${result.exitCode}${tail ? ` \u2014 ${tail.slice(0, 600)}` : ""}`,
5531
+ t0
5532
+ );
5533
+ }
5534
+ const stdout = result.stdout.trim();
5535
+ if (stdout.length === 0) {
5536
+ return unverified("render check worker exited without producing a verdict.", t0);
5537
+ }
5538
+ try {
5539
+ return JSON.parse(stdout);
5540
+ } catch (err) {
5541
+ return unverified(
5542
+ `render check worker verdict was not valid JSON \u2014 ${err instanceof Error ? err.message : String(err)}`,
5543
+ t0
5544
+ );
5545
+ }
5546
+ }
5547
+ async function runRenderCheckViaWorker(input) {
5548
+ const t0 = Date.now();
5549
+ let spawn;
5550
+ try {
5551
+ spawn = resolveWorkerSpawn2();
5552
+ } catch (err) {
5553
+ return unverified(err instanceof Error ? err.message : String(err), t0);
5554
+ }
5555
+ const result = await runSandboxed({
5556
+ command: spawn.command,
5557
+ args: spawn.args,
5558
+ timeoutMs: CHECK_TIMEOUT_MS,
5559
+ maxStdoutBytes: CHECK_STDOUT_CAP,
5560
+ nodeHeapMb: CHECK_NODE_HEAP_MB,
5561
+ stdin: JSON.stringify(input),
5562
+ // NODE_ENV steers React's production vs development build — keep
5563
+ // parity with the caller; everything else stays on the sandbox's
5564
+ // default allowlist.
5565
+ env: { NODE_ENV: process.env.NODE_ENV ?? "production" }
5566
+ });
5567
+ return mapSandboxResultToCheckResult(result, t0);
5568
+ }
5569
+ var CHECK_TIMEOUT_MS, CHECK_STDOUT_CAP, CHECK_NODE_HEAP_MB, WORKER_BASENAME;
5570
+ var init_render_check_host = __esm({
5571
+ "src/harness/check/runtime-render/render-check-host.ts"() {
5572
+ CHECK_TIMEOUT_MS = 3e4;
5573
+ CHECK_STDOUT_CAP = 2 * 1024 * 1024;
5574
+ CHECK_NODE_HEAP_MB = 512;
5575
+ WORKER_BASENAME = "render-check-worker";
5576
+ }
5577
+ });
5578
+
5194
5579
  // src/harness/hashline.ts
5195
5580
  var hashline_exports = {};
5196
5581
  __export(hashline_exports, {
@@ -6025,7 +6410,11 @@ async function runLLMEvaluation(context, config, preWarmedContext) {
6025
6410
  designSystemSummary: context.designContext
6026
6411
  });
6027
6412
  const numberedSource = context.sourceCode.split("\n").map((line, i) => `${String(i + 1).padStart(4)} | ${line}`).join("\n");
6028
- const agent = createAgent({ provider: routerProvider, routeOverride: config.routeOverride });
6413
+ const agent = createAgent({
6414
+ provider: routerProvider,
6415
+ routeOverride: config.routeOverride,
6416
+ onRetry: config.onRetry
6417
+ });
6029
6418
  const evalStart = Date.now();
6030
6419
  const totalCriteriaCount = TIER_1_CRITERIA.length + TIER_2_CRITERIA.length;
6031
6420
  const results = await Promise.all([
@@ -9393,6 +9782,7 @@ var visual_evaluator_exports = {};
9393
9782
  __export(visual_evaluator_exports, {
9394
9783
  buildRenderHTML: () => buildRenderHTML,
9395
9784
  bundleForRendering: () => bundleForRendering,
9785
+ callMultimodalLLM: () => callMultimodalLLM,
9396
9786
  runVisualEval: () => runVisualEval,
9397
9787
  runVisualEvaluation: () => runVisualEvaluation
9398
9788
  });
@@ -9517,63 +9907,23 @@ async function captureScreenshot(html, viewport = { width: 1280, height: 800 })
9517
9907
  return null;
9518
9908
  }
9519
9909
  }
9520
- async function callMultimodalLLM(provider, model, prompt, screenshot, originalPrompt) {
9910
+ async function callMultimodalLLM(config, model, prompt, screenshot, originalPrompt) {
9521
9911
  const userPrompt = `## Original Request
9522
9912
  ${originalPrompt}
9523
9913
 
9524
9914
  Evaluate the screenshot of the generated component.`;
9525
- if (provider === "claude") {
9526
- const { default: Anthropic2 } = await import('@anthropic-ai/sdk');
9527
- const client2 = new Anthropic2({
9528
- apiKey: process.env.ANTHROPIC_API_KEY,
9529
- baseURL: "https://api.anthropic.com"
9530
- });
9531
- const response2 = await client2.messages.create({
9532
- model: model.startsWith("anthropic/") ? model.slice(10) : model,
9533
- max_tokens: 1500,
9534
- system: prompt,
9535
- messages: [{
9536
- role: "user",
9537
- content: [
9538
- {
9539
- type: "image",
9540
- source: {
9541
- type: "base64",
9542
- media_type: "image/png",
9543
- data: screenshot.toString("base64")
9544
- }
9545
- },
9546
- { type: "text", text: userPrompt }
9547
- ]
9548
- }]
9549
- });
9550
- return {
9551
- text: response2.content.filter((b) => b.type === "text").map((b) => b.text).join(""),
9552
- inputTokens: response2.usage.input_tokens,
9553
- outputTokens: response2.usage.output_tokens
9554
- };
9555
- }
9556
- const { GoogleGenAI } = await import('@google/genai');
9557
- const client = new GoogleGenAI({
9558
- apiKey: process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY
9915
+ const agent = createVisionAgent({
9916
+ provider: config.provider === "claude" ? "anthropic" : config.provider,
9917
+ routeOverride: config.routeOverride,
9918
+ onRetry: config.onRetry
9559
9919
  });
9560
- const response = await client.models.generateContent({
9561
- model: model.startsWith("google/") ? model.slice(7) : model,
9562
- contents: [{
9563
- role: "user",
9564
- parts: [
9565
- { inlineData: { mimeType: "image/png", data: screenshot.toString("base64") } },
9566
- { text: userPrompt }
9567
- ]
9568
- }],
9569
- config: { systemInstruction: prompt }
9570
- });
9571
- const usage = response.usageMetadata;
9572
- return {
9573
- text: response.candidates?.[0]?.content?.parts?.map((p) => p.text || "").join("") || "",
9574
- inputTokens: usage?.promptTokenCount ?? 0,
9575
- outputTokens: usage?.candidatesTokenCount ?? 0
9576
- };
9920
+ return agent.callVision(
9921
+ model,
9922
+ prompt,
9923
+ userPrompt,
9924
+ { mediaType: "image/png", base64: screenshot.toString("base64") },
9925
+ 1500
9926
+ );
9577
9927
  }
9578
9928
  async function runVisualEvaluation(context, config) {
9579
9929
  const startTime = Date.now();
@@ -9591,7 +9941,7 @@ async function runVisualEvaluation(context, config) {
9591
9941
  console.log(`[visual-eval] screenshot: ${screenshot.length}B (${Date.now() - startTime}ms)`);
9592
9942
  const model = config.model ?? getDefaultVisualModel(config.provider);
9593
9943
  const response = await callMultimodalLLM(
9594
- config.provider,
9944
+ config,
9595
9945
  model,
9596
9946
  VISUAL_EVAL_PROMPT,
9597
9947
  screenshot,
@@ -9665,6 +10015,7 @@ async function runVisualEval(context, config) {
9665
10015
  var VISUAL_EVAL_PROMPT;
9666
10016
  var init_visual_evaluator = __esm({
9667
10017
  "src/evaluation/visual-evaluator.ts"() {
10018
+ init_llm_router();
9668
10019
  VISUAL_EVAL_PROMPT = `You are a visual quality evaluator for ggui \u2014 a platform where AI agents generate React UI components on demand.
9669
10020
 
9670
10021
  ## Context
@@ -10127,10 +10478,12 @@ These tokens adapt automatically to any theme (light, dark, branded). **ALWAYS u
10127
10478
  - Borders/dividers \u2192 \`var(--ggui-color-outline)\` or \`var(--ggui-color-outlineVariant)\`
10128
10479
 
10129
10480
  ### State Colors
10130
- - var(--ggui-color-success) \u2014 success states
10131
- - var(--ggui-color-warning) \u2014 warning states
10132
- - var(--ggui-color-error) \u2014 error states, destructive actions
10133
- - var(--ggui-color-info) \u2014 informational
10481
+ Semantic families ship as SCALES; the \`500\` stop is the role color (per-mode brightness), \`50\` the tint, \`800\` the on-tint text:
10482
+ - var(--ggui-color-success-500) \u2014 success states (tint bg: success-50, text on tint: success-800)
10483
+ - var(--ggui-color-warning-500) \u2014 warning states (warning-50 / warning-800)
10484
+ - var(--ggui-color-error-500) \u2014 error states, destructive actions (error-50 / error-800)
10485
+ - var(--ggui-color-info-500) \u2014 informational (info-50 / info-800)
10486
+ Prefer the primitives' \`tone="success|warning|error|info"\` prop over hand-written vars \u2014 it resolves to the same stops.
10134
10487
 
10135
10488
  ### IMPORTANT: Color Rules
10136
10489
  - **NEVER** use hardcoded hex colors. ONLY use var(--ggui-color-*) tokens.
@@ -10430,16 +10783,19 @@ function generateSampleProps(spec) {
10430
10783
  return props;
10431
10784
  }
10432
10785
  function synthesizeFromSchema(schema) {
10433
- switch (schema.type) {
10786
+ const declared = Array.isArray(schema.type) ? schema.type.find((t) => t !== "null") : schema.type;
10787
+ switch (declared) {
10434
10788
  case "string":
10435
10789
  return "sample";
10436
10790
  case "number":
10791
+ case "integer":
10437
10792
  return 0;
10438
10793
  case "boolean":
10439
10794
  return false;
10795
+ case "null":
10796
+ return null;
10440
10797
  case "array": {
10441
- const itemSchema = schema.items;
10442
- if (itemSchema) return [synthesizeFromSchema(itemSchema) ?? null];
10798
+ if (schema.items) return [synthesizeFromSchema(schema.items) ?? null];
10443
10799
  return [];
10444
10800
  }
10445
10801
  case "object": {
@@ -12362,13 +12718,14 @@ function generateBoilerplate(_userPrompt, contract, shellType, screen, composedS
12362
12718
  const propsFields = [];
12363
12719
  const propsProperties = contract?.propsSpec?.properties ?? {};
12364
12720
  for (const [key, entry] of Object.entries(propsProperties)) {
12365
- const required = entry.required !== false;
12721
+ const required = entry.required === true;
12366
12722
  const nullable = entry.schema.nullable === true;
12367
12723
  const tsType = jsonSchemaTypeToTs(entry.schema);
12368
12724
  const fullType = nullable ? `${tsType} | null` : tsType;
12369
12725
  const parts = [];
12370
12726
  if (entry.description) parts.push(entry.description);
12371
12727
  if (entry.default !== void 0) parts.push(`(default: ${JSON.stringify(entry.default)})`);
12728
+ if (!required) parts.push("(optional \u2014 the agent may omit it; guard or fall back)");
12372
12729
  const desc = parts.length > 0 ? ` // ${parts.join(" ")}` : "";
12373
12730
  propsFields.push(` ${key}${required ? "" : "?"}: ${fullType};${desc}`);
12374
12731
  }
@@ -12927,6 +13284,7 @@ A \`DataContract\` declares everything a GguiSession exchanges with the outside
12927
13284
 
12928
13285
  Props arrive at mount and may be partial on first render; later prop changes reach the SAME mounted component via \`ggui_amend\` (in-place repaint) on the live channel. Stream channels start empty and fill over time. Context slots start at their declared default (often \`null\`). **Never assume a field exists before you read it.**
12929
13286
 
13287
+ - **Optional contract props**: a \`Props\` field marked \`?\` in the boilerplate may be omitted by the agent ENTIRELY \u2014 the wire gate only enforces \`required: true\` entries, and the harness verifies your component with those props stripped. Guard them (\`?? default\`, \`?.\`, or an empty-state branch) from the FIRST render.
12930
13288
  - **Array iteration**: always default to \`[]\` before \`.map\`/\`.filter\`/\`.length\`. Use \`(props.items ?? []).map(...)\` not \`props.items.map(...)\`. Same for stream.all, stream.latest, etc.
12931
13289
  - **Object access**: optional-chain through nested fields. \`props.user?.name ?? 'Anonymous'\` not \`props.user.name\`.
12932
13290
  - **Number ops**: default before arithmetic. \`(props.count ?? 0) + 1\` not \`props.count + 1\`.
@@ -14339,6 +14697,106 @@ var STATE_MERGE_CHECKS = [
14339
14697
  }
14340
14698
  ];
14341
14699
 
14700
+ // src/evaluation/axis-checks/checks/state-ui-affordance.ts
14701
+ var STATE_ARIA_OR_ROLE = /aria-(checked|pressed|selected|expanded|current)\s*=|role\s*=/;
14702
+ var STATE_PRIMITIVE_IN_SUBTREE = /<(Checkbox|Toggle|Tabs|Accordion)[\s/>]/;
14703
+ var SEMANTIC_PRIMITIVES = /* @__PURE__ */ new Set(["Button", "Checkbox", "Toggle", "Link", "MenuItem"]);
14704
+ function collectSetters(src) {
14705
+ const setters = /* @__PURE__ */ new Set();
14706
+ for (const m of src.matchAll(/const\s*\[\s*\w+\s*,\s*(set\w+)\s*\]\s*=\s*useState/g)) {
14707
+ setters.add(m[1]);
14708
+ }
14709
+ return setters;
14710
+ }
14711
+ function collectHandlers(src, setters) {
14712
+ const handlers = /* @__PURE__ */ new Set();
14713
+ for (const m of src.matchAll(/const\s+(\w+)\s*=\s*(?:\([^)]*\)|\w+)\s*=>\s*\{?/g)) {
14714
+ const body = src.slice(m.index + m[0].length, m.index + m[0].length + 400);
14715
+ for (const s of setters) {
14716
+ if (body.includes(s)) {
14717
+ handlers.add(m[1]);
14718
+ break;
14719
+ }
14720
+ }
14721
+ }
14722
+ return handlers;
14723
+ }
14724
+ function attrBlock(src, start) {
14725
+ let depth = 0;
14726
+ for (let i = start; i < src.length; i++) {
14727
+ const c = src[i];
14728
+ if (c === "{") depth++;
14729
+ else if (c === "}") depth--;
14730
+ else if (c === ">" && depth === 0) return { block: src.slice(start, i), end: i };
14731
+ }
14732
+ return { block: src.slice(start, start + 900), end: start + 900 };
14733
+ }
14734
+ function onClickValue(block) {
14735
+ const m = block.match(/onClick\s*=\s*\{/);
14736
+ if (!m || m.index === void 0) return null;
14737
+ const open = m.index + m[0].length - 1;
14738
+ let depth = 0;
14739
+ for (let i = open; i < block.length; i++) {
14740
+ if (block[i] === "{") depth++;
14741
+ else if (block[i] === "}") {
14742
+ depth--;
14743
+ if (depth === 0) return block.slice(open + 1, i);
14744
+ }
14745
+ }
14746
+ return block.slice(open + 1);
14747
+ }
14748
+ function subtree(src, pos, tag) {
14749
+ const close = src.indexOf(`</${tag}>`, pos);
14750
+ return src.slice(pos, close === -1 ? pos + 700 : close);
14751
+ }
14752
+ function runStateAriaPresent(input) {
14753
+ if (input.compiledCode === null) return [];
14754
+ const src = input.sourceCode;
14755
+ const setters = collectSetters(src);
14756
+ const handlers = collectHandlers(src, setters);
14757
+ const issues = [];
14758
+ const flaggedLines = /* @__PURE__ */ new Set();
14759
+ const localComponents = /* @__PURE__ */ new Set();
14760
+ for (const m of src.matchAll(/(?:function\s+([A-Z]\w*)\s*\(|const\s+([A-Z]\w*)\s*=)/g)) {
14761
+ localComponents.add(m[1] ?? m[2]);
14762
+ }
14763
+ for (const m of src.matchAll(/<([A-Za-z][A-Za-z0-9]*)\b/g)) {
14764
+ const tag = m[1];
14765
+ if (m.index === void 0) continue;
14766
+ if (localComponents.has(tag)) continue;
14767
+ const { block, end } = attrBlock(src, m.index + m[0].length);
14768
+ const clickValue = onClickValue(block);
14769
+ if (clickValue === null) continue;
14770
+ const mutatesAffordance = [...setters].some((s) => clickValue.includes(s)) || [...handlers].some((h) => new RegExp(`\\b${h}\\b`).test(clickValue)) || // Extracted-subcomponent shape: the toggle handler arrives as an
14771
+ // `on[A-Z]…` callback prop and is invoked from onClick — inside a
14772
+ // ui-affordance component that IS the affordance mutation.
14773
+ /\bon[A-Z]\w*\b/.test(clickValue);
14774
+ if (!mutatesAffordance) continue;
14775
+ if (SEMANTIC_PRIMITIVES.has(tag)) continue;
14776
+ if (STATE_ARIA_OR_ROLE.test(block)) continue;
14777
+ if (STATE_PRIMITIVE_IN_SUBTREE.test(subtree(src, end, tag))) continue;
14778
+ const line = src.slice(0, m.index).split("\n").length;
14779
+ if (flaggedLines.has(line)) continue;
14780
+ flaggedLines.add(line);
14781
+ issues.push(
14782
+ mkIssue(
14783
+ "state.ui_affordance.state_aria_present",
14784
+ `Line ${line}: <${tag}> toggles the UI affordance from its onClick but exposes no state ARIA \u2014 its checked/selected/expanded state is invisible to assistive technology (styling is the only carrier).`,
14785
+ `Add the matching state attribute to the SAME element that carries the onClick: \`role="checkbox"\` + \`aria-checked={\u2026}\` for a toggle row, \`aria-pressed\` for a toggle button, \`aria-selected\` for a selection, \`aria-expanded\` for an open/close affordance \u2014 or replace the hand-rolled control with the state-carrying primitive (Checkbox / Toggle / Tabs / Accordion). \`as={Clickable}\` adds button + keyboard semantics but NOT state.`
14786
+ )
14787
+ );
14788
+ }
14789
+ return issues;
14790
+ }
14791
+ var STATE_UI_AFFORDANCE_CHECKS = [
14792
+ {
14793
+ id: "state.ui_affordance.state_aria_present",
14794
+ axis: "state",
14795
+ values: ["ui-affordance"],
14796
+ run: runStateAriaPresent
14797
+ }
14798
+ ];
14799
+
14342
14800
  // src/evaluation/axis-checks/checks/realtime.ts
14343
14801
  var REALTIME_ACTIVE = ["merge", "append", "status", "presence", "mixed"];
14344
14802
  function runStreamHandlerPerEvent(input) {
@@ -15028,6 +15486,7 @@ var EXTRA_CHECKS = [
15028
15486
  var REGISTRY = [
15029
15487
  ...UNIVERSAL_CHECKS,
15030
15488
  ...STATE_MERGE_CHECKS,
15489
+ ...STATE_UI_AFFORDANCE_CHECKS,
15031
15490
  ...REALTIME_CHECKS,
15032
15491
  ...WRITES_CHECKS,
15033
15492
  ...STATE_PAYLOAD_CHECKS,
@@ -15198,8 +15657,8 @@ ${cjsResult.code}
15198
15657
  // src/harness/check/runtime-render/load-component.ts
15199
15658
  async function loadComponent(input) {
15200
15659
  const { sourceCode, moduleResolutions = {} } = input;
15201
- const { createRequire: createRequire3 } = await import('module');
15202
- const require_ = createRequire3(import.meta.url);
15660
+ const { createRequire: createRequire5 } = await import('module');
15661
+ const require_ = createRequire5(import.meta.url);
15203
15662
  const sandboxRequire = (id) => {
15204
15663
  if (id in moduleResolutions) return moduleResolutions[id];
15205
15664
  if (id === "react/jsx-runtime" || id === "react/jsx-dev-runtime") {
@@ -15455,6 +15914,47 @@ function wrapDetection(kind, jsx, native, custom) {
15455
15914
  function dedupe(arr) {
15456
15915
  return Array.from(new Set(arr));
15457
15916
  }
15917
+ var installed = false;
15918
+ function installProductionActShim() {
15919
+ if (installed) return;
15920
+ installed = true;
15921
+ const require_ = createRequire(import.meta.url);
15922
+ let reactCjs;
15923
+ try {
15924
+ reactCjs = require_("react");
15925
+ } catch {
15926
+ return;
15927
+ }
15928
+ if (typeof reactCjs.act === "function") {
15929
+ return;
15930
+ }
15931
+ try {
15932
+ const testUtils = require_("react-dom/test-utils");
15933
+ const reactDom = require_("react-dom");
15934
+ testUtils.act = createFlushSyncAct(reactDom.flushSync);
15935
+ } catch {
15936
+ }
15937
+ }
15938
+ function createFlushSyncAct(flushSync) {
15939
+ return (callback) => {
15940
+ let callbackResult;
15941
+ flushSync(() => {
15942
+ callbackResult = callback();
15943
+ });
15944
+ if (isThenable(callbackResult)) {
15945
+ const pending = callbackResult;
15946
+ return new Promise((resolve3, reject) => {
15947
+ pending.then(() => resolve3(), reject);
15948
+ });
15949
+ }
15950
+ return new Promise((resolve3) => {
15951
+ queueMicrotask(resolve3);
15952
+ });
15953
+ };
15954
+ }
15955
+ function isThenable(value) {
15956
+ return typeof value === "object" && value !== null && typeof value.then === "function";
15957
+ }
15458
15958
 
15459
15959
  // src/internal/open-record.ts
15460
15960
  function openRecord(o) {
@@ -15517,6 +16017,14 @@ function parentLink(node) {
15517
16017
 
15518
16018
  // src/harness/check/runtime-render/render-check.ts
15519
16019
  async function runRenderCheck(input) {
16020
+ const g = hostGlobals();
16021
+ if ("window" in g && "document" in g) {
16022
+ return runRenderCheckInProcess(input);
16023
+ }
16024
+ const { runRenderCheckViaWorker: runRenderCheckViaWorker2 } = await Promise.resolve().then(() => (init_render_check_host(), render_check_host_exports));
16025
+ return runRenderCheckViaWorker2(input);
16026
+ }
16027
+ async function runRenderCheckInProcess(input) {
15520
16028
  const t0 = Date.now();
15521
16029
  const issues = [];
15522
16030
  const teardown = await setupHappyDom();
@@ -15572,6 +16080,7 @@ async function runRenderCheck(input) {
15572
16080
  const probe = createProbe();
15573
16081
  const wireConfig = createProbeWireConfig(probe);
15574
16082
  uninstallSpy = probe.installPostMessageSpy();
16083
+ installProductionActShim();
15575
16084
  const { render, cleanup } = await import('@testing-library/react');
15576
16085
  const userEventModule = await import('@testing-library/user-event');
15577
16086
  const userEvent = userEventModule.default ?? userEventModule;
@@ -15727,6 +16236,50 @@ async function runRenderCheck(input) {
15727
16236
  if (issue) issues.push(issue);
15728
16237
  }
15729
16238
  }
16239
+ const declaredProps = input.contract?.propsSpec?.properties;
16240
+ if (declaredProps) {
16241
+ const optionalFilled = Object.entries(declaredProps).filter(
16242
+ ([name, entry]) => entry.required !== true && input.mockupProps[name] !== void 0
16243
+ ).map(([name]) => name);
16244
+ if (optionalFilled.length > 0) {
16245
+ const requiredOnlyProps = { ...input.mockupProps };
16246
+ for (const name of optionalFilled) delete requiredOnlyProps[name];
16247
+ const omitBoundaryRef = {};
16248
+ class OmitErrorBoundary extends React.Component {
16249
+ state = { caught: false };
16250
+ static getDerivedStateFromError() {
16251
+ return { caught: true };
16252
+ }
16253
+ componentDidCatch(error) {
16254
+ omitBoundaryRef.error ??= error;
16255
+ }
16256
+ render() {
16257
+ if (this.state.caught) return null;
16258
+ return this.props.children;
16259
+ }
16260
+ }
16261
+ try {
16262
+ render(
16263
+ React.createElement(GguiWireProvider, {
16264
+ config: wireConfig,
16265
+ children: React.createElement(OmitErrorBoundary, {
16266
+ children: React.createElement(Component, requiredOnlyProps)
16267
+ })
16268
+ })
16269
+ );
16270
+ } catch (e) {
16271
+ omitBoundaryRef.error ??= e instanceof Error ? e : new Error(String(e));
16272
+ }
16273
+ if (omitBoundaryRef.error) {
16274
+ issues.push({
16275
+ check: "optional-props-omitted",
16276
+ outcome: "failed",
16277
+ subject: optionalFilled.join(", "),
16278
+ reason: `Render crashed when the optional prop(s) ${optionalFilled.map((n) => `'${n}'`).join(", ")} were omitted: ${omitBoundaryRef.error.message}. The contract marks them optional (no \`required: true\`), so a legitimate render may leave them out.`
16279
+ });
16280
+ }
16281
+ }
16282
+ }
15730
16283
  } finally {
15731
16284
  cleanup();
15732
16285
  }
@@ -16298,6 +16851,15 @@ function synthesizeFromSchema2(schema, hint, warnings, depth) {
16298
16851
  if (schema.anyOf && schema.anyOf.length > 0) {
16299
16852
  return synthesizeFromSchema2(schema.anyOf[0], hint, warnings, depth + 1);
16300
16853
  }
16854
+ if (Array.isArray(schema.type)) {
16855
+ const primary = schema.type.find((t) => t !== "null") ?? "null";
16856
+ return synthesizeFromSchema2(
16857
+ { ...schema, type: primary },
16858
+ hint,
16859
+ warnings,
16860
+ depth + 1
16861
+ );
16862
+ }
16301
16863
  switch (schema.type) {
16302
16864
  case "string": {
16303
16865
  switch (schema.format) {
@@ -16468,6 +17030,16 @@ function toEvalIssue(issue) {
16468
17030
  description: `Component crashed at runtime: ${issue.reason}`,
16469
17031
  fix: classifyRenderCrashFix(issue.reason)
16470
17032
  };
17033
+ case "optional-props-omitted":
17034
+ return {
17035
+ tier: 0,
17036
+ result,
17037
+ category: "crash",
17038
+ subcategory,
17039
+ severity: "critical",
17040
+ description: `Component crashed when optional props were omitted: ${issue.reason}`,
17041
+ fix: `Guard every optional prop before member access (props.${subject.split(",")[0]?.trim() ?? "x"}?.map(...), a default via destructuring, or an early empty-state return). Optional props (no \`required: true\` in the contract) may be absent at runtime even though the harness fills them.`
17042
+ };
16471
17043
  case "action-wiring": {
16472
17044
  const fix = issue.outcome === "unverified" ? `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 \u2014 manual/browser verification is required to confirm. Otherwise wire to a native onClick={() => ${subject}(payload)} on <button> or design-system <Button>.` : `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.`;
16473
17045
  return {
@@ -16514,6 +17086,7 @@ async function warmupRuntimeRenderProbe() {
16514
17086
  missing += 1;
16515
17087
  }
16516
17088
  };
17089
+ installProductionActShim();
16517
17090
  await Promise.all([
16518
17091
  tryLoad("happy-dom"),
16519
17092
  tryLoad("@testing-library/react"),
@@ -17590,6 +18163,14 @@ function mapProviderForEvaluator(provider) {
17590
18163
 
17591
18164
  // src/harness/coding/run-eval-round.ts
17592
18165
  var MAX_FEEDBACK_ISSUES = 3;
18166
+ function selectActionableFeedback(issues) {
18167
+ const pool = issues.filter((i) => i.result === "fail" || i.result === "warn");
18168
+ const reserved = pool.find((i) => i.tier === 0 && i.result === "fail");
18169
+ return [
18170
+ ...reserved ? [reserved] : [],
18171
+ ...pool.filter((i) => i !== reserved).sort((a, b) => (a.result === "fail" ? 0 : 1) - (b.result === "fail" ? 0 : 1))
18172
+ ].slice(0, MAX_FEEDBACK_ISSUES);
18173
+ }
17593
18174
  function fingerprintFail(issue) {
17594
18175
  const cat = issue.category ?? "";
17595
18176
  const sub = issue.subcategory ?? "";
@@ -17764,7 +18345,14 @@ async function runEvalRound(ctx, input) {
17764
18345
  // back to `process.env` either (the eval calls run
17765
18346
  // inside the same concurrent generation, so the race is
17766
18347
  // real here too, not just on the coding turn).
17767
- routeOverride: evaluationAgent.routeOverride
18348
+ routeOverride: evaluationAgent.routeOverride,
18349
+ // #489 — thread the same provider-429-retry observer.
18350
+ // Without this, a rate-limited retry inside the
18351
+ // evaluation agent's apiCall() happens (the base-class
18352
+ // retry loop is unconditional) but never reaches the
18353
+ // caller, silently missing the host application's
18354
+ // structured log.
18355
+ onRetry: evaluationAgent.onRetry
17768
18356
  },
17769
18357
  preWarmedContext
17770
18358
  ) : null,
@@ -17775,7 +18363,15 @@ async function runEvalRound(ctx, input) {
17775
18363
  model: visualEvalAgent.model,
17776
18364
  passThreshold: visualThreshold,
17777
18365
  sampleProps: visualEvaluation?.sampleProps,
17778
- viewport: visualEvaluation?.viewport
18366
+ viewport: visualEvaluation?.viewport,
18367
+ // #484/#489 — same threading as the LLM-eval leg above:
18368
+ // the visual eval's multimodal call runs inside the same
18369
+ // concurrent generation, so it needs the same routing
18370
+ // override (never fall back to `process.env`) and the
18371
+ // same provider-429-retry observer (retries reach the
18372
+ // host application's structured log).
18373
+ routeOverride: visualEvalAgent.routeOverride,
18374
+ onRetry: visualEvalAgent.onRetry
17779
18375
  }
17780
18376
  ) : null
17781
18377
  ]);
@@ -17994,7 +18590,7 @@ async function runEvalRound(ctx, input) {
17994
18590
  lastDiffFailed: false
17995
18591
  };
17996
18592
  }
17997
- const allActionable = evalResult.issues.filter((i) => i.result === "fail" || i.result === "warn").sort((a, b) => (a.result === "fail" ? 0 : 1) - (b.result === "fail" ? 0 : 1)).slice(0, MAX_FEEDBACK_ISSUES);
18593
+ const allActionable = selectActionableFeedback(evalResult.issues);
17998
18594
  const issueLines = allActionable.map((issue) => {
17999
18595
  const tag = issue.result === "fail" ? "FAIL" : "WARN";
18000
18596
  return `[${tag}] ${issue.category}${issue.subcategory ? "/" + issue.subcategory : ""}: ${issue.description}
@@ -18009,7 +18605,16 @@ async function runEvalRound(ctx, input) {
18009
18605
  evalResult,
18010
18606
  evalRoundsUsed,
18011
18607
  prevModeSubcats: updatedPrevModeSubcats,
18012
- prevFailFingerprints: currFailFingerprints,
18608
+ // DELIVERED fails only (Exp 54 P1 addendum, proposal 2): the
18609
+ // stuck detector's premise is "the LLM saw these and did not
18610
+ // resolve them" — recording fails that the MAX_FEEDBACK_ISSUES
18611
+ // cap cut from the feedback turns truncation into a premature
18612
+ // stuck-exit (the model never saw the issue it is blamed for
18613
+ // not fixing). An undelivered fail recurring next round now
18614
+ // reads as NEW, gets delivered, and earns its fix round.
18615
+ prevFailFingerprints: new Set(
18616
+ allActionable.filter((i) => i.result === "fail").map(fingerprintFail)
18617
+ ),
18013
18618
  preWarmedContext,
18014
18619
  evalTokens,
18015
18620
  evalLlmMs,
@@ -18504,12 +19109,14 @@ async function dispatchGeneration(params) {
18504
19109
  const codingAgent = {
18505
19110
  provider: routerProvider,
18506
19111
  model: resolvedModel,
18507
- routeOverride: params.routeOverride
19112
+ routeOverride: params.routeOverride,
19113
+ onRetry: params.onRetry
18508
19114
  };
18509
19115
  const evaluationAgent = params.models?.evaluation ? {
18510
19116
  provider: routerProvider,
18511
19117
  model: params.models.evaluation,
18512
- routeOverride: params.routeOverride
19118
+ routeOverride: params.routeOverride,
19119
+ onRetry: params.onRetry
18513
19120
  } : void 0;
18514
19121
  const effectiveContract = params.contract;
18515
19122
  const sessionParams = {
@@ -18870,11 +19477,15 @@ ${JSON.stringify(clientCapabilities, null, 2)}
18870
19477
  const uiRequirements = [];
18871
19478
  if (contract.propsSpec?.properties) {
18872
19479
  for (const [name, entry] of Object.entries(contract.propsSpec.properties)) {
18873
- const required = entry.required;
18874
- if (required !== false) {
19480
+ const required = entry.required === true;
19481
+ if (required) {
18875
19482
  uiRequirements.push(
18876
19483
  `- **Required prop \`${name}\`** must appear somewhere in rendered DOM (display the value, or use it to drive a label/aria/key).`
18877
19484
  );
19485
+ } else {
19486
+ uiRequirements.push(
19487
+ `- **Optional prop \`${name}\`** may be ABSENT at runtime \u2014 the component must render without it (guard the access or apply its default) and must display it when it IS provided.`
19488
+ );
18878
19489
  }
18879
19490
  }
18880
19491
  }
@@ -18958,7 +19569,8 @@ function createUiGenerator(options = {}) {
18958
19569
  maxEvalRounds,
18959
19570
  qualityConfig,
18960
19571
  gadgetCatalog,
18961
- disableEnvMutation = false
19572
+ disableEnvMutation = false,
19573
+ onRetry
18962
19574
  } = options;
18963
19575
  const identity = resolveIdentity(options);
18964
19576
  return {
@@ -19036,7 +19648,8 @@ function createUiGenerator(options = {}) {
19036
19648
  // `dispatchGeneration`.
19037
19649
  ...input.gadgetTypes !== void 0 ? { gadgetTypes: input.gadgetTypes } : {},
19038
19650
  enableRuntimeRender,
19039
- ...routeOverride !== void 0 ? { routeOverride } : {}
19651
+ ...routeOverride !== void 0 ? { routeOverride } : {},
19652
+ ...onRetry !== void 0 ? { onRetry } : {}
19040
19653
  });
19041
19654
  const metadata = {
19042
19655
  provider: input.llm.provider,