@ggui-ai/ui-gen 0.14.0 → 0.16.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 +21 -4
- package/dist/adapters/generation-dispatch.js +915 -351
- package/dist/adapters/generation-dispatch.js.map +1 -1
- package/dist/adapters/index.js +28 -23
- package/dist/adapters/index.js.map +1 -1
- package/dist/advanced/index.d.ts +17 -12
- package/dist/advanced/index.js +968 -375
- package/dist/advanced/index.js.map +1 -1
- package/dist/boilerplate.d.ts +16 -1
- package/dist/boilerplate.js +579 -245
- package/dist/boilerplate.js.map +1 -1
- package/dist/check/index.d.ts +17 -3
- package/dist/check/index.js +34 -26
- package/dist/check/index.js.map +1 -1
- package/dist/{create-ui-generator-D6TKuv9V.d.ts → create-ui-generator-CCIeQaEe.d.ts} +77 -10
- package/dist/design-mode-BHEE7flj.d.ts +67 -0
- package/dist/{dispatch-BuZfTh9r.d.ts → dispatch-BA5dJcg-.d.ts} +4 -1
- package/dist/evaluation/axis-checks/index.d.ts +4 -3
- package/dist/evaluation/axis-checks/index.js +3 -1
- 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 +1 -0
- package/dist/evaluation/axis-checks/registry.js.map +1 -1
- package/dist/evaluation/index.d.ts +143 -3
- package/dist/evaluation/index.js +3866 -92
- package/dist/evaluation/index.js.map +1 -1
- package/dist/{generate-BTfvELgl.d.ts → generate-8L_bc2iP.d.ts} +10 -1
- package/dist/harness/check/runtime-render/index.d.ts +2 -1
- package/dist/harness/index.d.ts +2 -1
- package/dist/harness/index.js +589 -248
- 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 +3 -0
- 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 +19 -3
- package/dist/harness/runtime.js +602 -261
- package/dist/harness/runtime.js.map +1 -1
- package/dist/harness/types-public.d.ts +28 -1
- package/dist/harness/types-public.js +589 -248
- package/dist/harness/types-public.js.map +1 -1
- package/dist/harness/validator-trace-sink.d.ts +2 -1
- package/dist/index.d.ts +8 -4
- package/dist/index.js +975 -373
- package/dist/index.js.map +1 -1
- package/dist/patch.d.ts +2 -1
- package/dist/{prompts-BVhtH3M1.d.ts → prompts-k6m6reqe.d.ts} +11 -1
- package/dist/providers/index.d.ts +2 -2
- package/dist/providers/index.js.map +1 -1
- 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-D24JLJgl.d.ts} +49 -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 +2 -1
- package/package.json +12 -12
- package/src/adapters/generation-dispatch.ts +57 -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 +329 -0
- package/src/boilerplate/generate.ts +10 -1
- package/src/boilerplate/hard-sections.ts +242 -0
- package/src/boilerplate/render.ts +32 -8
- package/src/boilerplate/system-prompt.ts +81 -229
- package/src/boilerplate/templates/base-free.tsx.tmpl +24 -0
- package/src/check/run-tier0.ts +80 -20
- package/src/check/type-checker.ts +12 -2
- package/src/coding-agent/tools.ts +8 -2
- package/src/contract-context.ts +28 -0
- package/src/create-harness.ts +11 -0
- package/src/create-ui-generator.ts +71 -25
- package/src/design-mode.ts +182 -0
- package/src/design-system-docs.ts +17 -13
- package/src/evaluation/axis-checks/dispatch.ts +4 -0
- package/src/evaluation/axis-checks/extras.ts +5 -0
- package/src/evaluation/cost-tracker.ts +2 -3
- package/src/evaluation/index.ts +46 -2
- package/src/evaluation/llm-evaluator.ts +77 -21
- package/src/evaluation/types-public.ts +103 -8
- package/src/evaluation/visual-evaluator.ts +301 -29
- package/src/harness/coding/dupe-break.ts +0 -0
- package/src/harness/coding/init-session.ts +1 -0
- package/src/harness/coding/run-coding-turn.ts +3 -0
- package/src/harness/coding/run-eval-round.ts +15 -2
- package/src/harness/llm-trace-sink.ts +3 -3
- package/src/harness/result-types.ts +15 -3
- package/src/harness/runtime.ts +18 -0
- package/src/harness/types-public.ts +27 -0
- package/src/index.ts +26 -0
- package/src/internal/extract-call-signatures.ts +2 -2
- package/src/providers/bedrock.ts +4 -4
- package/src/run-check.ts +1 -0
- package/src/tools/get-primitives-ts.ts +1 -1
- package/src/validation/allowed-imports.ts +8 -2
- package/src/validation/primitives.ts +1 -1
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
|
//
|
|
@@ -591,7 +632,7 @@ export async function runTier0Checks(
|
|
|
591
632
|
subcategory: 'hardcoded-color-fn',
|
|
592
633
|
severity: 'critical',
|
|
593
634
|
description: `Hardcoded ${colorFnMatch[1]}() breaks theme switching — use design tokens.`,
|
|
594
|
-
fix: 'Replace with a primitive variant OR a
|
|
635
|
+
fix: 'Replace with a primitive variant OR a layering role: var(--ggui-color-container), var(--ggui-color-onContainer), var(--ggui-color-outline), etc.',
|
|
595
636
|
line: lineNum,
|
|
596
637
|
});
|
|
597
638
|
}
|
|
@@ -616,7 +657,7 @@ export async function runTier0Checks(
|
|
|
616
657
|
subcategory: 'off-manifest-token',
|
|
617
658
|
severity: 'critical',
|
|
618
659
|
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-
|
|
660
|
+
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
661
|
line: lineNum,
|
|
621
662
|
});
|
|
622
663
|
}
|
|
@@ -627,9 +668,11 @@ export async function runTier0Checks(
|
|
|
627
668
|
// `#fff` defeats its colors. Only the STRING-literal form fails;
|
|
628
669
|
// the numeric escape (`gap={12}`) is untouched, so a genuine
|
|
629
670
|
// off-scale pixel value still has a path.
|
|
630
|
-
const rawSpacingMatch =
|
|
631
|
-
|
|
632
|
-
|
|
671
|
+
const rawSpacingMatch = enforceDesignVocabulary
|
|
672
|
+
? line.match(
|
|
673
|
+
/\b(gap|padding|paddingX|paddingY|margin|radius)\s*=\s*["'][\d.]+(?:px|rem|em)["']/,
|
|
674
|
+
)
|
|
675
|
+
: null;
|
|
633
676
|
if (rawSpacingMatch) {
|
|
634
677
|
issues.push({
|
|
635
678
|
tier: 0,
|
|
@@ -693,14 +736,16 @@ export async function runTier0Checks(
|
|
|
693
736
|
subcategory: 'named-color',
|
|
694
737
|
severity: 'critical',
|
|
695
738
|
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-
|
|
739
|
+
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
740
|
line: lineNum,
|
|
698
741
|
});
|
|
699
742
|
}
|
|
700
743
|
}
|
|
701
744
|
|
|
702
745
|
// Raw pixel values in spacing CSS properties
|
|
703
|
-
const pxMatch =
|
|
746
|
+
const pxMatch = enforceDesignVocabulary
|
|
747
|
+
? line.match(/(?:padding|margin|gap|borderRadius)\s*:\s*['"]?\d+px/)
|
|
748
|
+
: null;
|
|
704
749
|
if (pxMatch && !line.includes('var(--ggui-')) {
|
|
705
750
|
issues.push({
|
|
706
751
|
tier: 0,
|
|
@@ -708,14 +753,16 @@ export async function runTier0Checks(
|
|
|
708
753
|
category: 'tokens',
|
|
709
754
|
subcategory: 'raw-pixels',
|
|
710
755
|
description: 'Raw pixel value in spacing — must use design tokens',
|
|
711
|
-
fix: 'Replace with var(--ggui-spacing
|
|
756
|
+
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
757
|
line: lineNum,
|
|
713
758
|
});
|
|
714
759
|
}
|
|
715
760
|
|
|
716
761
|
// Numeric spacing props on components (e.g., padding={24}, gap={8})
|
|
717
762
|
// These bypass the design system — should use var(--ggui-spacing-*)
|
|
718
|
-
const numericSpacingMatch =
|
|
763
|
+
const numericSpacingMatch = enforceDesignVocabulary
|
|
764
|
+
? line.match(/\b(padding|paddingX|paddingY|gap|margin)\s*=\s*\{?\s*(\d+)\s*\}?/)
|
|
765
|
+
: null;
|
|
719
766
|
if (numericSpacingMatch && !line.includes('var(--ggui-')) {
|
|
720
767
|
issues.push({
|
|
721
768
|
tier: 0,
|
|
@@ -735,7 +782,7 @@ export async function runTier0Checks(
|
|
|
735
782
|
// onClick on them is a plain type error the type-checker catches — don't
|
|
736
783
|
// steer those toward `as={Clickable}` (it won't compile on them).
|
|
737
784
|
const clickablePattern = /<(?:Card|Box|Stack|Row)\s[^>]*onClick/;
|
|
738
|
-
for (let i = 0; i < lines.length; i++) {
|
|
785
|
+
for (let i = 0; enforceDesignVocabulary && i < lines.length; i++) {
|
|
739
786
|
if (clickablePattern.test(lines[i]) && !lines[i].includes('as={Clickable}') && !lines[i].includes('as={Pressable}')) {
|
|
740
787
|
issues.push({
|
|
741
788
|
tier: 0, result: 'warn', category: 'contract', subcategory: 'clickable-wrapper',
|
|
@@ -752,9 +799,21 @@ export async function runTier0Checks(
|
|
|
752
799
|
// another (the inner gesture bubbles to the outer handler). Captured
|
|
753
800
|
// in scenario-07: a `Card as={Clickable} onClick` row containing a
|
|
754
801
|
// `Checkbox onChange`, both calling the same action → double-toggle.
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
802
|
+
//
|
|
803
|
+
// Binding extraction. The constrained regex is the pre-`designMode`
|
|
804
|
+
// one, kept verbatim: it matches `useAction(` only, so a binding
|
|
805
|
+
// declared with the generic the boilerplate itself emits —
|
|
806
|
+
// `useAction<ActionXPayload>('x')` — is NOT collected there (a
|
|
807
|
+
// pre-existing gap, left in place because the constrained gate must
|
|
808
|
+
// stay byte-identical for the A/B; collapse the two regexes once that
|
|
809
|
+
// pin is lifted). `free` mode accepts the optional generic so the
|
|
810
|
+
// one-gesture-surface invariant actually fires on boilerplate-shaped
|
|
811
|
+
// hook lines.
|
|
812
|
+
const bindingRe =
|
|
813
|
+
designMode === 'free'
|
|
814
|
+
? /(?:const|let)\s+(\w+)\s*=\s*useAction\s*(?:<[^>]*>)?\s*\(/g
|
|
815
|
+
: /(?:const|let)\s+(\w+)\s*=\s*useAction\s*\(/g;
|
|
816
|
+
const actionBindings = [...sourceCode.matchAll(bindingRe)]
|
|
758
817
|
.map((m) => m[1])
|
|
759
818
|
.filter((name): name is string => typeof name === 'string');
|
|
760
819
|
for (const binding of actionBindings) {
|
|
@@ -794,7 +853,7 @@ export async function runTier0Checks(
|
|
|
794
853
|
// CAUSE one turn earlier and prevents shipping the broken a11y nest.
|
|
795
854
|
if (actionBindings.length > 0) {
|
|
796
855
|
issues.push(
|
|
797
|
-
...detectCertainDoubleWiredActions(sourceCode, actionBindings),
|
|
856
|
+
...detectCertainDoubleWiredActions(sourceCode, actionBindings, designMode),
|
|
798
857
|
);
|
|
799
858
|
}
|
|
800
859
|
|
|
@@ -862,7 +921,7 @@ export async function runTier0Checks(
|
|
|
862
921
|
const ALIGN_VALUES = new Set(['start', 'center', 'end', 'stretch']);
|
|
863
922
|
const JUSTIFY_VALUES = new Set(['start', 'center', 'end', 'between', 'around', 'evenly']);
|
|
864
923
|
const tagRegex = /<(Stack|Row)\b([^>]*)>/gs;
|
|
865
|
-
for (const tag of sourceCode.matchAll(tagRegex)) {
|
|
924
|
+
for (const tag of enforceDesignVocabulary ? sourceCode.matchAll(tagRegex) : []) {
|
|
866
925
|
const tagName = tag[1];
|
|
867
926
|
const attrs = tag[2];
|
|
868
927
|
const tagLine = sourceCode.slice(0, tag.index).split('\n').length;
|
|
@@ -1316,8 +1375,9 @@ export async function runTier0Checks(
|
|
|
1316
1375
|
export async function runTier0(
|
|
1317
1376
|
sourceCode: string,
|
|
1318
1377
|
contract?: DataContract,
|
|
1378
|
+
designMode: DesignMode = DEFAULT_DESIGN_MODE,
|
|
1319
1379
|
): Promise<EvalResult> {
|
|
1320
|
-
const issues = await runTier0Checks(sourceCode, contract);
|
|
1380
|
+
const issues = await runTier0Checks(sourceCode, contract, undefined, undefined, designMode);
|
|
1321
1381
|
const pass: string[] = [];
|
|
1322
1382
|
|
|
1323
1383
|
// Build pass list from categories that had no issues
|
|
@@ -316,10 +316,18 @@ async function loadVfs(): Promise<Map<string, VfsEntry>> {
|
|
|
316
316
|
/**
|
|
317
317
|
* Try to resolve a relative import within the VFS.
|
|
318
318
|
* Given `containingFile` (e.g. `node_modules/@ggui-ai/design/primitives/index.d.ts`)
|
|
319
|
-
* and `importPath` (e.g. `./Card`
|
|
319
|
+
* and `importPath` (e.g. `./Card` or, as every published declaration says
|
|
320
|
+
* since 0.15.0, `./Card.js`), try:
|
|
320
321
|
* - <resolved>.d.ts
|
|
321
322
|
* - <resolved>/index.d.ts
|
|
322
323
|
* - <resolved>.ts
|
|
324
|
+
* A `.js` / `.mjs` / `.cjs` extension on the specifier is the ESM
|
|
325
|
+
* convention for "the module that compiles to this file" — TypeScript's
|
|
326
|
+
* NodeNext maps `./Card.js` to `./Card.d.ts`, and so does this: the
|
|
327
|
+
* extension is stripped before the candidates are tried. Without that, a
|
|
328
|
+
* `.js`-suffixed re-export resolved to nothing, the imported names typed
|
|
329
|
+
* as `any`, and the checker reported zero errors on code it must reject
|
|
330
|
+
* (ggui 0.15.0 publish-gate, type-checker.test.ts).
|
|
323
331
|
*/
|
|
324
332
|
function resolveRelativeInVfs(
|
|
325
333
|
vfs: Map<string, VfsEntry>,
|
|
@@ -329,7 +337,9 @@ function resolveRelativeInVfs(
|
|
|
329
337
|
// Strip leading '/' from containingFile — TS may prepend getCurrentDirectory()
|
|
330
338
|
const normalized = containingFile.startsWith('/') ? containingFile.slice(1) : containingFile;
|
|
331
339
|
const dir = path.posix.dirname(normalized);
|
|
332
|
-
|
|
340
|
+
// `./x.js` (`.mjs`, `.cjs`) names the declaration `./x.d.ts` — strip it.
|
|
341
|
+
const specifier = importPath.replace(/\.(?:m|c)?js$/, '');
|
|
342
|
+
const resolved = path.posix.normalize(`${dir}/${specifier}`);
|
|
333
343
|
|
|
334
344
|
// Try .d.ts extension
|
|
335
345
|
const withDts = `${resolved}.d.ts`;
|
|
@@ -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
|
@@ -35,6 +35,11 @@ import type {
|
|
|
35
35
|
} from '@ggui-ai/protocol';
|
|
36
36
|
import { HOOK_NAME_RE, listContractGadgets } from '@ggui-ai/protocol';
|
|
37
37
|
import { propsSpecToTypeScript } from './check/index.js';
|
|
38
|
+
import {
|
|
39
|
+
canvasDisplayModeForShell,
|
|
40
|
+
canvasForViewportWidth,
|
|
41
|
+
type CanvasClass,
|
|
42
|
+
} from './design-mode.js';
|
|
38
43
|
|
|
39
44
|
/**
|
|
40
45
|
* Rendering context — how and where the component will be displayed.
|
|
@@ -87,6 +92,29 @@ const DEVICE_HINTS: Record<string, string> = {
|
|
|
87
92
|
spatial: `**Spatial:** Fixed-size floating panel (~600x400). High contrast. Large text. No hover (gaze/hand input).`,
|
|
88
93
|
};
|
|
89
94
|
|
|
95
|
+
/**
|
|
96
|
+
* Map a host rendering context onto the canvas class the `free` prompt
|
|
97
|
+
* states and the visual evaluator judges at. The class is derived from
|
|
98
|
+
* DISPLAY MODE + width: a `chat` shell is always the inline card
|
|
99
|
+
* whatever the host viewport is (the bubble, not the window, bounds the
|
|
100
|
+
* component); a known viewport width refines the fullscreen / partial
|
|
101
|
+
* shells; otherwise the device category decides.
|
|
102
|
+
*/
|
|
103
|
+
export function canvasForRenderingContext(ctx: RenderingContext): CanvasClass {
|
|
104
|
+
const mode = canvasDisplayModeForShell(ctx.shell);
|
|
105
|
+
if (mode === 'inline') return 'xs-chat-card';
|
|
106
|
+
if (ctx.viewport) return canvasForViewportWidth(mode, ctx.viewport.width);
|
|
107
|
+
switch (ctx.device) {
|
|
108
|
+
case 'mobile':
|
|
109
|
+
return 'mobile-fullscreen-small';
|
|
110
|
+
case 'tablet':
|
|
111
|
+
case 'spatial':
|
|
112
|
+
return 'md';
|
|
113
|
+
case 'desktop':
|
|
114
|
+
return ctx.shell === 'partial' ? 'md' : 'lg';
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
90
118
|
/** Build rendering-context block to inject into user prompt. */
|
|
91
119
|
export function buildRenderingContext(ctx: RenderingContext): string {
|
|
92
120
|
const parts = [
|
package/src/create-harness.ts
CHANGED
|
@@ -20,6 +20,7 @@ import { defaultApplyPatch } from "./patch.js";
|
|
|
20
20
|
import { computeHarnessId, computeHarnessName, hashClassification } from "./hash.js";
|
|
21
21
|
import { pickWorkflow } from "./workflows.js";
|
|
22
22
|
import { DEFAULT_HARNESS_POLICY, isDefaultHarnessPolicy } from "./policy.js";
|
|
23
|
+
import { DEFAULT_DESIGN_MODE } from "./design-mode.js";
|
|
23
24
|
import type {
|
|
24
25
|
CreateHarnessInput,
|
|
25
26
|
Harness,
|
|
@@ -69,6 +70,9 @@ function countCacheTiers(fragments: readonly { cacheTier: CacheTier }[]): Record
|
|
|
69
70
|
export function createHarness(input: CreateHarnessInput): Harness {
|
|
70
71
|
const { classification, contract, prompt, shellType, screen, overrides } = input;
|
|
71
72
|
const ctx: HarnessConstructionContext = { classification, contract, prompt };
|
|
73
|
+
// Which triad to materialise — reaches the HOW leg (prompt), the WHAT
|
|
74
|
+
// leg (boilerplate) and, via `harness.designMode`, every CHECK consumer.
|
|
75
|
+
const designMode = input.designMode ?? DEFAULT_DESIGN_MODE;
|
|
72
76
|
|
|
73
77
|
// ── Compose fragments from classification ────────────────────────────────
|
|
74
78
|
const composed = compose(classification);
|
|
@@ -84,6 +88,8 @@ export function createHarness(input: CreateHarnessInput): Harness {
|
|
|
84
88
|
shellType,
|
|
85
89
|
screen,
|
|
86
90
|
axisDelta: composed.promptText,
|
|
91
|
+
designMode,
|
|
92
|
+
...(input.canvas !== undefined ? { canvas: input.canvas } : {}),
|
|
87
93
|
});
|
|
88
94
|
const howFragments = composed.fragments.filter(
|
|
89
95
|
(f) => f.promptText && f.promptText.trim().length > 0,
|
|
@@ -112,6 +118,7 @@ export function createHarness(input: CreateHarnessInput): Harness {
|
|
|
112
118
|
screen,
|
|
113
119
|
composed.boilerplateSections,
|
|
114
120
|
input.appGadgets,
|
|
121
|
+
designMode,
|
|
115
122
|
);
|
|
116
123
|
const whatFragments = composed.fragments.filter(
|
|
117
124
|
(f) => f.boilerplateMarker && f.boilerplateMarker.trim().length > 0,
|
|
@@ -179,6 +186,9 @@ export function createHarness(input: CreateHarnessInput): Harness {
|
|
|
179
186
|
// Only non-default policies contribute to the harness id — preserves
|
|
180
187
|
// byte-identical id across default-policy runs.
|
|
181
188
|
if (!isDefaultHarnessPolicy(policy)) overrideLabels.push("policy");
|
|
189
|
+
// Only a non-default design mode contributes — the constrained id is
|
|
190
|
+
// byte-identical to the pre-`designMode` harness.
|
|
191
|
+
if (designMode !== DEFAULT_DESIGN_MODE) overrideLabels.push(`designMode:${designMode}`);
|
|
182
192
|
|
|
183
193
|
const id = computeHarnessId({
|
|
184
194
|
classificationHash,
|
|
@@ -210,6 +220,7 @@ export function createHarness(input: CreateHarnessInput): Harness {
|
|
|
210
220
|
id,
|
|
211
221
|
name,
|
|
212
222
|
classification,
|
|
223
|
+
designMode,
|
|
213
224
|
how,
|
|
214
225
|
what,
|
|
215
226
|
check,
|
|
@@ -35,7 +35,10 @@ import type {
|
|
|
35
35
|
GadgetDescriptor,
|
|
36
36
|
GenerationError,
|
|
37
37
|
JsonObject,
|
|
38
|
+
GeneratorId,
|
|
39
|
+
ModelRef,
|
|
38
40
|
} from '@ggui-ai/protocol';
|
|
41
|
+
import { isGeneratorId, modelRefOfRoute } from '@ggui-ai/protocol';
|
|
39
42
|
import type { GadgetCatalogAdapter } from '@ggui-ai/gadgets';
|
|
40
43
|
import type {
|
|
41
44
|
GenerationMetadata,
|
|
@@ -53,19 +56,22 @@ import {
|
|
|
53
56
|
import { createGeneratorTools } from './adapters/index.js';
|
|
54
57
|
import { dispatchGeneration } from './adapters/generation-dispatch.js';
|
|
55
58
|
import type { ProviderName } from './adapters/types.js';
|
|
59
|
+
import type { GenerationResult } from './harness/result-types.js';
|
|
56
60
|
import {
|
|
61
|
+
canvasForRenderingContext,
|
|
57
62
|
injectContracts,
|
|
58
63
|
injectRenderingContext,
|
|
59
64
|
injectVariance,
|
|
60
65
|
} from './contract-context.js';
|
|
61
66
|
import type { RenderingContext } from './contract-context.js';
|
|
67
|
+
import type { DesignMode } from './design-mode.js';
|
|
62
68
|
import { resolveRoute, applyRouteToEnv } from './adapters/provider-router.js';
|
|
63
69
|
import type { QualityConfig } from './evaluation/types-public.js';
|
|
64
70
|
import type { AgentConfig } from './harness/llm-router.js';
|
|
65
71
|
|
|
66
72
|
/** The slug for the OSS default seed generator. */
|
|
67
73
|
const DEFAULT_TIER: GeneratorTier = 'default';
|
|
68
|
-
const DEFAULT_MODEL = 'haiku-4-5';
|
|
74
|
+
const DEFAULT_MODEL: ModelRef = 'anthropic/claude-haiku-4-5';
|
|
69
75
|
|
|
70
76
|
export interface CreateUiGeneratorOptions {
|
|
71
77
|
/**
|
|
@@ -86,24 +92,25 @@ export interface CreateUiGeneratorOptions {
|
|
|
86
92
|
/** Quality config controlling eval tiers + improvement behavior. */
|
|
87
93
|
readonly qualityConfig?: QualityConfig;
|
|
88
94
|
/**
|
|
89
|
-
* Generator identity — registry key +
|
|
95
|
+
* Generator identity — registry key + declared model.
|
|
90
96
|
*
|
|
91
|
-
* The tier
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
* `ui-gen-default
|
|
97
|
+
* The tier alone determines the {@link UiGenerator.slug} the factory
|
|
98
|
+
* bakes onto the returned generator (`ui-gen-<tier>`); `model` is the
|
|
99
|
+
* generator's declared model field and is never part of the slug
|
|
100
|
+
* (ggui#923). Defaults to tier `'default'` → slug `ui-gen-default`,
|
|
101
|
+
* the OSS seed, with the factory's default model.
|
|
95
102
|
*
|
|
96
103
|
* The actual model used per request still comes from
|
|
97
104
|
* `UiGenerateInput.llm.model` (operators may override via BYOK).
|
|
98
105
|
* The identity here is the registry-level handle, not a runtime
|
|
99
106
|
* model constraint.
|
|
100
107
|
*
|
|
101
|
-
* Mutually-exclusive shortcut: pass `slug` instead and the
|
|
102
|
-
* parses
|
|
103
|
-
* factory throws if
|
|
108
|
+
* Mutually-exclusive shortcut: pass `slug` instead of `tier` and the
|
|
109
|
+
* factory parses the tier from it; `model` may accompany either form.
|
|
110
|
+
* The factory throws if `slug` and `tier` are both supplied.
|
|
104
111
|
*/
|
|
105
112
|
readonly tier?: GeneratorTier;
|
|
106
|
-
readonly model?:
|
|
113
|
+
readonly model?: ModelRef;
|
|
107
114
|
readonly slug?: string;
|
|
108
115
|
/**
|
|
109
116
|
* Per-deployment gadget descriptor source. Wired once at factory
|
|
@@ -163,6 +170,35 @@ export interface CreateUiGeneratorOptions {
|
|
|
163
170
|
* still log via `console.warn`, just without a caller-side hook.
|
|
164
171
|
*/
|
|
165
172
|
readonly onRetry?: AgentConfig['onRetry'];
|
|
173
|
+
/**
|
|
174
|
+
* Generation observer. Invoked once per successful `generate()` with
|
|
175
|
+
* the FULL harness result — source, compiled code, token counters,
|
|
176
|
+
* timing breakdown, evaluation rounds and (when configured) the
|
|
177
|
+
* per-canvas visual scores — before it is projected onto the
|
|
178
|
+
* `UiGenerateResult` envelope, which carries only what the protocol
|
|
179
|
+
* needs. Lets a host record or forward the complete run without
|
|
180
|
+
* re-deriving it from the envelope. Bound ONCE at construction
|
|
181
|
+
* (mirrors `onRetry`); not invoked when generation fails. An
|
|
182
|
+
* exception thrown by the observer is the caller's own and fails
|
|
183
|
+
* that generation like any other error on the path — keep it
|
|
184
|
+
* non-throwing.
|
|
185
|
+
*/
|
|
186
|
+
readonly onGenerated?: (result: GenerationResult) => void;
|
|
187
|
+
/**
|
|
188
|
+
* Which generation triad to run. `constrained` (default) is the
|
|
189
|
+
* design-system triad — byte-identical to the generator's behaviour
|
|
190
|
+
* before this option existed. `free` relaxes only the design
|
|
191
|
+
* vocabulary (raw HTML, inline CSS, literal spacing / typography /
|
|
192
|
+
* radius; the design package importable but optional) while keeping
|
|
193
|
+
* every hard contract: the boilerplate-typed wire hooks, the import
|
|
194
|
+
* allowlist + security bans, the host envelope, the budgets, the
|
|
195
|
+
* blueprint-reuse shape, and brand-bearing color on the closed
|
|
196
|
+
* `--ggui-color-*` token manifest. Reaches the prompt, boilerplate,
|
|
197
|
+
* tier-0 gate, axis checks and evaluator criteria through the harness.
|
|
198
|
+
* The rendering canvas the `free` prompt states is derived from
|
|
199
|
+
* `input.rendering` (device × shell, refined by viewport width).
|
|
200
|
+
*/
|
|
201
|
+
readonly designMode?: DesignMode;
|
|
166
202
|
}
|
|
167
203
|
|
|
168
204
|
export function createUiGenerator(
|
|
@@ -176,6 +212,8 @@ export function createUiGenerator(
|
|
|
176
212
|
gadgetCatalog,
|
|
177
213
|
disableEnvMutation = false,
|
|
178
214
|
onRetry,
|
|
215
|
+
onGenerated,
|
|
216
|
+
designMode,
|
|
179
217
|
} = options;
|
|
180
218
|
|
|
181
219
|
const identity = resolveIdentity(options);
|
|
@@ -268,6 +306,10 @@ export function createUiGenerator(
|
|
|
268
306
|
const promptWithRendering = rendering
|
|
269
307
|
? injectRenderingContext(input.request.prompt, rendering)
|
|
270
308
|
: input.request.prompt;
|
|
309
|
+
// Canvas class for the `free` prompt — TOLD, never guessed.
|
|
310
|
+
// Ignored by the constrained prompt (its shell/screen
|
|
311
|
+
// descriptors stay byte-identical).
|
|
312
|
+
const canvas = rendering ? canvasForRenderingContext(rendering) : undefined;
|
|
271
313
|
const promptWithVariance = injectVariance(
|
|
272
314
|
promptWithRendering,
|
|
273
315
|
input.variance,
|
|
@@ -316,12 +358,16 @@ export function createUiGenerator(
|
|
|
316
358
|
enableRuntimeRender,
|
|
317
359
|
...(routeOverride !== undefined ? { routeOverride } : {}),
|
|
318
360
|
...(onRetry !== undefined ? { onRetry } : {}),
|
|
361
|
+
...(designMode !== undefined ? { designMode } : {}),
|
|
362
|
+
...(canvas !== undefined ? { canvas } : {}),
|
|
319
363
|
});
|
|
320
364
|
|
|
365
|
+
onGenerated?.(result);
|
|
366
|
+
|
|
321
367
|
const metadata: GenerationMetadata = {
|
|
322
368
|
provider: input.llm.provider,
|
|
323
369
|
generator: identity.slug,
|
|
324
|
-
model: input.llm
|
|
370
|
+
model: modelRefOfRoute(input.llm),
|
|
325
371
|
inputTokens: result.tokens.input,
|
|
326
372
|
outputTokens: result.tokens.output,
|
|
327
373
|
latencyMs: Date.now() - startedAt,
|
|
@@ -362,7 +408,7 @@ export function createUiGenerator(
|
|
|
362
408
|
metadata: {
|
|
363
409
|
provider: input.llm.provider,
|
|
364
410
|
generator: identity.slug,
|
|
365
|
-
model: input.llm
|
|
411
|
+
model: modelRefOfRoute(input.llm),
|
|
366
412
|
inputTokens: 0,
|
|
367
413
|
outputTokens: 0,
|
|
368
414
|
latencyMs: Date.now() - startedAt,
|
|
@@ -381,29 +427,29 @@ export function createUiGenerator(
|
|
|
381
427
|
}
|
|
382
428
|
|
|
383
429
|
function resolveIdentity(opts: CreateUiGeneratorOptions): {
|
|
384
|
-
slug:
|
|
430
|
+
slug: GeneratorId;
|
|
385
431
|
tier: GeneratorTier;
|
|
386
|
-
model:
|
|
432
|
+
model: ModelRef;
|
|
387
433
|
} {
|
|
388
434
|
const hasSlug = typeof opts.slug === 'string' && opts.slug.length > 0;
|
|
389
|
-
|
|
390
|
-
if (hasSlug && hasTierOrModel) {
|
|
435
|
+
if (hasSlug && opts.tier !== undefined) {
|
|
391
436
|
throw new Error(
|
|
392
|
-
'createUiGenerator: pass either { slug } or { tier
|
|
437
|
+
'createUiGenerator: pass either { slug } or { tier } — not both.',
|
|
393
438
|
);
|
|
394
439
|
}
|
|
440
|
+
const model = opts.model ?? DEFAULT_MODEL;
|
|
395
441
|
if (hasSlug) {
|
|
396
|
-
const
|
|
397
|
-
|
|
442
|
+
const slug = opts.slug!;
|
|
443
|
+
const parsed = parseGeneratorSlug(slug);
|
|
444
|
+
if (!parsed || !isGeneratorId(slug)) {
|
|
398
445
|
throw new Error(
|
|
399
|
-
`createUiGenerator: slug ${JSON.stringify(
|
|
446
|
+
`createUiGenerator: slug ${JSON.stringify(slug)} is not a valid ui-gen-<tier> identifier.`,
|
|
400
447
|
);
|
|
401
448
|
}
|
|
402
|
-
return { slug
|
|
449
|
+
return { slug, tier: parsed.tier, model };
|
|
403
450
|
}
|
|
404
451
|
const tier = opts.tier ?? DEFAULT_TIER;
|
|
405
|
-
const
|
|
406
|
-
const slug = formatGeneratorSlug({ tier, model });
|
|
452
|
+
const slug = formatGeneratorSlug({ tier });
|
|
407
453
|
if (!isValidGeneratorSlug(slug)) {
|
|
408
454
|
// Unreachable: formatGeneratorSlug throws on malformed components,
|
|
409
455
|
// so a slug it returns must parse. Defensive check kept to surface
|
|
@@ -442,7 +488,7 @@ function mapRendering(
|
|
|
442
488
|
|
|
443
489
|
function failWithoutMetadata(
|
|
444
490
|
input: UiGenerateInput,
|
|
445
|
-
generatorSlug:
|
|
491
|
+
generatorSlug: GeneratorId,
|
|
446
492
|
startedAt: number,
|
|
447
493
|
error: GenerationError,
|
|
448
494
|
): UiGenerateResult {
|
|
@@ -452,7 +498,7 @@ function failWithoutMetadata(
|
|
|
452
498
|
metadata: {
|
|
453
499
|
provider: input.llm.provider,
|
|
454
500
|
generator: generatorSlug,
|
|
455
|
-
model: input.llm
|
|
501
|
+
model: modelRefOfRoute(input.llm),
|
|
456
502
|
inputTokens: 0,
|
|
457
503
|
outputTokens: 0,
|
|
458
504
|
latencyMs: Date.now() - startedAt,
|