@ggui-ai/ui-gen 0.11.0 → 0.13.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 (32) hide show
  1. package/dist/adapters/generation-dispatch.js +180 -18
  2. package/dist/adapters/generation-dispatch.js.map +1 -1
  3. package/dist/advanced/index.d.ts +1 -1
  4. package/dist/advanced/index.js +180 -18
  5. package/dist/advanced/index.js.map +1 -1
  6. package/dist/blueprint-validator.js +146 -0
  7. package/dist/blueprint-validator.js.map +1 -1
  8. package/dist/boilerplate.js +7 -7
  9. package/dist/boilerplate.js.map +1 -1
  10. package/dist/check/index.js +27 -11
  11. package/dist/check/index.js.map +1 -1
  12. package/dist/harness/check/runtime-render/index.d.ts +1 -1
  13. package/dist/harness/check/runtime-render/index.js +146 -0
  14. package/dist/harness/check/runtime-render/index.js.map +1 -1
  15. package/dist/harness/check/runtime-render/render-check-worker.js +126 -0
  16. package/dist/harness/check/runtime-render/render-check-worker.js.map +1 -1
  17. package/dist/harness/index.js +7 -7
  18. package/dist/harness/index.js.map +1 -1
  19. package/dist/harness/runtime.js +7 -7
  20. package/dist/harness/runtime.js.map +1 -1
  21. package/dist/harness/types-public.js +7 -7
  22. package/dist/harness/types-public.js.map +1 -1
  23. package/dist/index.js +180 -18
  24. package/dist/index.js.map +1 -1
  25. package/dist/provider-adapter-contract.js +1161 -387
  26. package/dist/provider-adapter-contract.js.map +1 -1
  27. package/dist/{render-check-BZoIE8-u.d.ts → render-check-WfOVO9kU.d.ts} +1 -1
  28. package/package.json +13 -13
  29. package/src/boilerplate/system-prompt.ts +7 -7
  30. package/src/check/run-tier0.ts +35 -16
  31. package/src/harness/check/runtime-render/adapter.ts +28 -0
  32. package/src/harness/check/runtime-render/render-check.ts +224 -1
@@ -2,7 +2,7 @@ import { GeneratorTier, UiGenerator } from '@ggui-ai/mcp-server-core';
2
2
  import { JsonObject } from '@ggui-ai/protocol';
3
3
  import { BehaviorFailure, PlaywrightModule } from '@ggui-ai/ui-visual-tester';
4
4
  import { C as CreateUiGeneratorOptions } from '../create-ui-generator-CF0EL0Li.js';
5
- import { R as RenderCheckIssue } from '../render-check-BZoIE8-u.js';
5
+ import { R as RenderCheckIssue } from '../render-check-WfOVO9kU.js';
6
6
  import '@ggui-ai/gadgets';
7
7
  import '../types-public-DIpjC-OA.js';
8
8
  import '../axes-CzLEMDeB.js';
@@ -3270,17 +3270,33 @@ ${errorDetail}`,
3270
3270
  }
3271
3271
  }
3272
3272
  const hexMatch = line.match(/#[0-9a-fA-F]{3,8}\b/);
3273
- if (hexMatch && !line.includes("var(--ggui-") && !line.includes("// fallback") && !assetEscapedLines.has(lineNum)) {
3274
- issues.push({
3275
- tier: 0,
3276
- result: "fail",
3277
- category: "tokens",
3278
- subcategory: "hex-color",
3279
- severity: "critical",
3280
- description: `Hardcoded color "${hexMatch[0]}" breaks theme switching \u2014 use design tokens.`,
3281
- fix: `Replace with a primitive variant (Button variant="primary"|Badge variant="success"|...) OR a token reference like var(--ggui-color-primary-500, ${hexMatch[0]}). Hardcoded colors mean the operator's theme has no effect on this surface.`,
3282
- line: lineNum
3283
- });
3273
+ if (hexMatch && !assetEscapedLines.has(lineNum)) {
3274
+ const fallbackVarMatch = line.match(
3275
+ /var\((--ggui-[a-zA-Z0-9-]+)\s*,[^)]*#[0-9a-fA-F]{3,8}/
3276
+ );
3277
+ if (fallbackVarMatch) {
3278
+ issues.push({
3279
+ tier: 0,
3280
+ result: "fail",
3281
+ category: "tokens",
3282
+ subcategory: "token-fallback",
3283
+ severity: "critical",
3284
+ description: `Literal fallback "${hexMatch[0]}" inside a token reference \u2014 the runtime injects every token, and the fallback paints the wrong brand exactly when the theme matters most.`,
3285
+ fix: `Drop the fallback: write var(${fallbackVarMatch[1]}) bare. If the token might not exist, that is a vocabulary problem to fix by choosing a token from the design-system docs, never by a literal.`,
3286
+ line: lineNum
3287
+ });
3288
+ } else if (!line.includes("// fallback")) {
3289
+ issues.push({
3290
+ tier: 0,
3291
+ result: "fail",
3292
+ category: "tokens",
3293
+ subcategory: "hex-color",
3294
+ severity: "critical",
3295
+ description: `Hardcoded color "${hexMatch[0]}" breaks theme switching \u2014 use design tokens.`,
3296
+ fix: `Replace with a primitive variant (Button variant="primary"|Badge variant="success"|...) OR a bare token reference like var(--ggui-color-primary-500) \u2014 never a literal fallback. Hardcoded colors mean the operator's theme has no effect on this surface.`,
3297
+ line: lineNum
3298
+ });
3299
+ }
3284
3300
  }
3285
3301
  const colorFnMatch = line.match(/\b(rgba?|hsla?)\s*\(/);
3286
3302
  if (colorFnMatch && !line.includes("var(--ggui-") && !line.includes("// fallback") && !line.includes("$value") && !assetEscapedLines.has(lineNum)) {
@@ -13498,15 +13514,15 @@ EVERY color, spacing, typography, shadow, and radius value MUST come from design
13498
13514
 
13499
13515
  MANDATORY:
13500
13516
  1. NEVER use hardcoded hex colors like \`#7c3aed\` \u2014 ONLY \`var(--ggui-color-*)\` tokens.
13501
- 2. NEVER use CSS gradients with custom colors. If you need a gradient: \`linear-gradient(to bottom, var(--ggui-color-primary-500, #0ea5e9), var(--ggui-color-primary-700, #0369a1))\`.
13517
+ 2. NEVER use CSS gradients with custom colors. If you need a gradient: \`linear-gradient(to bottom, var(--ggui-color-primary-500), var(--ggui-color-primary-700))\`.
13502
13518
  3. NEVER invent your own palette. The system provides primary, neutral, success, warning, error, and info \u2014 use ONLY these.
13503
- 4. ALWAYS include fallback values: \`var(--ggui-color-primary-600, #0284c7)\`.
13519
+ 4. NEVER include literal fallback values in color tokens \u2014 write \`var(--ggui-color-primary-600)\` bare, never \`var(--ggui-color-primary-600, #0284c7)\`. The runtime injects every token; a literal fallback paints the wrong brand exactly when the operator's theme matters most.
13504
13520
 
13505
13521
  Token categories:
13506
- - Brand: \`var(--ggui-color-primary-600, #0284c7)\`, \`var(--ggui-color-primary-50, #f0f9ff)\`
13507
- - Text: \`var(--ggui-color-onSurface, #18181b)\`, \`var(--ggui-color-onSurfaceVariant, #52525b)\`
13508
- - Backgrounds: \`var(--ggui-color-surface, #fafafa)\`, \`var(--ggui-color-surfaceVariant, #f4f4f5)\`
13509
- - Borders: \`var(--ggui-color-outline, #d4d4d8)\`
13522
+ - Brand: \`var(--ggui-color-primary-600)\`, \`var(--ggui-color-primary-50)\`
13523
+ - Text: \`var(--ggui-color-onSurface)\`, \`var(--ggui-color-onSurfaceVariant)\`
13524
+ - Backgrounds: \`var(--ggui-color-surface)\`, \`var(--ggui-color-surfaceVariant)\`
13525
+ - Borders: \`var(--ggui-color-outline)\`
13510
13526
  - Spacing: \`var(--ggui-spacing-4, 16px)\`, \`var(--ggui-spacing-6, 24px)\`
13511
13527
  - Typography: \`var(--ggui-font-size-sm, 14px)\`, \`var(--ggui-font-weight-semibold, 600)\`
13512
13528
  - Shadows: \`var(--ggui-shape-shadow-sm)\`, \`var(--ggui-shape-shadow-md)\`, \`var(--ggui-shape-shadow-lg)\`
@@ -13691,7 +13707,7 @@ Hero sections should feel airy. Use \`padding="xl"\` (32px) on the main card, no
13691
13707
 
13692
13708
  - [ ] Imports ONLY from: react, @ggui-ai/design, @ggui-ai/wire
13693
13709
  - [ ] No raw HTML elements (\`<button>\`, \`<input>\`, \`<div>\` for layout) \u2014 uses primitives
13694
- - [ ] ZERO hardcoded hex colors \u2014 every color is \`var(--ggui-color-*, fallback)\`
13710
+ - [ ] ZERO hardcoded hex colors \u2014 every color is a bare \`var(--ggui-color-*)\`, no literal fallback
13695
13711
  - [ ] No raw pixel values for spacing \u2014 all via \`var(--ggui-spacing-*)\` tokens
13696
13712
  - [ ] Primary palette used throughout (headers, borders, icons) \u2014 not just buttons
13697
13713
  - [ ] Typed Props interface exported; request-specific data is a prop with default
@@ -16214,6 +16230,22 @@ async function runRenderCheckInProcess(input) {
16214
16230
  if (issue) issues.push(issue);
16215
16231
  }
16216
16232
  }
16233
+ if (input.contract?.actionSpec) {
16234
+ for (const [name, entry] of Object.entries(input.contract.actionSpec)) {
16235
+ const idField = idLikeRequiredStringField(
16236
+ entry.schema
16237
+ );
16238
+ if (idField === null) continue;
16239
+ const issue = await checkSelectionIdentity({
16240
+ container,
16241
+ probe,
16242
+ user,
16243
+ actionName: name,
16244
+ idField
16245
+ });
16246
+ if (issue) issues.push(issue);
16247
+ }
16248
+ }
16217
16249
  if (input.contract?.propsSpec) {
16218
16250
  for (const issue of checkPropCoverage({
16219
16251
  container,
@@ -16223,6 +16255,48 @@ async function runRenderCheckInProcess(input) {
16223
16255
  issues.push(issue);
16224
16256
  }
16225
16257
  }
16258
+ if (input.contract?.propsSpec) {
16259
+ const propsSpec5c = input.contract.propsSpec;
16260
+ const normalize = (s) => (s ?? "").replace(/\s+/g, " ").trim();
16261
+ const baseText = normalize(container.textContent);
16262
+ for (const [propName, entry] of Object.entries(propsSpec5c.properties)) {
16263
+ if (!entry.required) continue;
16264
+ const orig = input.mockupProps[propName];
16265
+ if (typeof orig !== "string" && typeof orig !== "number") continue;
16266
+ const marker = pickScalarMarker(orig);
16267
+ if (marker === null || !baseText.includes(marker)) continue;
16268
+ const varied = typeof orig === "string" ? "Zzyzx Qw" : orig + 137731;
16269
+ const variedProbe = createProbe();
16270
+ const variedConfig = createProbeWireConfig(variedProbe);
16271
+ let variedResult;
16272
+ try {
16273
+ variedResult = render(
16274
+ React.createElement(GguiWireProvider, {
16275
+ config: variedConfig,
16276
+ children: React.createElement(ProbeErrorBoundary, {
16277
+ children: React.createElement(Component, {
16278
+ ...input.mockupProps,
16279
+ [propName]: varied
16280
+ })
16281
+ })
16282
+ })
16283
+ );
16284
+ } catch {
16285
+ continue;
16286
+ }
16287
+ await flushPromises();
16288
+ const variedText = normalize(variedResult.container.textContent);
16289
+ variedResult.unmount();
16290
+ if (variedText === baseText) {
16291
+ issues.push({
16292
+ check: "prop-sensitivity",
16293
+ outcome: "failed",
16294
+ subject: propName,
16295
+ reason: `props.${propName} is displayed ('${marker}' appears in the DOM) but changing its value to '${String(varied)}' left the rendered output BYTE-IDENTICAL \u2014 the value is baked into the component instead of derived from the prop. A cached reuse of this blueprint under a different ${propName} would show the original value.`
16296
+ });
16297
+ }
16298
+ }
16299
+ }
16226
16300
  if (input.contract?.streamSpec) {
16227
16301
  const streamSpec = input.contract.streamSpec;
16228
16302
  for (const [name] of Object.entries(streamSpec)) {
@@ -16554,6 +16628,74 @@ async function checkActionWiring(input) {
16554
16628
  }
16555
16629
  };
16556
16630
  }
16631
+ function idLikeRequiredStringField(schema) {
16632
+ if (schema === void 0) return null;
16633
+ const properties = schema["properties"];
16634
+ const required = schema["required"];
16635
+ if (properties === null || typeof properties !== "object") return null;
16636
+ if (!Array.isArray(required)) return null;
16637
+ for (const [propName, propSchema] of Object.entries(
16638
+ properties
16639
+ )) {
16640
+ if (!/id$/i.test(propName)) continue;
16641
+ if (!required.includes(propName)) continue;
16642
+ const t = propSchema !== null && typeof propSchema === "object" ? propSchema["type"] : void 0;
16643
+ if (t === "string") return propName;
16644
+ }
16645
+ return null;
16646
+ }
16647
+ async function checkSelectionIdentity(input) {
16648
+ const { container, probe, user, actionName, idField } = input;
16649
+ const clickables = Array.from(
16650
+ container.querySelectorAll('button, [role="button"]')
16651
+ );
16652
+ const byLabel = /* @__PURE__ */ new Map();
16653
+ for (const el of clickables) {
16654
+ const label = (el.textContent ?? "").replace(/\s+/g, " ").trim();
16655
+ if (label.length === 0) continue;
16656
+ const group = byLabel.get(label);
16657
+ if (group) group.push(el);
16658
+ else byLabel.set(label, [el]);
16659
+ }
16660
+ for (const [label, group] of byLabel) {
16661
+ if (group.length < 2) continue;
16662
+ const payloadFor = async (el) => {
16663
+ const before = probe.getFireLog().length;
16664
+ try {
16665
+ await user.click(el);
16666
+ } catch {
16667
+ return void 0;
16668
+ }
16669
+ await flushPromises();
16670
+ const events = probe.getFireLog().slice(before).filter(
16671
+ (e) => e.kind === "action.fired" && e.name === actionName
16672
+ );
16673
+ return events.length > 0 ? events[events.length - 1].payload : void 0;
16674
+ };
16675
+ const first = await payloadFor(group[0]);
16676
+ const second = await payloadFor(group[1]);
16677
+ if (first === void 0 || second === void 0) continue;
16678
+ const idOf = (payload) => {
16679
+ if (payload === null || typeof payload !== "object") return void 0;
16680
+ const v = payload[idField];
16681
+ return typeof v === "string" ? v : void 0;
16682
+ };
16683
+ const firstId = idOf(first);
16684
+ const secondId = idOf(second);
16685
+ if (firstId === void 0 || secondId === void 0) continue;
16686
+ if (firstId === secondId) {
16687
+ return {
16688
+ check: "selection-identity",
16689
+ outcome: "failed",
16690
+ subject: actionName,
16691
+ reason: `Two clickable elements share the visible label '${label}', and clicking EACH fired '${actionName}' with the same ${idField} ('${firstId}') \u2014 the action payload is keyed on the display value, not the element's identity. Distinct cells must send their own ${idField} (the contract requires it), or selecting one "${label}" acts on every "${label}".`,
16692
+ elementHint: describeElement(group[0])
16693
+ };
16694
+ }
16695
+ return null;
16696
+ }
16697
+ return null;
16698
+ }
16557
16699
  async function simulateAndCheck(input) {
16558
16700
  const { container, user, probe, wiringKind, actionName, actionLabel, eventKind } = input;
16559
16701
  const candidates = findCandidateElements(container, wiringKind, actionName, actionLabel);
@@ -17052,6 +17194,26 @@ function toEvalIssue(issue) {
17052
17194
  fix
17053
17195
  };
17054
17196
  }
17197
+ case "selection-identity":
17198
+ return {
17199
+ tier: 0,
17200
+ result,
17201
+ category: "contract",
17202
+ subcategory,
17203
+ severity: "critical",
17204
+ description: `${issue.reason}${elementHint}${diagSuffix}`,
17205
+ fix: `Key the action payload (and any selection state) on the item's ID from the data, never on its display text. Common fix: map over items and pass the item's id into the handler \u2014 onClick={() => ${subject}({ ...payload, id: item.id })} \u2014 and store selection as the id (setSelected(item.id)), not the label.`
17206
+ };
17207
+ case "prop-sensitivity":
17208
+ return {
17209
+ tier: 0,
17210
+ result,
17211
+ category: "contract",
17212
+ subcategory,
17213
+ severity: "critical",
17214
+ description: `${issue.reason}${diagSuffix}`,
17215
+ fix: `Derive the display AND any logic from props.${subject} \u2014 render {props.${subject}} and branch on the prop's value, never on the literal value the request happened to mention (e.g. props.${subject} ?? 'January' followed by January-special-cased logic bakes the request into the component). The contract delivers the value; trust it.`
17216
+ };
17055
17217
  case "prop-coverage":
17056
17218
  return {
17057
17219
  tier: 0,