@ggui-ai/ui-gen 0.20.0 → 0.21.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 (80) hide show
  1. package/dist/adapters/base.d.ts +7 -0
  2. package/dist/adapters/base.js +1 -0
  3. package/dist/adapters/base.js.map +1 -1
  4. package/dist/adapters/generation-dispatch.d.ts +17 -3
  5. package/dist/adapters/generation-dispatch.js +959 -3484
  6. package/dist/adapters/generation-dispatch.js.map +1 -1
  7. package/dist/adapters/index.js +35 -3
  8. package/dist/adapters/index.js.map +1 -1
  9. package/dist/advanced/index.d.ts +4 -4
  10. package/dist/advanced/index.js +1039 -3573
  11. package/dist/advanced/index.js.map +1 -1
  12. package/dist/blueprint-validator.js +10 -3
  13. package/dist/blueprint-validator.js.map +1 -1
  14. package/dist/boilerplate.js +2 -2
  15. package/dist/boilerplate.js.map +1 -1
  16. package/dist/check/index.d.ts +1 -1
  17. package/dist/check/index.js.map +1 -1
  18. package/dist/{create-ui-generator-CxPvudbs.d.ts → create-ui-generator-D7kOXNjJ.d.ts} +2 -2
  19. package/dist/{dispatch-N8wdNOX-.d.ts → dispatch-DGvhmARp.d.ts} +1 -1
  20. package/dist/evaluation/axis-checks/index.d.ts +3 -3
  21. package/dist/evaluation/axis-checks/index.js.map +1 -1
  22. package/dist/evaluation/axis-checks/registry.d.ts +1 -1
  23. package/dist/evaluation/index.d.ts +38 -6
  24. package/dist/evaluation/index.js +649 -3370
  25. package/dist/evaluation/index.js.map +1 -1
  26. package/dist/harness/check/runtime-render/index.d.ts +2 -2
  27. package/dist/harness/check/runtime-render/index.js +10 -3
  28. package/dist/harness/check/runtime-render/index.js.map +1 -1
  29. package/dist/harness/check/runtime-render/render-check-worker.js +5 -3
  30. package/dist/harness/check/runtime-render/render-check-worker.js.map +1 -1
  31. package/dist/harness/index.d.ts +2 -2
  32. package/dist/harness/index.js +562 -30
  33. package/dist/harness/index.js.map +1 -1
  34. package/dist/harness/llm-trace-sink.d.ts +7 -1
  35. package/dist/harness/llm-trace-sink.js.map +1 -1
  36. package/dist/harness/result-types.d.ts +1 -1
  37. package/dist/harness/runtime.d.ts +11 -2
  38. package/dist/harness/runtime.js +3 -3
  39. package/dist/harness/runtime.js.map +1 -1
  40. package/dist/harness/types-public.d.ts +1 -1
  41. package/dist/harness/types-public.js +2 -2
  42. package/dist/harness/types-public.js.map +1 -1
  43. package/dist/harness/validator-trace-sink.d.ts +1 -1
  44. package/dist/index.d.ts +3 -3
  45. package/dist/index.js +1039 -3573
  46. package/dist/index.js.map +1 -1
  47. package/dist/{llm-router-D7fjBhtr.d.ts → llm-router-DwwUK1bz.d.ts} +16 -3
  48. package/dist/patch.d.ts +1 -1
  49. package/dist/providers/index.js +5 -4
  50. package/dist/providers/index.js.map +1 -1
  51. package/dist/{render-check-WfOVO9kU.d.ts → render-check-C0CqVceF.d.ts} +17 -0
  52. package/dist/{types-public-DKR_h70H.d.ts → types-public-CzYqZclY.d.ts} +44 -1
  53. package/dist/validation/index.d.ts +1 -1
  54. package/dist/validation/index.js +1 -1
  55. package/dist/validation/index.js.map +1 -1
  56. package/dist/workflows.d.ts +1 -1
  57. package/package.json +16 -12
  58. package/src/adapters/base.ts +8 -0
  59. package/src/adapters/claude/claude-code-login.ts +73 -0
  60. package/src/adapters/claude/sdk.ts +8 -1
  61. package/src/adapters/generation-dispatch.ts +19 -2
  62. package/src/adapters/openai/tokens.ts +18 -0
  63. package/src/adapters/provider-router.ts +28 -0
  64. package/src/boilerplate/generate.ts +19 -4
  65. package/src/boilerplate/hard-sections.ts +1 -1
  66. package/src/create-ui-generator.ts +19 -10
  67. package/src/evaluation/design-identity.ts +16 -5
  68. package/src/evaluation/types-public.ts +42 -0
  69. package/src/evaluation/visual-evaluator.ts +231 -24
  70. package/src/harness/check/runtime-render/adapter.ts +9 -0
  71. package/src/harness/check/runtime-render/render-check.ts +26 -5
  72. package/src/harness/claude-code-login-query.ts +528 -0
  73. package/src/harness/coding/assemble-result.ts +10 -1
  74. package/src/harness/coding/run-eval-round.ts +179 -34
  75. package/src/harness/enforced-coding.ts +20 -0
  76. package/src/harness/llm-router.ts +390 -55
  77. package/src/harness/llm-trace-sink.ts +7 -1
  78. package/src/harness/runtime.ts +6 -0
  79. package/src/providers/openai.ts +31 -19
  80. package/src/validation/primitives.ts +1 -1
@@ -22,13 +22,20 @@ import { listContractGadgets } from "@ggui-ai/protocol";
22
22
  import type { Classification } from "../../classifier/index.js";
23
23
  import type { AgentWorkspace } from "../../coding-agent/workspace.js";
24
24
  import type { CostTracker } from "../../evaluation/cost-tracker.js";
25
- import type { EvalIssue, EvalResult, RuntimeProbeMeta, VisualEvalSummary } from "../../evaluation/types-public.js";
25
+ import type { EvalIssue, EvalResult, RuntimeProbeMeta, VisualEvalSummary, VisualCoverage } from "../../evaluation/types-public.js";
26
26
  import { notApplicableCoverage } from "../../evaluation/types-public.js";
27
- import { mapProviderForEvaluator } from "../enforced-coding.js";
27
+ import { mapProviderForEvaluator, visionJudgeProvider } from "../enforced-coding.js";
28
28
  import { runCheck } from "../index.js";
29
29
  import { isRecoverableRenderCrash } from "../check/runtime-render/index.js";
30
30
  import type { Harness } from "../types-public.js";
31
31
  import type { AgentSpec, SingleComponentParams } from "../runtime.js";
32
+ import type {
33
+ CanvasFitReading,
34
+ VisualEvalContext,
35
+ VisualEvalOutcome,
36
+ VisualFitConfig,
37
+ VisualFitOutcome,
38
+ } from "../../evaluation/visual-evaluator.js";
32
39
 
33
40
  type PreWarmedEvalContext =
34
41
  import("../../evaluation/llm-evaluator.js").PreWarmedEvalContext;
@@ -122,6 +129,23 @@ export interface EvalRoundResult {
122
129
  /** Cap feedback to N issues per eval-fix turn so the LLM stays surgical. */
123
130
  const MAX_FEEDBACK_ISSUES = 3;
124
131
 
132
+ /**
133
+ * ggui#1195 — the visual leg's deterministic fit verdict: the inline card's
134
+ * content is taller than the box it was judged at (`canvas-overflow`,
135
+ * critical ⇒ fail). Recognised by the marker the evaluator emits, never by
136
+ * the description's wording.
137
+ */
138
+ function isFitFail(issue: EvalIssue): boolean {
139
+ return issue.result === "fail" && issue.category === "visual" && issue.subcategory === "canvas-overflow";
140
+ }
141
+ /** The same marker as {@link isFitFail}, on the fingerprint `fingerprintFail` builds (`category|subcategory|…`). */
142
+ const FIT_FINGERPRINT_PREFIX = "visual|canvas-overflow|";
143
+
144
+ /** `[fit]`-tagged so the LLM can see the violation is the box, not the judge's taste. */
145
+ function formatFitFeedback(issue: EvalIssue): string {
146
+ return `[fit] ${issue.category}/${issue.subcategory ?? ""}: ${issue.description}\n Fix: ${issue.fix ?? ""}`;
147
+ }
148
+
125
149
  /**
126
150
  * `criteriaCoverage` reason stamped on the same-image low-risk bypass
127
151
  * exits: tier-1/2 evaluation is never invoked there BY DESIGN, so every
@@ -131,6 +155,65 @@ const MAX_FEEDBACK_ISSUES = 3;
131
155
  /** Why an eval result carries no criteria: the round itself threw (ggui#1110). */
132
156
  const EVAL_ROUND_THREW_REASON = "eval round threw";
133
157
 
158
+ /**
159
+ * The in-loop visual leg, made total (ggui#1248). The judge runs on a VISION
160
+ * provider: the agent's provider narrowed by `visionJudgeProvider`, never
161
+ * asserted into the vision union — a lane whose generation provider has no
162
+ * vision path (OpenAI, OpenRouter) gets a leg recorded as SKIPPED with the
163
+ * reason, and the judge is not called. And whatever the leg does, it resolves:
164
+ * a rejection becomes the same skipped coverage with the error, so the round
165
+ * never takes its thrown path on account of the visual leg and the runtime
166
+ * probe still runs — a bar that requires the probe decides on the probe, not
167
+ * on a judge that could not be reached.
168
+ *
169
+ * The leg's FIT half does not need the judge (ggui#1195): the fit verdict is a
170
+ * frame at the declared box and a scroll height, so a no-vision lane with
171
+ * canvases runs `measureFit` alone. Its canvas-overflow fail rides the round's
172
+ * issues like the judge path's would — so the `[fit]` round the cap grants
173
+ * can fire on that lane — and the coverage still reads `skipped` for the judge,
174
+ * with what the fit half measured (or why it could not) appended. Without
175
+ * that half, a card taller than its declared chat box on such a lane was
176
+ * never told so in the loop, so the composition fix was never asked for.
177
+ */
178
+ function runVisualLeg(
179
+ visualEvalAgent: AgentSpec,
180
+ run: (provider: "claude" | "google") => Promise<VisualEvalOutcome>,
181
+ measureFit: (() => Promise<VisualFitOutcome>) | undefined,
182
+ ): Promise<VisualEvalOutcome> {
183
+ const provider = visionJudgeProvider(visualEvalAgent.provider);
184
+ if (provider === undefined) {
185
+ const noJudge = `no vision judge for provider '${visualEvalAgent.provider}' (model ${visualEvalAgent.model}) — the in-loop visual judge runs on a vision provider (anthropic, google)`;
186
+ if (measureFit === undefined) {
187
+ return Promise.resolve({ issues: [], coverage: { status: "skipped", reason: noJudge } });
188
+ }
189
+ const notMeasured = (why: string): VisualEvalOutcome => ({
190
+ issues: [],
191
+ coverage: { status: "skipped", reason: `${noJudge}; fit not measured: ${why}` },
192
+ });
193
+ return measureFit().then(
194
+ (fit): VisualEvalOutcome =>
195
+ fit.status === "measured"
196
+ ? {
197
+ issues: fit.issues,
198
+ coverage: { status: "skipped", reason: `${noJudge}; fit measured without the judge: ${describeFitReadings(fit.readings)}` },
199
+ }
200
+ : notMeasured(fit.reason),
201
+ (e: unknown): VisualEvalOutcome => notMeasured(e instanceof Error ? e.message : String(e)),
202
+ );
203
+ }
204
+ return run(provider).catch((e: unknown): VisualEvalOutcome => ({
205
+ issues: [],
206
+ coverage: { status: "skipped", reason: `visual leg failed: ${e instanceof Error ? e.message : String(e)}` },
207
+ }));
208
+ }
209
+
210
+ /** The fit half's receipt on the coverage: `xs-chat-card 384×516 content 535px (overflow)`, one per canvas. */
211
+ function describeFitReadings(readings: readonly CanvasFitReading[]): string {
212
+ return readings
213
+ .map((r) => `${r.canvas} ${r.viewport.width}×${r.viewport.height} content ${r.contentHeight ?? "?"}px${r.overflow ? " (overflow)" : ""}`)
214
+ .join(", ");
215
+ }
216
+
134
217
  const LOW_RISK_BYPASS_REASON =
135
218
  "same-image low-risk bypass: axis checks clean, tier-1/2 evaluation not invoked";
136
219
 
@@ -448,6 +531,30 @@ export async function runEvalRound(
448
531
  let llmResult: (EvalResult & { inputTokens: number; outputTokens: number }) | null = null;
449
532
  let visualIssues: EvalIssue[] | null = null;
450
533
 
534
+ // The visual leg's page and frame — ONE value each, read by the judge and
535
+ // by its fit half alike, so both frame the same component the same way.
536
+ const visualContext: VisualEvalContext = {
537
+ compiledCode,
538
+ originalPrompt: userPrompt,
539
+ profile: harness.profile,
540
+ // The caller's theme, when named — the round paints what the app paints.
541
+ ...(visualEvaluation?.cssTokens !== undefined ? { cssTokens: visualEvaluation.cssTokens } : {}),
542
+ };
543
+ const visualFrame: VisualFitConfig = {
544
+ // The in-loop visual round renders the same sample the runtime probe
545
+ // does (the caller's fixture) unless the config names its own.
546
+ sampleProps: visualEvaluation?.sampleProps ?? fixtureProps,
547
+ // Per-canvas judging (arm-neutral) — absent = single shot.
548
+ ...(visualEvaluation?.canvases !== undefined ? { canvases: visualEvaluation.canvases } : {}),
549
+ // ggui#1195 — the declared box per canvas (same-target with the composer).
550
+ ...(visualEvaluation?.canvasViewports !== undefined ? { canvasViewports: visualEvaluation.canvasViewports } : {}),
551
+ };
552
+ // The fit verdict is per canvas: a single-shot leg has no box to hold content against.
553
+ const measureFit =
554
+ visualMod && visualFrame.canvases !== undefined && visualFrame.canvases.length > 0
555
+ ? () => visualMod.runVisualFit(visualContext, visualFrame)
556
+ : undefined;
557
+
451
558
  const evalLlmStart = Date.now();
452
559
  const [llm, visual] = await Promise.all([
453
560
  llmEvalMod
@@ -483,35 +590,25 @@ export async function runEvalRound(
483
590
  )
484
591
  : null,
485
592
  visualMod
486
- ? visualMod.runVisualEval(
487
- {
488
- compiledCode,
489
- originalPrompt: userPrompt,
490
- profile: harness.profile,
491
- // The caller's theme, when named — the round paints what the app paints.
492
- ...(visualEvaluation?.cssTokens !== undefined ? { cssTokens: visualEvaluation.cssTokens } : {}),
493
- },
494
- {
495
- provider: mapProviderForEvaluator(visualEvalAgent.provider) as "claude" | "google",
496
- model: visualEvalAgent.model,
497
- passThreshold: visualThreshold,
498
- // The in-loop visual round renders the same sample the runtime probe
499
- // does (the caller's fixture) unless the config names its own.
500
- sampleProps: visualEvaluation?.sampleProps ?? fixtureProps,
501
- viewport: visualEvaluation?.viewport,
502
- // Per-canvas judging (arm-neutral) — absent = single shot.
503
- ...(visualEvaluation?.canvases !== undefined
504
- ? { canvases: visualEvaluation.canvases }
505
- : {}),
506
- // #484/#489 — same threading as the LLM-eval leg above:
507
- // the visual eval's multimodal call runs inside the same
508
- // concurrent generation, so it needs the same routing
509
- // override (never fall back to `process.env`) and the
510
- // same provider-429-retry observer (retries reach the
511
- // host application's structured log).
512
- routeOverride: visualEvalAgent.routeOverride,
513
- onRetry: visualEvalAgent.onRetry,
514
- },
593
+ ? runVisualLeg(
594
+ visualEvalAgent,
595
+ (provider) =>
596
+ visualMod.runVisualEval(visualContext, {
597
+ provider,
598
+ model: visualEvalAgent.model,
599
+ passThreshold: visualThreshold,
600
+ viewport: visualEvaluation?.viewport,
601
+ ...visualFrame,
602
+ // #484/#489 — same threading as the LLM-eval leg above:
603
+ // the visual eval's multimodal call runs inside the same
604
+ // concurrent generation, so it needs the same routing
605
+ // override (never fall back to `process.env`) and the
606
+ // same provider-429-retry observer (retries reach the
607
+ // host application's structured log).
608
+ routeOverride: visualEvalAgent.routeOverride,
609
+ onRetry: visualEvalAgent.onRetry,
610
+ }),
611
+ measureFit,
515
612
  )
516
613
  : null,
517
614
  ]);
@@ -519,6 +616,12 @@ export async function runEvalRound(
519
616
  llmResult = llm;
520
617
  visualIssues = visual?.issues ?? null;
521
618
  const visualSummary: VisualEvalSummary | undefined = visual?.summary;
619
+ // ggui#1221 — the judge leg's coverage is ALWAYS stamped: ran / skipped (with the
620
+ // reason) / not-applicable when no visual leg was configured for this round.
621
+ const visualCoverage: VisualCoverage = visual?.coverage ?? { status: 'not-applicable', reason: 'visual leg not configured' };
622
+ if (visualCoverage.status === 'skipped') {
623
+ console.log(`[eval] visual-judge: SKIPPED (${visualCoverage.reason ?? 'no reason recorded'})`);
624
+ }
522
625
 
523
626
  // Track costs from LLM eval calls
524
627
  if (llmResult) {
@@ -549,6 +652,7 @@ export async function runEvalRound(
549
652
  // requested and the leg ran; every later re-stamp spreads this
550
653
  // base so it survives to the assembled result.
551
654
  ...(visualSummary !== undefined ? { visual: visualSummary } : {}),
655
+ visualCoverage,
552
656
  };
553
657
 
554
658
  // ── Log merged results, with mode-check subcategory breakdown ──
@@ -683,13 +787,19 @@ export async function runEvalRound(
683
787
  // to localize them; the +2 max-rounds extension is the right escape
684
788
  // hatch, not stuck-loop exit. Keep stuck-loop active for non-runtime
685
789
  // fail-sets (the original tier-1 nitpick case it was built for).
686
- const onlyRuntimeFails =
790
+ // ggui#1195 follow-up: a FIT fail (`visual|canvas-overflow|…`) is exempt
791
+ // the same way — its escape hatch is the one `[fit]` round at the cap, and
792
+ // a card whose measured height did not move between rounds would otherwise
793
+ // exit here as "stuck" before that round is ever reached (the adversarial
794
+ // review ran the commit's own test and found round 2's break WAS the stuck
795
+ // exit). Bounded: the cap block grants the fit round exactly once.
796
+ const onlyExtensionExemptFails =
687
797
  currFailFingerprints.size > 0 &&
688
- [...currFailFingerprints].every((fp) => fp.includes("|runtime:"));
798
+ [...currFailFingerprints].every((fp) => fp.includes("|runtime:") || fp.startsWith(FIT_FINGERPRINT_PREFIX));
689
799
  if (
690
800
  evalRoundsUsed > 1 &&
691
801
  currFailFingerprints.size > 0 &&
692
- !onlyRuntimeFails &&
802
+ !onlyExtensionExemptFails &&
693
803
  [...currFailFingerprints].every((fp) => prevFailFingerprints.has(fp))
694
804
  ) {
695
805
  console.log(
@@ -747,6 +857,11 @@ export async function runEvalRound(
747
857
  // stuck-loop detector won't fire (some fails change), but the cap
748
858
  // hits with the runtime fail unresolved.
749
859
  const RUNTIME_EXTENSION_BONUS = 2;
860
+ // ggui#1195 — a fit fail (the hello does not fit its declared first
861
+ // screen — the founder's ruling is FAIL, not warn) active at the cap is
862
+ // worth ONE more round: the fix is a composition change the LLM can make
863
+ // in a turn, and without the round the card ships cut off.
864
+ const FIT_EXTENSION_BONUS = 1;
750
865
  if (evalRoundsUsed >= maxEvalRounds) {
751
866
  // 2026-04-27 (in-loop probe trigger): probe is now off the per-turn
752
867
  // hot path; `blocking` from runCheck won't contain `runtime:*` fails
@@ -764,6 +879,36 @@ export async function runEvalRound(
764
879
  const allowRuntimeExtension =
765
880
  hasRuntimeFail &&
766
881
  evalRoundsUsed < maxEvalRounds + RUNTIME_EXTENSION_BONUS;
882
+ const fitFails = evalResult.issues.filter(isFitFail);
883
+ const allowFitExtension =
884
+ !allowRuntimeExtension &&
885
+ fitFails.length > 0 &&
886
+ evalRoundsUsed < maxEvalRounds + FIT_EXTENSION_BONUS;
887
+ if (allowFitExtension) {
888
+ const fitLines = fitFails.slice(0, MAX_FEEDBACK_ISSUES).map(formatFitFeedback);
889
+ evalResult = {
890
+ ...evalResult,
891
+ issues: [...evalResult.issues, ...exitProbe.probeIssues],
892
+ runtimeProbe: exitProbe.meta,
893
+ };
894
+ console.log(
895
+ `[simple] eval round ${evalRoundsUsed}: fit fail active at cap — granting +1 retry (ggui#1195; ${fitFails.length} canvas-overflow)`,
896
+ );
897
+ return {
898
+ control: "feedback",
899
+ evalDone: false,
900
+ evalResult,
901
+ evalRoundsUsed,
902
+ prevModeSubcats: updatedPrevModeSubcats,
903
+ prevFailFingerprints: new Set([...currFailFingerprints, ...fitFails.map(fingerprintFail)]),
904
+ preWarmedContext,
905
+ evalTokens,
906
+ evalLlmMs,
907
+ lastResultText: fitLines.join("\n\n"),
908
+ isEvalFeedback: true,
909
+ lastDiffFailed: false,
910
+ };
911
+ }
767
912
  if (!allowRuntimeExtension) {
768
913
  // Fold probe issues (if any) into evalResult for telemetry and
769
914
  // ALWAYS stamp the probe's execution meta, even though we're not
@@ -18,3 +18,23 @@
18
18
  export function mapProviderForEvaluator(provider: 'anthropic' | 'openai' | 'google' | 'openrouter'): 'claude' | 'openai' | 'google' | 'openrouter' {
19
19
  return provider === 'anthropic' ? 'claude' : provider;
20
20
  }
21
+
22
+ /**
23
+ * The visual judge's provider for an agent's provider (ggui#1248) — only the
24
+ * providers whose SDK path takes an image (`createVisionAgent`: Anthropic,
25
+ * Google). `undefined` for a provider with no vision path, so a caller must
26
+ * decide what the visual leg does there instead of asserting it away: the
27
+ * in-loop judge inherits the generation's agent by default, and an OpenAI or
28
+ * OpenRouter lane reaching `createVisionAgent` got `undefined` back and threw.
29
+ */
30
+ export function visionJudgeProvider(provider: 'anthropic' | 'openai' | 'google' | 'openrouter'): 'claude' | 'google' | undefined {
31
+ switch (provider) {
32
+ case 'anthropic':
33
+ return 'claude';
34
+ case 'google':
35
+ return 'google';
36
+ case 'openai':
37
+ case 'openrouter':
38
+ return undefined;
39
+ }
40
+ }