@ggui-ai/ui-gen 0.23.0 → 0.25.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 (123) hide show
  1. package/dist/adapter-Cg_rkpGq.d.ts +201 -0
  2. package/dist/adapters/generation-dispatch.d.ts +58 -15
  3. package/dist/adapters/generation-dispatch.js +1418 -180
  4. package/dist/adapters/generation-dispatch.js.map +1 -1
  5. package/dist/adapters/index.js +3 -1
  6. package/dist/adapters/index.js.map +1 -1
  7. package/dist/advanced/index.d.ts +12 -8
  8. package/dist/advanced/index.js +1467 -239
  9. package/dist/advanced/index.js.map +1 -1
  10. package/dist/{axes-CzLEMDeB.d.ts → axes-CKVCzR_x.d.ts} +1 -1
  11. package/dist/bank-CT4eEkta.d.ts +118 -0
  12. package/dist/blueprint-validator.js +153 -59
  13. package/dist/blueprint-validator.js.map +1 -1
  14. package/dist/boilerplate.js +24 -12
  15. package/dist/boilerplate.js.map +1 -1
  16. package/dist/check/index.d.ts +12 -4
  17. package/dist/check/index.js +94 -2
  18. package/dist/check/index.js.map +1 -1
  19. package/dist/classifier/index.d.ts +2 -2
  20. package/dist/compose.d.ts +2 -2
  21. package/dist/{create-ui-generator-D0kbWwln.d.ts → create-ui-generator-DFnnArrv.d.ts} +43 -11
  22. package/dist/{dispatch-wxaIMwZt.d.ts → dispatch-CStl9eAw.d.ts} +5 -3
  23. package/dist/evaluation/axis-checks/index.d.ts +6 -5
  24. package/dist/evaluation/axis-checks/index.js +90 -2
  25. package/dist/evaluation/axis-checks/index.js.map +1 -1
  26. package/dist/evaluation/axis-checks/registry.d.ts +4 -3
  27. package/dist/evaluation/axis-checks/registry.js +88 -1
  28. package/dist/evaluation/axis-checks/registry.js.map +1 -1
  29. package/dist/evaluation/index.d.ts +301 -23
  30. package/dist/evaluation/index.js +746 -30
  31. package/dist/evaluation/index.js.map +1 -1
  32. package/dist/evaluation/types.d.ts +9 -1
  33. package/dist/evaluation/types.js.map +1 -1
  34. package/dist/fragments/index.d.ts +3 -3
  35. package/dist/harness/check/runtime-render/index.d.ts +30 -11
  36. package/dist/harness/check/runtime-render/index.js +203 -60
  37. package/dist/harness/check/runtime-render/index.js.map +1 -1
  38. package/dist/harness/check/runtime-render/render-check-worker.d.ts +8 -1
  39. package/dist/harness/check/runtime-render/render-check-worker.js +72 -32
  40. package/dist/harness/check/runtime-render/render-check-worker.js.map +1 -1
  41. package/dist/harness/index.d.ts +5 -4
  42. package/dist/harness/index.js +33 -15
  43. package/dist/harness/index.js.map +1 -1
  44. package/dist/harness/prompts.js.map +1 -1
  45. package/dist/harness/result-types.d.ts +4 -3
  46. package/dist/harness/runtime.d.ts +11 -4
  47. package/dist/harness/runtime.js +26 -14
  48. package/dist/harness/runtime.js.map +1 -1
  49. package/dist/harness/types-public.d.ts +13 -4
  50. package/dist/harness/types-public.js +33 -15
  51. package/dist/harness/types-public.js.map +1 -1
  52. package/dist/harness/validator-trace-sink.d.ts +4 -3
  53. package/dist/hash.d.ts +1 -1
  54. package/dist/index.d.ts +59 -8
  55. package/dist/index.js +1476 -241
  56. package/dist/index.js.map +1 -1
  57. package/dist/{llm-router-DwwUK1bz.d.ts → llm-router-nfO7wPec.d.ts} +3 -1
  58. package/dist/patch.d.ts +4 -3
  59. package/dist/policy.d.ts +8 -5
  60. package/dist/policy.js.map +1 -1
  61. package/dist/tools/get-primitives-ts.d.ts +1 -1
  62. package/dist/tools/get-primitives-ts.js +1 -1
  63. package/dist/tools/get-primitives-ts.js.map +1 -1
  64. package/dist/tools/render-check-worker.d.ts +7 -1
  65. package/dist/tools/render-check-worker.js +55 -23
  66. package/dist/tools/render-check-worker.js.map +1 -1
  67. package/dist/{types-BOvHNG7K.d.ts → types-Dh1s4K9j.d.ts} +1 -1
  68. package/dist/{types-public-Ow-tNtFE.d.ts → types-public-BJTLpzWn.d.ts} +208 -15
  69. package/dist/validation/index.d.ts +1 -1
  70. package/dist/validation/index.js +1 -1
  71. package/dist/validation/index.js.map +1 -1
  72. package/dist/workflows.d.ts +4 -3
  73. package/package.json +12 -12
  74. package/src/adapters/generation-dispatch.ts +173 -98
  75. package/src/adapters/tools.ts +2 -1
  76. package/src/boilerplate/free-design-prompt.ts +1 -1
  77. package/src/boilerplate/generate.ts +26 -1
  78. package/src/boilerplate/hard-sections.ts +13 -13
  79. package/src/boilerplate/system-prompt.ts +1 -1
  80. package/src/check/index.ts +1 -0
  81. package/src/check/run-tier0.ts +80 -1
  82. package/src/check/sample-props.ts +63 -0
  83. package/src/coding-agent/tools.ts +4 -1
  84. package/src/create-harness.ts +3 -3
  85. package/src/create-ui-generator.ts +204 -95
  86. package/src/evaluation/__fixtures__/png.ts +100 -0
  87. package/src/evaluation/axis-checks/checks/state-merge.ts +1 -1
  88. package/src/evaluation/axis-checks/checks/universal.ts +111 -0
  89. package/src/evaluation/axis-checks/dispatch.ts +4 -1
  90. package/src/evaluation/criteria/bank.ts +85 -0
  91. package/src/evaluation/criteria/context.ts +52 -0
  92. package/src/evaluation/criteria/resolve.ts +159 -0
  93. package/src/evaluation/criteria/select.ts +79 -0
  94. package/src/evaluation/index.ts +23 -0
  95. package/src/evaluation/ink-extent.ts +200 -0
  96. package/src/evaluation/theme-css.ts +12 -5
  97. package/src/evaluation/types-public.ts +200 -11
  98. package/src/evaluation/types.ts +9 -0
  99. package/src/evaluation/visual-evaluator.ts +636 -30
  100. package/src/harness/check/runtime-render/adapter.ts +221 -75
  101. package/src/harness/check/runtime-render/index.ts +75 -2
  102. package/src/harness/check/runtime-render/render-check-host.ts +82 -19
  103. package/src/harness/check/runtime-render/render-check-worker.ts +83 -29
  104. package/src/harness/check/runtime-render/render-check.ts +154 -117
  105. package/src/harness/coding/generate-task-runner.ts +60 -4
  106. package/src/harness/coding/init-session.ts +13 -0
  107. package/src/harness/coding/run-coding-turn.ts +7 -3
  108. package/src/harness/coding/run-eval-round.ts +257 -15
  109. package/src/harness/policy.ts +11 -0
  110. package/src/harness/primitive-slice.ts +104 -0
  111. package/src/harness/runtime.ts +5 -0
  112. package/src/harness/types-public.ts +9 -1
  113. package/src/harness-tool-names.ts +38 -0
  114. package/src/index.ts +16 -0
  115. package/src/policy.ts +7 -4
  116. package/src/run-check.ts +2 -0
  117. package/src/tools/get-primitives-ts.ts +1 -1
  118. package/src/tools/get-wire.ts +1 -1
  119. package/src/tools/render-check-worker.ts +74 -23
  120. package/src/tools/render-check.ts +0 -60
  121. package/src/tools.ts +11 -1
  122. package/src/validation/primitives.ts +1 -1
  123. package/dist/render-check-Blwj8gB6.d.ts +0 -105
@@ -31,66 +31,97 @@
31
31
  * Callers who want a lightweight single-shot path can build it
32
32
  * themselves.
33
33
  */
34
- import type { SingleComponentParams } from './harness/runtime.js';
34
+ import type { SingleComponentParams } from "./harness/runtime.js";
35
35
  import type {
36
36
  GadgetDescriptor,
37
37
  GenerationError,
38
38
  JsonObject,
39
39
  GeneratorId,
40
40
  ModelRef,
41
- } from '@ggui-ai/protocol';
42
- import { isGeneratorId, modelRefOfRoute } from '@ggui-ai/protocol';
43
- import type { GadgetCatalogAdapter } from '@ggui-ai/gadgets';
41
+ AppGenerationProfileEffort,
42
+ } from "@ggui-ai/protocol";
43
+ import { isGeneratorId, modelRefOfRoute } from "@ggui-ai/protocol";
44
+ import type { GadgetCatalogAdapter } from "@ggui-ai/gadgets";
44
45
  import type {
45
46
  GenerationMetadata,
47
+ GenerationRuntimeProbe,
48
+ GenerationRuntimeProbeOutcome,
49
+ GenerationRuntimeProbeRepair,
46
50
  GeneratorTier,
47
51
  LlmProvider,
48
52
  UiGenerateInput,
49
53
  UiGenerateResult,
50
54
  UiGenerator,
51
- } from '@ggui-ai/mcp-server-core';
55
+ } from "@ggui-ai/mcp-server-core";
52
56
  import {
53
57
  formatGeneratorSlug,
54
58
  isValidGeneratorSlug,
55
59
  parseGeneratorSlug,
56
- } from '@ggui-ai/mcp-server-core';
57
- import { createGeneratorTools } from './adapters/index.js';
58
- import { dispatchGeneration } from './adapters/generation-dispatch.js';
59
- import { generatorBuild } from './generator-build.js';
60
- import { hasProfile } from './boilerplate/styling-profile.js';
61
- import { effortDials } from './effort.js';
62
- import type { ProviderName } from './adapters/types.js';
63
- import type { GenerationResult } from './harness/result-types.js';
60
+ } from "@ggui-ai/mcp-server-core";
61
+ import { createGeneratorTools } from "./adapters/index.js";
62
+ import { dispatchGeneration, type VisualEvalAgentSpec } from "./adapters/generation-dispatch.js";
63
+ import {
64
+ createRuntimeRenderCheck,
65
+ type RuntimeRenderProbeConfig,
66
+ } from "./harness/check/runtime-render/adapter.js";
67
+ import { generatorBuild } from "./generator-build.js";
68
+ import { hasProfile } from "./boilerplate/styling-profile.js";
69
+ import { effortDials } from "./effort.js";
70
+ import type { ProviderName } from "./adapters/types.js";
71
+ import type { GenerationResult } from "./harness/result-types.js";
64
72
  import {
65
73
  canvasForRenderingContext,
66
74
  injectContracts,
67
75
  injectRenderingContext,
68
76
  injectVariance,
69
- } from './contract-context.js';
70
- import type { RenderingContext } from './contract-context.js';
71
- import type { DesignMode } from './design-mode.js';
72
- import { DEFAULT_DESIGN_MODE } from './design-mode.js';
73
- import { resolveRoute, applyRouteToEnv } from './adapters/provider-router.js';
74
- import type { QualityConfig } from './evaluation/types-public.js';
75
- import type { EvaluationConfig } from './evaluation/types.js';
76
- import type { AgentConfig } from './harness/llm-router.js';
77
+ } from "./contract-context.js";
78
+ import type { RenderingContext } from "./contract-context.js";
79
+ import type { DesignMode } from "./design-mode.js";
80
+ import { DEFAULT_DESIGN_MODE } from "./design-mode.js";
81
+ import { resolveRoute, applyRouteToEnv } from "./adapters/provider-router.js";
82
+ import type { QualityConfig, RuntimeProbeMeta, RuntimeProbeRepair } from "./evaluation/types-public.js";
83
+ import type { EvaluationConfig } from "./evaluation/types.js";
84
+ import type { AgentConfig } from "./harness/llm-router.js";
77
85
 
78
86
  /** The slug for the OSS default seed generator. */
79
- const DEFAULT_TIER: GeneratorTier = 'default';
80
- const DEFAULT_MODEL: ModelRef = 'anthropic/claude-haiku-4-5';
87
+ const DEFAULT_TIER: GeneratorTier = "default";
88
+ const DEFAULT_MODEL: ModelRef = "anthropic/claude-haiku-4-5";
81
89
 
82
90
  export interface CreateUiGeneratorOptions {
83
91
  /**
84
- * Wire `DEFAULT_RUNTIME_RENDER_CHECK` into the harness's check leg.
85
- * When `true`, every coding turn ends with a happy-dom runtime-render
86
- * probe that catches contract-wiring bugs (missing `useAction()`,
87
- * wrong stream channel name, etc.).
92
+ * Wire the runtime-render check into the harness's check leg — one
93
+ * instance per generator, built from `runtimeRenderProbe` (the default
94
+ * bounds when it is absent). When `true`, the runtime-render probe runs
95
+ * ONCE at the exit decision,
96
+ * after the coding turns, in an isolated subprocess: it mounts the
97
+ * compiled component against the contract and catches crash-class and
98
+ * contract-wiring bugs (a render that throws, a missing `useAction()`,
99
+ * a wrong stream channel name). It is never on the per-turn hot path.
100
+ *
101
+ * Without an `evaluation` config the round is observe + one repair turn
102
+ * (ggui#1380): a recoverable render crash buys exactly one repair turn
103
+ * and one re-probe, then the generation serves what it has; a probe that
104
+ * timed out, could not run, or had nothing to probe never blocks and
105
+ * never repairs. The probe's status and timing ride the result's
106
+ * metadata (`runtimeProbe`, `evalMs`). With an `evaluation` config the
107
+ * probe is the evaluation round's exit gate instead.
88
108
  *
89
- * Default: `false` for OSS — keeps cold-start light. The probe pulls
90
- * happy-dom + @testing-library on first use (~700-1500ms cold). Bench
91
- * enables it via dispatch's own default for stricter quality gating.
109
+ * Default: `false` — keeps cold-start light. The probe pulls happy-dom
110
+ * + @testing-library on first use (~700-1500ms cold). A serving
111
+ * deployment that wants the probe's verdict on every card sets it.
92
112
  */
93
113
  readonly enableRuntimeRender?: boolean;
114
+ /**
115
+ * How the runtime-render probe runs its isolated worker (ggui#1380). A
116
+ * serving deployment sets the probe's wall-clock bound, worker heap and
117
+ * concurrency here; the defaults are the evaluation lane's (30 s, 512 MB,
118
+ * no concurrency cap). The generator builds ONE check from it and hands
119
+ * that instance to every generation it dispatches — the concurrency cap
120
+ * lives on the instance, so it bounds the workers this generator has
121
+ * live at once. Only read when `enableRuntimeRender` is on. Never part
122
+ * of the prompt or the identity.
123
+ */
124
+ readonly runtimeRenderProbe?: RuntimeRenderProbeConfig;
94
125
  /** Maximum coding attempts per generation pass. */
95
126
  readonly maxAttempts?: number;
96
127
  /** Maximum evaluation rounds. */
@@ -108,7 +139,14 @@ export interface CreateUiGeneratorOptions {
108
139
  * stylesheet to render under). Forwarded verbatim; never part of the
109
140
  * prompt or the identity. Default: none — the round runs as today.
110
141
  */
111
- readonly visualEvaluation?: SingleComponentParams['visualEvaluation'];
142
+ readonly visualEvaluation?: SingleComponentParams["visualEvaluation"];
143
+ /**
144
+ * ggui#1250 — the in-loop VISUAL judge as its own agent (a vision provider, a model, and that
145
+ * provider's key when the caller resolves keys itself). Absent (default): the judge is the
146
+ * evaluation agent's provider, which on a lane without a vision path records the score half as
147
+ * skipped (#1248). Forwarded verbatim to the dispatch; never part of the prompt or the identity.
148
+ */
149
+ readonly visualEvalAgent?: VisualEvalAgentSpec;
112
150
  /** Quality config controlling eval tiers + improvement behavior. */
113
151
  readonly qualityConfig?: QualityConfig;
114
152
  /**
@@ -189,7 +227,7 @@ export interface CreateUiGeneratorOptions {
189
227
  * request. Absent (default) is a no-op — retries still happen and
190
228
  * still log via `console.warn`, just without a caller-side hook.
191
229
  */
192
- readonly onRetry?: AgentConfig['onRetry'];
230
+ readonly onRetry?: AgentConfig["onRetry"];
193
231
  /**
194
232
  * Generation observer. Invoked once per successful `generate()` with
195
233
  * the FULL harness result — source, compiled code, token counters,
@@ -221,15 +259,15 @@ export interface CreateUiGeneratorOptions {
221
259
  readonly designMode?: DesignMode;
222
260
  }
223
261
 
224
- export function createUiGenerator(
225
- options: CreateUiGeneratorOptions = {},
226
- ): UiGenerator {
262
+ export function createUiGenerator(options: CreateUiGeneratorOptions = {}): UiGenerator {
227
263
  const {
228
264
  enableRuntimeRender = false,
265
+ runtimeRenderProbe,
229
266
  maxAttempts,
230
267
  maxEvalRounds,
231
268
  evaluation,
232
269
  visualEvaluation,
270
+ visualEvalAgent,
233
271
  qualityConfig,
234
272
  gadgetCatalog,
235
273
  disableEnvMutation = false,
@@ -237,6 +275,11 @@ export function createUiGenerator(
237
275
  onGenerated,
238
276
  designMode,
239
277
  } = options;
278
+ // ggui#1380 — ONE runtime-render check per generator: the instance owns
279
+ // the probe's concurrency cap, so it must outlive a single generation.
280
+ const runtimeRender = enableRuntimeRender
281
+ ? createRuntimeRenderCheck(runtimeRenderProbe)
282
+ : undefined;
240
283
 
241
284
  const identity = resolveIdentity(options);
242
285
 
@@ -273,9 +316,9 @@ export function createUiGenerator(
273
316
  });
274
317
  } catch (err) {
275
318
  return failWithoutMetadata(input, identity.slug, startedAt, {
276
- code: 'PRODUCTION_FAILED',
319
+ code: "PRODUCTION_FAILED",
277
320
  message: err instanceof Error ? err.message : String(err),
278
- details: { kind: 'route-resolution-failed' },
321
+ details: { kind: "route-resolution-failed" },
279
322
  });
280
323
  }
281
324
 
@@ -307,19 +350,22 @@ export function createUiGenerator(
307
350
  // flag is the only way the decision reaches `createAgent` /
308
351
  // `createVisionAgent` (which select the login client on it).
309
352
  const routeOverride =
310
- route.auth === 'claude-code-login'
353
+ route.auth === "claude-code-login"
311
354
  ? { claudeCodeLogin: true }
312
355
  : disableEnvMutation
313
356
  ? {
314
357
  apiKey:
315
- route.env['ANTHROPIC_API_KEY'] ??
316
- route.env['OPENAI_API_KEY'] ??
317
- route.env['GEMINI_API_KEY'] ??
318
- route.env['OPENROUTER_API_KEY'],
319
- useBedrock: route.env['CLAUDE_CODE_USE_BEDROCK'] === '1',
358
+ route.env["ANTHROPIC_API_KEY"] ??
359
+ route.env["OPENAI_API_KEY"] ??
360
+ route.env["GEMINI_API_KEY"] ??
361
+ route.env["OPENROUTER_API_KEY"],
362
+ useBedrock: route.env["CLAUDE_CODE_USE_BEDROCK"] === "1",
320
363
  }
321
364
  : undefined;
322
365
 
366
+ // ggui#1459 — the named level whose dials this generation applied; set
367
+ // where the dials are resolved, read by both metadata arms below.
368
+ let effortRan: AppGenerationProfileEffort | undefined;
323
369
  try {
324
370
  const tools = createGeneratorTools({
325
371
  ...(input.contract ? { contract: input.contract } : {}),
@@ -342,11 +388,9 @@ export function createUiGenerator(
342
388
  // descriptors stay byte-identical).
343
389
  const canvas = rendering ? canvasForRenderingContext(rendering) : undefined;
344
390
  const shellType = harnessShellForRendering(rendering);
345
- const promptWithVariance = injectVariance(
346
- promptWithRendering,
347
- input.variance,
348
- { profileDeclared: hasProfile(input.profile) },
349
- );
391
+ const promptWithVariance = injectVariance(promptWithRendering, input.variance, {
392
+ profileDeclared: hasProfile(input.profile),
393
+ });
350
394
 
351
395
  // Resolve appGadgets by precedence:
352
396
  // 1. input.appGadgets (caller pre-fetched, handler path)
@@ -359,23 +403,22 @@ export function createUiGenerator(
359
403
  input.appGadgets !== undefined
360
404
  ? input.appGadgets
361
405
  : gadgetCatalog !== undefined && input.appId !== undefined
362
- ? await gadgetCatalog.list(input.appId)
363
- : undefined;
406
+ ? await gadgetCatalog.list(input.appId)
407
+ : undefined;
364
408
 
365
- const userPrompt = injectContracts(
366
- promptWithVariance,
367
- input.contract,
368
- resolvedAppGadgets,
369
- );
409
+ const userPrompt = injectContracts(promptWithVariance, input.contract, resolvedAppGadgets);
370
410
 
371
411
  // `effort` (ggui#1059): a named level's dials override the
372
412
  // deployment's options for THIS generation; absent ⇒ untouched.
373
413
  // Prompt bytes never move with it (dials only).
374
414
  const dials = effortDials(input.profile?.effort);
415
+ if (dials !== undefined) effortRan = input.profile?.effort;
375
416
  const effectiveMaxAttempts = dials?.maxAttempts ?? maxAttempts;
376
417
  const effectiveMaxEvalRounds = dials?.maxEvalRounds ?? maxEvalRounds;
377
418
  const effectiveEvaluation =
378
- dials !== undefined && evaluation !== undefined ? { ...evaluation, maxRounds: dials.maxEvalRounds } : evaluation;
419
+ dials !== undefined && evaluation !== undefined
420
+ ? { ...evaluation, maxRounds: dials.maxEvalRounds }
421
+ : evaluation;
379
422
  const effectiveVisualEvaluation =
380
423
  dials !== undefined && visualEvaluation !== undefined
381
424
  ? { ...visualEvaluation, passThreshold: dials.selfEvalPassThreshold }
@@ -389,21 +432,22 @@ export function createUiGenerator(
389
432
  ...(input.contract ? { contract: input.contract } : {}),
390
433
  originalPrompt: input.request.prompt,
391
434
  ...(effectiveMaxAttempts !== undefined ? { maxAttempts: effectiveMaxAttempts } : {}),
392
- ...(effectiveMaxEvalRounds !== undefined ? { maxEvalRounds: effectiveMaxEvalRounds } : {}),
435
+ ...(effectiveMaxEvalRounds !== undefined
436
+ ? { maxEvalRounds: effectiveMaxEvalRounds }
437
+ : {}),
393
438
  ...(effectiveEvaluation !== undefined ? { evaluation: effectiveEvaluation } : {}),
394
- ...(effectiveVisualEvaluation !== undefined ? { visualEvaluation: effectiveVisualEvaluation } : {}),
395
- ...(qualityConfig !== undefined ? { qualityConfig } : {}),
396
- ...(resolvedAppGadgets !== undefined
397
- ? { appGadgets: resolvedAppGadgets }
439
+ ...(effectiveVisualEvaluation !== undefined
440
+ ? { visualEvaluation: effectiveVisualEvaluation }
398
441
  : {}),
442
+ ...(qualityConfig !== undefined ? { qualityConfig } : {}),
443
+ ...(resolvedAppGadgets !== undefined ? { appGadgets: resolvedAppGadgets } : {}),
399
444
  // Forward the third-party wrapper `.d.ts` map the render
400
445
  // handler pre-fetched. Reaches both the code-gen prompt's
401
446
  // `Type:` lines and the coding-agent typecheck overlay via
402
447
  // `dispatchGeneration`.
403
- ...(input.gadgetTypes !== undefined
404
- ? { gadgetTypes: input.gadgetTypes }
405
- : {}),
448
+ ...(input.gadgetTypes !== undefined ? { gadgetTypes: input.gadgetTypes } : {}),
406
449
  enableRuntimeRender,
450
+ ...(runtimeRender !== undefined ? { runtimeRender } : {}),
407
451
  ...(routeOverride !== undefined ? { routeOverride } : {}),
408
452
  ...(onRetry !== undefined ? { onRetry } : {}),
409
453
  ...(designMode !== undefined ? { designMode } : {}),
@@ -411,12 +455,16 @@ export function createUiGenerator(
411
455
  // ggui#1279 — the shell the declared rendering composes for (chat only; see harnessShellForRendering).
412
456
  ...(shellType !== undefined ? { shellType } : {}),
413
457
  ...(input.profile !== undefined ? { profile: input.profile } : {}),
458
+ // ggui#1250 — the named in-loop visual judge, when this generator was built with one.
459
+ ...(visualEvalAgent !== undefined ? { visualEvalAgent } : {}),
414
460
  // Fixture props reach the in-loop probe + visual round only (never the prompt, never identity).
415
461
  ...(input.fixtureProps !== undefined ? { fixtureProps: input.fixtureProps } : {}),
416
462
  });
417
463
 
418
464
  onGenerated?.(result);
419
465
 
466
+ const runtimeProbe = metadataRuntimeProbe(result);
467
+ const evalMs = metadataEvalMs(result);
420
468
  const metadata: GenerationMetadata = {
421
469
  provider: input.llm.provider,
422
470
  generator: identity.slug,
@@ -428,6 +476,8 @@ export function createUiGenerator(
428
476
  attempts: result.turnsUsed,
429
477
  // ggui#1280 — the build that made this generation (template keys, never per-request).
430
478
  build: generatorBuild(designMode ?? DEFAULT_DESIGN_MODE),
479
+ // ggui#1459 — the level that ran; absent when none was applied.
480
+ ...(effortRan !== undefined ? { effort: effortRan } : {}),
431
481
  // Prompt-cache counters are provider-specific (Claude reports
432
482
  // them; others omit). Pass through truthfully — absent on the
433
483
  // adapter result stays absent here, never defaulted to 0.
@@ -437,6 +487,10 @@ export function createUiGenerator(
437
487
  ...(result.cacheCreationTokens !== undefined
438
488
  ? { cacheCreationTokens: result.cacheCreationTokens }
439
489
  : {}),
490
+ // ggui#1380 — the runtime probe's record and the eval rounds' wall,
491
+ // when the harness ran a round; absent stays absent.
492
+ ...(runtimeProbe !== undefined ? { runtimeProbe } : {}),
493
+ ...(evalMs !== undefined ? { evalMs } : {}),
440
494
  };
441
495
 
442
496
  return {
@@ -451,12 +505,12 @@ export function createUiGenerator(
451
505
  };
452
506
  } catch (err) {
453
507
  const message = err instanceof Error ? err.message : String(err);
454
- const details: JsonObject = { kind: 'harness-failed' };
455
- if (err instanceof Error && err.stack) details['stack'] = err.stack;
508
+ const details: JsonObject = { kind: "harness-failed" };
509
+ if (err instanceof Error && err.stack) details["stack"] = err.stack;
456
510
  return {
457
511
  ok: false,
458
512
  error: {
459
- code: 'PRODUCTION_FAILED',
513
+ code: "PRODUCTION_FAILED",
460
514
  message,
461
515
  details,
462
516
  },
@@ -470,6 +524,8 @@ export function createUiGenerator(
470
524
  cacheHit: false,
471
525
  // ggui#1280 — a failed generation still names the build that failed.
472
526
  build: generatorBuild(designMode ?? DEFAULT_DESIGN_MODE),
527
+ // ggui#1459 — and the level it ran, when one was applied.
528
+ ...(effortRan !== undefined ? { effort: effortRan } : {}),
473
529
  },
474
530
  };
475
531
  } finally {
@@ -488,11 +544,9 @@ function resolveIdentity(opts: CreateUiGeneratorOptions): {
488
544
  tier: GeneratorTier;
489
545
  model: ModelRef;
490
546
  } {
491
- const hasSlug = typeof opts.slug === 'string' && opts.slug.length > 0;
547
+ const hasSlug = typeof opts.slug === "string" && opts.slug.length > 0;
492
548
  if (hasSlug && opts.tier !== undefined) {
493
- throw new Error(
494
- 'createUiGenerator: pass either { slug } or { tier } — not both.',
495
- );
549
+ throw new Error("createUiGenerator: pass either { slug } or { tier } — not both.");
496
550
  }
497
551
  const model = opts.model ?? DEFAULT_MODEL;
498
552
  if (hasSlug) {
@@ -500,7 +554,7 @@ function resolveIdentity(opts: CreateUiGeneratorOptions): {
500
554
  const parsed = parseGeneratorSlug(slug);
501
555
  if (!parsed || !isGeneratorId(slug)) {
502
556
  throw new Error(
503
- `createUiGenerator: slug ${JSON.stringify(slug)} is not a valid ui-gen-<tier> identifier.`,
557
+ `createUiGenerator: slug ${JSON.stringify(slug)} is not a valid ui-gen-<tier> identifier.`
504
558
  );
505
559
  }
506
560
  return { slug, tier: parsed.tier, model };
@@ -512,23 +566,80 @@ function resolveIdentity(opts: CreateUiGeneratorOptions): {
512
566
  // so a slug it returns must parse. Defensive check kept to surface
513
567
  // any future drift between the two helpers.
514
568
  throw new Error(
515
- `createUiGenerator: formatted slug ${JSON.stringify(slug)} round-trips as invalid — formatGeneratorSlug/isValidGeneratorSlug drift.`,
569
+ `createUiGenerator: formatted slug ${JSON.stringify(slug)} round-trips as invalid — formatGeneratorSlug/isValidGeneratorSlug drift.`
516
570
  );
517
571
  }
518
572
  return { slug, tier, model };
519
573
  }
520
574
 
575
+ /**
576
+ * ggui#1380 — one probe's meta, projected onto the metadata's outcome ARM BY
577
+ * ARM: the outcome fields (`status`, `verdict`, `failChecks`, `elapsedMs`)
578
+ * are copied; the engine's own detail (`reason`, `renderMs`, `hostLoad`)
579
+ * stays on the harness result. No cast — each arm is rebuilt as a literal of
580
+ * the target arm, so a field the metadata does not carry cannot ride along
581
+ * and a field the arm requires cannot be dropped. Absent stays absent.
582
+ */
583
+ function metadataProbeOutcome(meta: RuntimeProbeMeta): GenerationRuntimeProbeOutcome {
584
+ const elapsed = {
585
+ ...(meta.elapsedMs !== undefined ? { elapsedMs: meta.elapsedMs } : {}),
586
+ ...(meta.queuedMs !== undefined ? { queuedMs: meta.queuedMs } : {}),
587
+ };
588
+ if (meta.status !== "ran") return { status: meta.status, ...elapsed };
589
+ if (meta.verdict === "pass") return { status: "ran", verdict: "pass", ...elapsed };
590
+ return { status: "ran", verdict: "fail", failChecks: meta.failChecks, ...elapsed };
591
+ }
592
+
593
+ /**
594
+ * ggui#1380 — the repair record, arm by arm: `compiled: false` is carried as
595
+ * is (no re-probe, so no after); `compiled: true` carries the re-probe's meta
596
+ * projected with the same function as the record's own outcome.
597
+ */
598
+ function metadataProbeRepair(repair: RuntimeProbeRepair): GenerationRuntimeProbeRepair {
599
+ return repair.compiled
600
+ ? { attempted: true, compiled: true, trigger: metadataProbeOutcome(repair.trigger) }
601
+ : { attempted: true, compiled: false };
602
+ }
603
+
604
+ /**
605
+ * ggui#1380 — the harness result's probe record, projected onto the metadata
606
+ * a host reads. The outcome fields are the LAST probe's (the re-probe's when
607
+ * the repair compiled, else the pre-repair probe's — exactly what the harness
608
+ * stamps on `evalResult.runtimeProbe`), and `repair` is present only when a
609
+ * repair turn was bought. Absent when no probe ran — never a default status.
610
+ */
611
+ function metadataRuntimeProbe(result: GenerationResult): GenerationRuntimeProbe | undefined {
612
+ const probe = result.evalResult?.runtimeProbe;
613
+ if (probe === undefined) return undefined;
614
+ const repair = result.evalResult?.runtimeProbeRepair;
615
+ return {
616
+ ...metadataProbeOutcome(probe),
617
+ ...(repair !== undefined ? { repair: metadataProbeRepair(repair) } : {}),
618
+ };
619
+ }
620
+
621
+ /**
622
+ * ggui#1380 — the eval rounds' wall-clock, present only when a round ran:
623
+ * a generation with no round has nothing to time, and the field stays
624
+ * absent rather than reporting a structural 0.
625
+ */
626
+ function metadataEvalMs(result: GenerationResult): number | undefined {
627
+ const breakdown = result.breakdown;
628
+ if (breakdown === undefined || breakdown.evalRounds === 0) return undefined;
629
+ return breakdown.evalMs;
630
+ }
631
+
521
632
  function mapLlmProviderToDispatchProvider(provider: LlmProvider): ProviderName {
522
633
  switch (provider) {
523
- case 'anthropic':
524
- case 'bedrock':
525
- return 'claude';
526
- case 'openai':
527
- return 'openai';
528
- case 'google':
529
- return 'google';
530
- case 'openrouter':
531
- return 'openrouter';
634
+ case "anthropic":
635
+ case "bedrock":
636
+ return "claude";
637
+ case "openai":
638
+ return "openai";
639
+ case "google":
640
+ return "google";
641
+ case "openrouter":
642
+ return "openrouter";
532
643
  }
533
644
  }
534
645
 
@@ -542,20 +653,18 @@ function mapLlmProviderToDispatchProvider(provider: LlmProvider): ProviderName {
542
653
  * yet, so both pass no shell and those generations stay byte-identical — named
543
654
  * here rather than guessed.
544
655
  */
545
- function harnessShellForRendering(rendering: RenderingContext | undefined): 'chat' | undefined {
656
+ function harnessShellForRendering(rendering: RenderingContext | undefined): "chat" | undefined {
546
657
  if (rendering === undefined) return undefined;
547
658
  switch (rendering.shell) {
548
- case 'chat':
549
- return 'chat';
550
- case 'fullscreen':
551
- case 'partial':
659
+ case "chat":
660
+ return "chat";
661
+ case "fullscreen":
662
+ case "partial":
552
663
  return undefined;
553
664
  }
554
665
  }
555
666
 
556
- function mapRendering(
557
- rendering: UiGenerateInput['rendering'],
558
- ): RenderingContext | undefined {
667
+ function mapRendering(rendering: UiGenerateInput["rendering"]): RenderingContext | undefined {
559
668
  if (!rendering) return undefined;
560
669
  return {
561
670
  device: rendering.device,
@@ -568,7 +677,7 @@ function failWithoutMetadata(
568
677
  input: UiGenerateInput,
569
678
  generatorSlug: GeneratorId,
570
679
  startedAt: number,
571
- error: GenerationError,
680
+ error: GenerationError
572
681
  ): UiGenerateResult {
573
682
  return {
574
683
  ok: false,
@@ -600,18 +709,18 @@ export function extractComponentCode(raw: string): string {
600
709
  const fences: Array<{ lang: string; body: string }> = [];
601
710
  let match: RegExpExecArray | null;
602
711
  while ((match = fencePattern.exec(raw)) !== null) {
603
- const lang = (match[1] ?? '').toLowerCase();
604
- const body = match[2] ?? '';
712
+ const lang = (match[1] ?? "").toLowerCase();
713
+ const body = match[2] ?? "";
605
714
  fences.push({ lang, body });
606
715
  }
607
716
 
608
717
  if (fences.length > 0) {
609
- const priority = ['tsx', 'jsx', 'typescript', 'ts', 'javascript', 'js', ''];
718
+ const priority = ["tsx", "jsx", "typescript", "ts", "javascript", "js", ""];
610
719
  for (const preferred of priority) {
611
720
  const hit = fences.find((f) => f.lang === preferred);
612
721
  if (hit) return hit.body.trim();
613
722
  }
614
- const firstBody = fences[0]?.body ?? '';
723
+ const firstBody = fences[0]?.body ?? "";
615
724
  return firstBody.trim();
616
725
  }
617
726
 
@@ -0,0 +1,100 @@
1
+ /**
2
+ * A PNG writer for tests (ggui#1120): 8-bit RGB / RGBA, non-interlaced, one scanline filter for the
3
+ * whole image — the shapes the judge's browser writes, so `decodePng` is exercised on every filter it
4
+ * must undo. Test-only: nothing in `src` imports it.
5
+ */
6
+ import { crc32, deflateSync } from 'node:zlib';
7
+
8
+ export type Rgb = readonly [number, number, number];
9
+
10
+ export interface PngSpec {
11
+ readonly width: number;
12
+ readonly height: number;
13
+ readonly channels: 3 | 4;
14
+ /** The colour at (x, y). */
15
+ readonly pixel: (x: number, y: number) => Rgb;
16
+ /** The scanline filter applied to every row (0 none, 1 sub, 2 up, 3 average, 4 paeth). */
17
+ readonly filter?: 0 | 1 | 2 | 3 | 4;
18
+ /** Overrides for the malformed cases. */
19
+ readonly bitDepth?: number;
20
+ readonly colorType?: number;
21
+ readonly interlace?: number;
22
+ }
23
+
24
+ function chunk(type: string, data: Buffer): Buffer {
25
+ const head = Buffer.alloc(8);
26
+ head.writeUInt32BE(data.length, 0);
27
+ head.write(type, 4, 'latin1');
28
+ const crc = Buffer.alloc(4);
29
+ crc.writeUInt32BE(crc32(data, crc32(Buffer.from(type, 'latin1'))) >>> 0, 0);
30
+ return Buffer.concat([head, data, crc]);
31
+ }
32
+
33
+ export function encodePng(spec: PngSpec): Buffer {
34
+ const { width, height, channels } = spec;
35
+ const filter = spec.filter ?? 0;
36
+ const stride = width * channels;
37
+ const rows: number[][] = [];
38
+ for (let y = 0; y < height; y++) {
39
+ const row: number[] = [];
40
+ for (let x = 0; x < width; x++) {
41
+ const [r, g, b] = spec.pixel(x, y);
42
+ row.push(r, g, b);
43
+ if (channels === 4) row.push(255);
44
+ }
45
+ rows.push(row);
46
+ }
47
+ const raw = Buffer.alloc((stride + 1) * height);
48
+ for (let y = 0; y < height; y++) {
49
+ raw[y * (stride + 1)] = filter;
50
+ for (let i = 0; i < stride; i++) {
51
+ const x = rows[y]![i]!;
52
+ const a = i >= channels ? rows[y]![i - channels]! : 0;
53
+ const b = y > 0 ? rows[y - 1]![i]! : 0;
54
+ const c = y > 0 && i >= channels ? rows[y - 1]![i - channels]! : 0;
55
+ let out: number;
56
+ switch (filter) {
57
+ case 0:
58
+ out = x;
59
+ break;
60
+ case 1:
61
+ out = x - a;
62
+ break;
63
+ case 2:
64
+ out = x - b;
65
+ break;
66
+ case 3:
67
+ out = x - ((a + b) >> 1);
68
+ break;
69
+ case 4: {
70
+ const p = a + b - c;
71
+ const pa = Math.abs(p - a);
72
+ const pb = Math.abs(p - b);
73
+ const pc = Math.abs(p - c);
74
+ out = x - (pa <= pb && pa <= pc ? a : pb <= pc ? b : c);
75
+ break;
76
+ }
77
+ }
78
+ raw[y * (stride + 1) + 1 + i] = out & 0xff;
79
+ }
80
+ }
81
+ const ihdr = Buffer.alloc(13);
82
+ ihdr.writeUInt32BE(width, 0);
83
+ ihdr.writeUInt32BE(height, 4);
84
+ ihdr[8] = spec.bitDepth ?? 8;
85
+ ihdr[9] = spec.colorType ?? (channels === 4 ? 6 : 2);
86
+ ihdr[10] = 0;
87
+ ihdr[11] = 0;
88
+ ihdr[12] = spec.interlace ?? 0;
89
+ return Buffer.concat([
90
+ Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]),
91
+ chunk('IHDR', ihdr),
92
+ chunk('IDAT', deflateSync(raw)),
93
+ chunk('IEND', Buffer.alloc(0)),
94
+ ]);
95
+ }
96
+
97
+ /** One flat colour — the blank. */
98
+ export function flatPng(width: number, height: number, colour: Rgb, channels: 3 | 4 = 3, filter: PngSpec['filter'] = 0): Buffer {
99
+ return encodePng({ width, height, channels, pixel: () => colour, filter });
100
+ }
@@ -108,7 +108,7 @@ function runMapKeyIsId(input: AxisCheckInput): EvalIssue[] {
108
108
  mkIssue(
109
109
  "render.map_key_is_id",
110
110
  `Array key uses index variable "${key}" — reorders and stream merges will break React reconciliation.`,
111
- `Replace key={${key}} with key={item.id} (or item.symbol / whatever the entity id field is).`,
111
+ `Replace key={${key}} with key={item.id} — or, when the items carry no id, with the field that names each one (item.label / item.symbol / item.name).`,
112
112
  ),
113
113
  );
114
114
  }