@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.
- package/dist/adapters/generation-dispatch.d.ts +24 -4
- package/dist/adapters/generation-dispatch.js +5188 -3920
- package/dist/adapters/generation-dispatch.js.map +1 -1
- package/dist/adapters/index.js +33 -26
- package/dist/adapters/index.js.map +1 -1
- package/dist/advanced/index.d.ts +22 -13
- package/dist/advanced/index.js +4889 -3561
- package/dist/advanced/index.js.map +1 -1
- package/dist/blueprint-validator.js +8 -12
- package/dist/blueprint-validator.js.map +1 -1
- package/dist/boilerplate.d.ts +19 -1
- package/dist/boilerplate.js +671 -254
- package/dist/boilerplate.js.map +1 -1
- package/dist/check/index.d.ts +17 -3
- package/dist/check/index.js +34 -25
- package/dist/check/index.js.map +1 -1
- package/dist/compose.js +1 -1
- package/dist/compose.js.map +1 -1
- package/dist/create-ui-generator-C8Ki9Wz1.d.ts +189 -0
- package/dist/design-mode-DLufhrNp.d.ts +61 -0
- package/dist/{dispatch-BuZfTh9r.d.ts → dispatch-D2htzi9h.d.ts} +4 -1
- package/dist/evaluation/axis-checks/index.d.ts +4 -3
- package/dist/evaluation/axis-checks/index.js +300 -14
- package/dist/evaluation/axis-checks/index.js.map +1 -1
- package/dist/evaluation/axis-checks/registry.d.ts +2 -1
- package/dist/evaluation/axis-checks/registry.js +253 -1
- package/dist/evaluation/axis-checks/registry.js.map +1 -1
- package/dist/evaluation/index.d.ts +297 -5
- package/dist/evaluation/index.js +4515 -120
- package/dist/evaluation/index.js.map +1 -1
- package/dist/fragments/index.js +1 -1
- package/dist/fragments/index.js.map +1 -1
- package/dist/{generate-BTfvELgl.d.ts → generate-9IeNT4ZY.d.ts} +10 -1
- package/dist/harness/check/runtime-render/index.d.ts +3 -1
- package/dist/harness/check/runtime-render/index.js +8 -12
- package/dist/harness/check/runtime-render/index.js.map +1 -1
- package/dist/harness/index.d.ts +3 -1
- package/dist/harness/index.js +913 -441
- package/dist/harness/index.js.map +1 -1
- package/dist/harness/llm-trace-sink.d.ts +3 -3
- package/dist/harness/llm-trace-sink.js.map +1 -1
- package/dist/harness/prompts.d.ts +2 -1
- package/dist/harness/prompts.js +6 -3
- package/dist/harness/prompts.js.map +1 -1
- package/dist/harness/result-types.d.ts +17 -5
- package/dist/harness/result-types.js.map +1 -1
- package/dist/harness/runtime.d.ts +28 -3
- package/dist/harness/runtime.js +702 -274
- package/dist/harness/runtime.js.map +1 -1
- package/dist/harness/types-public.d.ts +39 -1
- package/dist/harness/types-public.js +913 -441
- package/dist/harness/types-public.js.map +1 -1
- package/dist/harness/validator-trace-sink.d.ts +2 -1
- package/dist/index.d.ts +32 -6
- package/dist/index.js +4899 -3560
- package/dist/index.js.map +1 -1
- package/dist/patch.d.ts +3 -1
- package/dist/{prompts-BVhtH3M1.d.ts → prompts-YU9piOgs.d.ts} +21 -45
- package/dist/providers/index.d.ts +2 -2
- package/dist/providers/index.js.map +1 -1
- package/dist/styling-profile-CxfvV48g.d.ts +35 -0
- package/dist/tools/get-primitives-ts.d.ts +1 -1
- package/dist/tools/get-primitives-ts.js +1 -1
- package/dist/tools/get-primitives-ts.js.map +1 -1
- package/dist/{types-public-CkNWEov9.d.ts → types-public-DVSssdc-.d.ts} +85 -5
- package/dist/validation/index.d.ts +1 -1
- package/dist/validation/index.js +4 -4
- package/dist/validation/index.js.map +1 -1
- package/dist/workflows.d.ts +3 -1
- package/package.json +12 -12
- package/src/adapters/generation-dispatch.ts +62 -4
- package/src/adapters/provider-router.ts +7 -6
- package/src/adapters/tools.ts +5 -5
- package/src/advanced/generator.ts +11 -10
- package/src/advanced/index.ts +5 -4
- package/src/boilerplate/free-design-prompt.ts +336 -0
- package/src/boilerplate/generate.ts +10 -1
- package/src/boilerplate/hard-sections.ts +257 -0
- package/src/boilerplate/render.ts +32 -8
- package/src/boilerplate/styling-profile.ts +166 -0
- package/src/boilerplate/system-prompt.ts +101 -237
- package/src/boilerplate/templates/base-free.tsx.tmpl +24 -0
- package/src/check/run-tier0.ts +82 -20
- package/src/coding-agent/tools.ts +8 -2
- package/src/contract-context.ts +43 -14
- package/src/create-harness.ts +15 -0
- package/src/create-ui-generator.ts +112 -27
- package/src/design-mode.ts +192 -0
- package/src/design-system-docs.ts +24 -17
- package/src/effort.ts +48 -0
- package/src/evaluation/axis-checks/checks/grid.ts +148 -0
- package/src/evaluation/axis-checks/checks/universal.ts +244 -0
- package/src/evaluation/axis-checks/dispatch.ts +82 -13
- package/src/evaluation/axis-checks/extras.ts +5 -0
- package/src/evaluation/axis-checks/helpers.ts +12 -0
- package/src/evaluation/axis-checks/registry.ts +2 -0
- package/src/evaluation/cost-tracker.ts +2 -3
- package/src/evaluation/design-identity.ts +96 -0
- package/src/evaluation/index.ts +63 -2
- package/src/evaluation/llm-evaluator.ts +89 -21
- package/src/evaluation/theme-css.ts +14 -0
- package/src/evaluation/types-public.ts +137 -8
- package/src/evaluation/visual-evaluator.ts +759 -54
- package/src/fragments/render.ts +1 -1
- package/src/harness/check/runtime-render/prepare-mockup.ts +18 -11
- package/src/harness/coding/dupe-break.ts +0 -0
- package/src/harness/coding/init-session.ts +2 -0
- package/src/harness/coding/run-coding-turn.ts +9 -14
- package/src/harness/coding/run-eval-round.ts +46 -5
- package/src/harness/llm-trace-sink.ts +3 -3
- package/src/harness/result-types.ts +15 -3
- package/src/harness/runtime.ts +28 -0
- package/src/harness/types-public.ts +38 -0
- package/src/index.ts +28 -0
- package/src/internal/extract-call-signatures.ts +2 -2
- package/src/pin-fixtures.ts +98 -0
- package/src/providers/bedrock.ts +4 -4
- package/src/run-check.ts +11 -10
- package/src/tools/get-primitives-ts.ts +1 -1
- package/src/tools.ts +2 -1
- package/src/validation/allowed-imports.ts +8 -2
- package/src/validation/primitives.ts +1 -1
- package/dist/create-ui-generator-D6TKuv9V.d.ts +0 -106
package/src/check/run-tier0.ts
CHANGED
|
@@ -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
|
|
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 (
|
|
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
|
|
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:
|
|
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
|
|
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-
|
|
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 =
|
|
631
|
-
|
|
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-
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
-
|
|
756
|
-
|
|
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': {
|
package/src/contract-context.ts
CHANGED
|
@@ -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.
|
|
42
|
-
*
|
|
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
|
|
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:
|
|
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
|
|
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(
|
|
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
|
}
|
package/src/create-harness.ts
CHANGED
|
@@ -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,
|