@ggui-ai/ui-gen 0.16.0 → 0.17.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 (101) hide show
  1. package/dist/adapters/generation-dispatch.d.ts +6 -3
  2. package/dist/adapters/generation-dispatch.js +4944 -4239
  3. package/dist/adapters/generation-dispatch.js.map +1 -1
  4. package/dist/adapters/index.js +8 -5
  5. package/dist/adapters/index.js.map +1 -1
  6. package/dist/advanced/index.d.ts +12 -8
  7. package/dist/advanced/index.js +4462 -3726
  8. package/dist/advanced/index.js.map +1 -1
  9. package/dist/blueprint-validator.js +8 -12
  10. package/dist/blueprint-validator.js.map +1 -1
  11. package/dist/boilerplate.d.ts +5 -2
  12. package/dist/boilerplate.js +95 -12
  13. package/dist/boilerplate.js.map +1 -1
  14. package/dist/check/index.d.ts +2 -2
  15. package/dist/check/index.js +2 -0
  16. package/dist/check/index.js.map +1 -1
  17. package/dist/compose.js +1 -1
  18. package/dist/compose.js.map +1 -1
  19. package/dist/{create-ui-generator-CCIeQaEe.d.ts → create-ui-generator-C8Ki9Wz1.d.ts} +18 -2
  20. package/dist/{design-mode-BHEE7flj.d.ts → design-mode-DLufhrNp.d.ts} +0 -6
  21. package/dist/{dispatch-BA5dJcg-.d.ts → dispatch-D2htzi9h.d.ts} +2 -2
  22. package/dist/evaluation/axis-checks/index.d.ts +4 -4
  23. package/dist/evaluation/axis-checks/index.js +297 -13
  24. package/dist/evaluation/axis-checks/index.js.map +1 -1
  25. package/dist/evaluation/axis-checks/registry.d.ts +2 -2
  26. package/dist/evaluation/axis-checks/registry.js +252 -1
  27. package/dist/evaluation/axis-checks/registry.js.map +1 -1
  28. package/dist/evaluation/index.d.ts +160 -8
  29. package/dist/evaluation/index.js +702 -81
  30. package/dist/evaluation/index.js.map +1 -1
  31. package/dist/fragments/index.js +1 -1
  32. package/dist/fragments/index.js.map +1 -1
  33. package/dist/{generate-8L_bc2iP.d.ts → generate-9IeNT4ZY.d.ts} +1 -1
  34. package/dist/harness/check/runtime-render/index.d.ts +3 -2
  35. package/dist/harness/check/runtime-render/index.js +8 -12
  36. package/dist/harness/check/runtime-render/index.js.map +1 -1
  37. package/dist/harness/index.d.ts +3 -2
  38. package/dist/harness/index.js +385 -254
  39. package/dist/harness/index.js.map +1 -1
  40. package/dist/harness/prompts.d.ts +2 -2
  41. package/dist/harness/prompts.js +3 -3
  42. package/dist/harness/prompts.js.map +1 -1
  43. package/dist/harness/result-types.d.ts +3 -3
  44. package/dist/harness/runtime.d.ts +13 -4
  45. package/dist/harness/runtime.js +106 -19
  46. package/dist/harness/runtime.js.map +1 -1
  47. package/dist/harness/types-public.d.ts +13 -2
  48. package/dist/harness/types-public.js +385 -254
  49. package/dist/harness/types-public.js.map +1 -1
  50. package/dist/harness/validator-trace-sink.d.ts +2 -2
  51. package/dist/index.d.ts +29 -7
  52. package/dist/index.js +4465 -3727
  53. package/dist/index.js.map +1 -1
  54. package/dist/patch.d.ts +3 -2
  55. package/dist/{prompts-k6m6reqe.d.ts → prompts-YU9piOgs.d.ts} +12 -46
  56. package/dist/styling-profile-CxfvV48g.d.ts +35 -0
  57. package/dist/tools/get-primitives-ts.d.ts +1 -1
  58. package/dist/tools/get-primitives-ts.js +1 -1
  59. package/dist/tools/get-primitives-ts.js.map +1 -1
  60. package/dist/{types-public-D24JLJgl.d.ts → types-public-DVSssdc-.d.ts} +39 -3
  61. package/dist/validation/index.d.ts +1 -1
  62. package/dist/validation/index.js +1 -1
  63. package/dist/validation/index.js.map +1 -1
  64. package/dist/workflows.d.ts +3 -2
  65. package/package.json +12 -12
  66. package/src/adapters/generation-dispatch.ts +6 -1
  67. package/src/boilerplate/free-design-prompt.ts +7 -0
  68. package/src/boilerplate/hard-sections.ts +16 -1
  69. package/src/boilerplate/styling-profile.ts +166 -0
  70. package/src/boilerplate/system-prompt.ts +20 -8
  71. package/src/check/run-tier0.ts +2 -0
  72. package/src/contract-context.ts +15 -14
  73. package/src/create-harness.ts +4 -0
  74. package/src/create-ui-generator.ts +41 -2
  75. package/src/design-mode.ts +11 -1
  76. package/src/design-system-docs.ts +7 -4
  77. package/src/effort.ts +48 -0
  78. package/src/evaluation/axis-checks/checks/grid.ts +148 -0
  79. package/src/evaluation/axis-checks/checks/universal.ts +244 -0
  80. package/src/evaluation/axis-checks/dispatch.ts +78 -13
  81. package/src/evaluation/axis-checks/helpers.ts +12 -0
  82. package/src/evaluation/axis-checks/registry.ts +2 -0
  83. package/src/evaluation/design-identity.ts +96 -0
  84. package/src/evaluation/index.ts +17 -0
  85. package/src/evaluation/llm-evaluator.ts +13 -1
  86. package/src/evaluation/theme-css.ts +14 -0
  87. package/src/evaluation/types-public.ts +35 -1
  88. package/src/evaluation/visual-evaluator.ts +478 -45
  89. package/src/fragments/render.ts +1 -1
  90. package/src/harness/check/runtime-render/prepare-mockup.ts +18 -11
  91. package/src/harness/coding/init-session.ts +1 -0
  92. package/src/harness/coding/run-coding-turn.ts +6 -14
  93. package/src/harness/coding/run-eval-round.ts +31 -3
  94. package/src/harness/runtime.ts +10 -0
  95. package/src/harness/types-public.ts +11 -0
  96. package/src/index.ts +2 -0
  97. package/src/pin-fixtures.ts +98 -0
  98. package/src/run-check.ts +10 -10
  99. package/src/tools/get-primitives-ts.ts +1 -1
  100. package/src/tools.ts +2 -1
  101. package/src/validation/primitives.ts +1 -1
@@ -1,4 +1,4 @@
1
- import { mkdirSync, writeFileSync, unlinkSync, existsSync, rm, createReadStream, createWriteStream } from 'fs';
1
+ import { readFileSync, readdirSync, statSync, mkdirSync, writeFileSync, unlinkSync, existsSync, rm, createReadStream, createWriteStream } from 'fs';
2
2
  import 'fs/promises';
3
3
  import { tmpdir } from 'os';
4
4
  import { resolve, dirname, join, basename } from 'path';
@@ -9,9 +9,12 @@ import { fileURLToPath } from 'url';
9
9
  import { query, createSdkMcpServer, tool } from '@anthropic-ai/claude-agent-sdk';
10
10
  import Anthropic from '@anthropic-ai/sdk';
11
11
  import { z } from 'zod';
12
- import { listContractGadgets, HOOK_NAME_RE, STDLIB_GADGETS_PACKAGE } from '@ggui-ai/protocol';
12
+ import { LUCIDE_ICON_NAMES } from '@ggui-ai/design';
13
+ import { listContractGadgets, HOOK_NAME_RE, STDLIB_GADGETS_PACKAGE, APP_GENERATION_PROFILE_BOUNDS } from '@ggui-ai/protocol';
14
+ import { createHash } from 'crypto';
13
15
  import { build } from 'esbuild';
14
- import { getCssTokens } from '@ggui-ai/design/rendering';
16
+ import { getCssTokens, composeThemeCss, fillFitRule } from '@ggui-ai/design/rendering';
17
+ import { createRequire } from 'module';
15
18
 
16
19
  var __create = Object.create;
17
20
  var __defProp = Object.defineProperty;
@@ -1404,8 +1407,8 @@ var require_streamx = __commonJS({
1404
1407
  return this;
1405
1408
  },
1406
1409
  next() {
1407
- return new Promise(function(resolve2, reject) {
1408
- promiseResolve = resolve2;
1410
+ return new Promise(function(resolve3, reject) {
1411
+ promiseResolve = resolve3;
1409
1412
  promiseReject = reject;
1410
1413
  const data = stream.read();
1411
1414
  if (data !== null) ondata(data);
@@ -1438,11 +1441,11 @@ var require_streamx = __commonJS({
1438
1441
  }
1439
1442
  function destroy(err) {
1440
1443
  stream.destroy(err);
1441
- return new Promise((resolve2, reject) => {
1442
- if (stream._duplexState & DESTROYED) return resolve2({ value: void 0, done: true });
1444
+ return new Promise((resolve3, reject) => {
1445
+ if (stream._duplexState & DESTROYED) return resolve3({ value: void 0, done: true });
1443
1446
  stream.once("close", function() {
1444
1447
  if (err) reject(err);
1445
- else resolve2({ value: void 0, done: true });
1448
+ else resolve3({ value: void 0, done: true });
1446
1449
  });
1447
1450
  });
1448
1451
  }
@@ -1486,8 +1489,8 @@ var require_streamx = __commonJS({
1486
1489
  const writes = pending + (ws._duplexState & WRITE_WRITING ? 1 : 0);
1487
1490
  if (writes === 0) return Promise.resolve(true);
1488
1491
  if (state.drains === null) state.drains = [];
1489
- return new Promise((resolve2) => {
1490
- state.drains.push({ writes, resolve: resolve2 });
1492
+ return new Promise((resolve3) => {
1493
+ state.drains.push({ writes, resolve: resolve3 });
1491
1494
  });
1492
1495
  }
1493
1496
  write(data) {
@@ -1592,10 +1595,10 @@ var require_streamx = __commonJS({
1592
1595
  cb(null);
1593
1596
  }
1594
1597
  function pipelinePromise(...streams) {
1595
- return new Promise((resolve2, reject) => {
1598
+ return new Promise((resolve3, reject) => {
1596
1599
  return pipeline2(...streams, (err) => {
1597
1600
  if (err) return reject(err);
1598
- resolve2();
1601
+ resolve3();
1599
1602
  });
1600
1603
  });
1601
1604
  }
@@ -2253,16 +2256,16 @@ var require_extract = __commonJS({
2253
2256
  entryCallback = null;
2254
2257
  cb(err);
2255
2258
  }
2256
- function onnext(resolve2, reject) {
2259
+ function onnext(resolve3, reject) {
2257
2260
  if (error) {
2258
2261
  return reject(error);
2259
2262
  }
2260
2263
  if (entryStream) {
2261
- resolve2({ value: entryStream, done: false });
2264
+ resolve3({ value: entryStream, done: false });
2262
2265
  entryStream = null;
2263
2266
  return;
2264
2267
  }
2265
- promiseResolve = resolve2;
2268
+ promiseResolve = resolve3;
2266
2269
  promiseReject = reject;
2267
2270
  consumeCallback(null);
2268
2271
  if (extract3._finished && promiseResolve) {
@@ -2290,11 +2293,11 @@ var require_extract = __commonJS({
2290
2293
  function destroy(err) {
2291
2294
  extract3.destroy(err);
2292
2295
  consumeCallback(err);
2293
- return new Promise((resolve2, reject) => {
2294
- if (extract3.destroyed) return resolve2({ value: void 0, done: true });
2296
+ return new Promise((resolve3, reject) => {
2297
+ if (extract3.destroyed) return resolve3({ value: void 0, done: true });
2295
2298
  extract3.once("close", function() {
2296
2299
  if (err) reject(err);
2297
- else resolve2({ value: void 0, done: true });
2300
+ else resolve3({ value: void 0, done: true });
2298
2301
  });
2299
2302
  });
2300
2303
  }
@@ -3190,9 +3193,9 @@ var init_helper = __esm({
3190
3193
  try {
3191
3194
  await pipeline(Readable.fromWeb(response.body), (0, import_tar_fs.extract)(destDir));
3192
3195
  } catch (error) {
3193
- await new Promise((resolve2) => {
3196
+ await new Promise((resolve3) => {
3194
3197
  rm(destDir, { force: true, recursive: true }, () => {
3195
- resolve2();
3198
+ resolve3();
3196
3199
  });
3197
3200
  });
3198
3201
  throw error;
@@ -3211,14 +3214,14 @@ var init_lambdafs = __esm({
3211
3214
  GZIP_EXTENSION_REGEX = /gz$/i;
3212
3215
  inflate = (filePath) => {
3213
3216
  const output = filePath.includes("swiftshader") ? tmpdir() : join(tmpdir(), basename(filePath).replace(ARCHIVE_EXTENSION_REGEX, ""));
3214
- return new Promise((resolve2, reject) => {
3217
+ return new Promise((resolve3, reject) => {
3215
3218
  if (filePath.includes("swiftshader")) {
3216
3219
  if (existsSync(`${output}/libGLESv2.so`)) {
3217
- resolve2(output);
3220
+ resolve3(output);
3218
3221
  return;
3219
3222
  }
3220
3223
  } else if (existsSync(output)) {
3221
- resolve2(output);
3224
+ resolve3(output);
3222
3225
  return;
3223
3226
  }
3224
3227
  const isBrotli = BROTLI_EXTENSION_REGEX.test(filePath);
@@ -3234,12 +3237,12 @@ var init_lambdafs = __esm({
3234
3237
  if (isTar) {
3235
3238
  target = (0, import_tar_fs2.extract)(output);
3236
3239
  target.once("finish", () => {
3237
- resolve2(output);
3240
+ resolve3(output);
3238
3241
  });
3239
3242
  } else {
3240
3243
  target = createWriteStream(output, { mode: 448 });
3241
3244
  target.once("close", () => {
3242
- resolve2(output);
3245
+ resolve3(output);
3243
3246
  });
3244
3247
  }
3245
3248
  target.once("error", handleError);
@@ -3818,7 +3821,7 @@ function extractRetryAfterSec(e) {
3818
3821
  return Number.isFinite(parsed) && parsed >= 0 ? Math.floor(parsed) : void 0;
3819
3822
  }
3820
3823
  function sleep(ms) {
3821
- return new Promise((resolve2) => setTimeout(resolve2, ms));
3824
+ return new Promise((resolve3) => setTimeout(resolve3, ms));
3822
3825
  }
3823
3826
  var MAX_RETRY_ATTEMPTS = 2;
3824
3827
  var RETRY_AFTER_CAP_SEC = 15;
@@ -5555,6 +5558,9 @@ function mkIssue(subcategory, description, fix, result = "fail") {
5555
5558
  function cap(s) {
5556
5559
  return s.charAt(0).toUpperCase() + s.slice(1);
5557
5560
  }
5561
+ function axisCheckTraceEnabled() {
5562
+ return process.env["GGUI_AXIS_CHECK_TRACE"] === "1";
5563
+ }
5558
5564
 
5559
5565
  // src/evaluation/axis-checks/checks/universal.ts
5560
5566
  var ALL_RENDER_VALUES = [
@@ -5658,7 +5664,172 @@ function runPropSeedNoResync(input) {
5658
5664
  }
5659
5665
  return issues;
5660
5666
  }
5667
+ var KNOWN_ICON_KEYS = new Set(
5668
+ LUCIDE_ICON_NAMES.map((n) => n.replace(/[-_]/g, "").toLowerCase())
5669
+ );
5670
+ function isKnownIconName(name) {
5671
+ return KNOWN_ICON_KEYS.has(name.replace(/[-_]/g, "").toLowerCase());
5672
+ }
5673
+ var ICON_NAME_LITERAL_RX = /<Icon\b[^>]*?\bname\s*=\s*(?:"([^"]*)"|'([^']*)'|\{\s*"([^"]*)"\s*\}|\{\s*'([^']*)'\s*\})/g;
5674
+ function findUnknownIconNames(sourceCode) {
5675
+ const unknown = /* @__PURE__ */ new Set();
5676
+ for (const m of sourceCode.matchAll(ICON_NAME_LITERAL_RX)) {
5677
+ const name = m[1] ?? m[2] ?? m[3] ?? m[4] ?? "";
5678
+ if (name.length === 0) continue;
5679
+ if (/[^\x20-\x7e]/.test(name)) continue;
5680
+ if (!isKnownIconName(name)) unknown.add(name);
5681
+ }
5682
+ return [...unknown];
5683
+ }
5684
+ function runIconNameKnown(input) {
5685
+ if (input.compiledCode === null) return [];
5686
+ const unknown = findUnknownIconNames(input.sourceCode);
5687
+ if (unknown.length === 0) return [];
5688
+ const list = unknown.map((n) => `"${n}"`).join(", ");
5689
+ return [
5690
+ mkIssue(
5691
+ "universal.icon_name_known",
5692
+ `<Icon name=\u2026> uses ${unknown.length} name(s) outside the design system's Lucide subset: ${list} \u2014 an unknown name renders an empty box.`,
5693
+ "Call get_available_icons and pick a listed name (kebab-case), or use an emoji directly."
5694
+ )
5695
+ ];
5696
+ }
5697
+ var LADDER_STOP_AS_TEXT_RX = /(?<![\w-])color\s*(?:=\s*\{?\s*|:\s*)["'`]?\s*var\(--ggui-color-primary-(\d{2,3})\s*[,)]/g;
5698
+ var TEXT_INK_STOP_MIN = 300;
5699
+ var TEXT_INK_STOP_MAX = 600;
5700
+ function findLadderStopsAsText(sourceCode) {
5701
+ const found = /* @__PURE__ */ new Set();
5702
+ for (const m of sourceCode.matchAll(LADDER_STOP_AS_TEXT_RX)) {
5703
+ const digits = m[1] ?? "";
5704
+ const stop = Number(digits);
5705
+ if (stop >= TEXT_INK_STOP_MIN && stop <= TEXT_INK_STOP_MAX) found.add(`primary-${digits}`);
5706
+ }
5707
+ return [...found];
5708
+ }
5709
+ function runAccentTextInk(input) {
5710
+ if (input.compiledCode === null) return [];
5711
+ const stops = findLadderStopsAsText(input.sourceCode);
5712
+ if (stops.length === 0) return [];
5713
+ const list = stops.map((s) => `\`${s}\``).join(", ");
5714
+ return [
5715
+ mkIssue(
5716
+ "universal.accent_text_ink",
5717
+ `Text is painted with a brand-ladder fill stop as its color (${list}) \u2014 on a low-saturation brand these stops are mid-grey on the surface and the text fails readability.`,
5718
+ 'Accent text (eyebrows, taglines, links, labels, inline arrows) takes var(--ggui-color-link) or <Text tone="emphasized">; body text takes var(--ggui-color-onContainer); keep primary-* stops on fills and borders.'
5719
+ )
5720
+ ];
5721
+ }
5722
+ var SCOPED_SURFACE_TAG_RX = /<(Card|Box)\b([^<>]*?)surface\s*=\s*["'](inverted|hero)["']([^<>]*?)>/g;
5723
+ var STYLE_BACKGROUND_RX = /\bstyle\s*=\s*\{\{[^}]*\b(background|backgroundColor|backgroundImage)\s*:/;
5724
+ function findScopedSurfaceBackgrounds(sourceCode) {
5725
+ const hits = [];
5726
+ for (const m of sourceCode.matchAll(SCOPED_SURFACE_TAG_RX)) {
5727
+ const attrs = `${m[2] ?? ""} ${m[4] ?? ""}`;
5728
+ if (STYLE_BACKGROUND_RX.test(attrs)) hits.push([m[1] ?? "", m[3] ?? ""]);
5729
+ }
5730
+ return hits;
5731
+ }
5732
+ function runScopedSurfaceOwnsGround(input) {
5733
+ if (input.compiledCode === null) return [];
5734
+ if (input.designMode === "free") return [];
5735
+ const hits = findScopedSurfaceBackgrounds(input.sourceCode);
5736
+ if (hits.length === 0) return [];
5737
+ const [tag, surface] = hits[0];
5738
+ return [
5739
+ mkIssue(
5740
+ "universal.scoped_surface_owns_ground",
5741
+ `<${tag} surface="${surface}"> sets its own background in style \u2014 a scoped surface owns its ground; repainting it puts the scope's swapped inks over the wrong ground (ink on ink).`,
5742
+ `Remove the background from that ${tag}'s style \u2014 surface="${surface}" paints the ground; pick another surface if a different ground is wanted.`
5743
+ )
5744
+ ];
5745
+ }
5746
+ var CAPS_TEXT_RX = /<Text\b(?=[^>]*\bcaps\b)[^>]*>([\s\S]*?)<\/Text>/g;
5747
+ function findInventedCapsLabels(sourceCode) {
5748
+ const out = [];
5749
+ for (const m of sourceCode.matchAll(CAPS_TEXT_RX)) {
5750
+ const inner = (m[1] ?? "").trim();
5751
+ if (inner.length === 0) continue;
5752
+ if (inner.includes("{")) continue;
5753
+ out.push(inner.replace(/\s+/g, " ").slice(0, 60));
5754
+ }
5755
+ return out;
5756
+ }
5757
+ function runCapsLabelInvented(input) {
5758
+ if (input.compiledCode === null) return [];
5759
+ const invented = findInventedCapsLabels(input.sourceCode);
5760
+ if (invented.length === 0) return [];
5761
+ const list = invented.map((t) => `"${t}"`).join(", ");
5762
+ return [
5763
+ mkIssue(
5764
+ "universal.caps_label_invented",
5765
+ `${invented.length} caps label(s) carry invented copy the props do not supply: ${list} \u2014 an eyebrow, kicker or overline the agent cannot change is chatter on every render.`,
5766
+ "Remove the label, or bind its text to a prop (`<Text caps>{props.section}</Text>`); copy comes from the contract, never from the component."
5767
+ )
5768
+ ];
5769
+ }
5770
+ var VIEWPORT_UNIT = String.raw`(?:calc\(\s*)?100(?:vh|dvh|svh|lvh)\b(?:[^)"'\x60;\n]*\))?`;
5771
+ var VIEWPORT_STYLE_OBJECT_RX = new RegExp(
5772
+ String.raw`\b(?:minHeight|height|minBlockSize|blockSize)\s*:\s*["'\x60]\s*${VIEWPORT_UNIT}["'\x60]?`,
5773
+ "g"
5774
+ );
5775
+ var VIEWPORT_CSS_RX = new RegExp(
5776
+ String.raw`(?<![\w-])(?:min-)?(?:height|block-size)\s*:\s*${VIEWPORT_UNIT}`,
5777
+ "g"
5778
+ );
5779
+ var VIEWPORT_CLASS_RX = /(?<![\w-])(?:min-)?h-(?:screen|dvh|svh|lvh)\b/g;
5780
+ function findViewportSizing(sourceCode) {
5781
+ const hits = [];
5782
+ for (const rx of [VIEWPORT_STYLE_OBJECT_RX, VIEWPORT_CSS_RX, VIEWPORT_CLASS_RX]) {
5783
+ for (const m of sourceCode.matchAll(rx)) {
5784
+ hits.push({ at: m.index ?? 0, text: m[0].replace(/\s+/g, " ").trim().slice(0, 60) });
5785
+ }
5786
+ }
5787
+ return hits.sort((a, b) => a.at - b.at).map((h) => h.text);
5788
+ }
5789
+ function runViewportSized(input) {
5790
+ if (input.compiledCode === null) return [];
5791
+ const sized = findViewportSizing(input.sourceCode);
5792
+ if (sized.length === 0) return [];
5793
+ const list = sized.map((t) => "`" + t + "`").join(", ");
5794
+ return [
5795
+ mkIssue(
5796
+ "universal.viewport_sized",
5797
+ `${sized.length} element(s) are sized to the viewport: ${list} \u2014 the frame owns the height; a content-sized frame is measured from the content, so a viewport-sized root never shrinks and grows the frame on every re-measure.`,
5798
+ "Remove the viewport height \u2014 size to content, never to the viewport. Under fullscreen the frame stretches your root already (centre inside it with `display: flex; flex-direction: column; justify-content: center`); to fill a parent that has a height use `height: 100%` or `flex: 1`."
5799
+ )
5800
+ ];
5801
+ }
5661
5802
  var UNIVERSAL_CHECKS = [
5803
+ {
5804
+ id: "universal.icon_name_known",
5805
+ axis: "render",
5806
+ values: ALL_RENDER_VALUES,
5807
+ run: runIconNameKnown
5808
+ },
5809
+ {
5810
+ id: "universal.caps_label_invented",
5811
+ axis: "render",
5812
+ values: ALL_RENDER_VALUES,
5813
+ run: runCapsLabelInvented
5814
+ },
5815
+ {
5816
+ id: "universal.viewport_sized",
5817
+ axis: "render",
5818
+ values: ALL_RENDER_VALUES,
5819
+ run: runViewportSized
5820
+ },
5821
+ {
5822
+ id: "universal.accent_text_ink",
5823
+ axis: "render",
5824
+ values: ALL_RENDER_VALUES,
5825
+ run: runAccentTextInk
5826
+ },
5827
+ {
5828
+ id: "universal.scoped_surface_owns_ground",
5829
+ axis: "render",
5830
+ values: ALL_RENDER_VALUES,
5831
+ run: runScopedSurfaceOwnsGround
5832
+ },
5662
5833
  {
5663
5834
  id: "universal.prop_coverage",
5664
5835
  axis: "render",
@@ -6461,6 +6632,87 @@ var TOOLING_CHECKS = [
6461
6632
  }
6462
6633
  ];
6463
6634
 
6635
+ // src/evaluation/axis-checks/checks/grid.ts
6636
+ var BOARD_PROP_RX = /^(columns?|lanes?|stages?|lists?|buckets?|swimlanes?)$/i;
6637
+ var BOARD_PROMPT_RX = /\b(kanban|board)\b/i;
6638
+ var BOARD_NAMES = ["columns", "lanes", "stages", "lists", "buckets", "swimlanes"];
6639
+ var LAYOUT_TAG_RX = /<(\/?)(Grid|Row|Stack|Card|Box|Container|ScrollArea|div|section)\b([^<>]*?)>/g;
6640
+ var SIDE_BY_SIDE = /* @__PURE__ */ new Set(["Grid", "Row"]);
6641
+ function boardPropNames(contract) {
6642
+ const propsField = contract?.propsSpec;
6643
+ const properties = propsField?.properties ?? {};
6644
+ return Object.entries(properties).filter(([name, p]) => BOARD_PROP_RX.test(name) && (p?.type === "array" || p?.schema?.type === "array")).map(([name]) => name);
6645
+ }
6646
+ function jsxMapPositions(sourceCode, name) {
6647
+ const rx = new RegExp(
6648
+ `\\b${name}(?:\\?\\.)?\\.map\\(\\s*\\(?\\s*\\w+(?:\\s*,\\s*\\w+)?\\s*\\)?\\s*=>\\s*(?:\\(?\\s*<|\\{[^{}]{0,600}?\\breturn\\s*\\(?\\s*<)`,
6649
+ "g"
6650
+ );
6651
+ return [...sourceCode.matchAll(rx)].map((m) => m.index ?? 0);
6652
+ }
6653
+ function enclosingLayoutTag(sourceCode, pos) {
6654
+ const stack = [];
6655
+ for (const m of sourceCode.slice(0, pos).matchAll(LAYOUT_TAG_RX)) {
6656
+ const closing = m[1] === "/";
6657
+ const name = m[2] ?? "";
6658
+ const attrs = m[3] ?? "";
6659
+ if (closing) {
6660
+ if (stack[stack.length - 1] === name) stack.pop();
6661
+ continue;
6662
+ }
6663
+ if (attrs.trimEnd().endsWith("/")) continue;
6664
+ stack.push(name);
6665
+ }
6666
+ return stack[stack.length - 1];
6667
+ }
6668
+ function describeBoardRead(input) {
6669
+ const declared = boardPropNames(input.contract);
6670
+ const names = declared.length > 0 ? declared : BOARD_PROMPT_RX.test(input.originalPrompt) ? [...BOARD_NAMES] : BOARD_NAMES.filter((n) => jsxMapPositions(input.sourceCode, n).length > 0);
6671
+ const perName = names.map((name) => {
6672
+ const positions = jsxMapPositions(input.sourceCode, name);
6673
+ return { name, maps: positions.length, enclosing: positions.map((pos) => enclosingLayoutTag(input.sourceCode, pos) ?? "nothing") };
6674
+ });
6675
+ return { declared, names, perName };
6676
+ }
6677
+ function findStackedBoardColumns(input) {
6678
+ const stacked = [];
6679
+ for (const { name, enclosing } of describeBoardRead(input).perName) {
6680
+ for (const tag of enclosing) if (!SIDE_BY_SIDE.has(tag)) stacked.push([name, tag]);
6681
+ }
6682
+ return stacked;
6683
+ }
6684
+ function runBoardColumnsSideBySide(input) {
6685
+ if (input.compiledCode === null) return [];
6686
+ const standDown = input.designMode === "free";
6687
+ const stacked = standDown ? [] : findStackedBoardColumns(input);
6688
+ if (axisCheckTraceEnabled()) {
6689
+ const line = {
6690
+ ...describeBoardRead(input),
6691
+ designMode: input.designMode ?? "constrained",
6692
+ standDown,
6693
+ result: standDown ? "stand-down" : stacked.length > 0 ? "fail" : "pass"
6694
+ };
6695
+ console.log(JSON.stringify({ boardColumnsTrace: line }));
6696
+ }
6697
+ if (stacked.length === 0) return [];
6698
+ const [name, tag] = stacked[0];
6699
+ return [
6700
+ mkIssue(
6701
+ "grid.board_columns_side_by_side",
6702
+ `The board's \`${name}\` are composed under <${tag}> \u2014 a vertical stack of column sections; at md and wider the columns must sit side by side.`,
6703
+ `Wrap the ${name} map in <Grid columns={{ base: 1, md: ${name}.length }} gap="md"> (each column its own <Stack> of cards); never a <Stack> of column sections.`
6704
+ )
6705
+ ];
6706
+ }
6707
+ var GRID_CHECKS = [
6708
+ {
6709
+ id: "grid.board_columns_side_by_side",
6710
+ axis: "render",
6711
+ values: ["grid"],
6712
+ run: runBoardColumnsSideBySide
6713
+ }
6714
+ ];
6715
+
6464
6716
  // src/evaluation/axis-checks/extras.ts
6465
6717
  function mkIssue2(subcategory, description, fix, result = "warn") {
6466
6718
  return { tier: 0, result, category: "mode", subcategory, description, fix };
@@ -6600,29 +6852,164 @@ var REGISTRY = [
6600
6852
  ...WRITES_CHECKS,
6601
6853
  ...STATE_PAYLOAD_CHECKS,
6602
6854
  ...TOOLING_CHECKS,
6855
+ ...GRID_CHECKS,
6603
6856
  ...EXTRA_CHECKS
6604
6857
  ];
6605
-
6606
- // src/evaluation/axis-checks/dispatch.ts
6607
6858
  function runAxisChecks(classification, input) {
6608
- if (input.compiledCode === null) return [];
6609
- const axisInput = {
6859
+ const facts = {
6610
6860
  sourceCode: input.sourceCode,
6611
- compiledCode: input.compiledCode,
6612
6861
  ...input.contract !== void 0 ? { contract: input.contract } : {},
6613
6862
  originalPrompt: input.originalPrompt,
6614
6863
  classification,
6615
6864
  ...input.designMode !== void 0 ? { designMode: input.designMode } : {}
6616
6865
  };
6866
+ if (input.compiledCode === null) {
6867
+ traceAxisChecksSkipped(facts, "compiledCode null \u2014 no check ran");
6868
+ return [];
6869
+ }
6870
+ const axisInput = { ...facts, compiledCode: input.compiledCode };
6871
+ return runGatedAxisChecks(
6872
+ REGISTRY.filter((check) => matches(classification.vector, check)),
6873
+ axisInput
6874
+ ).issues;
6875
+ }
6876
+ function runGatedAxisChecks(checks, input) {
6617
6877
  const issues = [];
6618
- const firedIds = /* @__PURE__ */ new Set();
6619
- for (const check of REGISTRY) {
6620
- if (!matches(classification.vector, check)) continue;
6621
- if (firedIds.has(check.id)) continue;
6622
- firedIds.add(check.id);
6623
- issues.push(...check.run(axisInput));
6878
+ const firedIds = [];
6879
+ const seen = /* @__PURE__ */ new Set();
6880
+ for (const check of checks) {
6881
+ if (seen.has(check.id)) continue;
6882
+ seen.add(check.id);
6883
+ firedIds.push(check.id);
6884
+ issues.push(...check.run(input));
6885
+ }
6886
+ if (axisCheckTraceEnabled()) {
6887
+ const line = {
6888
+ ...traceFacts(input),
6889
+ matched: [...firedIds],
6890
+ issues: issues.map((i) => ({ id: i.subcategory ?? i.category, result: i.result }))
6891
+ };
6892
+ console.log(JSON.stringify({ axisCheckTrace: line }));
6624
6893
  }
6625
- return issues;
6894
+ return { issues, firedIds };
6895
+ }
6896
+ function traceAxisChecksSkipped(facts, note) {
6897
+ if (!axisCheckTraceEnabled()) return;
6898
+ const line = { ...traceFacts(facts), matched: [], issues: [], note };
6899
+ console.log(JSON.stringify({ axisCheckTrace: line }));
6900
+ }
6901
+ function traceFacts(input) {
6902
+ const propsSpec = input.contract?.propsSpec;
6903
+ return {
6904
+ sourceSha256: createHash("sha256").update(input.sourceCode, "utf8").digest("hex"),
6905
+ originalPrompt: input.originalPrompt,
6906
+ designMode: input.designMode ?? "constrained",
6907
+ classification: input.classification.vector,
6908
+ propsSpecKeys: Object.keys(propsSpec ?? {}),
6909
+ propsSpecPropertyKeys: Object.keys(propsSpec?.properties ?? {})
6910
+ };
6911
+ }
6912
+ var STYLING_PROFILE_TRUNCATION_MARKER = " \u2026[truncated at the profile bound]";
6913
+ var STYLING_PROFILE_JUDGE_FRAME = `The app's operator declared the styling profile below. Judge visual treatment RELATIVE to it \u2014 a dense profile is not "cramped", a flat one is not "unpolished", a sparse one is not "empty", and a directed composition is not missing what its Direction leaves out; completeness, hierarchy, accessibility and correctness are unchanged by it.`;
6914
+ var AESTHETIC_BRIEF_MAX_CHARS = 2e3;
6915
+ function sanitizeProfileMember(text, member) {
6916
+ return sanitizeProfileText(text, APP_GENERATION_PROFILE_BOUNDS[member]);
6917
+ }
6918
+ function sanitizeProfileText(text, bound) {
6919
+ if (typeof text !== "string") return "";
6920
+ const lines = [];
6921
+ let inFence = false;
6922
+ for (const raw of text.split(/\r?\n/)) {
6923
+ const line = raw.replace(/[ \t]+/g, " ").trim();
6924
+ if (line.startsWith("```")) {
6925
+ inFence = !inFence;
6926
+ continue;
6927
+ }
6928
+ if (inFence || line.length === 0 || line.startsWith("#")) continue;
6929
+ lines.push(line);
6930
+ }
6931
+ let out = lines.join("\n").replace(/`/g, "");
6932
+ if (out.length > bound) out = out.slice(0, bound) + STYLING_PROFILE_TRUNCATION_MARKER;
6933
+ return out;
6934
+ }
6935
+ function aestheticRefLabel(profile) {
6936
+ const ref = profile?.aesthetic;
6937
+ if (ref === void 0 || sanitizeProfileText(profile?.aestheticBrief, AESTHETIC_BRIEF_MAX_CHARS).length === 0) return "";
6938
+ return ref.version !== void 0 ? `${ref.id}@${ref.version}` : ref.id;
6939
+ }
6940
+ function sanitizeProfile(profile) {
6941
+ const aestheticRef = aestheticRefLabel(profile);
6942
+ return {
6943
+ styling: sanitizeProfileMember(profile?.styling, "styling"),
6944
+ density: sanitizeProfileMember(profile?.density, "density"),
6945
+ layout: sanitizeProfileMember(profile?.layout, "layout"),
6946
+ direction: sanitizeProfileMember(profile?.direction, "direction"),
6947
+ aesthetic: aestheticRef.length > 0 ? sanitizeProfileText(profile?.aestheticBrief, AESTHETIC_BRIEF_MAX_CHARS) : "",
6948
+ aestheticRef
6949
+ };
6950
+ }
6951
+ function renderProfileQuote(profile) {
6952
+ const p = sanitizeProfile(profile);
6953
+ const parts = [];
6954
+ if (p.styling.length > 0) parts.push(p.styling.split("\n").map((line) => `> ${line}`).join("\n"));
6955
+ if (p.density.length > 0) parts.push(`> **Density**: ${p.density}`);
6956
+ if (p.layout.length > 0) parts.push(`> **Layout**: ${p.layout}`);
6957
+ if (p.direction.length > 0) parts.push(`> **Direction**: ${p.direction}`);
6958
+ if (p.aesthetic.length > 0) {
6959
+ const [first, ...rest] = p.aesthetic.split("\n");
6960
+ parts.push([`> **Aesthetic** (\`${p.aestheticRef}\`): ${first ?? ""}`, ...rest.map((line) => `> ${line}`)].join("\n"));
6961
+ }
6962
+ return parts.join("\n>\n");
6963
+ }
6964
+ function buildStylingProfileJudgeBlock(profile) {
6965
+ const quote = renderProfileQuote(profile);
6966
+ if (quote.length === 0) return "";
6967
+ return ["## Declared styling profile", "", STYLING_PROFILE_JUDGE_FRAME, "", quote].join("\n");
6968
+ }
6969
+ function isDesignRenderingInput(relativePath) {
6970
+ const rel = relativePath.split("\\").join("/");
6971
+ if (rel.startsWith("__tests__/") || rel.includes("/__tests__/")) return false;
6972
+ if (/\.test\.[cm]?[jt]sx?$/.test(rel)) return false;
6973
+ if (/\.stories\.[cm]?[jt]sx?$/.test(rel)) return false;
6974
+ if (rel.endsWith(".md")) return false;
6975
+ return true;
6976
+ }
6977
+ function designTreeSha256(root) {
6978
+ const files = [];
6979
+ const walk = (dir) => {
6980
+ for (const name of readdirSync(dir).sort()) {
6981
+ const full = resolve(dir, name);
6982
+ if (statSync(full).isDirectory()) walk(full);
6983
+ else if (isDesignRenderingInput(full.slice(root.length + 1))) files.push(full);
6984
+ }
6985
+ };
6986
+ walk(root);
6987
+ const h = createHash("sha256");
6988
+ for (const f of files) {
6989
+ h.update(f.slice(root.length + 1) + "\n");
6990
+ h.update(readFileSync(f));
6991
+ h.update("\n");
6992
+ }
6993
+ return h.digest("hex");
6994
+ }
6995
+ var identityCache = /* @__PURE__ */ new Map();
6996
+ function judgeDesignIdentity(src) {
6997
+ const key = resolve(src);
6998
+ const cached = identityCache.get(key);
6999
+ if (cached !== void 0) return cached;
7000
+ let identity;
7001
+ try {
7002
+ identity = { src: key, srcSha256: designTreeSha256(key) };
7003
+ } catch (e) {
7004
+ const reason = e instanceof Error ? e.message : String(e);
7005
+ console.warn(`[visual-eval] design tree unreadable at ${key} \u2014 the judgement carries no design identity (${reason})`);
7006
+ identity = null;
7007
+ }
7008
+ identityCache.set(key, identity);
7009
+ return identity;
7010
+ }
7011
+ function resetJudgeDesignIdentityCache() {
7012
+ identityCache.clear();
6626
7013
  }
6627
7014
 
6628
7015
  // src/design-mode.ts
@@ -6640,6 +7027,9 @@ var CANVAS_VIEWPORTS = {
6640
7027
  lg: { width: 1024, height: 768 },
6641
7028
  xl: { width: 1440, height: 900 }
6642
7029
  };
7030
+ function displayModeForCanvas(canvas) {
7031
+ return canvas === "xs-chat-card" ? "inline" : "fullscreen";
7032
+ }
6643
7033
  function canvasDisplayModeForShell(shell) {
6644
7034
  return shell === "chat" || shell === "inline" ? "inline" : "fullscreen";
6645
7035
  }
@@ -6698,24 +7088,76 @@ var CANVAS_DESCRIPTORS = {
6698
7088
  };
6699
7089
  function describeCanvas(canvas) {
6700
7090
  const d = CANVAS_DESCRIPTORS[canvas];
6701
- return `This component renders on the \`${canvas}\` canvas (${d.label}): ${d.minWidthPx}\u2013${d.maxWidthPx}px wide, ${d.host}. Design for the whole range \u2014 the host may size the iframe anywhere inside it.`;
7091
+ return `This component renders on the \`${canvas}\` canvas (${d.label}): ${d.minWidthPx}\u2013${d.maxWidthPx}px wide, ${d.host}. Design for the whole range \u2014 the host may size the iframe anywhere inside it, and the host owns the height: size to your content, never to the viewport.`;
6702
7092
  }
6703
7093
 
6704
7094
  // src/evaluation/visual-evaluator.ts
7095
+ function canvasFitPolicy(canvas) {
7096
+ switch (canvas) {
7097
+ case "xs-chat-card":
7098
+ return { capture: "viewport", overflow: "fail" };
7099
+ case "mobile-fullscreen-small":
7100
+ return { capture: "full-page", overflow: "warn" };
7101
+ default:
7102
+ return { capture: "full-page", overflow: "none" };
7103
+ }
7104
+ }
7105
+ var JUDGE_SCOPE_CLASS = "ggui-judge-scope";
7106
+ function canvasFit(canvas) {
7107
+ return displayModeForCanvas(canvas) === "fullscreen" ? "fill" : void 0;
7108
+ }
7109
+ function canvasOverflowIssue(canvas, viewport, contentHeight, verdict) {
7110
+ const hidden = contentHeight - viewport.height;
7111
+ return {
7112
+ dimension: "canvas-overflow",
7113
+ severity: verdict === "fail" ? "critical" : "major",
7114
+ description: `Rendered content is ${contentHeight}px tall on the ${canvas} canvas (${viewport.width}\xD7${viewport.height}) \u2014 ${hidden}px ${verdict === "fail" ? "is cut off: the inline card does not scroll" : "sits below the first screen"}.`,
7115
+ fix: "Fit the composition to the canvas: fewer and shorter sections, one compact row of chips, no hero taller than a third of the box, no fixed min-heights or tall paddings \u2014 measure against the canvas, not the page."
7116
+ };
7117
+ }
6705
7118
  function resolveUiGenPackageDir() {
6706
7119
  const selfDir = dirname(fileURLToPath(import.meta.url));
6707
7120
  return resolve(selfDir, "..", "..");
6708
7121
  }
7122
+ function resolveWirePackageDir() {
7123
+ const uiGenDir = resolveUiGenPackageDir();
7124
+ const candidates = [
7125
+ resolve(uiGenDir, "node_modules", "@ggui-ai", "wire"),
7126
+ // workspace / nested install
7127
+ resolve(uiGenDir, "..", "wire"),
7128
+ // pnpm deploy: `.pnpm/<ui-gen>@<ver>/node_modules/@ggui-ai/{ui-gen,wire}`
7129
+ resolve(uiGenDir, "..", "..", "@ggui-ai", "wire")
7130
+ // hoisted flat install beside the scope dir
7131
+ ];
7132
+ for (const dir of candidates) {
7133
+ if (existsSync(resolve(dir, "package.json"))) return dir;
7134
+ }
7135
+ try {
7136
+ const entry = fileURLToPath(import.meta.resolve("@ggui-ai/wire"));
7137
+ let dir = dirname(entry);
7138
+ for (let i = 0; i < 6; i++) {
7139
+ if (existsSync(resolve(dir, "package.json"))) return dir;
7140
+ dir = dirname(dir);
7141
+ }
7142
+ } catch {
7143
+ }
7144
+ return null;
7145
+ }
6709
7146
  function resolveDesignPackageDir() {
6710
- const selfDir = dirname(fileURLToPath(import.meta.url));
6711
- return resolve(selfDir, "..", "..", "..", "design");
7147
+ try {
7148
+ return dirname(createRequire(import.meta.url).resolve("@ggui-ai/design/package.json"));
7149
+ } catch {
7150
+ const selfDir = dirname(fileURLToPath(import.meta.url));
7151
+ return resolve(selfDir, "..", "..", "..", "design");
7152
+ }
6712
7153
  }
6713
- async function bundleForRendering(compiledCode, sampleProps) {
7154
+ async function bundleForRendering(compiledCode, sampleProps, designSrc = resolve(resolveDesignPackageDir(), "src")) {
6714
7155
  const tmpDir = resolve(tmpdir(), "ggui-visual-eval-" + Date.now());
6715
7156
  mkdirSync(tmpDir, { recursive: true });
6716
7157
  const componentFile = resolve(tmpDir, "component.tsx");
6717
7158
  const entryFile = resolve(tmpDir, "entry.tsx");
6718
- const designPkgDir = resolveDesignPackageDir();
7159
+ const designPkgDir = resolve(designSrc, "..");
7160
+ const wirePkgDir = resolveWirePackageDir();
6719
7161
  writeFileSync(componentFile, compiledCode);
6720
7162
  const entryCode = `
6721
7163
  import React from 'react';
@@ -6763,6 +7205,10 @@ try {
6763
7205
  jsxImportSource: "react",
6764
7206
  external: ["react", "react-dom", "react-dom/client", "react/jsx-runtime"],
6765
7207
  alias: {
7208
+ // The wire hooks: resolved from ui-gen's own module graph so the
7209
+ // alias holds in a workspace checkout AND in a `pnpm deploy` image
7210
+ // (where deps are siblings, not children, of this package).
7211
+ ...wirePkgDir !== null ? { "@ggui-ai/wire": wirePkgDir } : {},
6766
7212
  // D1: generated code imports the single `@ggui-ai/design` barrel.
6767
7213
  // The subpath aliases stay for any first-party / legacy code.
6768
7214
  "@ggui-ai/design": resolve(designPkgDir, "src", "index.ts"),
@@ -6774,7 +7220,10 @@ try {
6774
7220
  // Bare imports in the generated component resolve against ui-gen's
6775
7221
  // own node_modules — the temp entry dir has none. Without this every
6776
7222
  // wire-bearing component (`@ggui-ai/wire`) failed to bundle.
6777
- nodePaths: [resolve(resolveUiGenPackageDir(), "node_modules")],
7223
+ // Workspace layout (deps under this package's node_modules) and the
7224
+ // `pnpm deploy` layout (deps as siblings of this package) — both
7225
+ // searched for any other allowlisted bare import.
7226
+ nodePaths: [resolve(resolveUiGenPackageDir(), "node_modules"), resolve(resolveUiGenPackageDir(), "..")],
6778
7227
  logLevel: "silent"
6779
7228
  });
6780
7229
  return result.outputFiles[0]?.text ?? "";
@@ -6789,7 +7238,7 @@ try {
6789
7238
  }
6790
7239
  }
6791
7240
  }
6792
- function buildRenderHTML(bundledCode, cssTokens) {
7241
+ function buildRenderHTML(bundledCode, cssTokens, fit) {
6793
7242
  const tokens = cssTokens ?? getCssTokens();
6794
7243
  return `<!DOCTYPE html>
6795
7244
  <html>
@@ -6805,10 +7254,11 @@ function buildRenderHTML(bundledCode, cssTokens) {
6805
7254
  color: var(--ggui-color-neutral-900, #111827);
6806
7255
  }
6807
7256
  .error { color: #dc2626; padding: 16px; font-family: monospace; white-space: pre-wrap; }
7257
+ ${fit === "fill" ? fillFitRule(JUDGE_SCOPE_CLASS) : ""}
6808
7258
  </style>
6809
7259
  </head>
6810
7260
  <body>
6811
- <div id="root"></div>
7261
+ <div id="root"${fit === "fill" ? ` class="${JUDGE_SCOPE_CLASS}"` : ""}></div>
6812
7262
  <script type="importmap">
6813
7263
  {
6814
7264
  "imports": {
@@ -6854,7 +7304,17 @@ async function launchWithPuppeteer(options) {
6854
7304
  const puppeteer = await import('puppeteer-core');
6855
7305
  return puppeteer.default.launch(options);
6856
7306
  }
6857
- async function captureScreenshot(html, viewport = DEFAULT_SCREENSHOT_VIEWPORT, deps = {}) {
7307
+ var CONTENT_HEIGHT_EXPRESSION = "Math.max(document.documentElement.scrollHeight, document.body ? document.body.scrollHeight : 0)";
7308
+ async function measureContentHeight(page) {
7309
+ try {
7310
+ const h = await page.evaluate(CONTENT_HEIGHT_EXPRESSION);
7311
+ return typeof h === "number" && Number.isFinite(h) ? Math.round(h) : null;
7312
+ } catch (e) {
7313
+ console.warn(`[visual-eval] content height unavailable: ${e instanceof Error ? e.message : String(e)}`);
7314
+ return null;
7315
+ }
7316
+ }
7317
+ async function captureScreenshotDetailed(html, viewport = DEFAULT_SCREENSHOT_VIEWPORT, deps = {}, capture = "full-page") {
6858
7318
  try {
6859
7319
  const launchOptions = await resolveLaunchOptions(viewport, deps);
6860
7320
  const browser = await (deps.launch ?? launchWithPuppeteer)(launchOptions);
@@ -6866,16 +7326,21 @@ async function captureScreenshot(html, viewport = DEFAULT_SCREENSHOT_VIEWPORT, d
6866
7326
  await page.waitForSelector("#root > *", { timeout: 1e4 }).catch(() => {
6867
7327
  });
6868
7328
  await new Promise((r) => setTimeout(r, deps.settleMs ?? 1e3));
6869
- const screenshot = await page.screenshot({ type: "png", fullPage: true });
6870
- return Buffer.from(screenshot);
7329
+ const contentHeight = await measureContentHeight(page);
7330
+ const screenshot = await page.screenshot({ type: "png", fullPage: capture === "full-page" });
7331
+ return { png: Buffer.from(screenshot), contentHeight };
6871
7332
  } finally {
6872
7333
  await browser.close();
6873
7334
  }
6874
7335
  } catch (e) {
6875
- console.warn(`[visual-eval] screenshot failed: ${e instanceof Error ? e.message : e}`);
6876
- return null;
7336
+ const reason = `screenshot failed: ${e instanceof Error ? e.message : String(e)}`;
7337
+ console.warn(`[visual-eval] ${reason}`);
7338
+ return { png: null, reason, contentHeight: null };
6877
7339
  }
6878
7340
  }
7341
+ async function captureScreenshot(html, viewport = DEFAULT_SCREENSHOT_VIEWPORT, deps = {}) {
7342
+ return (await captureScreenshotDetailed(html, viewport, deps)).png;
7343
+ }
6879
7344
  var VISUAL_EVAL_PROMPT = `You are a visual quality evaluator for ggui \u2014 a platform where AI agents generate React UI components on demand.
6880
7345
 
6881
7346
  ## Context
@@ -6903,6 +7368,8 @@ The screenshot shows a generated React component built from a design system with
6903
7368
  - Error messages like "Render error" or blank white space = rendering score 0
6904
7369
  - A component that renders but is just a wall of text with no structure should score low on layout and hierarchy
6905
7370
 
7371
+ List AT MOST 6 issues, most severe first, one sentence each for "description" and "fix" \u2014 a longer list is cut off and lost.
7372
+
6906
7373
  Respond with ONLY a JSON object (no markdown, no explanation):
6907
7374
  {
6908
7375
  "completeness": <0-100>,
@@ -6914,11 +7381,16 @@ Respond with ONLY a JSON object (no markdown, no explanation):
6914
7381
  ],
6915
7382
  "critique": "<2-3 sentences: what's good, what needs improvement, does it achieve the user's goal?>"
6916
7383
  }`;
6917
- async function callMultimodalLLM(config, model, prompt, screenshot, originalPrompt) {
7384
+ var VISUAL_JUDGE_MAX_OUTPUT_TOKENS = 4096;
7385
+ var VISUAL_JUDGE_PROMPT_VERSION = "v2-bounded-issues";
7386
+ var VISUAL_JUDGE_PROMPT_DIGEST = createHash("sha256").update(VISUAL_EVAL_PROMPT, "utf8").digest("hex");
7387
+ async function callMultimodalLLM(config, model, prompt, screenshot, originalPrompt, profileBlock = "") {
6918
7388
  const userPrompt = `## Original Request
6919
7389
  ${originalPrompt}
6920
7390
 
6921
- Evaluate the screenshot of the generated component.`;
7391
+ ` + (profileBlock.length > 0 ? `${profileBlock}
7392
+
7393
+ ` : "") + "Evaluate the screenshot of the generated component.";
6922
7394
  const agent = createVisionAgent({
6923
7395
  provider: config.provider === "claude" ? "anthropic" : config.provider,
6924
7396
  routeOverride: config.routeOverride,
@@ -6929,16 +7401,69 @@ Evaluate the screenshot of the generated component.`;
6929
7401
  prompt,
6930
7402
  userPrompt,
6931
7403
  { mediaType: "image/png", base64: screenshot.toString("base64") },
6932
- 1500
7404
+ VISUAL_JUDGE_MAX_OUTPUT_TOKENS
6933
7405
  );
6934
7406
  }
7407
+ function judgeCountFor(config, canvas) {
7408
+ const k = Math.max(1, Math.floor(config.judgeK ?? 1));
7409
+ if (k === 1) return 1;
7410
+ return config.judgeKCanvases === void 0 || config.judgeKCanvases.includes(canvas) ? k : 1;
7411
+ }
7412
+ function medianOf(values) {
7413
+ const sorted = [...values].sort((a, b) => a - b);
7414
+ return sorted[Math.floor((sorted.length - 1) / 2)];
7415
+ }
7416
+ function populationSigma(values) {
7417
+ if (values.length < 2) return 0;
7418
+ const mean = values.reduce((a, b) => a + b, 0) / values.length;
7419
+ const variance = values.reduce((a, b) => a + (b - mean) ** 2, 0) / values.length;
7420
+ return Math.round(Math.sqrt(variance) * 10) / 10;
7421
+ }
7422
+ async function judgeAndParse(judge, config, model, screenshot, originalPrompt, profileBlock, canvas) {
7423
+ let firstReason;
7424
+ for (let attempt = 1; attempt <= 2; attempt++) {
7425
+ const response = await judge(config, model, VISUAL_EVAL_PROMPT, screenshot, originalPrompt, profileBlock);
7426
+ try {
7427
+ const result = parseVisualResponse(response.text, config.passThreshold);
7428
+ return { kind: "ok", result, inputTokens: response.inputTokens, outputTokens: response.outputTokens };
7429
+ } catch (e) {
7430
+ const salvaged = salvageTruncatedVisualAnswer(response.text, config.passThreshold);
7431
+ if (salvaged !== null) {
7432
+ console.warn(`[visual-eval] visual_judge_truncated${canvas ? ` canvas=${canvas}` : ""}: answer cut after ${response.text.length} chars \u2014 score recovered from the closed prefix, issues list partial`);
7433
+ return { kind: "ok", result: salvaged, inputTokens: response.inputTokens, outputTokens: response.outputTokens };
7434
+ }
7435
+ const reason = `judge answer unparsable: ${e instanceof Error ? e.message : String(e)}`;
7436
+ if (attempt === 1) {
7437
+ firstReason = reason;
7438
+ console.warn(`[visual-eval] visual_judge_retry${canvas ? ` canvas=${canvas}` : ""}: ${reason}`);
7439
+ } else {
7440
+ console.warn(`[visual-eval] visual_judge_retry failed again${canvas ? ` canvas=${canvas}` : ""}: ${reason}`);
7441
+ }
7442
+ }
7443
+ }
7444
+ return { kind: "unparsable", reason: firstReason ?? "judge answer unparsable" };
7445
+ }
6935
7446
  async function runVisualEvaluation(context, config, deps = {}) {
7447
+ return (await runVisualEvaluationDetailed(context, config, deps)).result;
7448
+ }
7449
+ async function runVisualEvaluationDetailed(context, config, deps = {}) {
6936
7450
  const startTime = Date.now();
6937
7451
  const judge = deps.judge ?? callMultimodalLLM;
6938
- const bundledCode = await bundleForRendering(
6939
- context.compiledCode,
6940
- config.sampleProps ?? {}
6941
- );
7452
+ const designSrc = config.designSrcDir ?? resolve(resolveDesignPackageDir(), "src");
7453
+ const design = judgeDesignIdentity(designSrc);
7454
+ const stamp = (r) => ({
7455
+ ...r,
7456
+ ...design !== null ? { design } : {},
7457
+ ...config.themeMode !== void 0 ? { themeMode: config.themeMode } : {}
7458
+ });
7459
+ let bundledCode;
7460
+ try {
7461
+ bundledCode = await bundleForRendering(context.compiledCode, config.sampleProps ?? {}, designSrc);
7462
+ } catch (e) {
7463
+ const unavailableReason = `bundle failed: ${e instanceof Error ? e.message : String(e)}`;
7464
+ console.warn(`[visual-eval] ${unavailableReason}`);
7465
+ return { result: null, unavailableReason };
7466
+ }
6942
7467
  console.log(`[visual-eval] bundled: ${bundledCode.length}B (${Date.now() - startTime}ms)`);
6943
7468
  const html = buildRenderHTML(bundledCode, context.cssTokens);
6944
7469
  const model = config.model ?? getDefaultVisualModel(config.provider);
@@ -6947,25 +7472,62 @@ async function runVisualEvaluation(context, config, deps = {}) {
6947
7472
  const perCanvasResults = [];
6948
7473
  for (const canvas of config.canvases) {
6949
7474
  const viewport = CANVAS_VIEWPORTS[canvas];
6950
- const screenshot2 = await captureScreenshot(html, viewport, deps);
7475
+ const policy = canvasFitPolicy(canvas);
7476
+ const fit = canvasFit(canvas);
7477
+ const canvasHtml = fit !== void 0 ? buildRenderHTML(bundledCode, context.cssTokens, fit) : html;
7478
+ const attempt2 = await captureScreenshotDetailed(canvasHtml, viewport, deps, policy.capture);
7479
+ const screenshot2 = attempt2.png;
6951
7480
  if (!screenshot2) {
6952
- console.warn(`[visual-eval] no browser available at canvas ${canvas} \u2014 skipping visual evaluation`);
6953
- return null;
7481
+ const unavailableReason = attempt2.reason ?? "no browser available";
7482
+ console.warn(`[visual-eval] ${unavailableReason} at canvas ${canvas} \u2014 skipping visual evaluation`);
7483
+ return { result: null, unavailableReason, canvas };
7484
+ }
7485
+ const k = judgeCountFor(config, canvas);
7486
+ const profileBlock = buildStylingProfileJudgeBlock(context.profile);
7487
+ const answers = await Promise.all(
7488
+ Array.from({ length: k }, () => judgeAndParse(judge, config, model, screenshot2, context.originalPrompt, profileBlock, canvas))
7489
+ );
7490
+ const parsed = answers.filter((a) => a.kind === "ok");
7491
+ if (parsed.length === 0) {
7492
+ const first = answers.find((a) => a.kind === "unparsable");
7493
+ return { result: null, unavailableReason: first?.reason ?? "judge answer unparsable", canvas };
7494
+ }
7495
+ const samples = parsed.map((a) => a.result.finalScore);
7496
+ const median = medianOf(samples);
7497
+ const representative = parsed[samples.indexOf(median)];
7498
+ const result2 = representative.result;
7499
+ result2.finalScore = median;
7500
+ result2.passed = median >= config.passThreshold;
7501
+ result2.inputTokens = parsed.reduce((sum, a) => sum + a.inputTokens, 0);
7502
+ result2.outputTokens = parsed.reduce((sum, a) => sum + a.outputTokens, 0);
7503
+ const response2 = { inputTokens: result2.inputTokens, outputTokens: result2.outputTokens };
7504
+ const judgeRecord = {
7505
+ k,
7506
+ rule: "median",
7507
+ samples,
7508
+ sigma: populationSigma(samples),
7509
+ notes: parsed.map((a) => a.result.critique ?? "")
7510
+ };
7511
+ const contentHeight = attempt2.contentHeight;
7512
+ const overflow = contentHeight !== null && contentHeight > viewport.height;
7513
+ if (contentHeight !== null && contentHeight > viewport.height && policy.overflow !== "none") {
7514
+ result2.issues.push(canvasOverflowIssue(canvas, viewport, contentHeight, policy.overflow));
7515
+ if (policy.overflow === "fail") result2.passed = false;
6954
7516
  }
6955
- const response2 = await judge(config, model, VISUAL_EVAL_PROMPT, screenshot2, context.originalPrompt);
6956
- const result2 = parseVisualResponse(response2.text, config.passThreshold);
6957
- result2.inputTokens = response2.inputTokens;
6958
- result2.outputTokens = response2.outputTokens;
6959
7517
  perCanvasResults.push(result2);
6960
7518
  perCanvas.push({
6961
7519
  canvas,
6962
7520
  viewport,
6963
7521
  score: result2.finalScore,
6964
7522
  passed: result2.passed,
6965
- screenshotPng: screenshot2
7523
+ screenshotPng: screenshot2,
7524
+ contentHeight,
7525
+ overflow,
7526
+ judge: judgeRecord,
7527
+ ...fit !== void 0 ? { fit } : {}
6966
7528
  });
6967
7529
  console.log(
6968
- `[visual-eval] canvas=${canvas} ${viewport.width}\xD7${viewport.height} score=${result2.finalScore} ${result2.passed ? "pass" : "FAIL"} | in=${response2.inputTokens} out=${response2.outputTokens}`
7530
+ `[visual-eval] canvas=${canvas} ${viewport.width}\xD7${viewport.height} score=${result2.finalScore}${k > 1 ? ` (median of ${judgeRecord.samples.length}/${k}: ${judgeRecord.samples.join(",")} \u03C3=${judgeRecord.sigma})` : ""} ${result2.passed ? "pass" : "FAIL"} | content=${contentHeight ?? "?"}px${overflow ? ` OVERFLOW (${policy.overflow})` : ""} | in=${response2.inputTokens} out=${response2.outputTokens}`
6969
7531
  );
6970
7532
  }
6971
7533
  const aggregate = aggregateCanvasResults(perCanvas, perCanvasResults);
@@ -6973,27 +7535,34 @@ async function runVisualEvaluation(context, config, deps = {}) {
6973
7535
  console.log(
6974
7536
  `[visual-eval] score=${aggregate.finalScore} (mean of ${perCanvas.length} canvases; ${aggregate.passed ? "every canvas passed" : `${perCanvas.filter((c) => !c.passed).length} failed`}) (${elapsed2}ms)`
6975
7537
  );
6976
- return aggregate;
7538
+ return { result: stamp(aggregate) };
6977
7539
  }
6978
- const screenshot = await captureScreenshot(html, config.viewport, deps);
7540
+ const attempt = await captureScreenshotDetailed(html, config.viewport, deps);
7541
+ const screenshot = attempt.png;
6979
7542
  if (!screenshot) {
6980
- console.warn("[visual-eval] no browser available \u2014 skipping visual evaluation");
6981
- return null;
7543
+ const unavailableReason = attempt.reason ?? "no browser available";
7544
+ console.warn(`[visual-eval] ${unavailableReason} \u2014 skipping visual evaluation`);
7545
+ return { result: null, unavailableReason };
6982
7546
  }
6983
7547
  console.log(`[visual-eval] screenshot: ${screenshot.length}B (${Date.now() - startTime}ms)`);
6984
- const response = await judge(
7548
+ const answer = await judgeAndParse(
7549
+ judge,
6985
7550
  config,
6986
7551
  model,
6987
- VISUAL_EVAL_PROMPT,
6988
7552
  screenshot,
6989
- context.originalPrompt
7553
+ context.originalPrompt,
7554
+ buildStylingProfileJudgeBlock(context.profile)
6990
7555
  );
6991
- const result = parseVisualResponse(response.text, config.passThreshold);
6992
- result.inputTokens = response.inputTokens;
6993
- result.outputTokens = response.outputTokens;
7556
+ if (answer.kind === "unparsable") {
7557
+ return { result: null, unavailableReason: answer.reason };
7558
+ }
7559
+ const result = answer.result;
7560
+ result.inputTokens = answer.inputTokens;
7561
+ result.outputTokens = answer.outputTokens;
7562
+ const response = { inputTokens: answer.inputTokens, outputTokens: answer.outputTokens };
6994
7563
  const elapsed = Date.now() - startTime;
6995
7564
  console.log(`[visual-eval] score=${result.finalScore} (${elapsed}ms) | in=${response.inputTokens} out=${response.outputTokens}`);
6996
- return result;
7565
+ return { result: stamp(result) };
6997
7566
  }
6998
7567
  function aggregateCanvasResults(perCanvas, results) {
6999
7568
  const n = results.length;
@@ -7033,9 +7602,57 @@ function summarizeVisualResult(result) {
7033
7602
  canvas: c.canvas,
7034
7603
  viewport: c.viewport,
7035
7604
  score: c.score,
7036
- passed: c.passed
7605
+ passed: c.passed,
7606
+ contentHeight: c.contentHeight,
7607
+ overflow: c.overflow,
7608
+ judge: c.judge,
7609
+ ...c.fit !== void 0 ? { fit: c.fit } : {}
7037
7610
  }));
7038
- return { score: result.finalScore, passed: result.passed, canvases };
7611
+ return {
7612
+ score: result.finalScore,
7613
+ passed: result.passed,
7614
+ canvases,
7615
+ ...result.design !== void 0 ? { design: result.design } : {},
7616
+ ...result.themeMode !== void 0 ? { themeMode: result.themeMode } : {}
7617
+ };
7618
+ }
7619
+ function salvageTruncatedVisualAnswer(text, passThreshold) {
7620
+ const dim = (name) => {
7621
+ const m = text.match(new RegExp(`"${name}"\\s*:\\s*(\\d+(?:\\.\\d+)?)`));
7622
+ return m ? Number(m[1]) : null;
7623
+ };
7624
+ const completeness = dim("completeness");
7625
+ const layout = dim("layout");
7626
+ const hierarchy = dim("hierarchy");
7627
+ const aesthetics = dim("aesthetics");
7628
+ if (completeness === null || layout === null || hierarchy === null || aesthetics === null) return null;
7629
+ const issues = [];
7630
+ const issuesAt = text.indexOf('"issues"');
7631
+ if (issuesAt >= 0) {
7632
+ for (const m of text.slice(issuesAt).matchAll(/\{[^{}]*\}/g)) {
7633
+ try {
7634
+ const o = JSON.parse(m[0]);
7635
+ if (typeof o.description === "string") {
7636
+ issues.push({
7637
+ dimension: typeof o.dimension === "string" ? o.dimension : "layout",
7638
+ severity: typeof o.severity === "string" ? o.severity : "minor",
7639
+ description: o.description,
7640
+ fix: typeof o.fix === "string" ? o.fix : ""
7641
+ });
7642
+ }
7643
+ } catch {
7644
+ }
7645
+ }
7646
+ }
7647
+ const rebuilt = JSON.stringify({
7648
+ completeness,
7649
+ layout,
7650
+ hierarchy,
7651
+ aesthetics,
7652
+ issues,
7653
+ critique: `[truncated judge answer \u2014 score recovered from the closed prefix; ${issues.length} issue(s) kept, the rest and the critique were cut]`
7654
+ });
7655
+ return parseVisualResponse(rebuilt, passThreshold);
7039
7656
  }
7040
7657
  function parseVisualResponse(text, passThreshold) {
7041
7658
  const jsonMatch = text.match(/\{[\s\S]*\}/);
@@ -7097,7 +7714,11 @@ async function runVisualEval(context, config) {
7097
7714
  const summary = summarizeVisualResult(result);
7098
7715
  return summary === void 0 ? { issues } : { issues, summary };
7099
7716
  }
7717
+ function cssTokensForAppTheme(theme, mode = "light") {
7718
+ if (theme === void 0) return getCssTokens(mode);
7719
+ return composeThemeCss({ layer: "page", mode: theme.mode ?? mode, appTheme: theme });
7720
+ }
7100
7721
 
7101
- export { CANVAS_BREAKPOINTS, CANVAS_CLASSES, CANVAS_DESCRIPTORS, CANVAS_VIEWPORTS, CRITERIA, DEFAULT_EVAL_MAX_ROUNDS, DEFAULT_QUALITY_CONFIG, LLM_EVAL_STATIC_CRITERIA, MAX_EVAL_ROUNDS_HARD_LIMIT, buildCodingCriteriaSummary, buildFixPrompt, callMultimodalLLM, canvasDisplayModeForShell, canvasForRendering, canvasForViewportWidth, captureScreenshot, computeEvaluationScore, createEvaluationToolsServer, criteriaFor, describeCanvas, extractCompiledCode, extractCompiledCodeFromMessage, extractEvalResult, extractSourceCode, extractSourceCodeFromMessage, extractToolResultTexts, getActionableIssues, getCriteriaByPriority, getCriterionById, getEvaluatorSystemPrompt, getLLMCriteria, isBlocked, matches, notApplicableCoverage, priorityForIssue, resolveLaunchOptions, runAxisChecks, runEvaluation, runEvaluationLoop, runVisualEval, runVisualEvaluation, summarizeVisualResult };
7722
+ export { CANVAS_BREAKPOINTS, CANVAS_CLASSES, CANVAS_DESCRIPTORS, CANVAS_VIEWPORTS, CONTENT_HEIGHT_EXPRESSION, CRITERIA, DEFAULT_EVAL_MAX_ROUNDS, DEFAULT_QUALITY_CONFIG, LLM_EVAL_STATIC_CRITERIA, MAX_EVAL_ROUNDS_HARD_LIMIT, VISUAL_EVAL_PROMPT, VISUAL_JUDGE_MAX_OUTPUT_TOKENS, VISUAL_JUDGE_PROMPT_DIGEST, VISUAL_JUDGE_PROMPT_VERSION, buildCodingCriteriaSummary, buildFixPrompt, callMultimodalLLM, canvasDisplayModeForShell, canvasFitPolicy, canvasForRendering, canvasForViewportWidth, canvasOverflowIssue, captureScreenshot, captureScreenshotDetailed, computeEvaluationScore, createEvaluationToolsServer, criteriaFor, cssTokensForAppTheme, describeCanvas, designTreeSha256, extractCompiledCode, extractCompiledCodeFromMessage, extractEvalResult, extractSourceCode, extractSourceCodeFromMessage, extractToolResultTexts, getActionableIssues, getCriteriaByPriority, getCriterionById, getEvaluatorSystemPrompt, getLLMCriteria, isBlocked, judgeDesignIdentity, matches, notApplicableCoverage, priorityForIssue, resetJudgeDesignIdentityCache, resolveDesignPackageDir, resolveLaunchOptions, runAxisChecks, runEvaluation, runEvaluationLoop, runVisualEval, runVisualEvaluation, runVisualEvaluationDetailed, summarizeVisualResult };
7102
7723
  //# sourceMappingURL=index.js.map
7103
7724
  //# sourceMappingURL=index.js.map