@ggui-ai/ui-gen 0.9.0 → 0.10.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 (87) hide show
  1. package/dist/adapters/generation-dispatch.d.ts +9 -2
  2. package/dist/adapters/generation-dispatch.js +547 -85
  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 +563 -92
  8. package/dist/advanced/index.js.map +1 -1
  9. package/dist/blueprint-validator.js +109 -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 +110 -3
  29. package/dist/harness/check/runtime-render/index.js.map +1 -1
  30. package/dist/harness/index.d.ts +2 -2
  31. package/dist/harness/index.js +263 -18
  32. package/dist/harness/index.js.map +1 -1
  33. package/dist/harness/llm-trace-sink.d.ts +1 -1
  34. package/dist/harness/llm-trace-sink.js.map +1 -1
  35. package/dist/harness/prompts.js +17 -3
  36. package/dist/harness/prompts.js.map +1 -1
  37. package/dist/harness/result-types.d.ts +1 -1
  38. package/dist/harness/runtime.d.ts +9 -2
  39. package/dist/harness/runtime.js +20 -6
  40. package/dist/harness/runtime.js.map +1 -1
  41. package/dist/harness/types-public.d.ts +1 -1
  42. package/dist/harness/types-public.js +14 -2
  43. package/dist/harness/types-public.js.map +1 -1
  44. package/dist/harness/validator-trace-sink.d.ts +1 -1
  45. package/dist/index.d.ts +5 -3
  46. package/dist/index.js +563 -92
  47. package/dist/index.js.map +1 -1
  48. package/dist/{llm-router-1TyI0_TV.d.ts → llm-router-BEVtbAb9.d.ts} +61 -4
  49. package/dist/patch.d.ts +1 -1
  50. package/dist/provider-adapter.d.ts +16 -6
  51. package/dist/provider-adapter.js.map +1 -1
  52. package/dist/providers/index.d.ts +19 -6
  53. package/dist/providers/index.js.map +1 -1
  54. package/dist/{render-check-CNZjHD5g.d.ts → render-check-BNYGVDJd.d.ts} +1 -1
  55. package/dist/{types-public-C36k1q6s.d.ts → types-public-DIpjC-OA.d.ts} +1 -1
  56. package/dist/workflows.d.ts +1 -1
  57. package/package.json +12 -12
  58. package/src/adapters/generation-dispatch.ts +9 -0
  59. package/src/adapters/openrouter/client.ts +2 -2
  60. package/src/adapters/openrouter/types.ts +10 -0
  61. package/src/boilerplate/generate.ts +10 -2
  62. package/src/boilerplate/json-schema-ts.ts +22 -2
  63. package/src/boilerplate/system-prompt.ts +1 -0
  64. package/src/compile.ts +1 -1
  65. package/src/contract-context.ts +10 -2
  66. package/src/create-ui-generator.ts +14 -0
  67. package/src/design-system-docs.ts +6 -4
  68. package/src/evaluation/axis-checks/checks/state-ui-affordance.ts +162 -0
  69. package/src/evaluation/axis-checks/registry.ts +2 -0
  70. package/src/evaluation/index.ts +11 -0
  71. package/src/evaluation/llm-evaluator.ts +14 -1
  72. package/src/evaluation/visual-evaluator.ts +44 -65
  73. package/src/harness/check/runtime-render/adapter.ts +18 -0
  74. package/src/harness/check/runtime-render/index.ts +8 -0
  75. package/src/harness/check/runtime-render/prepare-mockup.ts +14 -0
  76. package/src/harness/check/runtime-render/production-act-shim.ts +158 -0
  77. package/src/harness/check/runtime-render/render-check.ts +72 -1
  78. package/src/harness/coding/run-eval-round.ts +61 -5
  79. package/src/harness/index.ts +1 -1
  80. package/src/harness/llm-router.ts +397 -18
  81. package/src/harness/llm-trace-sink.ts +2 -1
  82. package/src/harness/runtime.ts +7 -0
  83. package/src/index.ts +5 -5
  84. package/src/provider-adapter.ts +16 -6
  85. package/src/providers/bedrock.ts +5 -4
  86. package/src/providers/index.ts +16 -4
  87. package/src/tools/render-check.ts +19 -7
@@ -0,0 +1,158 @@
1
+ // packages/ui-gen/src/harness/check/runtime-render/production-act-shim.ts
2
+ //
3
+ // Minimal adaptation to two upstream constraints, neither of which
4
+ // this package controls:
5
+ //
6
+ // 1. React intentionally strips `act` from its PRODUCTION build.
7
+ // `react/cjs/react.production.js` does not export it — only the
8
+ // development build does (`typeof require('react').act` is
9
+ // `'function'` under a dev build, `undefined` under production).
10
+ // 2. `@testing-library/react`'s `act-compat` module hard-requires
11
+ // `act` at MODULE SCOPE: it snapshots
12
+ // `typeof React.act === 'function' ? React.act : DeprecatedReactTestUtils.act`
13
+ // into a `const` the first time it is imported in the process.
14
+ // Under a production build, that snapshot resolves to React
15
+ // DOM's `react-dom/test-utils` production `act` stub, which
16
+ // THROWS `React.act is not a function` the moment it is called.
17
+ //
18
+ // Hosts that run this probe inside a production Node process
19
+ // (server-side blueprint validation) hit this: every render throws
20
+ // through RTL's `act` wrapper before the probe ever sees the
21
+ // component. Hosts that run it under a development/test build (every
22
+ // CI/vitest run in this repo — `NODE_ENV=test` resolves the dev
23
+ // build) never see it; `act` is real there and this shim is a no-op.
24
+ //
25
+ // **Load-order constraint**: this MUST run before the FIRST import of
26
+ // `@testing-library/react` anywhere in the process — act-compat binds
27
+ // its `act` reference into a module-scope `const` at import time, so
28
+ // patching `react-dom/test-utils` AFTER that import has already
29
+ // happened does nothing (the snapshot already captured the broken
30
+ // stub). Patch `react-dom/test-utils`, never `react` itself — that
31
+ // entry point is deprecated and test-only, so this patch cannot
32
+ // perturb any production rendering path; nothing else in a server
33
+ // process legitimately imports it.
34
+ //
35
+ // `createRequire` (not `import`) reaches these modules deliberately:
36
+ // ESM namespace objects are frozen, so a normal `import` gives no way
37
+ // to mutate `act` on it. The CJS `module.exports` objects `require`
38
+ // resolves are mutable, and RTL's act-compat interop-copies THAT
39
+ // object's properties at its own load time — so mutating it here,
40
+ // before RTL loads, is what makes the patch visible to RTL at all.
41
+
42
+ import { createRequire } from "node:module";
43
+
44
+ let installed = false;
45
+
46
+ /**
47
+ * The one property this module reads/mutates on `react` and
48
+ * `react-dom/test-utils`'s CJS exports. A minimal local shape rather
49
+ * than the real (deprecated, possibly absent) type declarations for
50
+ * `react-dom/test-utils` — narrow enough to type-check the mutation
51
+ * without `as any` / `Record<string, unknown>`.
52
+ */
53
+ interface MutableActCarrier {
54
+ act?: unknown;
55
+ }
56
+
57
+ /** The one React DOM export this module needs — typed narrowly for the same reason. */
58
+ interface FlushSyncCarrier {
59
+ flushSync: (fn: () => void) => void;
60
+ }
61
+
62
+ /**
63
+ * Patch `react-dom/test-utils`'s `act` with a `flushSync`-backed
64
+ * replacement, but ONLY when the current process is running React's
65
+ * production build (no real `act` exported). Idempotent — safe to
66
+ * call more than once or from more than one call site; the actual
67
+ * patch only ever applies once per process.
68
+ *
69
+ * No-op (by design, not by accident) whenever `react`'s own `act`
70
+ * export is a real function — every dev/test build, including every
71
+ * vitest run in this repo. This shim changes zero behavior in CI.
72
+ */
73
+ export function installProductionActShim(): void {
74
+ if (installed) return;
75
+ installed = true;
76
+
77
+ const require_ = createRequire(import.meta.url);
78
+
79
+ let reactCjs: MutableActCarrier;
80
+ try {
81
+ reactCjs = require_("react");
82
+ } catch {
83
+ // React itself isn't resolvable in this process — the probe
84
+ // surfaces its own load error; nothing for this shim to patch.
85
+ return;
86
+ }
87
+
88
+ if (typeof reactCjs.act === "function") {
89
+ // Dev/test builds export a real `act` — strict no-op.
90
+ return;
91
+ }
92
+
93
+ try {
94
+ const testUtils: MutableActCarrier = require_("react-dom/test-utils");
95
+ const reactDom: FlushSyncCarrier = require_("react-dom");
96
+ testUtils.act = createFlushSyncAct(reactDom.flushSync);
97
+ } catch {
98
+ // Frozen/locked exports (or a react-dom build without flushSync)
99
+ // — leave it. The probe then surfaces the raw upstream error,
100
+ // which is the pre-fix behavior: an honest failure, not a
101
+ // silently-broken patch.
102
+ }
103
+ }
104
+
105
+ /**
106
+ * Build an `act`-compatible replacement backed by `flushSync`, which
107
+ * — unlike `act` — DOES exist in React's production build.
108
+ *
109
+ * `flushSync` is the single property this shim relies on from dev
110
+ * `act`: it forces the scheduled render to commit SYNCHRONOUSLY
111
+ * before returning, so a render throw propagates synchronously to the
112
+ * caller and the DOM is fully committed by the time this resolves.
113
+ *
114
+ * Returned function is act-style: if the callback itself returned a
115
+ * thenable (RTL's async act-wrapped interactions), the result
116
+ * resolves once that thenable settles (rejecting on its rejection);
117
+ * otherwise it resolves after one microtask, mirroring `act`'s own
118
+ * "flush the microtask queue" contract for the synchronous case.
119
+ *
120
+ * **Known, documented delta vs. a real dev `act`**: passive effects
121
+ * (`useEffect`) flush on a LATER task under `flushSync`, not
122
+ * synchronously at act-exit — `flushSync` only guarantees the
123
+ * synchronous commit, not effect flushing. The probe's uncaught-error
124
+ * handlers plus its 100ms teardown grace window (`render-check.ts`'s
125
+ * `cleanupAsyncInfra`) already cover a late effect throwing after this
126
+ * resolves, so this delta does not reopen the crash class the probe
127
+ * exists to catch.
128
+ *
129
+ * Exported separately so the flushSync/thenable semantics are unit
130
+ * testable without a real React runtime.
131
+ */
132
+ export function createFlushSyncAct(
133
+ flushSync: (fn: () => void) => void,
134
+ ): (callback: () => unknown) => Promise<void> {
135
+ return (callback: () => unknown): Promise<void> => {
136
+ let callbackResult: unknown;
137
+ flushSync(() => {
138
+ callbackResult = callback();
139
+ });
140
+ if (isThenable(callbackResult)) {
141
+ const pending = callbackResult;
142
+ return new Promise<void>((resolve, reject) => {
143
+ pending.then(() => resolve(), reject);
144
+ });
145
+ }
146
+ return new Promise<void>((resolve) => {
147
+ queueMicrotask(resolve);
148
+ });
149
+ };
150
+ }
151
+
152
+ function isThenable(value: unknown): value is PromiseLike<unknown> {
153
+ return (
154
+ typeof value === "object" &&
155
+ value !== null &&
156
+ typeof (value as { then?: unknown }).then === "function"
157
+ );
158
+ }
@@ -24,6 +24,7 @@ import { HOOK_NAME_RE, listContractGadgets } from "@ggui-ai/protocol";
24
24
  import { createProbe, createProbeWireConfig, type Probe } from "./probe.js";
25
25
  import { loadComponent } from "./load-component.js";
26
26
  import { findWiring, type WiringDetection } from "./find-wiring.js";
27
+ import { installProductionActShim } from "./production-act-shim.js";
27
28
  // All happy-dom / RTL / user-event narrowing flows through the typed
28
29
  // host boundary (one set of structural interfaces + validating guards;
29
30
  // DOM-lib-free — some downstream packages, e.g. cloud/amplify, exclude
@@ -70,7 +71,8 @@ export interface RenderCheckIssue {
70
71
  | "render-no-throw"
71
72
  | "action-wiring"
72
73
  | "prop-coverage"
73
- | "stream-rerender";
74
+ | "stream-rerender"
75
+ | "optional-props-omitted";
74
76
  readonly outcome: CheckOutcome;
75
77
  readonly subject?: string;
76
78
  readonly reason: string;
@@ -237,6 +239,9 @@ export async function runRenderCheck(
237
239
  // internals.
238
240
  uninstallSpy = probe.installPostMessageSpy();
239
241
 
242
+ // Must run before RTL's first import in this process — see
243
+ // production-act-shim.ts's docstring for why.
244
+ installProductionActShim();
240
245
  const { render, cleanup } = await import("@testing-library/react");
241
246
  // user-event v14 default export — see https://testing-library.com/docs/user-event/intro/
242
247
  const userEventModule = await import("@testing-library/user-event");
@@ -514,6 +519,72 @@ export async function runRenderCheck(
514
519
  if (issue) issues.push(issue);
515
520
  }
516
521
  }
522
+
523
+ // ── Check 7: Optional-props omission (BLOCK on crash, ggui#528) ──
524
+ // The main pass renders with EVERY declared prop filled, so a
525
+ // component that assumes an optional prop is present always
526
+ // passes here and crashes only live, when an agent legitimately
527
+ // omits it (the wire gate requires only `required === true`
528
+ // props). Render a second instance with the optional props
529
+ // STRIPPED — a throw is exactly the latent-undefined class this
530
+ // check exists to surface. Runs only when the contract declares
531
+ // at least one optional prop that the mockup filled; zero cost
532
+ // otherwise.
533
+ const declaredProps = input.contract?.propsSpec?.properties;
534
+ if (declaredProps) {
535
+ const optionalFilled = Object.entries(declaredProps)
536
+ .filter(([name, entry]) =>
537
+ (entry as { required?: boolean }).required !== true &&
538
+ input.mockupProps[name] !== undefined,
539
+ )
540
+ .map(([name]) => name);
541
+ if (optionalFilled.length > 0) {
542
+ const requiredOnlyProps: JsonObject = { ...input.mockupProps };
543
+ for (const name of optionalFilled) delete requiredOnlyProps[name];
544
+ const omitBoundaryRef: { error?: Error } = {};
545
+ class OmitErrorBoundary extends React.Component<
546
+ { children?: React.ReactNode },
547
+ { caught: boolean }
548
+ > {
549
+ state = { caught: false };
550
+ static getDerivedStateFromError(): { caught: boolean } {
551
+ return { caught: true };
552
+ }
553
+ componentDidCatch(error: Error): void {
554
+ omitBoundaryRef.error ??= error;
555
+ }
556
+ render(): React.ReactNode {
557
+ if (this.state.caught) return null;
558
+ return this.props.children;
559
+ }
560
+ }
561
+ try {
562
+ render(
563
+ React.createElement(GguiWireProvider, {
564
+ config: wireConfig,
565
+ children: React.createElement(OmitErrorBoundary, {
566
+ children: React.createElement(Component, requiredOnlyProps),
567
+ }),
568
+ }),
569
+ );
570
+ } catch (e) {
571
+ omitBoundaryRef.error ??=
572
+ e instanceof Error ? e : new Error(String(e));
573
+ }
574
+ if (omitBoundaryRef.error) {
575
+ issues.push({
576
+ check: "optional-props-omitted",
577
+ outcome: "failed",
578
+ subject: optionalFilled.join(", "),
579
+ reason:
580
+ `Render crashed when the optional prop(s) ${optionalFilled
581
+ .map((n) => `'${n}'`)
582
+ .join(", ")} were omitted: ${omitBoundaryRef.error.message}. ` +
583
+ `The contract marks them optional (no \`required: true\`), so a legitimate render may leave them out.`,
584
+ });
585
+ }
586
+ }
587
+ }
517
588
  } finally {
518
589
  cleanup();
519
590
  }
@@ -121,6 +121,29 @@ export interface EvalRoundResult {
121
121
  /** Cap feedback to N issues per eval-fix turn so the LLM stays surgical. */
122
122
  const MAX_FEEDBACK_ISSUES = 3;
123
123
 
124
+ /**
125
+ * Pick the ≤{@link MAX_FEEDBACK_ISSUES} issues an eval-fix turn feeds
126
+ * back. Provenance-aware (Exp 54 P1 addendum): ONE slot is reserved
127
+ * for the first deterministic tier-0 fail (axis/mode checks —
128
+ * machine-verified findings) when one exists; the rest fill
129
+ * fails-first then warns. Without the reservation, the merge order
130
+ * put deterministic findings last and any round with ≥3 LLM-judge
131
+ * fails silently starved them — the model never saw a precision-1.00
132
+ * structural finding while the stuck detector blamed it for not
133
+ * fixing it. Exactly one slot, so crash-class LLM fails keep the
134
+ * other two. Exported for the pins.
135
+ */
136
+ export function selectActionableFeedback(issues: readonly EvalIssue[]): EvalIssue[] {
137
+ const pool = issues.filter((i) => i.result === "fail" || i.result === "warn");
138
+ const reserved = pool.find((i) => i.tier === 0 && i.result === "fail");
139
+ return [
140
+ ...(reserved ? [reserved] : []),
141
+ ...pool
142
+ .filter((i) => i !== reserved)
143
+ .sort((a, b) => (a.result === "fail" ? 0 : 1) - (b.result === "fail" ? 0 : 1)),
144
+ ].slice(0, MAX_FEEDBACK_ISSUES);
145
+ }
146
+
124
147
  /**
125
148
  * Fingerprint a fail issue for cross-round stuck detection. Identifies the
126
149
  * "same fail" without being thrown by minor description-text variation.
@@ -424,6 +447,13 @@ export async function runEvalRound(
424
447
  // inside the same concurrent generation, so the race is
425
448
  // real here too, not just on the coding turn).
426
449
  routeOverride: evaluationAgent.routeOverride,
450
+ // #489 — thread the same provider-429-retry observer.
451
+ // Without this, a rate-limited retry inside the
452
+ // evaluation agent's apiCall() happens (the base-class
453
+ // retry loop is unconditional) but never reaches the
454
+ // caller, silently missing the host application's
455
+ // structured log.
456
+ onRetry: evaluationAgent.onRetry,
427
457
  },
428
458
  preWarmedContext,
429
459
  )
@@ -437,6 +467,14 @@ export async function runEvalRound(
437
467
  passThreshold: visualThreshold,
438
468
  sampleProps: visualEvaluation?.sampleProps,
439
469
  viewport: visualEvaluation?.viewport,
470
+ // #484/#489 — same threading as the LLM-eval leg above:
471
+ // the visual eval's multimodal call runs inside the same
472
+ // concurrent generation, so it needs the same routing
473
+ // override (never fall back to `process.env`) and the
474
+ // same provider-429-retry observer (retries reach the
475
+ // host application's structured log).
476
+ routeOverride: visualEvalAgent.routeOverride,
477
+ onRetry: visualEvalAgent.onRetry,
440
478
  },
441
479
  )
442
480
  : null,
@@ -758,10 +796,19 @@ export async function runEvalRound(
758
796
  // Feeding too many issues at once causes the LLM to make many simultaneous
759
797
  // changes, breaking JSX nesting in complex components. Prioritize fails
760
798
  // first, then warns, capped to keep fixes surgical.
761
- const allActionable = evalResult.issues
762
- .filter((i) => i.result === "fail" || i.result === "warn")
763
- .sort((a, b) => (a.result === "fail" ? 0 : 1) - (b.result === "fail" ? 0 : 1))
764
- .slice(0, MAX_FEEDBACK_ISSUES);
799
+ //
800
+ // Provenance-aware selection (Exp 54 P1 addendum): deterministic
801
+ // tier-0 fails (axis/mode checks — machine-verified, precision-1.00
802
+ // findings) get ONE reserved slot, first. Before this, the merge
803
+ // order (`allIssues = [...llm, ...visual, ...mode]`) + stable sort
804
+ // + slice meant any round where the LLM leg emitted ≥3 fails
805
+ // silently starved every deterministic finding — the model never
806
+ // saw it, the fingerprint set still recorded it, and the stuck
807
+ // detector exited on an issue that was never delivered (2/9 cells
808
+ // shipped a P0 structural defect this way, one displaced by
809
+ // design-token nits). Exactly one slot is reserved so crash-class
810
+ // LLM fails keep the other two.
811
+ const allActionable = selectActionableFeedback(evalResult.issues);
765
812
  const issueLines = allActionable.map((issue) => {
766
813
  const tag = issue.result === "fail" ? "FAIL" : "WARN";
767
814
  return `[${tag}] ${issue.category}${issue.subcategory ? "/" + issue.subcategory : ""}: ${issue.description}\n Fix: ${issue.fix}`;
@@ -777,7 +824,16 @@ export async function runEvalRound(
777
824
  evalResult,
778
825
  evalRoundsUsed,
779
826
  prevModeSubcats: updatedPrevModeSubcats,
780
- prevFailFingerprints: currFailFingerprints,
827
+ // DELIVERED fails only (Exp 54 P1 addendum, proposal 2): the
828
+ // stuck detector's premise is "the LLM saw these and did not
829
+ // resolve them" — recording fails that the MAX_FEEDBACK_ISSUES
830
+ // cap cut from the feedback turns truncation into a premature
831
+ // stuck-exit (the model never saw the issue it is blamed for
832
+ // not fixing). An undelivered fail recurring next round now
833
+ // reads as NEW, gets delivered, and earns its fix round.
834
+ prevFailFingerprints: new Set(
835
+ allActionable.filter((i) => i.result === "fail").map(fingerprintFail),
836
+ ),
781
837
  preWarmedContext,
782
838
  evalTokens,
783
839
  evalLlmMs,
@@ -27,7 +27,7 @@ export type {
27
27
  } from "./types-public.js";
28
28
  export { defaultApplyPatch, applyLineRanges } from "../patch.js";
29
29
  export { callLLM, createAgent } from "./llm-router.js";
30
- export type { AgentConfig, LLMResponse } from "./llm-router.js";
30
+ export type { AgentConfig, LLMResponse, ProviderRetryInfo } from "./llm-router.js";
31
31
  export {
32
32
  computeHarnessId,
33
33
  computeHarnessName,