@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
@@ -16,6 +16,7 @@ import ts from 'typescript';
16
16
  import type { DataContract } from '@ggui-ai/protocol';
17
17
  import { HOOK_NAME_RE, listContractGadgets } from '@ggui-ai/protocol';
18
18
  import { consumedTokenManifest } from '@ggui-ai/design/themes';
19
+ import { DEFAULT_DESIGN_MODE, type DesignMode } from '../design-mode.js';
19
20
  import { validateAllContracts, type ContractIssue } from './contract-validation.js';
20
21
  import { typecheck } from './type-checker.js';
21
22
  import { lintReactHooks, type ReactLintDiagnostic } from './react-linter.js';
@@ -95,10 +96,22 @@ function stripComments(code: string): string {
95
96
  function detectCertainDoubleWiredActions(
96
97
  sourceCode: string,
97
98
  actionBindings: readonly string[],
99
+ /**
100
+ * `constrained` keeps the design-package tag sets above. `free`
101
+ * retargets the SAME invariant ("one useAction binding → exactly one
102
+ * gesture surface") at raw elements: the outer host is ANY JSX element
103
+ * whose onClick/onPress calls a binding (`<div onClick>`, `<li>`,
104
+ * `<tr>`, `<button>`, `<a>`, `[role=button]`, a design primitive…),
105
+ * and the inner match is ANY descendant whose handler attribute calls
106
+ * the same binding. Tag names stop mattering because in free mode a
107
+ * raw element is a first-class gesture surface.
108
+ */
109
+ designMode: DesignMode = DEFAULT_DESIGN_MODE,
98
110
  ): EvalIssue[] {
99
111
  const issues: EvalIssue[] = [];
100
112
  if (actionBindings.length === 0) return issues;
101
113
  const bindings = new Set(actionBindings);
114
+ const anyElementIsInteractive = designMode === 'free';
102
115
  const TRAIT_HOST_TAGS = new Set(['Card', 'Box', 'Stack', 'Row']);
103
116
  const TRAITS_THAT_FIRE = new Set(['Clickable', 'Pressable']);
104
117
  const INTERACTIVE_DESCENDANTS = new Set([
@@ -148,7 +161,11 @@ function detectCertainDoubleWiredActions(
148
161
 
149
162
  function isTraitHostWithFiringAs(node: ts.JsxOpeningElement): boolean {
150
163
  const tag = tagNameText(node);
151
- if (tag === undefined || !TRAIT_HOST_TAGS.has(tag)) return false;
164
+ if (tag === undefined) return false;
165
+ // Free mode: every element with a handler is a gesture surface —
166
+ // the callee check in `visit` decides whether it fires a binding.
167
+ if (anyElementIsInteractive) return true;
168
+ if (!TRAIT_HOST_TAGS.has(tag)) return false;
152
169
  const asExpr = attrExpression(node, 'as');
153
170
  return (
154
171
  asExpr !== undefined &&
@@ -206,7 +223,10 @@ function detectCertainDoubleWiredActions(
206
223
  : undefined;
207
224
  if (opening !== undefined) {
208
225
  const tag = tagNameText(opening);
209
- if (tag !== undefined && INTERACTIVE_DESCENDANTS.has(tag)) {
226
+ if (
227
+ tag !== undefined &&
228
+ (anyElementIsInteractive || INTERACTIVE_DESCENDANTS.has(tag))
229
+ ) {
210
230
  for (const attrName of HANDLER_ATTRS) {
211
231
  const callee = extractCalleeName(attrExpression(opening, attrName));
212
232
  if (callee === expectedBinding) {
@@ -239,6 +259,9 @@ function detectCertainDoubleWiredActions(
239
259
  const outerLine =
240
260
  sf.getLineAndCharacterOfPosition(node.openingElement.getStart())
241
261
  .line + 1;
262
+ const outerLabel = anyElementIsInteractive
263
+ ? `<${outerTag}>`
264
+ : `<${outerTag} as={...}>`;
242
265
  issues.push({
243
266
  tier: 0,
244
267
  result: 'fail',
@@ -246,9 +269,11 @@ function detectCertainDoubleWiredActions(
246
269
  subcategory: 'double-wired-action:certain',
247
270
  severity: 'critical',
248
271
  description:
249
- `Nested-interactive double-wire: outer <${outerTag} as={...}> at line ${outerLine} and inner <${match.tag}> at line ${match.line} both dispatch the same useAction binding '${outerCallee}'. ` +
272
+ `Nested-interactive double-wire: outer ${outerLabel} at line ${outerLine} and inner <${match.tag}> at line ${match.line} both dispatch the same useAction binding '${outerCallee}'. ` +
250
273
  `One user click on the inner control fires its handler AND bubbles to the outer handler — '${outerCallee}' dispatches TWICE, the action runs back-to-back, and a toggle-style action silently reverts the user's change.`,
251
- fix: `Pick ONE surface for the gesture: either drop \`as={...}\` + onClick on the outer <${outerTag}> and let the inner <${match.tag}> own the gesture, OR remove the inner <${match.tag}> and let the outer <${outerTag} as={...}> own it. Don't wire both to the same useAction binding.`,
274
+ fix: anyElementIsInteractive
275
+ ? `Pick ONE gesture surface for '${outerCallee}': either remove the onClick from the outer <${outerTag}> and let the inner <${match.tag}> own the gesture, OR drop the inner <${match.tag}>'s handler and let the outer <${outerTag}> own it. Never wire both to the same useAction binding — stopPropagation is not a fix.`
276
+ : `Pick ONE surface for the gesture: either drop \`as={...}\` + onClick on the outer <${outerTag}> and let the inner <${match.tag}> own the gesture, OR remove the inner <${match.tag}> and let the outer <${outerTag} as={...}> own it. Don't wire both to the same useAction binding.`,
252
277
  line: outerLine,
253
278
  });
254
279
  }
@@ -346,9 +371,25 @@ export async function runTier0Checks(
346
371
  * narrowing). Standard-library-only callers omit it.
347
372
  */
348
373
  gadgetTypes?: Readonly<Record<string, string>>,
374
+ /**
375
+ * Which triad produced the source. `constrained` (default) runs every
376
+ * leg exactly as before. `free` gates OFF the design-VOCABULARY legs —
377
+ * `raw-spacing`, `raw-pixels`, `numeric-spacing-prop`, the
378
+ * `stack-row-*` prop checks and `clickable-wrapper` — because raw
379
+ * elements, inline CSS and literal spacing are first-class there. It
380
+ * KEEPS every contract leg: `hex-color`, `hardcoded-color-fn`,
381
+ * `named-color`, `token-fallback`, `off-manifest-token`,
382
+ * `asset-color-pair` (color stays on the closed token manifest),
383
+ * imports, security, Props / default-export, wire_* and gadget checks,
384
+ * and the double-wired-action detector (retargeted at raw elements).
385
+ */
386
+ designMode: DesignMode = DEFAULT_DESIGN_MODE,
349
387
  ): Promise<EvalIssue[]> {
350
388
  const issues: EvalIssue[] = [];
351
389
  const lines = sourceCode.split('\n');
390
+ // Design-vocabulary legs fire only when the prompt taught the
391
+ // vocabulary — see `designMode` above.
392
+ const enforceDesignVocabulary = designMode === 'constrained';
352
393
 
353
394
  // ── Compile check ─────────────────────────────────────────
354
395
  //
@@ -371,6 +412,7 @@ export async function runTier0Checks(
371
412
  tier: 0,
372
413
  result: 'fail',
373
414
  category: 'compile',
415
+ subcategory: 'build-error',
374
416
  severity: 'critical',
375
417
  description: `Component failed to compile:\n${errorDetail}`,
376
418
  fix: 'Fix the JSX/TypeScript syntax errors listed above',
@@ -494,6 +536,7 @@ export async function runTier0Checks(
494
536
  tier: 0,
495
537
  result: 'fail',
496
538
  category: 'imports',
539
+ subcategory: 'disallowed-package',
497
540
  severity: 'critical',
498
541
  description: `Import from "${pkg}" is not allowed`,
499
542
  fix: `Only import from: ${describeAllowedImports()}`,
@@ -591,7 +634,7 @@ export async function runTier0Checks(
591
634
  subcategory: 'hardcoded-color-fn',
592
635
  severity: 'critical',
593
636
  description: `Hardcoded ${colorFnMatch[1]}() breaks theme switching — use design tokens.`,
594
- fix: 'Replace with a primitive variant OR a semantic token: var(--ggui-color-surface), var(--ggui-color-onSurface), var(--ggui-color-outline), etc.',
637
+ fix: 'Replace with a primitive variant OR a layering role: var(--ggui-color-container), var(--ggui-color-onContainer), var(--ggui-color-outline), etc.',
595
638
  line: lineNum,
596
639
  });
597
640
  }
@@ -616,7 +659,7 @@ export async function runTier0Checks(
616
659
  subcategory: 'off-manifest-token',
617
660
  severity: 'critical',
618
661
  description: `Token "${name}" is not in the design system's token manifest — no theme defines it, so it renders unset.`,
619
- fix: `Choose a token that exists: the closed vocabulary is the manifest the design-system docs list (colors: var(--ggui-color-primary-500), var(--ggui-color-onSurface), …; spacing: var(--ggui-spacing-4); type: var(--ggui-font-size-sm)). Never invent a name and never add a literal fallback.`,
662
+ fix: `Choose a token that exists: the closed vocabulary is the manifest the design-system docs list (colors: var(--ggui-color-primary-500), var(--ggui-color-onContainer), …; spacing: var(--ggui-spacing-4); type: var(--ggui-font-size-sm)). Never invent a name and never add a literal fallback.`,
620
663
  line: lineNum,
621
664
  });
622
665
  }
@@ -627,9 +670,11 @@ export async function runTier0Checks(
627
670
  // `#fff` defeats its colors. Only the STRING-literal form fails;
628
671
  // the numeric escape (`gap={12}`) is untouched, so a genuine
629
672
  // off-scale pixel value still has a path.
630
- const rawSpacingMatch = line.match(
631
- /\b(gap|padding|paddingX|paddingY|margin|radius)\s*=\s*["'][\d.]+(?:px|rem|em)["']/,
632
- );
673
+ const rawSpacingMatch = enforceDesignVocabulary
674
+ ? line.match(
675
+ /\b(gap|padding|paddingX|paddingY|margin|radius)\s*=\s*["'][\d.]+(?:px|rem|em)["']/,
676
+ )
677
+ : null;
633
678
  if (rawSpacingMatch) {
634
679
  issues.push({
635
680
  tier: 0,
@@ -693,14 +738,16 @@ export async function runTier0Checks(
693
738
  subcategory: 'named-color',
694
739
  severity: 'critical',
695
740
  description: `Hardcoded CSS named color "${named}" breaks theme switching — use design tokens.`,
696
- fix: `Replace with a typed slot (Text tone="muted" / Box surface="accent" / Badge variant="success") OR a semantic token: var(--ggui-color-onSurfaceVariant), var(--ggui-color-primary-500), etc. The keyword "${named}" maps to a fixed RGB; the operator's theme has no effect on it.`,
741
+ fix: `Replace with a typed slot (Text tone="muted" / Box surface="accent" / Badge variant="success") OR a semantic token: var(--ggui-color-onSunken), var(--ggui-color-primary-500), etc. The keyword "${named}" maps to a fixed RGB; the operator's theme has no effect on it.`,
697
742
  line: lineNum,
698
743
  });
699
744
  }
700
745
  }
701
746
 
702
747
  // Raw pixel values in spacing CSS properties
703
- const pxMatch = line.match(/(?:padding|margin|gap|borderRadius)\s*:\s*['"]?\d+px/);
748
+ const pxMatch = enforceDesignVocabulary
749
+ ? line.match(/(?:padding|margin|gap|borderRadius)\s*:\s*['"]?\d+px/)
750
+ : null;
704
751
  if (pxMatch && !line.includes('var(--ggui-')) {
705
752
  issues.push({
706
753
  tier: 0,
@@ -708,14 +755,16 @@ export async function runTier0Checks(
708
755
  category: 'tokens',
709
756
  subcategory: 'raw-pixels',
710
757
  description: 'Raw pixel value in spacing — must use design tokens',
711
- fix: 'Replace with var(--ggui-spacing-*, fallback)',
758
+ fix: 'Replace with a bare var(--ggui-spacing-*) token — no literal fallback (the runtime injects every token) — or the scale name on the primitive prop (gap="md", padding="lg")',
712
759
  line: lineNum,
713
760
  });
714
761
  }
715
762
 
716
763
  // Numeric spacing props on components (e.g., padding={24}, gap={8})
717
764
  // These bypass the design system — should use var(--ggui-spacing-*)
718
- const numericSpacingMatch = line.match(/\b(padding|paddingX|paddingY|gap|margin)\s*=\s*\{?\s*(\d+)\s*\}?/);
765
+ const numericSpacingMatch = enforceDesignVocabulary
766
+ ? line.match(/\b(padding|paddingX|paddingY|gap|margin)\s*=\s*\{?\s*(\d+)\s*\}?/)
767
+ : null;
719
768
  if (numericSpacingMatch && !line.includes('var(--ggui-')) {
720
769
  issues.push({
721
770
  tier: 0,
@@ -735,7 +784,7 @@ export async function runTier0Checks(
735
784
  // onClick on them is a plain type error the type-checker catches — don't
736
785
  // steer those toward `as={Clickable}` (it won't compile on them).
737
786
  const clickablePattern = /<(?:Card|Box|Stack|Row)\s[^>]*onClick/;
738
- for (let i = 0; i < lines.length; i++) {
787
+ for (let i = 0; enforceDesignVocabulary && i < lines.length; i++) {
739
788
  if (clickablePattern.test(lines[i]) && !lines[i].includes('as={Clickable}') && !lines[i].includes('as={Pressable}')) {
740
789
  issues.push({
741
790
  tier: 0, result: 'warn', category: 'contract', subcategory: 'clickable-wrapper',
@@ -752,9 +801,21 @@ export async function runTier0Checks(
752
801
  // another (the inner gesture bubbles to the outer handler). Captured
753
802
  // in scenario-07: a `Card as={Clickable} onClick` row containing a
754
803
  // `Checkbox onChange`, both calling the same action → double-toggle.
755
- const actionBindings = [
756
- ...sourceCode.matchAll(/(?:const|let)\s+(\w+)\s*=\s*useAction\s*\(/g),
757
- ]
804
+ //
805
+ // Binding extraction. The constrained regex is the pre-`designMode`
806
+ // one, kept verbatim: it matches `useAction(` only, so a binding
807
+ // declared with the generic the boilerplate itself emits —
808
+ // `useAction<ActionXPayload>('x')` — is NOT collected there (a
809
+ // pre-existing gap, left in place because the constrained gate must
810
+ // stay byte-identical for the A/B; collapse the two regexes once that
811
+ // pin is lifted). `free` mode accepts the optional generic so the
812
+ // one-gesture-surface invariant actually fires on boilerplate-shaped
813
+ // hook lines.
814
+ const bindingRe =
815
+ designMode === 'free'
816
+ ? /(?:const|let)\s+(\w+)\s*=\s*useAction\s*(?:<[^>]*>)?\s*\(/g
817
+ : /(?:const|let)\s+(\w+)\s*=\s*useAction\s*\(/g;
818
+ const actionBindings = [...sourceCode.matchAll(bindingRe)]
758
819
  .map((m) => m[1])
759
820
  .filter((name): name is string => typeof name === 'string');
760
821
  for (const binding of actionBindings) {
@@ -794,7 +855,7 @@ export async function runTier0Checks(
794
855
  // CAUSE one turn earlier and prevents shipping the broken a11y nest.
795
856
  if (actionBindings.length > 0) {
796
857
  issues.push(
797
- ...detectCertainDoubleWiredActions(sourceCode, actionBindings),
858
+ ...detectCertainDoubleWiredActions(sourceCode, actionBindings, designMode),
798
859
  );
799
860
  }
800
861
 
@@ -862,7 +923,7 @@ export async function runTier0Checks(
862
923
  const ALIGN_VALUES = new Set(['start', 'center', 'end', 'stretch']);
863
924
  const JUSTIFY_VALUES = new Set(['start', 'center', 'end', 'between', 'around', 'evenly']);
864
925
  const tagRegex = /<(Stack|Row)\b([^>]*)>/gs;
865
- for (const tag of sourceCode.matchAll(tagRegex)) {
926
+ for (const tag of enforceDesignVocabulary ? sourceCode.matchAll(tagRegex) : []) {
866
927
  const tagName = tag[1];
867
928
  const attrs = tag[2];
868
929
  const tagLine = sourceCode.slice(0, tag.index).split('\n').length;
@@ -1316,8 +1377,9 @@ export async function runTier0Checks(
1316
1377
  export async function runTier0(
1317
1378
  sourceCode: string,
1318
1379
  contract?: DataContract,
1380
+ designMode: DesignMode = DEFAULT_DESIGN_MODE,
1319
1381
  ): Promise<EvalResult> {
1320
- const issues = await runTier0Checks(sourceCode, contract);
1382
+ const issues = await runTier0Checks(sourceCode, contract, undefined, undefined, designMode);
1321
1383
  const pass: string[] = [];
1322
1384
 
1323
1385
  // Build pass list from categories that had no issues
@@ -18,6 +18,7 @@ import type {
18
18
  ToolResult,
19
19
  CommitMetadata,
20
20
  } from './types';
21
+ import { DEFAULT_DESIGN_MODE, type DesignMode } from '../design-mode.js';
21
22
 
22
23
  /**
23
24
  * Extract documentation for a single component from the full primitives docs.
@@ -69,6 +70,8 @@ async function autoCommit(
69
70
  * stdlib hooks unconditionally.
70
71
  */
71
72
  gadgetTypes?: Readonly<Record<string, string>>,
73
+ /** Which tier-0 legs fire — see `runTier0Checks`'s `designMode`. */
74
+ designMode: DesignMode = DEFAULT_DESIGN_MODE,
72
75
  ): Promise<ToolResult> {
73
76
  const commitStart = Date.now();
74
77
  const raw = workspace.read();
@@ -118,6 +121,7 @@ async function autoCommit(
118
121
  contract,
119
122
  buildResult.errors,
120
123
  gadgetTypes,
124
+ designMode,
121
125
  );
122
126
  const tier0Fails = tier0Issues.filter(i => i.result === 'fail');
123
127
  const selfCheckPassed = tier0Fails.length === 0;
@@ -196,6 +200,8 @@ export async function executeTool(
196
200
  * callers omit it.
197
201
  */
198
202
  gadgetTypes?: Readonly<Record<string, string>>,
203
+ /** Which tier-0 legs fire on auto-commit — see `runTier0Checks`'s `designMode`. */
204
+ designMode: DesignMode = DEFAULT_DESIGN_MODE,
199
205
  ): Promise<ToolResult> {
200
206
  switch (tool) {
201
207
  case 'write':
@@ -216,7 +222,7 @@ export async function executeTool(
216
222
  const message = (input.commit_message as string) || `write ${lineCount} lines`;
217
223
 
218
224
  console.log(`[coding-agent] write: ${lineCount} lines → auto-commit`);
219
- return autoCommit(workspace, commitMeta, message, contract, contextPolicy, gadgetTypes);
225
+ return autoCommit(workspace, commitMeta, message, contract, contextPolicy, gadgetTypes, designMode);
220
226
  }
221
227
 
222
228
  case 'apply_changes': {
@@ -523,7 +529,7 @@ export async function executeTool(
523
529
  const message = (input.commit_message as string) || 'apply changes';
524
530
 
525
531
  console.log(`[coding-agent] apply_changes: ${changes.length} changes applied → auto-commit`);
526
- return autoCommit(workspace, commitMeta, message, contract, contextPolicy, gadgetTypes);
532
+ return autoCommit(workspace, commitMeta, message, contract, contextPolicy, gadgetTypes, designMode);
527
533
  }
528
534
 
529
535
  case 'get_components_info': {
@@ -26,6 +26,8 @@
26
26
  * sizing strategies and including the hint inline cuts a class of
27
27
  * "designed for fullscreen, rendered in a chat bubble" bugs.
28
28
  */
29
+ import { STYLING_PROFILE_PRECEDENCE_NOTE } from './boilerplate/styling-profile.js';
30
+ import type { RenderingContext as ProtocolRenderingContext } from '@ggui-ai/protocol';
29
31
  import type {
30
32
  ActionEntry,
31
33
  BlueprintVariance,
@@ -35,20 +37,19 @@ import type {
35
37
  } from '@ggui-ai/protocol';
36
38
  import { HOOK_NAME_RE, listContractGadgets } from '@ggui-ai/protocol';
37
39
  import { propsSpecToTypeScript } from './check/index.js';
40
+ import {
41
+ canvasDisplayModeForShell,
42
+ canvasForViewportWidth,
43
+ type CanvasClass,
44
+ } from './design-mode.js';
38
45
 
39
46
  /**
40
47
  * Rendering context — how and where the component will be displayed.
41
- * Affects layout strategy, sizing, and interaction patterns. Mirrors the
42
- * shape cloud's harness passes through `dispatchGeneration`.
48
+ * Affects layout strategy, sizing, and interaction patterns. The ONE
49
+ * vocabulary is `@ggui-ai/protocol`'s `renderingContextSchema` (ggui#1000);
50
+ * this is its type, derived — never re-declared here.
43
51
  */
44
- export interface RenderingContext {
45
- /** Device category — affects touch targets, column count, density. */
46
- readonly device: 'mobile' | 'tablet' | 'desktop' | 'spatial';
47
- /** Shell type — the container the component renders in. */
48
- readonly shell: 'chat' | 'fullscreen' | 'partial';
49
- /** Viewport dimensions in CSS pixels (optional). */
50
- readonly viewport?: { readonly width: number; readonly height: number };
51
- }
52
+ export type RenderingContext = ProtocolRenderingContext;
52
53
 
53
54
  const SHELL_HINTS: Record<string, string> = {
54
55
  chat: `**Chat Shell — Inline card in scrolling conversation**
@@ -61,9 +62,9 @@ const SHELL_HINTS: Record<string, string> = {
61
62
  - Width: width: 100% (fills parent bubble). Do NOT set max-width.`,
62
63
 
63
64
  fullscreen: `**Fullscreen Shell — Takes over entire viewport**
64
- - Container: width: 100vw, height: 100vh (100dvh on mobile for safe areas)
65
+ - Container: the whole viewport, edge to edge — the frame stretches your root to its full height (you never set a viewport height yourself)
65
66
  - Chrome: NONE — your component IS the entire UI. You own all visual chrome.
66
- - Sizing: fill the viewport. Use min-height: 100vh or height: 100%. Edge-to-edge.
67
+ - Sizing: size to content; to fill a parent that has a height use height: 100% or flex: 1. NEVER min-height: 100vh / 100dvh — the frame already stretches the root, and a viewport height only fights it (tier-0 fail).
67
68
  - Padding: component owns ALL padding. Use var(--ggui-spacing-6) or larger for breathing room.
68
69
  - Scrolling: component manages its own scroll if content exceeds viewport (overflow-y: auto)
69
70
  - Layout: can use multi-column, sidebars, headers/footers. Full creative control.
@@ -87,6 +88,29 @@ const DEVICE_HINTS: Record<string, string> = {
87
88
  spatial: `**Spatial:** Fixed-size floating panel (~600x400). High contrast. Large text. No hover (gaze/hand input).`,
88
89
  };
89
90
 
91
+ /**
92
+ * Map a host rendering context onto the canvas class the `free` prompt
93
+ * states and the visual evaluator judges at. The class is derived from
94
+ * DISPLAY MODE + width: a `chat` shell is always the inline card
95
+ * whatever the host viewport is (the bubble, not the window, bounds the
96
+ * component); a known viewport width refines the fullscreen / partial
97
+ * shells; otherwise the device category decides.
98
+ */
99
+ export function canvasForRenderingContext(ctx: RenderingContext): CanvasClass {
100
+ const mode = canvasDisplayModeForShell(ctx.shell);
101
+ if (mode === 'inline') return 'xs-chat-card';
102
+ if (ctx.viewport) return canvasForViewportWidth(mode, ctx.viewport.width);
103
+ switch (ctx.device) {
104
+ case 'mobile':
105
+ return 'mobile-fullscreen-small';
106
+ case 'tablet':
107
+ case 'spatial':
108
+ return 'md';
109
+ case 'desktop':
110
+ return ctx.shell === 'partial' ? 'md' : 'lg';
111
+ }
112
+ }
113
+
90
114
  /** Build rendering-context block to inject into user prompt. */
91
115
  export function buildRenderingContext(ctx: RenderingContext): string {
92
116
  const parts = [
@@ -355,7 +379,10 @@ export function injectContracts(
355
379
  * Returns an empty string when no fields are populated so the inject
356
380
  * helper can no-op cleanly.
357
381
  */
358
- export function buildVarianceContext(variance: BlueprintVariance): string {
382
+ export function buildVarianceContext(
383
+ variance: BlueprintVariance,
384
+ options?: { readonly profileDeclared?: boolean },
385
+ ): string {
359
386
  const lines: string[] = [];
360
387
  if (typeof variance.persona === 'string' && variance.persona.length > 0) {
361
388
  lines.push(`- **Persona**: ${variance.persona}`);
@@ -383,6 +410,7 @@ export function buildVarianceContext(variance: BlueprintVariance): string {
383
410
  'the visual treatment + copy register — but the contract shape (props,',
384
411
  'actions, contexts, streams) remains canonical. Variance changes HOW',
385
412
  'the UI looks and reads, not WHAT it does.',
413
+ ...(options?.profileDeclared === true ? [STYLING_PROFILE_PRECEDENCE_NOTE] : []),
386
414
  '',
387
415
  ...lines,
388
416
  ].join('\n');
@@ -392,9 +420,10 @@ export function buildVarianceContext(variance: BlueprintVariance): string {
392
420
  export function injectVariance(
393
421
  userPrompt: string,
394
422
  variance?: BlueprintVariance,
423
+ options?: { readonly profileDeclared?: boolean },
395
424
  ): string {
396
425
  if (!variance) return userPrompt;
397
- const block = buildVarianceContext(variance);
426
+ const block = buildVarianceContext(variance, options);
398
427
  if (!block) return userPrompt;
399
428
  return userPrompt + '\n\n' + block;
400
429
  }
@@ -12,6 +12,7 @@
12
12
  // `adapters/generation-dispatch.ts` at the dispatch boundary; callers
13
13
  // that omit them get minimal defaults.
14
14
 
15
+ import { hasProfile } from "./boilerplate/styling-profile.js";
15
16
  import type { CacheTier } from "./fragments/index.js";
16
17
  import { compose } from "./compose.js";
17
18
  import { buildSystemPrompt, generateBoilerplate } from "./boilerplate.js";
@@ -20,6 +21,7 @@ import { defaultApplyPatch } from "./patch.js";
20
21
  import { computeHarnessId, computeHarnessName, hashClassification } from "./hash.js";
21
22
  import { pickWorkflow } from "./workflows.js";
22
23
  import { DEFAULT_HARNESS_POLICY, isDefaultHarnessPolicy } from "./policy.js";
24
+ import { DEFAULT_DESIGN_MODE } from "./design-mode.js";
23
25
  import type {
24
26
  CreateHarnessInput,
25
27
  Harness,
@@ -69,6 +71,9 @@ function countCacheTiers(fragments: readonly { cacheTier: CacheTier }[]): Record
69
71
  export function createHarness(input: CreateHarnessInput): Harness {
70
72
  const { classification, contract, prompt, shellType, screen, overrides } = input;
71
73
  const ctx: HarnessConstructionContext = { classification, contract, prompt };
74
+ // Which triad to materialise — reaches the HOW leg (prompt), the WHAT
75
+ // leg (boilerplate) and, via `harness.designMode`, every CHECK consumer.
76
+ const designMode = input.designMode ?? DEFAULT_DESIGN_MODE;
72
77
 
73
78
  // ── Compose fragments from classification ────────────────────────────────
74
79
  const composed = compose(classification);
@@ -84,6 +89,9 @@ export function createHarness(input: CreateHarnessInput): Harness {
84
89
  shellType,
85
90
  screen,
86
91
  axisDelta: composed.promptText,
92
+ designMode,
93
+ ...(input.canvas !== undefined ? { canvas: input.canvas } : {}),
94
+ ...(input.profile !== undefined ? { profile: input.profile } : {}),
87
95
  });
88
96
  const howFragments = composed.fragments.filter(
89
97
  (f) => f.promptText && f.promptText.trim().length > 0,
@@ -112,6 +120,7 @@ export function createHarness(input: CreateHarnessInput): Harness {
112
120
  screen,
113
121
  composed.boilerplateSections,
114
122
  input.appGadgets,
123
+ designMode,
115
124
  );
116
125
  const whatFragments = composed.fragments.filter(
117
126
  (f) => f.boilerplateMarker && f.boilerplateMarker.trim().length > 0,
@@ -179,6 +188,10 @@ export function createHarness(input: CreateHarnessInput): Harness {
179
188
  // Only non-default policies contribute to the harness id — preserves
180
189
  // byte-identical id across default-policy runs.
181
190
  if (!isDefaultHarnessPolicy(policy)) overrideLabels.push("policy");
191
+ // Only a non-default design mode contributes — the constrained id is
192
+ // byte-identical to the pre-`designMode` harness.
193
+ if (designMode !== DEFAULT_DESIGN_MODE) overrideLabels.push(`designMode:${designMode}`);
194
+ if (hasProfile(input.profile)) overrideLabels.push("profile");
182
195
 
183
196
  const id = computeHarnessId({
184
197
  classificationHash,
@@ -210,6 +223,8 @@ export function createHarness(input: CreateHarnessInput): Harness {
210
223
  id,
211
224
  name,
212
225
  classification,
226
+ designMode,
227
+ ...(input.profile !== undefined ? { profile: input.profile } : {}),
213
228
  how,
214
229
  what,
215
230
  check,