@ggui-ai/ui-gen 0.23.0 → 0.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/dist/adapter-Du7P_5Fx.d.ts +201 -0
  2. package/dist/adapters/generation-dispatch.d.ts +33 -12
  3. package/dist/adapters/generation-dispatch.js +510 -128
  4. package/dist/adapters/generation-dispatch.js.map +1 -1
  5. package/dist/adapters/index.js.map +1 -1
  6. package/dist/advanced/index.d.ts +5 -3
  7. package/dist/advanced/index.js +551 -142
  8. package/dist/advanced/index.js.map +1 -1
  9. package/dist/blueprint-validator.js +153 -59
  10. package/dist/blueprint-validator.js.map +1 -1
  11. package/dist/boilerplate.js.map +1 -1
  12. package/dist/check/index.d.ts +2 -1
  13. package/dist/check/index.js +47 -1
  14. package/dist/check/index.js.map +1 -1
  15. package/dist/{create-ui-generator-D0kbWwln.d.ts → create-ui-generator-GIhzx08x.d.ts} +34 -10
  16. package/dist/{dispatch-wxaIMwZt.d.ts → dispatch-BnAH56p4.d.ts} +1 -1
  17. package/dist/evaluation/axis-checks/index.d.ts +4 -3
  18. package/dist/evaluation/axis-checks/index.js.map +1 -1
  19. package/dist/evaluation/axis-checks/registry.d.ts +2 -1
  20. package/dist/evaluation/index.d.ts +4 -3
  21. package/dist/evaluation/index.js.map +1 -1
  22. package/dist/harness/check/runtime-render/index.d.ts +29 -10
  23. package/dist/harness/check/runtime-render/index.js +203 -60
  24. package/dist/harness/check/runtime-render/index.js.map +1 -1
  25. package/dist/harness/check/runtime-render/render-check-worker.d.ts +8 -1
  26. package/dist/harness/check/runtime-render/render-check-worker.js +72 -32
  27. package/dist/harness/check/runtime-render/render-check-worker.js.map +1 -1
  28. package/dist/harness/index.d.ts +2 -1
  29. package/dist/harness/index.js.map +1 -1
  30. package/dist/harness/prompts.js.map +1 -1
  31. package/dist/harness/result-types.d.ts +2 -1
  32. package/dist/harness/runtime.d.ts +2 -1
  33. package/dist/harness/runtime.js.map +1 -1
  34. package/dist/harness/types-public.d.ts +11 -2
  35. package/dist/harness/types-public.js.map +1 -1
  36. package/dist/harness/validator-trace-sink.d.ts +2 -1
  37. package/dist/index.d.ts +38 -3
  38. package/dist/index.js +553 -143
  39. package/dist/index.js.map +1 -1
  40. package/dist/patch.d.ts +2 -1
  41. package/dist/policy.d.ts +7 -4
  42. package/dist/policy.js.map +1 -1
  43. package/dist/tools/render-check-worker.d.ts +7 -1
  44. package/dist/tools/render-check-worker.js +55 -23
  45. package/dist/tools/render-check-worker.js.map +1 -1
  46. package/dist/{types-public-Ow-tNtFE.d.ts → types-public-Bk6d1WLN.d.ts} +77 -11
  47. package/dist/workflows.d.ts +2 -1
  48. package/package.json +12 -12
  49. package/src/adapters/generation-dispatch.ts +137 -96
  50. package/src/check/run-tier0.ts +80 -1
  51. package/src/create-ui-generator.ts +185 -95
  52. package/src/evaluation/types-public.ts +73 -11
  53. package/src/harness/check/runtime-render/adapter.ts +221 -75
  54. package/src/harness/check/runtime-render/index.ts +75 -2
  55. package/src/harness/check/runtime-render/render-check-host.ts +82 -19
  56. package/src/harness/check/runtime-render/render-check-worker.ts +83 -29
  57. package/src/harness/check/runtime-render/render-check.ts +154 -117
  58. package/src/harness/coding/generate-task-runner.ts +57 -4
  59. package/src/harness/coding/init-session.ts +13 -0
  60. package/src/harness/coding/run-eval-round.ts +238 -15
  61. package/src/harness/policy.ts +11 -0
  62. package/src/harness/primitive-slice.ts +104 -0
  63. package/src/harness/types-public.ts +9 -1
  64. package/src/index.ts +4 -0
  65. package/src/policy.ts +7 -4
  66. package/src/tools/render-check-worker.ts +74 -23
  67. package/dist/render-check-Blwj8gB6.d.ts +0 -105
@@ -31,66 +31,96 @@
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
+ } from "@ggui-ai/protocol";
42
+ import { isGeneratorId, modelRefOfRoute } from "@ggui-ai/protocol";
43
+ import type { GadgetCatalogAdapter } from "@ggui-ai/gadgets";
44
44
  import type {
45
45
  GenerationMetadata,
46
+ GenerationRuntimeProbe,
47
+ GenerationRuntimeProbeOutcome,
48
+ GenerationRuntimeProbeRepair,
46
49
  GeneratorTier,
47
50
  LlmProvider,
48
51
  UiGenerateInput,
49
52
  UiGenerateResult,
50
53
  UiGenerator,
51
- } from '@ggui-ai/mcp-server-core';
54
+ } from "@ggui-ai/mcp-server-core";
52
55
  import {
53
56
  formatGeneratorSlug,
54
57
  isValidGeneratorSlug,
55
58
  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';
59
+ } from "@ggui-ai/mcp-server-core";
60
+ import { createGeneratorTools } from "./adapters/index.js";
61
+ import { dispatchGeneration } from "./adapters/generation-dispatch.js";
62
+ import {
63
+ createRuntimeRenderCheck,
64
+ type RuntimeRenderProbeConfig,
65
+ } from "./harness/check/runtime-render/adapter.js";
66
+ import { generatorBuild } from "./generator-build.js";
67
+ import { hasProfile } from "./boilerplate/styling-profile.js";
68
+ import { effortDials } from "./effort.js";
69
+ import type { ProviderName } from "./adapters/types.js";
70
+ import type { GenerationResult } from "./harness/result-types.js";
64
71
  import {
65
72
  canvasForRenderingContext,
66
73
  injectContracts,
67
74
  injectRenderingContext,
68
75
  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';
76
+ } from "./contract-context.js";
77
+ import type { RenderingContext } from "./contract-context.js";
78
+ import type { DesignMode } from "./design-mode.js";
79
+ import { DEFAULT_DESIGN_MODE } from "./design-mode.js";
80
+ import { resolveRoute, applyRouteToEnv } from "./adapters/provider-router.js";
81
+ import type { QualityConfig, RuntimeProbeMeta, RuntimeProbeRepair } from "./evaluation/types-public.js";
82
+ import type { EvaluationConfig } from "./evaluation/types.js";
83
+ import type { AgentConfig } from "./harness/llm-router.js";
77
84
 
78
85
  /** The slug for the OSS default seed generator. */
79
- const DEFAULT_TIER: GeneratorTier = 'default';
80
- const DEFAULT_MODEL: ModelRef = 'anthropic/claude-haiku-4-5';
86
+ const DEFAULT_TIER: GeneratorTier = "default";
87
+ const DEFAULT_MODEL: ModelRef = "anthropic/claude-haiku-4-5";
81
88
 
82
89
  export interface CreateUiGeneratorOptions {
83
90
  /**
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.).
91
+ * Wire the runtime-render check into the harness's check leg — one
92
+ * instance per generator, built from `runtimeRenderProbe` (the default
93
+ * bounds when it is absent). When `true`, the runtime-render probe runs
94
+ * ONCE at the exit decision,
95
+ * after the coding turns, in an isolated subprocess: it mounts the
96
+ * compiled component against the contract and catches crash-class and
97
+ * contract-wiring bugs (a render that throws, a missing `useAction()`,
98
+ * a wrong stream channel name). It is never on the per-turn hot path.
99
+ *
100
+ * Without an `evaluation` config the round is observe + one repair turn
101
+ * (ggui#1380): a recoverable render crash buys exactly one repair turn
102
+ * and one re-probe, then the generation serves what it has; a probe that
103
+ * timed out, could not run, or had nothing to probe never blocks and
104
+ * never repairs. The probe's status and timing ride the result's
105
+ * metadata (`runtimeProbe`, `evalMs`). With an `evaluation` config the
106
+ * probe is the evaluation round's exit gate instead.
88
107
  *
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.
108
+ * Default: `false` — keeps cold-start light. The probe pulls happy-dom
109
+ * + @testing-library on first use (~700-1500ms cold). A serving
110
+ * deployment that wants the probe's verdict on every card sets it.
92
111
  */
93
112
  readonly enableRuntimeRender?: boolean;
113
+ /**
114
+ * How the runtime-render probe runs its isolated worker (ggui#1380). A
115
+ * serving deployment sets the probe's wall-clock bound, worker heap and
116
+ * concurrency here; the defaults are the evaluation lane's (30 s, 512 MB,
117
+ * no concurrency cap). The generator builds ONE check from it and hands
118
+ * that instance to every generation it dispatches — the concurrency cap
119
+ * lives on the instance, so it bounds the workers this generator has
120
+ * live at once. Only read when `enableRuntimeRender` is on. Never part
121
+ * of the prompt or the identity.
122
+ */
123
+ readonly runtimeRenderProbe?: RuntimeRenderProbeConfig;
94
124
  /** Maximum coding attempts per generation pass. */
95
125
  readonly maxAttempts?: number;
96
126
  /** Maximum evaluation rounds. */
@@ -108,7 +138,7 @@ export interface CreateUiGeneratorOptions {
108
138
  * stylesheet to render under). Forwarded verbatim; never part of the
109
139
  * prompt or the identity. Default: none — the round runs as today.
110
140
  */
111
- readonly visualEvaluation?: SingleComponentParams['visualEvaluation'];
141
+ readonly visualEvaluation?: SingleComponentParams["visualEvaluation"];
112
142
  /** Quality config controlling eval tiers + improvement behavior. */
113
143
  readonly qualityConfig?: QualityConfig;
114
144
  /**
@@ -189,7 +219,7 @@ export interface CreateUiGeneratorOptions {
189
219
  * request. Absent (default) is a no-op — retries still happen and
190
220
  * still log via `console.warn`, just without a caller-side hook.
191
221
  */
192
- readonly onRetry?: AgentConfig['onRetry'];
222
+ readonly onRetry?: AgentConfig["onRetry"];
193
223
  /**
194
224
  * Generation observer. Invoked once per successful `generate()` with
195
225
  * the FULL harness result — source, compiled code, token counters,
@@ -221,11 +251,10 @@ export interface CreateUiGeneratorOptions {
221
251
  readonly designMode?: DesignMode;
222
252
  }
223
253
 
224
- export function createUiGenerator(
225
- options: CreateUiGeneratorOptions = {},
226
- ): UiGenerator {
254
+ export function createUiGenerator(options: CreateUiGeneratorOptions = {}): UiGenerator {
227
255
  const {
228
256
  enableRuntimeRender = false,
257
+ runtimeRenderProbe,
229
258
  maxAttempts,
230
259
  maxEvalRounds,
231
260
  evaluation,
@@ -237,6 +266,11 @@ export function createUiGenerator(
237
266
  onGenerated,
238
267
  designMode,
239
268
  } = options;
269
+ // ggui#1380 — ONE runtime-render check per generator: the instance owns
270
+ // the probe's concurrency cap, so it must outlive a single generation.
271
+ const runtimeRender = enableRuntimeRender
272
+ ? createRuntimeRenderCheck(runtimeRenderProbe)
273
+ : undefined;
240
274
 
241
275
  const identity = resolveIdentity(options);
242
276
 
@@ -273,9 +307,9 @@ export function createUiGenerator(
273
307
  });
274
308
  } catch (err) {
275
309
  return failWithoutMetadata(input, identity.slug, startedAt, {
276
- code: 'PRODUCTION_FAILED',
310
+ code: "PRODUCTION_FAILED",
277
311
  message: err instanceof Error ? err.message : String(err),
278
- details: { kind: 'route-resolution-failed' },
312
+ details: { kind: "route-resolution-failed" },
279
313
  });
280
314
  }
281
315
 
@@ -307,16 +341,16 @@ export function createUiGenerator(
307
341
  // flag is the only way the decision reaches `createAgent` /
308
342
  // `createVisionAgent` (which select the login client on it).
309
343
  const routeOverride =
310
- route.auth === 'claude-code-login'
344
+ route.auth === "claude-code-login"
311
345
  ? { claudeCodeLogin: true }
312
346
  : disableEnvMutation
313
347
  ? {
314
348
  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',
349
+ route.env["ANTHROPIC_API_KEY"] ??
350
+ route.env["OPENAI_API_KEY"] ??
351
+ route.env["GEMINI_API_KEY"] ??
352
+ route.env["OPENROUTER_API_KEY"],
353
+ useBedrock: route.env["CLAUDE_CODE_USE_BEDROCK"] === "1",
320
354
  }
321
355
  : undefined;
322
356
 
@@ -342,11 +376,9 @@ export function createUiGenerator(
342
376
  // descriptors stay byte-identical).
343
377
  const canvas = rendering ? canvasForRenderingContext(rendering) : undefined;
344
378
  const shellType = harnessShellForRendering(rendering);
345
- const promptWithVariance = injectVariance(
346
- promptWithRendering,
347
- input.variance,
348
- { profileDeclared: hasProfile(input.profile) },
349
- );
379
+ const promptWithVariance = injectVariance(promptWithRendering, input.variance, {
380
+ profileDeclared: hasProfile(input.profile),
381
+ });
350
382
 
351
383
  // Resolve appGadgets by precedence:
352
384
  // 1. input.appGadgets (caller pre-fetched, handler path)
@@ -359,14 +391,10 @@ export function createUiGenerator(
359
391
  input.appGadgets !== undefined
360
392
  ? input.appGadgets
361
393
  : gadgetCatalog !== undefined && input.appId !== undefined
362
- ? await gadgetCatalog.list(input.appId)
363
- : undefined;
394
+ ? await gadgetCatalog.list(input.appId)
395
+ : undefined;
364
396
 
365
- const userPrompt = injectContracts(
366
- promptWithVariance,
367
- input.contract,
368
- resolvedAppGadgets,
369
- );
397
+ const userPrompt = injectContracts(promptWithVariance, input.contract, resolvedAppGadgets);
370
398
 
371
399
  // `effort` (ggui#1059): a named level's dials override the
372
400
  // deployment's options for THIS generation; absent ⇒ untouched.
@@ -375,7 +403,9 @@ export function createUiGenerator(
375
403
  const effectiveMaxAttempts = dials?.maxAttempts ?? maxAttempts;
376
404
  const effectiveMaxEvalRounds = dials?.maxEvalRounds ?? maxEvalRounds;
377
405
  const effectiveEvaluation =
378
- dials !== undefined && evaluation !== undefined ? { ...evaluation, maxRounds: dials.maxEvalRounds } : evaluation;
406
+ dials !== undefined && evaluation !== undefined
407
+ ? { ...evaluation, maxRounds: dials.maxEvalRounds }
408
+ : evaluation;
379
409
  const effectiveVisualEvaluation =
380
410
  dials !== undefined && visualEvaluation !== undefined
381
411
  ? { ...visualEvaluation, passThreshold: dials.selfEvalPassThreshold }
@@ -389,21 +419,22 @@ export function createUiGenerator(
389
419
  ...(input.contract ? { contract: input.contract } : {}),
390
420
  originalPrompt: input.request.prompt,
391
421
  ...(effectiveMaxAttempts !== undefined ? { maxAttempts: effectiveMaxAttempts } : {}),
392
- ...(effectiveMaxEvalRounds !== undefined ? { maxEvalRounds: effectiveMaxEvalRounds } : {}),
422
+ ...(effectiveMaxEvalRounds !== undefined
423
+ ? { maxEvalRounds: effectiveMaxEvalRounds }
424
+ : {}),
393
425
  ...(effectiveEvaluation !== undefined ? { evaluation: effectiveEvaluation } : {}),
394
- ...(effectiveVisualEvaluation !== undefined ? { visualEvaluation: effectiveVisualEvaluation } : {}),
395
- ...(qualityConfig !== undefined ? { qualityConfig } : {}),
396
- ...(resolvedAppGadgets !== undefined
397
- ? { appGadgets: resolvedAppGadgets }
426
+ ...(effectiveVisualEvaluation !== undefined
427
+ ? { visualEvaluation: effectiveVisualEvaluation }
398
428
  : {}),
429
+ ...(qualityConfig !== undefined ? { qualityConfig } : {}),
430
+ ...(resolvedAppGadgets !== undefined ? { appGadgets: resolvedAppGadgets } : {}),
399
431
  // Forward the third-party wrapper `.d.ts` map the render
400
432
  // handler pre-fetched. Reaches both the code-gen prompt's
401
433
  // `Type:` lines and the coding-agent typecheck overlay via
402
434
  // `dispatchGeneration`.
403
- ...(input.gadgetTypes !== undefined
404
- ? { gadgetTypes: input.gadgetTypes }
405
- : {}),
435
+ ...(input.gadgetTypes !== undefined ? { gadgetTypes: input.gadgetTypes } : {}),
406
436
  enableRuntimeRender,
437
+ ...(runtimeRender !== undefined ? { runtimeRender } : {}),
407
438
  ...(routeOverride !== undefined ? { routeOverride } : {}),
408
439
  ...(onRetry !== undefined ? { onRetry } : {}),
409
440
  ...(designMode !== undefined ? { designMode } : {}),
@@ -417,6 +448,8 @@ export function createUiGenerator(
417
448
 
418
449
  onGenerated?.(result);
419
450
 
451
+ const runtimeProbe = metadataRuntimeProbe(result);
452
+ const evalMs = metadataEvalMs(result);
420
453
  const metadata: GenerationMetadata = {
421
454
  provider: input.llm.provider,
422
455
  generator: identity.slug,
@@ -437,6 +470,10 @@ export function createUiGenerator(
437
470
  ...(result.cacheCreationTokens !== undefined
438
471
  ? { cacheCreationTokens: result.cacheCreationTokens }
439
472
  : {}),
473
+ // ggui#1380 — the runtime probe's record and the eval rounds' wall,
474
+ // when the harness ran a round; absent stays absent.
475
+ ...(runtimeProbe !== undefined ? { runtimeProbe } : {}),
476
+ ...(evalMs !== undefined ? { evalMs } : {}),
440
477
  };
441
478
 
442
479
  return {
@@ -451,12 +488,12 @@ export function createUiGenerator(
451
488
  };
452
489
  } catch (err) {
453
490
  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;
491
+ const details: JsonObject = { kind: "harness-failed" };
492
+ if (err instanceof Error && err.stack) details["stack"] = err.stack;
456
493
  return {
457
494
  ok: false,
458
495
  error: {
459
- code: 'PRODUCTION_FAILED',
496
+ code: "PRODUCTION_FAILED",
460
497
  message,
461
498
  details,
462
499
  },
@@ -488,11 +525,9 @@ function resolveIdentity(opts: CreateUiGeneratorOptions): {
488
525
  tier: GeneratorTier;
489
526
  model: ModelRef;
490
527
  } {
491
- const hasSlug = typeof opts.slug === 'string' && opts.slug.length > 0;
528
+ const hasSlug = typeof opts.slug === "string" && opts.slug.length > 0;
492
529
  if (hasSlug && opts.tier !== undefined) {
493
- throw new Error(
494
- 'createUiGenerator: pass either { slug } or { tier } — not both.',
495
- );
530
+ throw new Error("createUiGenerator: pass either { slug } or { tier } — not both.");
496
531
  }
497
532
  const model = opts.model ?? DEFAULT_MODEL;
498
533
  if (hasSlug) {
@@ -500,7 +535,7 @@ function resolveIdentity(opts: CreateUiGeneratorOptions): {
500
535
  const parsed = parseGeneratorSlug(slug);
501
536
  if (!parsed || !isGeneratorId(slug)) {
502
537
  throw new Error(
503
- `createUiGenerator: slug ${JSON.stringify(slug)} is not a valid ui-gen-<tier> identifier.`,
538
+ `createUiGenerator: slug ${JSON.stringify(slug)} is not a valid ui-gen-<tier> identifier.`
504
539
  );
505
540
  }
506
541
  return { slug, tier: parsed.tier, model };
@@ -512,23 +547,80 @@ function resolveIdentity(opts: CreateUiGeneratorOptions): {
512
547
  // so a slug it returns must parse. Defensive check kept to surface
513
548
  // any future drift between the two helpers.
514
549
  throw new Error(
515
- `createUiGenerator: formatted slug ${JSON.stringify(slug)} round-trips as invalid — formatGeneratorSlug/isValidGeneratorSlug drift.`,
550
+ `createUiGenerator: formatted slug ${JSON.stringify(slug)} round-trips as invalid — formatGeneratorSlug/isValidGeneratorSlug drift.`
516
551
  );
517
552
  }
518
553
  return { slug, tier, model };
519
554
  }
520
555
 
556
+ /**
557
+ * ggui#1380 — one probe's meta, projected onto the metadata's outcome ARM BY
558
+ * ARM: the outcome fields (`status`, `verdict`, `failChecks`, `elapsedMs`)
559
+ * are copied; the engine's own detail (`reason`, `renderMs`, `hostLoad`)
560
+ * stays on the harness result. No cast — each arm is rebuilt as a literal of
561
+ * the target arm, so a field the metadata does not carry cannot ride along
562
+ * and a field the arm requires cannot be dropped. Absent stays absent.
563
+ */
564
+ function metadataProbeOutcome(meta: RuntimeProbeMeta): GenerationRuntimeProbeOutcome {
565
+ const elapsed = {
566
+ ...(meta.elapsedMs !== undefined ? { elapsedMs: meta.elapsedMs } : {}),
567
+ ...(meta.queuedMs !== undefined ? { queuedMs: meta.queuedMs } : {}),
568
+ };
569
+ if (meta.status !== "ran") return { status: meta.status, ...elapsed };
570
+ if (meta.verdict === "pass") return { status: "ran", verdict: "pass", ...elapsed };
571
+ return { status: "ran", verdict: "fail", failChecks: meta.failChecks, ...elapsed };
572
+ }
573
+
574
+ /**
575
+ * ggui#1380 — the repair record, arm by arm: `compiled: false` is carried as
576
+ * is (no re-probe, so no after); `compiled: true` carries the re-probe's meta
577
+ * projected with the same function as the record's own outcome.
578
+ */
579
+ function metadataProbeRepair(repair: RuntimeProbeRepair): GenerationRuntimeProbeRepair {
580
+ return repair.compiled
581
+ ? { attempted: true, compiled: true, trigger: metadataProbeOutcome(repair.trigger) }
582
+ : { attempted: true, compiled: false };
583
+ }
584
+
585
+ /**
586
+ * ggui#1380 — the harness result's probe record, projected onto the metadata
587
+ * a host reads. The outcome fields are the LAST probe's (the re-probe's when
588
+ * the repair compiled, else the pre-repair probe's — exactly what the harness
589
+ * stamps on `evalResult.runtimeProbe`), and `repair` is present only when a
590
+ * repair turn was bought. Absent when no probe ran — never a default status.
591
+ */
592
+ function metadataRuntimeProbe(result: GenerationResult): GenerationRuntimeProbe | undefined {
593
+ const probe = result.evalResult?.runtimeProbe;
594
+ if (probe === undefined) return undefined;
595
+ const repair = result.evalResult?.runtimeProbeRepair;
596
+ return {
597
+ ...metadataProbeOutcome(probe),
598
+ ...(repair !== undefined ? { repair: metadataProbeRepair(repair) } : {}),
599
+ };
600
+ }
601
+
602
+ /**
603
+ * ggui#1380 — the eval rounds' wall-clock, present only when a round ran:
604
+ * a generation with no round has nothing to time, and the field stays
605
+ * absent rather than reporting a structural 0.
606
+ */
607
+ function metadataEvalMs(result: GenerationResult): number | undefined {
608
+ const breakdown = result.breakdown;
609
+ if (breakdown === undefined || breakdown.evalRounds === 0) return undefined;
610
+ return breakdown.evalMs;
611
+ }
612
+
521
613
  function mapLlmProviderToDispatchProvider(provider: LlmProvider): ProviderName {
522
614
  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';
615
+ case "anthropic":
616
+ case "bedrock":
617
+ return "claude";
618
+ case "openai":
619
+ return "openai";
620
+ case "google":
621
+ return "google";
622
+ case "openrouter":
623
+ return "openrouter";
532
624
  }
533
625
  }
534
626
 
@@ -542,20 +634,18 @@ function mapLlmProviderToDispatchProvider(provider: LlmProvider): ProviderName {
542
634
  * yet, so both pass no shell and those generations stay byte-identical — named
543
635
  * here rather than guessed.
544
636
  */
545
- function harnessShellForRendering(rendering: RenderingContext | undefined): 'chat' | undefined {
637
+ function harnessShellForRendering(rendering: RenderingContext | undefined): "chat" | undefined {
546
638
  if (rendering === undefined) return undefined;
547
639
  switch (rendering.shell) {
548
- case 'chat':
549
- return 'chat';
550
- case 'fullscreen':
551
- case 'partial':
640
+ case "chat":
641
+ return "chat";
642
+ case "fullscreen":
643
+ case "partial":
552
644
  return undefined;
553
645
  }
554
646
  }
555
647
 
556
- function mapRendering(
557
- rendering: UiGenerateInput['rendering'],
558
- ): RenderingContext | undefined {
648
+ function mapRendering(rendering: UiGenerateInput["rendering"]): RenderingContext | undefined {
559
649
  if (!rendering) return undefined;
560
650
  return {
561
651
  device: rendering.device,
@@ -568,7 +658,7 @@ function failWithoutMetadata(
568
658
  input: UiGenerateInput,
569
659
  generatorSlug: GeneratorId,
570
660
  startedAt: number,
571
- error: GenerationError,
661
+ error: GenerationError
572
662
  ): UiGenerateResult {
573
663
  return {
574
664
  ok: false,
@@ -600,18 +690,18 @@ export function extractComponentCode(raw: string): string {
600
690
  const fences: Array<{ lang: string; body: string }> = [];
601
691
  let match: RegExpExecArray | null;
602
692
  while ((match = fencePattern.exec(raw)) !== null) {
603
- const lang = (match[1] ?? '').toLowerCase();
604
- const body = match[2] ?? '';
693
+ const lang = (match[1] ?? "").toLowerCase();
694
+ const body = match[2] ?? "";
605
695
  fences.push({ lang, body });
606
696
  }
607
697
 
608
698
  if (fences.length > 0) {
609
- const priority = ['tsx', 'jsx', 'typescript', 'ts', 'javascript', 'js', ''];
699
+ const priority = ["tsx", "jsx", "typescript", "ts", "javascript", "js", ""];
610
700
  for (const preferred of priority) {
611
701
  const hit = fences.find((f) => f.lang === preferred);
612
702
  if (hit) return hit.body.trim();
613
703
  }
614
- const firstBody = fences[0]?.body ?? '';
704
+ const firstBody = fences[0]?.body ?? "";
615
705
  return firstBody.trim();
616
706
  }
617
707
 
@@ -20,6 +20,7 @@
20
20
 
21
21
  export type Priority = "P0" | "P1" | "P2";
22
22
 
23
+ import type { GenerationRuntimeProbeOutcome, GenerationRuntimeProbeStatus } from "@ggui-ai/mcp-server-core";
23
24
  import type { CanvasClass, DesignMode } from "../design-mode.js";
24
25
 
25
26
  // ─── Issue shape ───────────────────────────────────────────────────────────
@@ -63,8 +64,10 @@ export interface EvalIssue {
63
64
  }
64
65
 
65
66
  /**
66
- * Runtime-render probe execution status — three-valued so "the probe
67
- * never ran" is never conflated with "the probe ran clean".
67
+ * Runtime-render probe execution status — four-valued so "the probe
68
+ * never ran" is never conflated with "the probe ran clean". ONE union: the
69
+ * same values a generation's metadata reports (`GenerationRuntimeProbeStatus`
70
+ * in `@ggui-ai/mcp-server-core`), declared there and named here.
68
71
  *
69
72
  * - `ran` — probe executed; its findings (possibly none) are
70
73
  * in the issue stream under `runtime:*` subcategories.
@@ -83,7 +86,7 @@ export interface EvalIssue {
83
86
  * `hostLoad` say how long it ran and how busy the
84
87
  * host was, so a reader can tell the two cases apart.
85
88
  */
86
- export type RuntimeProbeStatus = "ran" | "infra-skipped" | "not-applicable" | "timed-out";
89
+ export type RuntimeProbeStatus = GenerationRuntimeProbeStatus;
87
90
 
88
91
  /**
89
92
  * The host's 1-minute load average at a probe's start and end, beside the
@@ -98,21 +101,73 @@ export interface ProbeHostLoad {
98
101
  }
99
102
 
100
103
  /**
101
- * Probe execution meta stamped onto an `EvalResult` at the exit-decision
102
- * points that invoke the probe. Absence of this field means the eval
103
- * path never reached a probe invocation at all — consumers treat that
104
- * the same as not-run, never as a pass.
104
+ * The engine's own fields on a probe's meta, beside the outcome a
105
+ * generation's metadata reports: why a probe did not finish, its render
106
+ * time, the host's load around it. Present on every arm of
107
+ * {@link RuntimeProbeMeta}; each is absent when the probe had nothing to
108
+ * say about it.
105
109
  */
106
- export interface RuntimeProbeMeta {
107
- readonly status: RuntimeProbeStatus;
110
+ export interface RuntimeProbeMetaDetail {
108
111
  /** Populated for `infra-skipped` / `not-applicable` / `timed-out` — why the probe didn't finish. */
109
112
  readonly reason?: string;
110
- /** `timed-out` only: how long the probe ran before it was stopped. */
111
- readonly elapsedMs?: number;
113
+ /** `ran` only: the check's own render time, inside `elapsedMs`. */
114
+ readonly renderMs?: number;
112
115
  /** Host load around the probe, when it ran isolated (absent for an in-process probe). */
113
116
  readonly hostLoad?: ProbeHostLoad;
117
+ /** Time the check waited for a probe slot before it started, ms; present only when > 0 (ggui#1380 C2b). */
118
+ readonly queuedMs?: number;
114
119
  }
115
120
 
121
+ /**
122
+ * Probe execution meta stamped onto an `EvalResult` at the exit-decision
123
+ * points that invoke the probe. Absence of this field means the eval
124
+ * path never reached a probe invocation at all — consumers treat that
125
+ * the same as not-run, never as a pass.
126
+ *
127
+ * ONE union with the generation metadata's probe outcome
128
+ * (`GenerationRuntimeProbeOutcome` in `@ggui-ai/mcp-server-core`, declared
129
+ * there and carried here with {@link RuntimeProbeMetaDetail} on every arm),
130
+ * so the round's stamp and the metadata's record cannot say different
131
+ * things (ggui#1380):
132
+ *
133
+ * - `ran` + `verdict: 'pass'` — the probe executed and no check failed;
134
+ * - `ran` + `verdict: 'fail'` — ANY check failed; `failChecks` lists the
135
+ * distinct check kinds that did (`RenderCheckKind`, the engine's name
136
+ * for the same union), never empty, in the order `RENDER_CHECK_KINDS`
137
+ * declares them, parsed from the probe's own
138
+ * `runtime:<check>[:<subject>]` issues;
139
+ * - `timed-out` / `infra-skipped` / `not-applicable` — no verdict, and the
140
+ * type carries none. `elapsedMs` (on every status that reached the
141
+ * check) and `hostLoad` say how long it ran and how busy the host was.
142
+ *
143
+ * The verdict and the repair trigger are two different questions. The
144
+ * verdict is `fail` on any failing check. The one repair turn a probe-only
145
+ * round buys fires on a `render-no-throw` fail the engine recognises
146
+ * (`isRecoverableRenderCrash`) and on nothing else: a `fail` verdict whose
147
+ * checks are `prop-sensitivity`, `action-wiring` or any other kind is
148
+ * recorded here and never fed back.
149
+ */
150
+ export type RuntimeProbeMeta = GenerationRuntimeProbeOutcome & RuntimeProbeMetaDetail;
151
+
152
+ /**
153
+ * The one repair turn a probe-only round buys (ggui#1380). A serving
154
+ * deployment that wires the runtime probe and configures no evaluator runs
155
+ * the probe once after the coding turns; a render crash the engine
156
+ * recognises buys exactly one repair turn, then one re-probe. This records
157
+ * what came of it:
158
+ * - `compiled: false` — the repair turn's code did not pass self-check, so
159
+ * no re-probe ran (there is no `after`) and the pre-repair card is the
160
+ * one served;
161
+ * - `compiled: true` — the re-probe ran and `after` is its whole meta:
162
+ * its status, its verdict when it ran, and the checks that still fail.
163
+ * Whether the crash class the turn was bought for is still there is
164
+ * `after.verdict === 'fail' && after.failChecks.includes('render-no-throw')`.
165
+ * The two arms are exclusive by type.
166
+ */
167
+ export type RuntimeProbeRepair =
168
+ | { readonly attempted: true; readonly compiled: false; readonly trigger?: never }
169
+ | { readonly attempted: true; readonly compiled: true; readonly trigger: RuntimeProbeMeta };
170
+
116
171
  /**
117
172
  * Per-criterion execution status — three-valued so "the criterion
118
173
  * silently produced no verdict" is never conflated with "the criterion
@@ -226,6 +281,13 @@ export interface EvalResult {
226
281
  * probe runner; absent on eval paths that never invoke the probe.
227
282
  */
228
283
  runtimeProbe?: RuntimeProbeMeta;
284
+ /**
285
+ * The probe-only round's repair record (ggui#1380) — present only when a
286
+ * recoverable render crash bought the one repair turn. See
287
+ * {@link RuntimeProbeRepair}. `runtimeProbe` beside it is the re-probe's
288
+ * verdict when the repair compiled, else the pre-repair probe's.
289
+ */
290
+ runtimeProbeRepair?: RuntimeProbeRepair;
229
291
  /**
230
292
  * The contract-feedback round, when one fired this generation (ggui#1261):
231
293
  * which exit-probe findings bought it and the source as it stood when the