@ggui-ai/ui-gen 0.16.0 → 0.18.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 (105) hide show
  1. package/dist/adapters/generation-dispatch.d.ts +6 -3
  2. package/dist/adapters/generation-dispatch.js +5161 -4312
  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 +4685 -3801
  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 +120 -13
  13. package/dist/boilerplate.js.map +1 -1
  14. package/dist/check/index.d.ts +2 -2
  15. package/dist/check/index.js +72 -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 +347 -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 +302 -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 +752 -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 +410 -255
  39. package/dist/harness/index.js.map +1 -1
  40. package/dist/harness/prompts.d.ts +2 -2
  41. package/dist/harness/prompts.js +7 -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 +131 -20
  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 +410 -255
  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 +4691 -3805
  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 +9 -0
  68. package/src/boilerplate/generate.ts +8 -1
  69. package/src/boilerplate/hard-sections.ts +45 -1
  70. package/src/boilerplate/styling-profile.ts +166 -0
  71. package/src/boilerplate/system-prompt.ts +22 -8
  72. package/src/boilerplate/templates/layouts/fullscreen-desktop.tsx.tmpl +5 -3
  73. package/src/boilerplate/templates/layouts/fullscreen-mobile.tsx.tmpl +3 -3
  74. package/src/boilerplate/templates/layouts/fullscreen-universal.tsx.tmpl +5 -3
  75. package/src/check/run-tier0.ts +154 -0
  76. package/src/contract-context.ts +28 -14
  77. package/src/create-harness.ts +4 -0
  78. package/src/create-ui-generator.ts +41 -2
  79. package/src/design-mode.ts +11 -1
  80. package/src/design-system-docs.ts +7 -4
  81. package/src/effort.ts +48 -0
  82. package/src/evaluation/axis-checks/checks/grid.ts +148 -0
  83. package/src/evaluation/axis-checks/checks/universal.ts +308 -0
  84. package/src/evaluation/axis-checks/dispatch.ts +78 -13
  85. package/src/evaluation/axis-checks/helpers.ts +12 -0
  86. package/src/evaluation/axis-checks/registry.ts +2 -0
  87. package/src/evaluation/design-identity.ts +96 -0
  88. package/src/evaluation/index.ts +17 -0
  89. package/src/evaluation/llm-evaluator.ts +13 -1
  90. package/src/evaluation/theme-css.ts +14 -0
  91. package/src/evaluation/types-public.ts +35 -1
  92. package/src/evaluation/visual-evaluator.ts +478 -45
  93. package/src/fragments/render.ts +1 -1
  94. package/src/harness/check/runtime-render/prepare-mockup.ts +18 -11
  95. package/src/harness/coding/init-session.ts +1 -0
  96. package/src/harness/coding/run-coding-turn.ts +6 -14
  97. package/src/harness/coding/run-eval-round.ts +31 -3
  98. package/src/harness/runtime.ts +10 -0
  99. package/src/harness/types-public.ts +11 -0
  100. package/src/index.ts +2 -0
  101. package/src/pin-fixtures.ts +98 -0
  102. package/src/run-check.ts +10 -10
  103. package/src/tools/get-primitives-ts.ts +1 -1
  104. package/src/tools.ts +2 -1
  105. package/src/validation/primitives.ts +1 -1
@@ -1,6 +1,8 @@
1
+ import { LUCIDE_ICON_NAMES } from '@ggui-ai/design';
1
2
  import { listContractGadgets, HOOK_NAME_RE, STDLIB_GADGETS_PACKAGE } from '@ggui-ai/protocol';
3
+ import { createHash } from 'crypto';
2
4
 
3
- // src/evaluation/axis-checks/helpers.ts
5
+ // src/evaluation/axis-checks/checks/universal.ts
4
6
  var ID_FIELD_CANDIDATES = ["id", "uuid", "symbol", "key", "slug", "code"];
5
7
  function getItemsProperties(p) {
6
8
  return p.items?.properties ?? p.schema?.items?.properties;
@@ -157,6 +159,9 @@ function mkIssue(subcategory, description, fix, result = "fail") {
157
159
  function cap(s) {
158
160
  return s.charAt(0).toUpperCase() + s.slice(1);
159
161
  }
162
+ function axisCheckTraceEnabled() {
163
+ return process.env["GGUI_AXIS_CHECK_TRACE"] === "1";
164
+ }
160
165
 
161
166
  // src/evaluation/axis-checks/checks/universal.ts
162
167
  var ALL_RENDER_VALUES = [
@@ -260,7 +265,222 @@ function runPropSeedNoResync(input) {
260
265
  }
261
266
  return issues;
262
267
  }
268
+ var KNOWN_ICON_KEYS = new Set(
269
+ LUCIDE_ICON_NAMES.map((n) => n.replace(/[-_]/g, "").toLowerCase())
270
+ );
271
+ function isKnownIconName(name) {
272
+ return KNOWN_ICON_KEYS.has(name.replace(/[-_]/g, "").toLowerCase());
273
+ }
274
+ var ICON_NAME_LITERAL_RX = /<Icon\b[^>]*?\bname\s*=\s*(?:"([^"]*)"|'([^']*)'|\{\s*"([^"]*)"\s*\}|\{\s*'([^']*)'\s*\})/g;
275
+ function findUnknownIconNames(sourceCode) {
276
+ const unknown = /* @__PURE__ */ new Set();
277
+ for (const m of sourceCode.matchAll(ICON_NAME_LITERAL_RX)) {
278
+ const name = m[1] ?? m[2] ?? m[3] ?? m[4] ?? "";
279
+ if (name.length === 0) continue;
280
+ if (/[^\x20-\x7e]/.test(name)) continue;
281
+ if (!isKnownIconName(name)) unknown.add(name);
282
+ }
283
+ return [...unknown];
284
+ }
285
+ function runIconNameKnown(input) {
286
+ if (input.compiledCode === null) return [];
287
+ const unknown = findUnknownIconNames(input.sourceCode);
288
+ if (unknown.length === 0) return [];
289
+ const list = unknown.map((n) => `"${n}"`).join(", ");
290
+ return [
291
+ mkIssue(
292
+ "universal.icon_name_known",
293
+ `<Icon name=\u2026> uses ${unknown.length} name(s) outside the design system's Lucide subset: ${list} \u2014 an unknown name renders an empty box.`,
294
+ "Call get_available_icons and pick a listed name (kebab-case), or use an emoji directly."
295
+ )
296
+ ];
297
+ }
298
+ var LADDER_STOP_AS_TEXT_RX = /(?<![\w-])color\s*(?:=\s*\{?\s*|:\s*)["'`]?\s*var\(--ggui-color-primary-(\d{2,3})\s*[,)]/g;
299
+ var TEXT_INK_STOP_MIN = 300;
300
+ var TEXT_INK_STOP_MAX = 600;
301
+ function findLadderStopsAsText(sourceCode) {
302
+ const found = /* @__PURE__ */ new Set();
303
+ for (const m of sourceCode.matchAll(LADDER_STOP_AS_TEXT_RX)) {
304
+ const digits = m[1] ?? "";
305
+ const stop = Number(digits);
306
+ if (stop >= TEXT_INK_STOP_MIN && stop <= TEXT_INK_STOP_MAX) found.add(`primary-${digits}`);
307
+ }
308
+ return [...found];
309
+ }
310
+ function runAccentTextInk(input) {
311
+ if (input.compiledCode === null) return [];
312
+ const stops = findLadderStopsAsText(input.sourceCode);
313
+ if (stops.length === 0) return [];
314
+ const list = stops.map((s) => `\`${s}\``).join(", ");
315
+ return [
316
+ mkIssue(
317
+ "universal.accent_text_ink",
318
+ `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.`,
319
+ '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.'
320
+ )
321
+ ];
322
+ }
323
+ var SCOPED_SURFACE_TAG_RX = /<(Card|Box)\b([^<>]*?)surface\s*=\s*["'](inverted|hero)["']([^<>]*?)>/g;
324
+ var STYLE_BACKGROUND_RX = /\bstyle\s*=\s*\{\{[^}]*\b(background|backgroundColor|backgroundImage)\s*:/;
325
+ function findScopedSurfaceBackgrounds(sourceCode) {
326
+ const hits = [];
327
+ for (const m of sourceCode.matchAll(SCOPED_SURFACE_TAG_RX)) {
328
+ const attrs = `${m[2] ?? ""} ${m[4] ?? ""}`;
329
+ if (STYLE_BACKGROUND_RX.test(attrs)) hits.push([m[1] ?? "", m[3] ?? ""]);
330
+ }
331
+ return hits;
332
+ }
333
+ function runScopedSurfaceOwnsGround(input) {
334
+ if (input.compiledCode === null) return [];
335
+ if (input.designMode === "free") return [];
336
+ const hits = findScopedSurfaceBackgrounds(input.sourceCode);
337
+ if (hits.length === 0) return [];
338
+ const [tag, surface] = hits[0];
339
+ return [
340
+ mkIssue(
341
+ "universal.scoped_surface_owns_ground",
342
+ `<${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).`,
343
+ `Remove the background from that ${tag}'s style \u2014 surface="${surface}" paints the ground; pick another surface if a different ground is wanted.`
344
+ )
345
+ ];
346
+ }
347
+ var CAPS_TEXT_RX = /<Text\b(?=[^>]*\bcaps\b)[^>]*>([\s\S]*?)<\/Text>/g;
348
+ function findInventedCapsLabels(sourceCode) {
349
+ const out = [];
350
+ for (const m of sourceCode.matchAll(CAPS_TEXT_RX)) {
351
+ const inner = (m[1] ?? "").trim();
352
+ if (inner.length === 0) continue;
353
+ if (inner.includes("{")) continue;
354
+ out.push(inner.replace(/\s+/g, " ").slice(0, 60));
355
+ }
356
+ return out;
357
+ }
358
+ function runCapsLabelInvented(input) {
359
+ if (input.compiledCode === null) return [];
360
+ const invented = findInventedCapsLabels(input.sourceCode);
361
+ if (invented.length === 0) return [];
362
+ const list = invented.map((t) => `"${t}"`).join(", ");
363
+ return [
364
+ mkIssue(
365
+ "universal.caps_label_invented",
366
+ `${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.`,
367
+ "Remove the label, or bind its text to a prop (`<Text caps>{props.section}</Text>`); copy comes from the contract, never from the component."
368
+ )
369
+ ];
370
+ }
371
+ var VIEWPORT_UNIT = String.raw`(?:calc\(\s*)?100(?:vh|dvh|svh|lvh)\b(?:[^)"'\x60;\n]*\))?`;
372
+ var VIEWPORT_STYLE_OBJECT_RX = new RegExp(
373
+ String.raw`\b(?:minHeight|height|minBlockSize|blockSize)\s*:\s*["'\x60]\s*${VIEWPORT_UNIT}["'\x60]?`,
374
+ "g"
375
+ );
376
+ var VIEWPORT_CSS_RX = new RegExp(
377
+ String.raw`(?<![\w-])(?:min-)?(?:height|block-size)\s*:\s*${VIEWPORT_UNIT}`,
378
+ "g"
379
+ );
380
+ var VIEWPORT_CLASS_RX = /(?<![\w-])(?:min-)?h-(?:screen|dvh|svh|lvh)\b/g;
381
+ function findViewportSizing(sourceCode) {
382
+ const hits = [];
383
+ for (const rx of [VIEWPORT_STYLE_OBJECT_RX, VIEWPORT_CSS_RX, VIEWPORT_CLASS_RX]) {
384
+ for (const m of sourceCode.matchAll(rx)) {
385
+ hits.push({ at: m.index ?? 0, text: m[0].replace(/\s+/g, " ").trim().slice(0, 60) });
386
+ }
387
+ }
388
+ return hits.sort((a, b) => a.at - b.at).map((h) => h.text);
389
+ }
390
+ function runViewportSized(input) {
391
+ if (input.compiledCode === null) return [];
392
+ const sized = findViewportSizing(input.sourceCode);
393
+ if (sized.length === 0) return [];
394
+ const list = sized.map((t) => "`" + t + "`").join(", ");
395
+ return [
396
+ mkIssue(
397
+ "universal.viewport_sized",
398
+ `${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.`,
399
+ "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`."
400
+ )
401
+ ];
402
+ }
403
+ var TERMINAL_WORDS = /* @__PURE__ */ new Set([
404
+ "submit",
405
+ "confirm",
406
+ "approve",
407
+ "schedule",
408
+ "reserve",
409
+ "book",
410
+ "pay",
411
+ "checkout",
412
+ "purchase",
413
+ "order"
414
+ ]);
415
+ var TERMINAL_LABEL_RX = /\b(submit|confirm|approve|schedule|reserve|book|pay|checkout|purchase|place\s+order)\b/i;
416
+ var GUARD_RX = /\b(disabled|aria-disabled)\s*=/;
417
+ function nameWords(name) {
418
+ return name.split(/(?=[A-Z])|[_\-\s]+/).map((w) => w.toLowerCase()).filter((w) => w.length > 0);
419
+ }
420
+ function findUnguardedTerminalControls(sourceCode) {
421
+ if (GUARD_RX.test(sourceCode)) return [];
422
+ const names = /* @__PURE__ */ new Set();
423
+ for (const m of sourceCode.matchAll(/useAction(?:<[^>]*>)?\(\s*['"`]([^'"`]+)['"`]/g)) {
424
+ const name = m[1] ?? "";
425
+ if (nameWords(name).some((w) => TERMINAL_WORDS.has(w))) names.add(name);
426
+ }
427
+ for (const m of sourceCode.matchAll(/>([^<>{}]{1,60})<\/Button>/g)) {
428
+ const label = (m[1] ?? "").replace(/\s+/g, " ").trim();
429
+ if (label.length > 0 && TERMINAL_LABEL_RX.test(label)) names.add(label.slice(0, 40));
430
+ }
431
+ return [...names];
432
+ }
433
+ function runTerminalActionUnguarded(input) {
434
+ if (input.compiledCode === null) return [];
435
+ const found = findUnguardedTerminalControls(input.sourceCode);
436
+ if (found.length === 0) return [];
437
+ const list = found.map((t) => `"${t}"`).join(", ");
438
+ return [
439
+ mkIssue(
440
+ "universal.terminal_action_unguarded",
441
+ `${list} reads as an action the user means once, and nothing in this component disables the control after it fires \u2014 a double-click sends it twice.`,
442
+ 'Hold the fired state (`const [submitted, setSubmitted] = useState(false)`), set it in the handler, and pass `disabled={submitted}` to that control with a word that says so ("Submitted"). If the action IS meant to repeat \u2014 sending a message, adding an item \u2014 leave it armed and ignore this.',
443
+ "warn"
444
+ )
445
+ ];
446
+ }
263
447
  var UNIVERSAL_CHECKS = [
448
+ {
449
+ id: "universal.icon_name_known",
450
+ axis: "render",
451
+ values: ALL_RENDER_VALUES,
452
+ run: runIconNameKnown
453
+ },
454
+ {
455
+ id: "universal.caps_label_invented",
456
+ axis: "render",
457
+ values: ALL_RENDER_VALUES,
458
+ run: runCapsLabelInvented
459
+ },
460
+ {
461
+ id: "universal.viewport_sized",
462
+ axis: "render",
463
+ values: ALL_RENDER_VALUES,
464
+ run: runViewportSized
465
+ },
466
+ {
467
+ id: "universal.terminal_action_unguarded",
468
+ axis: "render",
469
+ values: ALL_RENDER_VALUES,
470
+ run: runTerminalActionUnguarded
471
+ },
472
+ {
473
+ id: "universal.accent_text_ink",
474
+ axis: "render",
475
+ values: ALL_RENDER_VALUES,
476
+ run: runAccentTextInk
477
+ },
478
+ {
479
+ id: "universal.scoped_surface_owns_ground",
480
+ axis: "render",
481
+ values: ALL_RENDER_VALUES,
482
+ run: runScopedSurfaceOwnsGround
483
+ },
264
484
  {
265
485
  id: "universal.prop_coverage",
266
486
  axis: "render",
@@ -1063,6 +1283,87 @@ var TOOLING_CHECKS = [
1063
1283
  }
1064
1284
  ];
1065
1285
 
1286
+ // src/evaluation/axis-checks/checks/grid.ts
1287
+ var BOARD_PROP_RX = /^(columns?|lanes?|stages?|lists?|buckets?|swimlanes?)$/i;
1288
+ var BOARD_PROMPT_RX = /\b(kanban|board)\b/i;
1289
+ var BOARD_NAMES = ["columns", "lanes", "stages", "lists", "buckets", "swimlanes"];
1290
+ var LAYOUT_TAG_RX = /<(\/?)(Grid|Row|Stack|Card|Box|Container|ScrollArea|div|section)\b([^<>]*?)>/g;
1291
+ var SIDE_BY_SIDE = /* @__PURE__ */ new Set(["Grid", "Row"]);
1292
+ function boardPropNames(contract) {
1293
+ const propsField = contract?.propsSpec;
1294
+ const properties = propsField?.properties ?? {};
1295
+ return Object.entries(properties).filter(([name, p]) => BOARD_PROP_RX.test(name) && (p?.type === "array" || p?.schema?.type === "array")).map(([name]) => name);
1296
+ }
1297
+ function jsxMapPositions(sourceCode, name) {
1298
+ const rx = new RegExp(
1299
+ `\\b${name}(?:\\?\\.)?\\.map\\(\\s*\\(?\\s*\\w+(?:\\s*,\\s*\\w+)?\\s*\\)?\\s*=>\\s*(?:\\(?\\s*<|\\{[^{}]{0,600}?\\breturn\\s*\\(?\\s*<)`,
1300
+ "g"
1301
+ );
1302
+ return [...sourceCode.matchAll(rx)].map((m) => m.index ?? 0);
1303
+ }
1304
+ function enclosingLayoutTag(sourceCode, pos) {
1305
+ const stack = [];
1306
+ for (const m of sourceCode.slice(0, pos).matchAll(LAYOUT_TAG_RX)) {
1307
+ const closing = m[1] === "/";
1308
+ const name = m[2] ?? "";
1309
+ const attrs = m[3] ?? "";
1310
+ if (closing) {
1311
+ if (stack[stack.length - 1] === name) stack.pop();
1312
+ continue;
1313
+ }
1314
+ if (attrs.trimEnd().endsWith("/")) continue;
1315
+ stack.push(name);
1316
+ }
1317
+ return stack[stack.length - 1];
1318
+ }
1319
+ function describeBoardRead(input) {
1320
+ const declared = boardPropNames(input.contract);
1321
+ const names = declared.length > 0 ? declared : BOARD_PROMPT_RX.test(input.originalPrompt) ? [...BOARD_NAMES] : BOARD_NAMES.filter((n) => jsxMapPositions(input.sourceCode, n).length > 0);
1322
+ const perName = names.map((name) => {
1323
+ const positions = jsxMapPositions(input.sourceCode, name);
1324
+ return { name, maps: positions.length, enclosing: positions.map((pos) => enclosingLayoutTag(input.sourceCode, pos) ?? "nothing") };
1325
+ });
1326
+ return { declared, names, perName };
1327
+ }
1328
+ function findStackedBoardColumns(input) {
1329
+ const stacked = [];
1330
+ for (const { name, enclosing } of describeBoardRead(input).perName) {
1331
+ for (const tag of enclosing) if (!SIDE_BY_SIDE.has(tag)) stacked.push([name, tag]);
1332
+ }
1333
+ return stacked;
1334
+ }
1335
+ function runBoardColumnsSideBySide(input) {
1336
+ if (input.compiledCode === null) return [];
1337
+ const standDown = input.designMode === "free";
1338
+ const stacked = standDown ? [] : findStackedBoardColumns(input);
1339
+ if (axisCheckTraceEnabled()) {
1340
+ const line = {
1341
+ ...describeBoardRead(input),
1342
+ designMode: input.designMode ?? "constrained",
1343
+ standDown,
1344
+ result: standDown ? "stand-down" : stacked.length > 0 ? "fail" : "pass"
1345
+ };
1346
+ console.log(JSON.stringify({ boardColumnsTrace: line }));
1347
+ }
1348
+ if (stacked.length === 0) return [];
1349
+ const [name, tag] = stacked[0];
1350
+ return [
1351
+ mkIssue(
1352
+ "grid.board_columns_side_by_side",
1353
+ `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.`,
1354
+ `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.`
1355
+ )
1356
+ ];
1357
+ }
1358
+ var GRID_CHECKS = [
1359
+ {
1360
+ id: "grid.board_columns_side_by_side",
1361
+ axis: "render",
1362
+ values: ["grid"],
1363
+ run: runBoardColumnsSideBySide
1364
+ }
1365
+ ];
1366
+
1066
1367
  // src/evaluation/axis-checks/extras.ts
1067
1368
  function mkIssue2(subcategory, description, fix, result = "warn") {
1068
1369
  return { tier: 0, result, category: "mode", subcategory, description, fix };
@@ -1202,6 +1503,7 @@ var REGISTRY = [
1202
1503
  ...WRITES_CHECKS,
1203
1504
  ...STATE_PAYLOAD_CHECKS,
1204
1505
  ...TOOLING_CHECKS,
1506
+ ...GRID_CHECKS,
1205
1507
  ...EXTRA_CHECKS
1206
1508
  ];
1207
1509
 
@@ -1215,27 +1517,59 @@ function matches(vector, check) {
1215
1517
  }
1216
1518
  return true;
1217
1519
  }
1218
-
1219
- // src/evaluation/axis-checks/dispatch.ts
1220
1520
  function runAxisChecks(classification, input) {
1221
- if (input.compiledCode === null) return [];
1222
- const axisInput = {
1521
+ const facts = {
1223
1522
  sourceCode: input.sourceCode,
1224
- compiledCode: input.compiledCode,
1225
1523
  ...input.contract !== void 0 ? { contract: input.contract } : {},
1226
1524
  originalPrompt: input.originalPrompt,
1227
1525
  classification,
1228
1526
  ...input.designMode !== void 0 ? { designMode: input.designMode } : {}
1229
1527
  };
1528
+ if (input.compiledCode === null) {
1529
+ traceAxisChecksSkipped(facts, "compiledCode null \u2014 no check ran");
1530
+ return [];
1531
+ }
1532
+ const axisInput = { ...facts, compiledCode: input.compiledCode };
1533
+ return runGatedAxisChecks(
1534
+ REGISTRY.filter((check) => matches(classification.vector, check)),
1535
+ axisInput
1536
+ ).issues;
1537
+ }
1538
+ function runGatedAxisChecks(checks, input) {
1230
1539
  const issues = [];
1231
- const firedIds = /* @__PURE__ */ new Set();
1232
- for (const check of REGISTRY) {
1233
- if (!matches(classification.vector, check)) continue;
1234
- if (firedIds.has(check.id)) continue;
1235
- firedIds.add(check.id);
1236
- issues.push(...check.run(axisInput));
1540
+ const firedIds = [];
1541
+ const seen = /* @__PURE__ */ new Set();
1542
+ for (const check of checks) {
1543
+ if (seen.has(check.id)) continue;
1544
+ seen.add(check.id);
1545
+ firedIds.push(check.id);
1546
+ issues.push(...check.run(input));
1237
1547
  }
1238
- return issues;
1548
+ if (axisCheckTraceEnabled()) {
1549
+ const line = {
1550
+ ...traceFacts(input),
1551
+ matched: [...firedIds],
1552
+ issues: issues.map((i) => ({ id: i.subcategory ?? i.category, result: i.result }))
1553
+ };
1554
+ console.log(JSON.stringify({ axisCheckTrace: line }));
1555
+ }
1556
+ return { issues, firedIds };
1557
+ }
1558
+ function traceAxisChecksSkipped(facts, note) {
1559
+ if (!axisCheckTraceEnabled()) return;
1560
+ const line = { ...traceFacts(facts), matched: [], issues: [], note };
1561
+ console.log(JSON.stringify({ axisCheckTrace: line }));
1562
+ }
1563
+ function traceFacts(input) {
1564
+ const propsSpec = input.contract?.propsSpec;
1565
+ return {
1566
+ sourceSha256: createHash("sha256").update(input.sourceCode, "utf8").digest("hex"),
1567
+ originalPrompt: input.originalPrompt,
1568
+ designMode: input.designMode ?? "constrained",
1569
+ classification: input.classification.vector,
1570
+ propsSpecKeys: Object.keys(propsSpec ?? {}),
1571
+ propsSpecPropertyKeys: Object.keys(propsSpec?.properties ?? {})
1572
+ };
1239
1573
  }
1240
1574
 
1241
1575
  export { EXTRA_CHECKS, REGISTRY, matches, runAxisChecks };