@ggui-ai/ui-gen 0.15.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 (123) hide show
  1. package/dist/adapters/generation-dispatch.d.ts +24 -4
  2. package/dist/adapters/generation-dispatch.js +5188 -3920
  3. package/dist/adapters/generation-dispatch.js.map +1 -1
  4. package/dist/adapters/index.js +33 -26
  5. package/dist/adapters/index.js.map +1 -1
  6. package/dist/advanced/index.d.ts +22 -13
  7. package/dist/advanced/index.js +4889 -3561
  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 +19 -1
  12. package/dist/boilerplate.js +671 -254
  13. package/dist/boilerplate.js.map +1 -1
  14. package/dist/check/index.d.ts +17 -3
  15. package/dist/check/index.js +34 -25
  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-C8Ki9Wz1.d.ts +189 -0
  20. package/dist/design-mode-DLufhrNp.d.ts +61 -0
  21. package/dist/{dispatch-BuZfTh9r.d.ts → dispatch-D2htzi9h.d.ts} +4 -1
  22. package/dist/evaluation/axis-checks/index.d.ts +4 -3
  23. package/dist/evaluation/axis-checks/index.js +300 -14
  24. package/dist/evaluation/axis-checks/index.js.map +1 -1
  25. package/dist/evaluation/axis-checks/registry.d.ts +2 -1
  26. package/dist/evaluation/axis-checks/registry.js +253 -1
  27. package/dist/evaluation/axis-checks/registry.js.map +1 -1
  28. package/dist/evaluation/index.d.ts +297 -5
  29. package/dist/evaluation/index.js +4515 -120
  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-BTfvELgl.d.ts → generate-9IeNT4ZY.d.ts} +10 -1
  34. package/dist/harness/check/runtime-render/index.d.ts +3 -1
  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 -1
  38. package/dist/harness/index.js +913 -441
  39. package/dist/harness/index.js.map +1 -1
  40. package/dist/harness/llm-trace-sink.d.ts +3 -3
  41. package/dist/harness/llm-trace-sink.js.map +1 -1
  42. package/dist/harness/prompts.d.ts +2 -1
  43. package/dist/harness/prompts.js +6 -3
  44. package/dist/harness/prompts.js.map +1 -1
  45. package/dist/harness/result-types.d.ts +17 -5
  46. package/dist/harness/result-types.js.map +1 -1
  47. package/dist/harness/runtime.d.ts +28 -3
  48. package/dist/harness/runtime.js +702 -274
  49. package/dist/harness/runtime.js.map +1 -1
  50. package/dist/harness/types-public.d.ts +39 -1
  51. package/dist/harness/types-public.js +913 -441
  52. package/dist/harness/types-public.js.map +1 -1
  53. package/dist/harness/validator-trace-sink.d.ts +2 -1
  54. package/dist/index.d.ts +32 -6
  55. package/dist/index.js +4899 -3560
  56. package/dist/index.js.map +1 -1
  57. package/dist/patch.d.ts +3 -1
  58. package/dist/{prompts-BVhtH3M1.d.ts → prompts-YU9piOgs.d.ts} +21 -45
  59. package/dist/providers/index.d.ts +2 -2
  60. package/dist/providers/index.js.map +1 -1
  61. package/dist/styling-profile-CxfvV48g.d.ts +35 -0
  62. package/dist/tools/get-primitives-ts.d.ts +1 -1
  63. package/dist/tools/get-primitives-ts.js +1 -1
  64. package/dist/tools/get-primitives-ts.js.map +1 -1
  65. package/dist/{types-public-CkNWEov9.d.ts → types-public-DVSssdc-.d.ts} +85 -5
  66. package/dist/validation/index.d.ts +1 -1
  67. package/dist/validation/index.js +4 -4
  68. package/dist/validation/index.js.map +1 -1
  69. package/dist/workflows.d.ts +3 -1
  70. package/package.json +12 -12
  71. package/src/adapters/generation-dispatch.ts +62 -4
  72. package/src/adapters/provider-router.ts +7 -6
  73. package/src/adapters/tools.ts +5 -5
  74. package/src/advanced/generator.ts +11 -10
  75. package/src/advanced/index.ts +5 -4
  76. package/src/boilerplate/free-design-prompt.ts +336 -0
  77. package/src/boilerplate/generate.ts +10 -1
  78. package/src/boilerplate/hard-sections.ts +257 -0
  79. package/src/boilerplate/render.ts +32 -8
  80. package/src/boilerplate/styling-profile.ts +166 -0
  81. package/src/boilerplate/system-prompt.ts +101 -237
  82. package/src/boilerplate/templates/base-free.tsx.tmpl +24 -0
  83. package/src/check/run-tier0.ts +82 -20
  84. package/src/coding-agent/tools.ts +8 -2
  85. package/src/contract-context.ts +43 -14
  86. package/src/create-harness.ts +15 -0
  87. package/src/create-ui-generator.ts +112 -27
  88. package/src/design-mode.ts +192 -0
  89. package/src/design-system-docs.ts +24 -17
  90. package/src/effort.ts +48 -0
  91. package/src/evaluation/axis-checks/checks/grid.ts +148 -0
  92. package/src/evaluation/axis-checks/checks/universal.ts +244 -0
  93. package/src/evaluation/axis-checks/dispatch.ts +82 -13
  94. package/src/evaluation/axis-checks/extras.ts +5 -0
  95. package/src/evaluation/axis-checks/helpers.ts +12 -0
  96. package/src/evaluation/axis-checks/registry.ts +2 -0
  97. package/src/evaluation/cost-tracker.ts +2 -3
  98. package/src/evaluation/design-identity.ts +96 -0
  99. package/src/evaluation/index.ts +63 -2
  100. package/src/evaluation/llm-evaluator.ts +89 -21
  101. package/src/evaluation/theme-css.ts +14 -0
  102. package/src/evaluation/types-public.ts +137 -8
  103. package/src/evaluation/visual-evaluator.ts +759 -54
  104. package/src/fragments/render.ts +1 -1
  105. package/src/harness/check/runtime-render/prepare-mockup.ts +18 -11
  106. package/src/harness/coding/dupe-break.ts +0 -0
  107. package/src/harness/coding/init-session.ts +2 -0
  108. package/src/harness/coding/run-coding-turn.ts +9 -14
  109. package/src/harness/coding/run-eval-round.ts +46 -5
  110. package/src/harness/llm-trace-sink.ts +3 -3
  111. package/src/harness/result-types.ts +15 -3
  112. package/src/harness/runtime.ts +28 -0
  113. package/src/harness/types-public.ts +38 -0
  114. package/src/index.ts +28 -0
  115. package/src/internal/extract-call-signatures.ts +2 -2
  116. package/src/pin-fixtures.ts +98 -0
  117. package/src/providers/bedrock.ts +4 -4
  118. package/src/run-check.ts +11 -10
  119. package/src/tools/get-primitives-ts.ts +1 -1
  120. package/src/tools.ts +2 -1
  121. package/src/validation/allowed-imports.ts +8 -2
  122. package/src/validation/primitives.ts +1 -1
  123. package/dist/create-ui-generator-D6TKuv9V.d.ts +0 -106
@@ -27,7 +27,7 @@ export const renderFragments: Record<string, HarnessFragment> = {
27
27
  value: "grid",
28
28
  cacheTier: "axisDelta",
29
29
  promptText:
30
- "## Render: grid\n2D tile layout. Use <CardGrid> or CSS `display: grid` with `grid-template-columns: repeat(N, 1fr)`. If items carry row/col fields, position each tile at (row, col). Do not scroll horizontally.",
30
+ "## Render: grid\nTwo shapes share this axis — read the contract:\n- A TILE grid (a gallery, a stat grid, items that may carry row/col fields): `<Grid columns={N} gap=\"md\">` (or `<CardGrid>`); if items carry row/col fields, position each tile at (row, col).\n- A BOARD (a columns / lanes / stages array whose items are cards — a kanban): the columns sit SIDE BY SIDE at md and wider — `<Grid columns={{ base: 1, md: columns.length }} gap=\"md\">` wrapping the columns map, each column its own `<Stack>` of `<Card>`s with its header and count; NEVER a `<Stack>` of column sections (that is a list — tier-0 fails it).\nNever hand-roll `display: grid`. Do not scroll horizontally.",
31
31
  },
32
32
  spatial: {
33
33
  axis: "render",
@@ -129,6 +129,16 @@ function synthesizeFromSchema(
129
129
  hint: string,
130
130
  warnings: string[],
131
131
  depth: number,
132
+ /**
133
+ * 1-based position when the value belongs to an array element (inherited
134
+ * by the element's own fields). Every synthesized string inside an element
135
+ * carries it, so two elements never share an id or a visible label — the
136
+ * runtime probe's selection-identity rule ("clicking each fired the action
137
+ * with the same id") is undecidable on identical items and read a correct
138
+ * component as broken (2026-09-10, the 1.6 hello: both quick replies were
139
+ * `Sample Id` / `Sample Label`).
140
+ */
141
+ ordinal?: number,
132
142
  ): SynthOk | SynthFail {
133
143
  if (!schema) return { kind: "fail", reason: "no schema" };
134
144
  if (depth > MAX_DEPTH) return { kind: "fail", reason: "schema too deep" };
@@ -146,10 +156,10 @@ function synthesizeFromSchema(
146
156
 
147
157
  // Union schemas — try first branch only
148
158
  if (schema.oneOf && schema.oneOf.length > 0) {
149
- return synthesizeFromSchema(schema.oneOf[0], hint, warnings, depth + 1);
159
+ return synthesizeFromSchema(schema.oneOf[0], hint, warnings, depth + 1, ordinal);
150
160
  }
151
161
  if (schema.anyOf && schema.anyOf.length > 0) {
152
- return synthesizeFromSchema(schema.anyOf[0], hint, warnings, depth + 1);
162
+ return synthesizeFromSchema(schema.anyOf[0], hint, warnings, depth + 1, ordinal);
153
163
  }
154
164
 
155
165
  // Draft-07 type array (`['string','null']` — the canonical nullable
@@ -158,12 +168,7 @@ function synthesizeFromSchema(
158
168
  // above bounds the recursion.
159
169
  if (Array.isArray(schema.type)) {
160
170
  const primary = schema.type.find((t) => t !== "null") ?? "null";
161
- return synthesizeFromSchema(
162
- { ...schema, type: primary },
163
- hint,
164
- warnings,
165
- depth + 1,
166
- );
171
+ return synthesizeFromSchema({ ...schema, type: primary }, hint, warnings, depth + 1, ordinal);
167
172
  }
168
173
 
169
174
  switch (schema.type) {
@@ -184,7 +189,9 @@ function synthesizeFromSchema(
184
189
  // Use field name as a hint so it appears in DOM and prop-coverage check sees it.
185
190
  // Capitalize first letter for natural-looking placeholder.
186
191
  const cap = hint.charAt(0).toUpperCase() + hint.slice(1);
187
- return { kind: "ok", value: `Sample ${cap}`, source: "schema-synth" };
192
+ // Inside an array element every string is distinct per element.
193
+ const value = ordinal === undefined ? `Sample ${cap}` : `Sample ${cap} ${ordinal}`;
194
+ return { kind: "ok", value, source: "schema-synth" };
188
195
  }
189
196
  }
190
197
  }
@@ -211,7 +218,7 @@ function synthesizeFromSchema(
211
218
  const items: JsonValue[] = [];
212
219
  for (let i = 0; i < 2; i++) {
213
220
  const itemHint = `${hint}Item${i + 1}`;
214
- const itemSynth = synthesizeFromSchema(schema.items, itemHint, warnings, depth + 1);
221
+ const itemSynth = synthesizeFromSchema(schema.items, itemHint, warnings, depth + 1, i + 1);
215
222
  if (itemSynth.kind === "ok") {
216
223
  // Inject an `id` for list-key purposes if the item is an object lacking one.
217
224
  if (
@@ -233,7 +240,7 @@ function synthesizeFromSchema(
233
240
  const propsMap = schema.properties ?? {};
234
241
  const required = new Set(schema.required ?? []);
235
242
  for (const [k, sub] of Object.entries(propsMap)) {
236
- const subSynth = synthesizeFromSchema(sub, k, warnings, depth + 1);
243
+ const subSynth = synthesizeFromSchema(sub, k, warnings, depth + 1, ordinal);
237
244
  if (subSynth.kind === "ok") {
238
245
  obj[k] = subSynth.value;
239
246
  } else if (required.has(k)) {
Binary file
@@ -168,6 +168,8 @@ export async function initSession(input: {
168
168
  originalPrompt: params.userPrompt,
169
169
  contract: params.contract,
170
170
  designContext: undefined,
171
+ designMode: harness.designMode,
172
+ profile: harness.profile,
171
173
  },
172
174
  {
173
175
  provider: mapProviderForEvaluator(evalSpec.provider),
@@ -33,6 +33,7 @@ import {
33
33
  WRITE_PLAN_TOOL,
34
34
  type LLMToolDef,
35
35
  } from "../../tools.js";
36
+ import { LUCIDE_ICON_NAMES } from "@ggui-ai/design";
36
37
  import { formatWithHashlines } from "../hashline.js";
37
38
  import {
38
39
  recordToolExchange,
@@ -592,20 +593,11 @@ ${closingInstruction}`;
592
593
  // Handle get_available_icons — return icon list without consuming a turn
593
594
  if (call.name === "get_available_icons") {
594
595
  if (!iconNamesCache) {
595
- try {
596
- // Dynamic import — resolve from design package dist
597
- const iconDataPath = new URL(
598
- "../../../../packages/design/dist/primitives/icon-data.js",
599
- import.meta.url,
600
- ).pathname;
601
- const { LUCIDE_ICONS } = await import(iconDataPath);
602
- iconNamesCache = Object.keys(LUCIDE_ICONS)
603
- .map((n) => n.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase())
604
- .join(", ");
605
- } catch {
606
- iconNamesCache =
607
- "sun, moon, cloud, cloud-rain, heart, star, search, home, plus, minus, check, x, arrow-left, arrow-right, chevron-down, chevron-up, menu, user, settings, bell, mail, phone, shopping-cart, download, upload, edit, trash-2, save, eye, lock, globe, map-pin, calendar, clock, zap, alert-circle, info, help-circle";
608
- }
596
+ // The one list the Icon primitive renders (ggui#1015) — the same
597
+ // export the check leg reads, so the tool never advertises a name
598
+ // the primitive does not know (the former dist-relative dynamic
599
+ // import fell back to a 40-name string whenever the path missed).
600
+ iconNamesCache = LUCIDE_ICON_NAMES.join(", ");
609
601
  }
610
602
  console.log(`[simple] turn ${turnsUsed}: get_available_icons`);
611
603
  let iconResult = `Available Lucide icon names (use with <Icon name="...">):\n${iconNamesCache}`;
@@ -674,6 +666,9 @@ ${closingInstruction}`;
674
666
  // typecheck overlays the real wrapper declarations and a generated
675
667
  // direct gadget import gets strict option/return narrowing.
676
668
  harness.what.gadgetTypes,
669
+ // Which tier-0 legs fire — the design-vocabulary legs stand down in
670
+ // `free` mode; every contract leg runs in both.
671
+ harness.designMode,
677
672
  );
678
673
  const toolMs = Date.now() - toolStart;
679
674
 
@@ -22,7 +22,7 @@ import { listContractGadgets } from "@ggui-ai/protocol";
22
22
  import type { Classification } from "../../classifier/index.js";
23
23
  import type { AgentWorkspace } from "../../coding-agent/workspace.js";
24
24
  import type { CostTracker } from "../../evaluation/cost-tracker.js";
25
- import type { EvalIssue, EvalResult, RuntimeProbeMeta } from "../../evaluation/types-public.js";
25
+ import type { EvalIssue, EvalResult, RuntimeProbeMeta, VisualEvalSummary } from "../../evaluation/types-public.js";
26
26
  import { notApplicableCoverage } from "../../evaluation/types-public.js";
27
27
  import { mapProviderForEvaluator } from "../enforced-coding.js";
28
28
  import { runCheck } from "../index.js";
@@ -128,6 +128,9 @@ const MAX_FEEDBACK_ISSUES = 3;
128
128
  * criterion is `not-applicable` (excluded from coverage denominators),
129
129
  * not `skipped` (which would count as silent absence).
130
130
  */
131
+ /** Why an eval result carries no criteria: the round itself threw (ggui#1110). */
132
+ const EVAL_ROUND_THREW_REASON = "eval round threw";
133
+
131
134
  const LOW_RISK_BYPASS_REASON =
132
135
  "same-image low-risk bypass: axis checks clean, tier-1/2 evaluation not invoked";
133
136
 
@@ -453,6 +456,11 @@ export async function runEvalRound(
453
456
  sourceCode: currentSource,
454
457
  originalPrompt: userPrompt,
455
458
  contract,
459
+ // Criteria + mother prompt follow the triad that produced
460
+ // the source (free mode: color-only tokens, arm-neutral
461
+ // visual judging).
462
+ designMode: harness.designMode,
463
+ profile: harness.profile,
456
464
  },
457
465
  {
458
466
  provider: mapProviderForEvaluator(evaluationAgent.provider),
@@ -476,13 +484,25 @@ export async function runEvalRound(
476
484
  : null,
477
485
  visualMod
478
486
  ? visualMod.runVisualEval(
479
- { compiledCode, originalPrompt: userPrompt },
487
+ {
488
+ compiledCode,
489
+ originalPrompt: userPrompt,
490
+ profile: harness.profile,
491
+ // The caller's theme, when named — the round paints what the app paints.
492
+ ...(visualEvaluation?.cssTokens !== undefined ? { cssTokens: visualEvaluation.cssTokens } : {}),
493
+ },
480
494
  {
481
495
  provider: mapProviderForEvaluator(visualEvalAgent.provider) as "claude" | "google",
482
496
  model: visualEvalAgent.model,
483
497
  passThreshold: visualThreshold,
484
- sampleProps: visualEvaluation?.sampleProps,
498
+ // The in-loop visual round renders the same sample the runtime probe
499
+ // does (the caller's fixture) unless the config names its own.
500
+ sampleProps: visualEvaluation?.sampleProps ?? fixtureProps,
485
501
  viewport: visualEvaluation?.viewport,
502
+ // Per-canvas judging (arm-neutral) — absent = single shot.
503
+ ...(visualEvaluation?.canvases !== undefined
504
+ ? { canvases: visualEvaluation.canvases }
505
+ : {}),
486
506
  // #484/#489 — same threading as the LLM-eval leg above:
487
507
  // the visual eval's multimodal call runs inside the same
488
508
  // concurrent generation, so it needs the same routing
@@ -497,7 +517,8 @@ export async function runEvalRound(
497
517
  ]);
498
518
  evalLlmMs = Date.now() - evalLlmStart;
499
519
  llmResult = llm;
500
- visualIssues = visual;
520
+ visualIssues = visual?.issues ?? null;
521
+ const visualSummary: VisualEvalSummary | undefined = visual?.summary;
501
522
 
502
523
  // Track costs from LLM eval calls
503
524
  if (llmResult) {
@@ -524,6 +545,10 @@ export async function runEvalRound(
524
545
  ...(llmResult?.criteriaCoverage !== undefined
525
546
  ? { criteriaCoverage: llmResult.criteriaCoverage }
526
547
  : {}),
548
+ // Per-canvas visual summary — present only when canvases were
549
+ // requested and the leg ran; every later re-stamp spreads this
550
+ // base so it survives to the assembled result.
551
+ ...(visualSummary !== undefined ? { visual: visualSummary } : {}),
527
552
  };
528
553
 
529
554
  // ── Log merged results, with mode-check subcategory breakdown ──
@@ -868,7 +893,23 @@ export async function runEvalRound(
868
893
  lastDiffFailed: false,
869
894
  };
870
895
  } catch (e) {
871
- console.error("[simple] eval failed:", e instanceof Error ? e.message : e);
896
+ // The round threw. Until ggui#1110 this returned `evalResult` UNDEFINED and
897
+ // said so only in a console line: downstream the generation carried no
898
+ // evaluation at all, an offline mint exported no `eval.json`, and the bar
899
+ // read the missing file as "probe skipped" — a thrown evaluation and an
900
+ // evaluation that never ran became the same fact, with the reason lost.
901
+ // Now the failure IS the result: the round reports what threw, on the
902
+ // channel a judge reads, and can never be mistaken for a pass (`pass` is
903
+ // empty, the probe status is `infra-skipped`, and a bar that requires the
904
+ // probe still refuses — with a cause instead of a silence).
905
+ const message = e instanceof Error ? e.message : String(e);
906
+ console.error("[simple] eval failed:", message);
907
+ evalResult = evalResult ?? {
908
+ issues: [],
909
+ pass: [],
910
+ criteriaCoverage: notApplicableCoverage(`${EVAL_ROUND_THREW_REASON}: ${message}`),
911
+ runtimeProbe: { status: "infra-skipped", reason: `${EVAL_ROUND_THREW_REASON}: ${message}` },
912
+ };
872
913
  return {
873
914
  control: "break",
874
915
  evalDone: false,
@@ -21,9 +21,9 @@
21
21
  * (createAgent → run-coding-turn → llm-evaluator → file-agent → …)
22
22
  * touches ~5 packages for a devtools-only surface. The OSS ggui server
23
23
  * is a single process per CLI invocation — global state has no
24
- * confusion-cost there. A hosted closed runtime isolates per request
25
- * via process-pool, so a global per-pool is also safe. If we ever
26
- * multi-tenant inside one process we'll thread it then.
24
+ * confusion-cost there. A hosted deployment isolates per request via a
25
+ * process pool, so a global per-pool is also safe. If we ever serve
26
+ * several apps inside one process we'll thread it then.
27
27
  *
28
28
  * **Default = no sink.** When unset, the router emits nothing and
29
29
  * spends no CPU formatting events. Passing `null` removes a previously
@@ -12,8 +12,8 @@
12
12
  import type { DataContract, JsonObject } from "@ggui-ai/protocol";
13
13
  import type { AdapterResult } from "../adapters/types.js";
14
14
  import type { RenderingContext } from "../contract-context.js";
15
- import type { EvaluationResult } from "../evaluation/types.js";
16
15
  import type { EvalResult } from "../evaluation/types-public.js";
16
+ import type { CanvasClass, DesignMode } from "../design-mode.js";
17
17
 
18
18
  // =============================================================================
19
19
  // Model Roles
@@ -52,8 +52,6 @@ export type ModelRoles = Partial<Record<ModelRole, string>>;
52
52
  export interface GenerationResult extends AdapterResult {
53
53
  /** Number of generation passes the harness performed */
54
54
  passesUsed: number;
55
- /** Evaluation results per round (legacy numeric scoring — backward compat) */
56
- evaluations?: EvaluationResult[];
57
55
  /** Three-tier evaluation result (tier 0 + LLM tier 1+2 + visual) */
58
56
  evalResult?: EvalResult;
59
57
  /** Whether background improvement should be spawned (auto-improve mode) */
@@ -62,6 +60,20 @@ export interface GenerationResult extends AdapterResult {
62
60
  selfCheckPassed?: boolean;
63
61
  /** Which model was used for each role (for benchmark reporting) */
64
62
  modelRolesUsed?: Record<string, string>;
63
+ /**
64
+ * The triad this generation ran under — recorded ONLY when the caller
65
+ * set `designMode` on the dispatch (a bench arm switch, an operator
66
+ * option). Absent = the constrained default path, byte-identical to
67
+ * the pre-`designMode` result.
68
+ */
69
+ designMode?: DesignMode;
70
+ /**
71
+ * The canvas class the `free` prompt stated — the explicit `canvas`
72
+ * when the caller passed one, else the shell × screen derivation the
73
+ * prompt itself used. Absent in constrained mode (that prompt states
74
+ * no canvas) unless the caller passed one explicitly.
75
+ */
76
+ canvas?: CanvasClass;
65
77
  /** Phase-level timing breakdown (ms) */
66
78
  timing?: Record<string, number>;
67
79
  /** Per-turn/per-outcome breakdown for benchmark analysis.
@@ -24,6 +24,7 @@
24
24
  // directly to the coding agent in the system prompt — there is no
25
25
  // separate planner stage.
26
26
 
27
+ import type { GenerationProfileInput } from "../boilerplate/styling-profile.js";
27
28
  import type { QualityConfig } from "../evaluation/types-public.js";
28
29
  import { DEFAULT_DESIGN_SYSTEM_DOCS } from "../design-system-docs.js";
29
30
  import { PRIMITIVES_DOCUMENTATION } from "../validation/index.js";
@@ -32,6 +33,7 @@ import { renderPitfallsBlock } from "./pitfalls.js";
32
33
  import type { GadgetDescriptor, DataContract, JsonObject } from "@ggui-ai/protocol";
33
34
  import { buildSystemPrompt as buildSystemPromptSkeleton } from "../boilerplate.js";
34
35
  import type { AgentConfig } from "./llm-router.js";
36
+ import type { CanvasClass, DesignMode } from "../design-mode.js";
35
37
 
36
38
  // Re-export the boilerplate generator so existing internal importers
37
39
  // (create-harness.ts, benchmarks/preview-boilerplate.ts) keep working.
@@ -82,6 +84,19 @@ export interface SingleComponentParams {
82
84
  passThreshold?: number;
83
85
  sampleProps?: JsonObject;
84
86
  viewport?: { width: number; height: number };
87
+ /**
88
+ * Per-canvas judging — see `VisualEvalConfig.canvases`. When set, the
89
+ * eval round screenshots + judges once per class and the harness
90
+ * stamps `EvalResult.visual` with the PNG-free summary. Unset =
91
+ * today's single screenshot at `viewport`.
92
+ */
93
+ canvases?: readonly CanvasClass[];
94
+ /**
95
+ * The stylesheet the in-loop visual round renders under (a caller's
96
+ * theme composed by `cssTokensForAppTheme`); absent = the design's
97
+ * default tokens. Never part of the prompt or the identity.
98
+ */
99
+ cssTokens?: string;
85
100
  };
86
101
  onProgress?: (event: unknown) => void;
87
102
  onInitialResult?: (result: {
@@ -133,6 +148,16 @@ export function buildSystemPrompt(
133
148
  * stdlib gadgets do not need one.
134
149
  */
135
150
  gadgetTypes?: Readonly<Record<string, string>>,
151
+ /**
152
+ * Which triad to teach — see `SystemPromptInputs.designMode`. Omitted
153
+ * (every pre-existing caller) means `constrained`, byte-identical to
154
+ * the pre-`designMode` prompt.
155
+ */
156
+ designMode?: DesignMode,
157
+ /** Rendering canvas class (`free` mode) — see `SystemPromptInputs.canvas`. */
158
+ canvas?: CanvasClass,
159
+ /** The app's generation profile (#991). */
160
+ profile?: GenerationProfileInput,
136
161
  ): string {
137
162
  return buildSystemPromptSkeleton({
138
163
  userRequest,
@@ -145,6 +170,9 @@ export function buildSystemPrompt(
145
170
  wireDoc: WIRE_DOCUMENTATION,
146
171
  appGadgets,
147
172
  gadgetTypes,
173
+ designMode,
174
+ canvas,
175
+ profile,
148
176
  // criteriaBlock left undefined — ui-gen fills default from open CRITERIA.
149
177
  });
150
178
  }
@@ -18,12 +18,14 @@
18
18
  // types hub for the harness module. `AxisCheck` + `AxisCheckInput` live
19
19
  // in `@ggui-ai/ui-gen/evaluation`.
20
20
 
21
+ import type { GenerationProfileInput } from "../boilerplate/styling-profile.js";
21
22
  import type { GadgetDescriptor, DataContract, JsonValue } from "@ggui-ai/protocol";
22
23
  import type { Classification } from "../classifier/axes.js";
23
24
  import type { EvalIssue, EvalTier, AxisCheck, RuntimeProbeStatus } from "../evaluation/types-public.js";
24
25
  import type { LLMToolDef } from "../llm.js";
25
26
  import type { CacheTier, HarnessFragment } from "../fragments/index.js";
26
27
  import type { HarnessPolicy, ProcessMode } from "../policy.js";
28
+ import type { CanvasClass, DesignMode } from "../design-mode.js";
27
29
 
28
30
  export type { ProcessMode };
29
31
 
@@ -293,6 +295,16 @@ export interface Harness {
293
295
  readonly id: HarnessId;
294
296
  readonly name: HarnessName;
295
297
  readonly classification: Classification;
298
+ /**
299
+ * Which triad this harness materialised — threaded from
300
+ * `CreateHarnessInput.designMode` into the HOW leg (prompt), the WHAT
301
+ * leg (boilerplate) and every CHECK consumer (tier-0 gate, axis
302
+ * checks, evaluator criteria). `constrained` unless the caller opted
303
+ * into `free`.
304
+ */
305
+ readonly designMode: DesignMode;
306
+ /** The app's generation profile in force for this harness (#991), if any. */
307
+ readonly profile?: GenerationProfileInput;
296
308
 
297
309
  readonly how: HowLeg;
298
310
  readonly what: WhatLeg;
@@ -383,6 +395,12 @@ export type SystemPromptBuilder = (input: {
383
395
  readonly shellType?: string;
384
396
  readonly screen?: string;
385
397
  readonly axisDelta?: string;
398
+ /** Which triad to teach — see `CreateHarnessInput.designMode`. */
399
+ readonly designMode?: DesignMode;
400
+ /** Rendering canvas class (`free` mode) — see `CreateHarnessInput.canvas`. */
401
+ readonly canvas?: CanvasClass;
402
+ /** The app's generation profile (#991) — see `CreateHarnessInput.profile`. */
403
+ readonly profile?: GenerationProfileInput;
386
404
  }) => string;
387
405
 
388
406
  export interface CreateHarnessInput {
@@ -392,6 +410,26 @@ export interface CreateHarnessInput {
392
410
  readonly shellType?: "chat" | "fullscreen" | "spatial";
393
411
  readonly screen?: "mobile" | "tablet" | "desktop" | "universal";
394
412
  readonly overrides?: HarnessOverrides;
413
+ /**
414
+ * Which triad to materialise. `constrained` (default) is today's
415
+ * design-system triad, byte-identical to the pre-`designMode`
416
+ * harness (same prompt, boilerplate, tier-0 legs, criteria and id).
417
+ * `free` relaxes only the design vocabulary; every hard contract
418
+ * stays. See `../design-mode.ts`.
419
+ */
420
+ readonly designMode?: DesignMode;
421
+ /**
422
+ * Rendering canvas class the `free` prompt states explicitly. When
423
+ * omitted the prompt derives it from `shellType` × `screen`. Ignored
424
+ * by the `constrained` prompt.
425
+ */
426
+ readonly canvas?: CanvasClass;
427
+ /**
428
+ * The app's generation profile (#991): operator-declared free text the
429
+ * HOW leg renders as one bounded section and every judge reads under its
430
+ * own frame. Absent = byte-identical prompts.
431
+ */
432
+ readonly profile?: GenerationProfileInput;
395
433
  /**
396
434
  * Pre-resolved static harness policy. When omitted, `createHarness`
397
435
  * falls back to `DEFAULT_HARNESS_POLICY` from `@ggui-ai/ui-gen/policy`.
package/src/index.ts CHANGED
@@ -21,6 +21,7 @@ export {
21
21
  } from './create-ui-generator.js';
22
22
  export type { CreateUiGeneratorOptions } from './create-ui-generator.js';
23
23
  export type { ProviderRetryInfo } from './harness/llm-router.js';
24
+ export type { GenerationResult } from './harness/result-types.js';
24
25
 
25
26
  // Generator registry seam re-export. The interface + slug helpers live
26
27
  // in `@ggui-ai/mcp-server-core`; the in-memory factory lives in
@@ -49,6 +50,31 @@ export {
49
50
  injectVariance,
50
51
  } from './contract-context.js';
51
52
  export type { RenderingContext } from './contract-context.js';
53
+ export { canvasForRenderingContext } from './contract-context.js';
54
+
55
+ // ── Design mode ──────────────────────────────────────────────────────
56
+ //
57
+ // `createUiGenerator({ designMode })` — `constrained` (default) or
58
+ // `free`; the canvas vocabulary the `free` prompt states.
59
+ export {
60
+ DESIGN_MODES,
61
+ DEFAULT_DESIGN_MODE,
62
+ CANVAS_CLASSES,
63
+ CANVAS_VIEWPORTS,
64
+ CANVAS_BREAKPOINTS,
65
+ CANVAS_DESCRIPTORS,
66
+ canvasDisplayModeForShell,
67
+ canvasForRendering,
68
+ canvasForViewportWidth,
69
+ describeCanvas,
70
+ } from './design-mode.js';
71
+ export type {
72
+ DesignMode,
73
+ CanvasClass,
74
+ CanvasViewport,
75
+ CanvasDisplayMode,
76
+ CanvasDescriptor,
77
+ } from './design-mode.js';
52
78
 
53
79
  // ── Anthropic SDK construction ──────────────────────────────────────
54
80
  //
@@ -56,3 +82,5 @@ export type { RenderingContext } from './contract-context.js';
56
82
  // go through this helper instead of `new Anthropic(...)` so future
57
83
  // header / baseURL tweaks land in one place.
58
84
  export { createAnthropicClient } from './adapters/claude/client.js';
85
+ export { AESTHETIC_BRIEF_MAX_CHARS, type GenerationProfileInput } from './boilerplate/styling-profile.js';
86
+ export { EFFORT_DIALS, EFFORT_TABLE_VERSION, effortDials, type EffortDials } from './effort.js';
@@ -419,7 +419,7 @@ export function extractCallSignaturesFromDts(
419
419
  return {};
420
420
  }
421
421
 
422
- const cacheKey = `${dtsContent}${[...hookNames].sort().join(',')}`;
422
+ const cacheKey = `${dtsContent}\u0000${[...hookNames].sort().join(',')}`;
423
423
  const cached = callSignatureCache.get(cacheKey);
424
424
  if (cached !== undefined) return cached;
425
425
 
@@ -470,7 +470,7 @@ export function extractComponentPropsFromDts(
470
470
  return {};
471
471
  }
472
472
 
473
- const cacheKey = `${dtsContent}${[...componentNames].sort().join(',')}`;
473
+ const cacheKey = `${dtsContent}\u0000${[...componentNames].sort().join(',')}`;
474
474
  const cached = componentPropsCache.get(cacheKey);
475
475
  if (cached !== undefined) return cached;
476
476
 
@@ -0,0 +1,98 @@
1
+ /**
2
+ * The two fixed fixtures every prompt pin renders — one action-bearing
3
+ * `chat`×`mobile` shell, one `fullscreen`×`desktop` board with a
4
+ * `contextSpec` — shared by `design-mode.pin.test.ts` (the stable prefix)
5
+ * and `shape-guidance.pin.test.ts` (the axis-conditioned Shape Guidance).
6
+ */
7
+ import type { DataContract } from '@ggui-ai/protocol';
8
+
9
+ export interface PinFixture {
10
+ readonly userRequest: string;
11
+ readonly shellType: 'chat' | 'fullscreen' | 'spatial';
12
+ readonly screen: 'mobile' | 'tablet' | 'desktop' | 'universal';
13
+ readonly contract: DataContract;
14
+ }
15
+
16
+ export const PIN_FIXTURES: readonly PinFixture[] = [
17
+ {
18
+ userRequest: 'A todo list where each item can be toggled done',
19
+ shellType: 'chat',
20
+ screen: 'mobile',
21
+ contract: {
22
+ propsSpec: {
23
+ properties: {
24
+ todos: {
25
+ required: true,
26
+ description: 'The todos to show',
27
+ schema: {
28
+ type: 'array',
29
+ items: {
30
+ type: 'object',
31
+ properties: {
32
+ id: { type: 'string' },
33
+ title: { type: 'string' },
34
+ done: { type: 'boolean' },
35
+ },
36
+ },
37
+ },
38
+ },
39
+ },
40
+ },
41
+ actionSpec: {
42
+ toggleTodo: {
43
+ label: 'Toggle',
44
+ description: 'Flip the done state of one todo',
45
+ schema: {
46
+ type: 'object',
47
+ properties: { id: { type: 'string' } },
48
+ required: ['id'],
49
+ },
50
+ },
51
+ },
52
+ },
53
+ },
54
+ {
55
+ userRequest: 'A kanban board with live task updates and a selected card',
56
+ shellType: 'fullscreen',
57
+ screen: 'desktop',
58
+ contract: {
59
+ propsSpec: {
60
+ properties: {
61
+ columns: {
62
+ required: true,
63
+ schema: { type: 'array', items: { type: 'string' } },
64
+ },
65
+ tasks: {
66
+ required: false,
67
+ schema: {
68
+ type: 'array',
69
+ items: {
70
+ type: 'object',
71
+ properties: {
72
+ id: { type: 'string' },
73
+ column: { type: 'string' },
74
+ title: { type: 'string' },
75
+ },
76
+ },
77
+ },
78
+ },
79
+ },
80
+ },
81
+ streamSpec: {
82
+ taskUpdates: {
83
+ description: 'Task CRUD feed',
84
+ schema: {
85
+ type: 'object',
86
+ properties: {
87
+ action: { type: 'string', enum: ['create', 'move', 'edit', 'delete'] },
88
+ id: { type: 'string' },
89
+ },
90
+ },
91
+ },
92
+ },
93
+ contextSpec: {
94
+ selectedId: { schema: { type: 'string', nullable: true } },
95
+ },
96
+ },
97
+ },
98
+ ];
@@ -2,13 +2,13 @@
2
2
  * Concrete AWS Bedrock `ProviderAdapter` — invokes Anthropic Claude
3
3
  * models on Bedrock via the official `@anthropic-ai/bedrock-sdk`
4
4
  * package. IAM-based auth (no API key in flight); the AWS credential
5
- * chain (IRSA pod token / `~/.aws/credentials` / env vars) supplies
5
+ * chain (a workload-identity token / `~/.aws/credentials` / env vars) supplies
6
6
  * SigV4 signatures automatically.
7
7
  *
8
8
  * ## Why this adapter exists
9
9
  *
10
10
  * Anyone running a one-shot LLM caller on an AWS-credentialed host
11
- * (EC2, ECS, Lambda, EKS) can target Bedrock without managing API keys: IAM is
11
+ * (EC2, ECS, Lambda and the like) can target Bedrock without managing API keys: IAM is
12
12
  * the auth boundary, AWS rotates host credentials automatically, and
13
13
  * a misconfigured IAM role surfaces as a clear `AccessDeniedException`
14
14
  * the SDK funnels through `mapError`. The alternative — parking a
@@ -165,8 +165,8 @@ export function bedrockEndpointFor(model: string): BedrockEndpoint {
165
165
  * Constructor options for the Bedrock adapter.
166
166
  *
167
167
  * `region` is the only required option in the common case — IAM
168
- * credentials come from the standard AWS chain (IRSA pod token in
169
- * EKS, instance role on EC2, env vars or shared credentials file
168
+ * credentials come from the standard AWS chain (a workload-identity token in
169
+ * a container cluster, instance role on EC2, env vars or shared credentials file
170
170
  * locally). Tests pass `clientFactory` to inject a mock SDK client.
171
171
  */
172
172
  export interface BedrockAdapterOptions {
package/src/run-check.ts CHANGED
@@ -22,6 +22,7 @@ import {
22
22
  newValidatorTraceId,
23
23
  truncateSourceForTrace,
24
24
  } from "./harness/validator-trace-sink.js";
25
+ import { runGatedAxisChecks, traceAxisChecksSkipped } from "./evaluation/axis-checks/dispatch.js";
25
26
 
26
27
  export interface RunCheckInput {
27
28
  readonly sourceCode: string;
@@ -66,6 +67,10 @@ export async function runCheck(input: RunCheckInput): Promise<CheckResult> {
66
67
  const startedAt = Date.now();
67
68
 
68
69
  if (compiledCode === null) {
70
+ traceAxisChecksSkipped(
71
+ { sourceCode, contract, originalPrompt: prompt, classification: harness.classification, designMode: harness.designMode },
72
+ "compiledCode null — no check ran",
73
+ );
69
74
  const result: CheckResult = {
70
75
  issues: [],
71
76
  axisIssueCount: 0,
@@ -113,17 +118,13 @@ export async function runCheck(input: RunCheckInput): Promise<CheckResult> {
113
118
  contract: contract,
114
119
  originalPrompt: prompt,
115
120
  classification: harness.classification,
121
+ designMode: harness.designMode,
116
122
  };
117
- let axisIssueCount = 0;
118
- const seenAxisIds = new Set<string>();
119
- for (const axisCheck of check.axisChecks) {
120
- if (seenAxisIds.has(axisCheck.id)) continue;
121
- seenAxisIds.add(axisCheck.id);
122
- firedIds.push(axisCheck.id);
123
- const axisIssues = axisCheck.run(axisInput);
124
- issues.push(...axisIssues);
125
- axisIssueCount += axisIssues.length;
126
- }
123
+ // The one runner `runAxisChecks` shares (ggui#1046 trace prints here, on the served path).
124
+ const axisRun = runGatedAxisChecks(check.axisChecks, axisInput);
125
+ issues.push(...axisRun.issues);
126
+ firedIds.push(...axisRun.firedIds);
127
+ const axisIssueCount = axisRun.issues.length;
127
128
 
128
129
  // ── Tier checks (deterministic, harness-owned) ────────────────────────
129
130
  let tierIssueCount = 0;