@ggui-ai/ui-gen 0.26.0 → 0.28.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 (77) hide show
  1. package/dist/{adapter-BJwn74rx.d.ts → adapter-h4_V69LI.d.ts} +1 -1
  2. package/dist/adapters/base.d.ts +6 -1
  3. package/dist/adapters/base.js.map +1 -1
  4. package/dist/adapters/generation-dispatch.d.ts +2 -2
  5. package/dist/adapters/generation-dispatch.js +233 -17
  6. package/dist/adapters/generation-dispatch.js.map +1 -1
  7. package/dist/adapters/index.js +36 -14
  8. package/dist/adapters/index.js.map +1 -1
  9. package/dist/advanced/index.d.ts +4 -4
  10. package/dist/advanced/index.js +268 -28
  11. package/dist/advanced/index.js.map +1 -1
  12. package/dist/boilerplate.js +4 -6
  13. package/dist/boilerplate.js.map +1 -1
  14. package/dist/check/index.d.ts +1 -7
  15. package/dist/check/index.js +50 -4
  16. package/dist/check/index.js.map +1 -1
  17. package/dist/{create-ui-generator-N4gRXpcL.d.ts → create-ui-generator-BrZHF0cf.d.ts} +2 -2
  18. package/dist/{dispatch-BUoXRj_r.d.ts → dispatch-B07TI30d.d.ts} +1 -1
  19. package/dist/evaluation/axis-checks/index.d.ts +3 -3
  20. package/dist/evaluation/axis-checks/index.js +97 -1
  21. package/dist/evaluation/axis-checks/index.js.map +1 -1
  22. package/dist/evaluation/axis-checks/registry.d.ts +1 -1
  23. package/dist/evaluation/axis-checks/registry.js +97 -1
  24. package/dist/evaluation/axis-checks/registry.js.map +1 -1
  25. package/dist/evaluation/index.d.ts +22 -12
  26. package/dist/evaluation/index.js +138 -14
  27. package/dist/evaluation/index.js.map +1 -1
  28. package/dist/evaluation/types.d.ts +1 -1
  29. package/dist/harness/check/runtime-render/index.d.ts +3 -3
  30. package/dist/harness/index.d.ts +1 -1
  31. package/dist/harness/index.js +6 -6
  32. package/dist/harness/index.js.map +1 -1
  33. package/dist/harness/prompts.js +1 -4
  34. package/dist/harness/prompts.js.map +1 -1
  35. package/dist/harness/result-types.d.ts +14 -3
  36. package/dist/harness/result-types.js.map +1 -1
  37. package/dist/harness/runtime.d.ts +1 -1
  38. package/dist/harness/runtime.js +6 -7
  39. package/dist/harness/runtime.js.map +1 -1
  40. package/dist/harness/types-public.d.ts +1 -1
  41. package/dist/harness/types-public.js +4 -6
  42. package/dist/harness/types-public.js.map +1 -1
  43. package/dist/harness/validator-trace-sink.d.ts +1 -1
  44. package/dist/index.d.ts +6 -6
  45. package/dist/index.js +268 -28
  46. package/dist/index.js.map +1 -1
  47. package/dist/patch.d.ts +1 -1
  48. package/dist/{types-public-B7wn4ce5.d.ts → types-public-h8IMnYuh.d.ts} +4 -0
  49. package/dist/{visual-evaluator-C9L-o6ap.d.ts → visual-evaluator-C2strxqD.d.ts} +1 -1
  50. package/dist/workflows.d.ts +1 -1
  51. package/package.json +13 -13
  52. package/src/adapters/base.ts +6 -1
  53. package/src/adapters/claude/message-parser.ts +5 -6
  54. package/src/adapters/claude/sdk-tool-pins.ts +44 -0
  55. package/src/adapters/claude/sdk.ts +3 -3
  56. package/src/adapters/claude/source-capture.ts +20 -0
  57. package/src/adapters/provider-router.ts +1 -0
  58. package/src/boilerplate/system-prompt.ts +3 -2
  59. package/src/check/run-tier0.ts +61 -0
  60. package/src/create-ui-generator.ts +4 -2
  61. package/src/design-system-docs.ts +2 -1
  62. package/src/evaluation/axis-checks/checks/state-merge.ts +1 -1
  63. package/src/evaluation/axis-checks/helpers.ts +125 -0
  64. package/src/evaluation/criteria/resolve.ts +19 -1
  65. package/src/evaluation/evaluator.ts +2 -2
  66. package/src/evaluation/llm-evaluator.ts +2 -2
  67. package/src/evaluation/loop.ts +25 -4
  68. package/src/evaluation/message-parsing.ts +11 -12
  69. package/src/evaluation/types-public.ts +4 -0
  70. package/src/evaluation/visual-evaluator.ts +1 -1
  71. package/src/harness/coding/assemble-result.ts +1 -0
  72. package/src/harness/coding/dupe-break.ts +70 -0
  73. package/src/harness/coding/generate-task-runner.ts +5 -1
  74. package/src/harness/coding/run-coding-turn.ts +56 -1
  75. package/src/harness/result-types.ts +12 -0
  76. package/src/index.ts +1 -1
  77. package/src/taught-tokens.ts +5 -6
package/dist/patch.d.ts CHANGED
@@ -2,7 +2,7 @@ import { PatchFn } from './harness/types-public.js';
2
2
  import '@ggui-ai/protocol';
3
3
  import './styling-profile-CxfvV48g.js';
4
4
  import './axes-CKVCzR_x.js';
5
- import './types-public-B7wn4ce5.js';
5
+ import './types-public-h8IMnYuh.js';
6
6
  import './llm.js';
7
7
  import '@ggui-ai/mcp-server-core';
8
8
  import './design-mode-DGWN_Gkm.js';
@@ -599,6 +599,10 @@ interface CanvasJudgeRecord {
599
599
  /**
600
600
  * ggui#1687 — the model asked for, and every model the provider said answered this canvas's calls (scoring and
601
601
  * criteria), deduplicated. `served` is empty when no provider named one: unknown, never "the requested model".
602
+ * When the request names an alias and the provider echoes the alias (measured on a hosted route, 125 of 125
603
+ * canvases), `served` confirms the alias was honoured but cannot tell two snapshots behind it apart. A judge that
604
+ * must is pinned to a dated snapshot id instead. With the alias, a snapshot change shows only as a step on a date in
605
+ * the judge's pooled within-call spread, read by week. That is a read someone runs, not a monitor.
602
606
  */
603
607
  readonly models?: {
604
608
  readonly requested: string;
@@ -1,4 +1,4 @@
1
- import { h as CriteriaContext, i as CriteriaChroma, j as CanvasJudgeRecord, k as CriteriaBlock, l as CanvasPresentationOutcome, n as CanvasHostPresentation, b as AgentConfig, L as LLMResponse, J as JudgeSalvageCounts, E as EvalIssue, o as VisualEvalSummary, p as VisualCoverage } from './types-public-B7wn4ce5.js';
1
+ import { h as CriteriaContext, i as CriteriaChroma, j as CanvasJudgeRecord, k as CriteriaBlock, l as CanvasPresentationOutcome, n as CanvasHostPresentation, b as AgentConfig, L as LLMResponse, J as JudgeSalvageCounts, E as EvalIssue, o as VisualEvalSummary, p as VisualCoverage } from './types-public-h8IMnYuh.js';
2
2
  import { G as GenerationProfileInput } from './styling-profile-CxfvV48g.js';
3
3
  import { C as CriteriaBank } from './bank-Ctdp3FUA.js';
4
4
  import { DataContract, JsonObject } from '@ggui-ai/protocol';
@@ -2,7 +2,7 @@ import { C as Classification } from './axes-CKVCzR_x.js';
2
2
  import { Workflow } from './harness/types-public.js';
3
3
  import '@ggui-ai/protocol';
4
4
  import './styling-profile-CxfvV48g.js';
5
- import './types-public-B7wn4ce5.js';
5
+ import './types-public-h8IMnYuh.js';
6
6
  import './llm.js';
7
7
  import '@ggui-ai/mcp-server-core';
8
8
  import './design-mode-DGWN_Gkm.js';
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@ggui-ai/ui-gen",
3
- "version": "0.26.0",
4
- "description": "UI generation harness for ggui — the LLM system prompt, multi-turn coding loop, and evaluation engine that turn a DataContract into a React component. Implements the UiGenerator contract from @ggui-ai/mcp-server-core; consumed by @ggui-ai/mcp-server.",
3
+ "version": "0.28.0",
4
+ "description": "UI generation harness for GGUI — the LLM system prompt, multi-turn coding loop, and evaluation engine that turn a DataContract into a React component. Implements the UiGenerator contract from @ggui-ai/mcp-server-core; consumed by @ggui-ai/mcp-server.",
5
5
  "license": "Apache-2.0",
6
6
  "keywords": [
7
7
  "ggui",
@@ -189,9 +189,9 @@
189
189
  "puppeteer-core": "^24.40.0 || ^25.0.0",
190
190
  "react": "^18.0.0 || ^19.0.0",
191
191
  "react-dom": "^18.0.0 || ^19.0.0",
192
- "@ggui-ai/mcp-server-core": "0.26.0",
193
- "@ggui-ai/protocol": "0.26.0",
194
- "@ggui-ai/project-config": "0.26.0"
192
+ "@ggui-ai/project-config": "0.28.0",
193
+ "@ggui-ai/mcp-server-core": "0.28.0",
194
+ "@ggui-ai/protocol": "0.28.0"
195
195
  },
196
196
  "peerDependenciesMeta": {
197
197
  "@google/adk": {
@@ -233,11 +233,11 @@
233
233
  "openai": "^7.9.0",
234
234
  "typescript": "^5.0.0",
235
235
  "zod": "^4.3.6",
236
- "@ggui-ai/design": "0.26.0",
237
- "@ggui-ai/gadgets": "0.26.0",
238
- "@ggui-ai/ui-visual-tester": "0.26.0",
239
- "@ggui-ai/sandbox": "0.26.0",
240
- "@ggui-ai/wire": "0.26.0"
236
+ "@ggui-ai/sandbox": "0.28.0",
237
+ "@ggui-ai/gadgets": "0.28.0",
238
+ "@ggui-ai/wire": "0.28.0",
239
+ "@ggui-ai/ui-visual-tester": "0.28.0",
240
+ "@ggui-ai/design": "0.28.0"
241
241
  },
242
242
  "devDependencies": {
243
243
  "@google/adk": "^2.0.0",
@@ -252,9 +252,9 @@
252
252
  "react-dom": "^19.0.0",
253
253
  "tsup": "^8.5.0",
254
254
  "vitest": "^3.2.6",
255
- "@ggui-ai/project-config": "0.26.0",
256
- "@ggui-ai/mcp-server-core": "0.26.0",
257
- "@ggui-ai/protocol": "0.26.0"
255
+ "@ggui-ai/mcp-server-core": "0.28.0",
256
+ "@ggui-ai/project-config": "0.28.0",
257
+ "@ggui-ai/protocol": "0.28.0"
258
258
  },
259
259
  "engines": {
260
260
  "node": ">=22.0.0"
@@ -45,8 +45,13 @@ export interface ClaudeSdkConfig extends AdapterConfig {
45
45
  env?: Record<string, string>;
46
46
  /** MCP server configs for Claude Agent SDK (keyed by name) */
47
47
  mcpServers?: Record<string, McpServerConfig>;
48
- /** Allowed tool names for Claude Agent SDK */
48
+ /** Tool names to approve. Absent → every tool of every server in `mcpServers` (ggui#1714). */
49
49
  allowedTools?: string[];
50
+ /**
51
+ * CLI built-in tools to offer the model (e.g. `['Write']`). Default: none.
52
+ * An explicit opt-in: a built-in that writes or executes acts on the host (ggui#1714).
53
+ */
54
+ builtinTools?: readonly string[];
50
55
  /** Subagent definitions for Claude Agent SDK */
51
56
  agents?: Record<string, AgentDefinition>;
52
57
  /** Custom CLI path for Docker deployments */
@@ -13,6 +13,7 @@ import type {
13
13
  SDKUserMessage,
14
14
  } from '@anthropic-ai/claude-agent-sdk';
15
15
  import type { JsonObject } from '@ggui-ai/protocol';
16
+ import { sourceCodeFromToolUse } from './source-capture';
16
17
 
17
18
  /**
18
19
  * Mutable accumulator for artifacts extracted from the SDK message stream.
@@ -197,12 +198,10 @@ function extractSourceCode(message: SDKMessage, artifacts: MessageArtifacts): vo
197
198
  if (!Array.isArray(content)) return;
198
199
 
199
200
  for (const block of content) {
200
- if (block.type === 'tool_use' && block.name === 'Write') {
201
- const input = block.input as { content?: string } | undefined;
202
- if (input?.content) {
203
- artifacts.sourceCode = input.content;
204
- }
205
- }
201
+ if (block.type !== 'tool_use') continue;
202
+ // ggui#1714 — the rule lives in source-capture.ts, shared with the evaluation loop.
203
+ const value = sourceCodeFromToolUse(block.name, block.input);
204
+ if (value !== undefined) artifacts.sourceCode = value;
206
205
  }
207
206
  }
208
207
 
@@ -0,0 +1,44 @@
1
+ // packages/ui-gen/src/adapters/claude/sdk-tool-pins.ts
2
+ //
3
+ // ggui#1714 — the tool surface of every Agent SDK `query()` this package runs.
4
+ //
5
+ // Left to its defaults, a `query()` session offers the model the CLI's whole
6
+ // built-in set (Bash, Edit, Read, Write, WebFetch, Task, …) and loads the host
7
+ // user's own Claude configuration (plugins, MCP servers from `~/.claude`).
8
+ // `allowedTools` does not narrow that: it only approves the names it lists.
9
+ // Measured on SDK 0.3.229 from the session's `system`/`init` message:
10
+ //
11
+ // as before this file → every built-in + the caller's tools
12
+ // `tools: []` alone → the caller's tools + a user-level plugin server
13
+ // `tools: []` + `settingSources: []` → the caller's tools only
14
+ //
15
+ // So the pins are both, on every path. With them in place no permission
16
+ // bypass is needed: a tool named in `allowedTools` runs, and a server-level
17
+ // entry (`mcp__<server>`) approves every tool of that server (both measured
18
+ // on a live turn; with no entry the call is denied and the run reports it).
19
+
20
+ import type { McpServerConfig, Options } from '@anthropic-ai/claude-agent-sdk';
21
+
22
+ export type SdkToolPins = Required<Pick<Options, 'tools' | 'settingSources' | 'allowedTools'>>;
23
+
24
+ export interface SdkToolPinInput {
25
+ /** The MCP servers the session gets, keyed by name. */
26
+ readonly mcpServers?: Record<string, McpServerConfig>;
27
+ /** Tool names to approve. Absent → every tool of every server in `mcpServers`. */
28
+ readonly allowedTools?: readonly string[];
29
+ /**
30
+ * The CLI built-ins to offer (e.g. `['Write']`) — an explicit opt-in.
31
+ * Each one is also approved, so it runs without a prompt; a built-in that
32
+ * writes or executes acts on the host with the session's permissions.
33
+ */
34
+ readonly builtinTools?: readonly string[];
35
+ }
36
+
37
+ export function sdkToolPins(input: SdkToolPinInput): SdkToolPins {
38
+ const builtins = [...(input.builtinTools ?? [])];
39
+ const approved =
40
+ input.allowedTools !== undefined
41
+ ? [...input.allowedTools]
42
+ : Object.keys(input.mcpServers ?? {}).map((name) => `mcp__${name}`);
43
+ return { tools: builtins, settingSources: [], allowedTools: [...approved, ...builtins] };
44
+ }
@@ -14,6 +14,7 @@ import type { AdapterResult, ProviderName, AdapterMode } from '../types';
14
14
  import { createArtifacts, processMessage } from './message-parser';
15
15
  import { createToolMcpServer } from './tool-bridge';
16
16
  import { claudeCodeLoginQueryOptions, loginChildEnv } from './claude-code-login';
17
+ import { sdkToolPins } from './sdk-tool-pins';
17
18
 
18
19
  export class ClaudeSdkAdapter extends GeneratorAdapter {
19
20
  readonly provider: ProviderName = 'claude';
@@ -73,9 +74,8 @@ export class ClaudeSdkAdapter extends GeneratorAdapter {
73
74
  systemPrompt: params.systemPrompt,
74
75
  ...(mcpServers && { mcpServers }),
75
76
  ...(cfg.agents && { agents: cfg.agents }),
76
- ...(allowedTools && { allowedTools }),
77
- permissionMode: 'bypassPermissions',
78
- allowDangerouslySkipPermissions: true,
77
+ // ggui#1714 — no CLI built-ins unless asked for, no host settings, no permission bypass.
78
+ ...sdkToolPins({ mcpServers, allowedTools, builtinTools: cfg.builtinTools }),
79
79
  ...(cfg.cliPath && { pathToClaudeCodeExecutable: cfg.cliPath }),
80
80
  ...(cfg.stderr && { stderr: cfg.stderr }),
81
81
  // ggui#1185 — tool-less, config-less, non-bare; the model is pinned above.
@@ -0,0 +1,20 @@
1
+ // packages/ui-gen/src/adapters/claude/source-capture.ts
2
+ //
3
+ // ggui#1714 — which tool call carries a component's source, decided once.
4
+ // Both Agent SDK paths (ClaudeSdkAdapter's message parser and the evaluation
5
+ // loop's extractor) read source through this function, so the rule cannot be
6
+ // fixed on one path and left stale on the other.
7
+ //
8
+ // `*__compile_component` → `input.code` (the bridged compile tool)
9
+ // `Write` → `input.content` (a CLI built-in, offered only by opt-in)
10
+
11
+ const COMPILE_TOOL_SUFFIX = '__compile_component';
12
+
13
+ /** The source a tool_use block carries, or undefined when it carries none. */
14
+ export function sourceCodeFromToolUse(name: unknown, input: unknown): string | undefined {
15
+ if (typeof name !== 'string') return undefined;
16
+ const field = name === 'Write' ? 'content' : name.endsWith(COMPILE_TOOL_SUFFIX) ? 'code' : undefined;
17
+ if (field === undefined || typeof input !== 'object' || input === null) return undefined;
18
+ const value: unknown = Reflect.get(input, field);
19
+ return typeof value === 'string' && value.length > 0 ? value : undefined;
20
+ }
@@ -29,6 +29,7 @@ import type { LlmRoute } from '@ggui-ai/protocol';
29
29
  export function getBedrockModelId(model: string): string {
30
30
  const BEDROCK_MAP: Record<string, string> = {
31
31
  'anthropic/claude-haiku-4-5': 'us.anthropic.claude-haiku-4-5-20251001-v1:0',
32
+ 'anthropic/claude-haiku-5-5': 'us.anthropic.claude-haiku-5-5', // listed ACTIVE in us-east-1 (ggui#1743)
32
33
  'anthropic/claude-sonnet-4-6': 'us.anthropic.claude-sonnet-4-6',
33
34
  'anthropic/claude-opus-4-6': 'us.anthropic.claude-opus-4-6-v1:0',
34
35
  // Claude 5-family (ggui#706, strings verified 2026-09-02 against
@@ -481,13 +481,14 @@ MANDATORY:
481
481
  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))\`.
482
482
  3. NEVER invent your own palette. The system provides primary, neutral, success, warning, error, and info — use ONLY these.
483
483
  4. NEVER include literal fallback values in ANY token reference — colors, spacing, typography, radius, shadows alike. Write \`var(--ggui-spacing-4)\` bare, never \`var(--ggui-spacing-4, 16px)\`; \`var(--ggui-color-primary-600)\` bare, never \`var(--ggui-color-primary-600, #0284c7)\`. The runtime injects every token; a literal fallback paints the wrong value exactly when the operator's theme matters most.
484
+ 5. NEVER set an inline \`border\`, \`borderColor\`, \`borderWidth\` or \`borderStyle\` on a kit control — \`Button\`, \`Input\`, \`Select\`, \`TextArea\`, \`Checkbox\`, \`RadioGroup\`, \`Toggle\`, \`SearchField\`. The control draws its own boundary, and it clears 3:1 on its surface in every theme: an outline chip is \`<Button variant="outline">\` exactly as it comes (its edge is \`controlAccentOutline\`). Where you draw a control-like edge on your own element, use \`var(--ggui-color-controlOutline)\`. A ramp step such as \`primary-200\` as a control's border is nearly invisible (about 1.1:1).
484
485
 
485
486
  Token categories:
486
487
  - Brand fills: \`var(--ggui-color-primary-600)\`, \`var(--ggui-color-primary-50)\`
487
488
  - Accent text: \`var(--ggui-color-link)\` — eyebrows, taglines, links, labels; readable per mode (never a bare \`primary-<stop>\` as a text color)
488
489
  - Text: \`var(--ggui-color-onContainer)\`, \`var(--ggui-color-onSunken)\`
489
490
  - Backgrounds: \`var(--ggui-color-container)\`, \`var(--ggui-color-sunken)\`
490
- - Borders: \`var(--ggui-color-outline)\`
491
+ - Borders: \`var(--ggui-color-outline)\` — dividers and the edges of non-interactive boxes; a control's edge is the kit's (rule 5)
491
492
  - Spacing: \`var(--ggui-spacing-4)\`, \`var(--ggui-spacing-6)\`
492
493
  - Typography: \`var(--ggui-font-size-sm)\`, \`var(--ggui-font-weight-semibold)\`
493
494
  - Shadows: \`var(--ggui-shape-shadow-sm)\`, \`var(--ggui-shape-shadow-md)\`, \`var(--ggui-shape-shadow-lg)\`
@@ -502,7 +503,7 @@ Use the FULL primary palette throughout the component — NOT only on submit but
502
503
  | Element | Token | Purpose |
503
504
  |---------|-------|---------|
504
505
  | Section headers, hero areas, highlight strips | \`primary-50\` / \`primary-100\` | Subtle branded backgrounds |
505
- | Borders, dividers, focus rings, input focus | \`primary-200\` / \`primary-300\` | Branded structure |
506
+ | Dividers, section rules, edges of non-interactive boxes | \`primary-200\` / \`primary-300\` | Branded structure — never a control's border (rule 5) |
506
507
  | Accent TEXT — eyebrows, taglines, links, labels, inline arrows | \`link\` (\`var(--ggui-color-link)\`) or \`<Text tone="emphasized">\` | The brand's READABLE ink, derived per mode to clear 4.5:1 — a bare \`primary-300…600\` as a text color fails tier-0 |
507
508
  | Icon fills, badges, active indicators | \`primary-500\` / \`primary-600\` | Core accent fill |
508
509
  | Buttons, CTAs, filled interactive elements | \`primary-600\` / \`primary-700\` | Primary actions |
@@ -571,6 +571,50 @@ function detectComponentNeverReturns(
571
571
  * Wraps the same logic as runSelfChecks (adapters/tools.ts) but emits EvalIssue[].
572
572
  * Also runs contract validation when contract are provided.
573
573
  */
574
+ /** The kit controls whose boundary the design system draws (ggui#1697). */
575
+ const KIT_CONTROLS = ['Button', 'Input', 'Select', 'TextArea', 'Checkbox', 'RadioGroup', 'Toggle', 'SearchField'] as const;
576
+
577
+ /**
578
+ * Opening tags of kit controls, read brace- and quote-aware: an attribute like `onClick={() => x}` holds a `>` that a
579
+ * plain `[^>]*` pattern stops at, which would hide every attribute after it.
580
+ */
581
+ function kitControlTags(source: string): { name: string; attrs: string; line: number }[] {
582
+ const out: { name: string; attrs: string; line: number }[] = [];
583
+ const open = new RegExp(`<(${KIT_CONTROLS.join('|')})\\b`, 'g');
584
+ for (const m of source.matchAll(open)) {
585
+ const start = (m.index ?? 0) + m[0].length;
586
+ let depth = 0;
587
+ let quote: string | null = null;
588
+ let i = start;
589
+ for (; i < source.length; i += 1) {
590
+ const c = source[i];
591
+ if (quote !== null) {
592
+ if (c === quote && source[i - 1] !== '\\') quote = null;
593
+ } else if (c === '"' || c === "'" || c === '`') quote = c;
594
+ else if (c === '{') depth += 1;
595
+ else if (c === '}') depth -= 1;
596
+ else if (c === '>' && depth === 0) break;
597
+ }
598
+ out.push({ name: m[1] ?? '', attrs: source.slice(start, i), line: source.slice(0, m.index ?? 0).split('\n').length });
599
+ }
600
+ return out;
601
+ }
602
+
603
+ /** A border key (not a radius) in the tag's inline style, or in a same-file object its `style={name}` points at. */
604
+ function inlineBorderKey(attrs: string, source: string): string | null {
605
+ const BORDER = /\b(border(?:Top|Right|Bottom|Left)?(?:Color|Width|Style)?)\s*:/;
606
+ const style = attrs.match(/\bstyle=\{\{([\s\S]*)\}\}/);
607
+ if (style !== null) {
608
+ const hit = (style[1] ?? '').match(BORDER);
609
+ if (hit !== null) return hit[1] ?? null;
610
+ }
611
+ const named = attrs.match(/\bstyle=\{\s*([A-Za-z_$][\w$]*)\s*\}/);
612
+ if (named === null) return null;
613
+ const decl = source.match(new RegExp(`\\b(?:const|let)\\s+${named[1]}\\b[^=]*=\\s*\\{([^{}]*)\\}`));
614
+ const hit = decl !== null ? (decl[1] ?? '').match(BORDER) : null;
615
+ return hit !== null ? hit[1] ?? null : null;
616
+ }
617
+
574
618
  export async function runTier0Checks(
575
619
  sourceCode: string,
576
620
  contract?: DataContract,
@@ -1169,6 +1213,23 @@ export async function runTier0Checks(
1169
1213
  }
1170
1214
  }
1171
1215
 
1216
+ // ── A kit control's boundary is the kit's (ggui#1697) ──────
1217
+ // A control draws its own edge, derived to clear 3:1 on its surface (`controlOutline`, or
1218
+ // `controlAccentOutline` on an outline Button); an inline border paints over it, and the ramp step
1219
+ // the model reached for (`primary-200`) measured about 1.1:1. The prompt's rule 5 says the same, so
1220
+ // this check enforces what the model was told. The description starts with the check's id so the
1221
+ // generator's logged violation lines (`✗ …`) count its fires per generation, in-turn repairs included.
1222
+ for (const tag of enforceDesignVocabulary ? kitControlTags(sourceCode) : []) {
1223
+ const key = inlineBorderKey(tag.attrs, sourceCode);
1224
+ if (key === null) continue;
1225
+ issues.push({
1226
+ tier: 0, result: 'fail', category: 'tokens', subcategory: 'control-inline-border', severity: 'critical',
1227
+ description: `control-inline-border: <${tag.name}> sets an inline \`${key}\` — a kit control draws its own boundary, and an inline border paints over it`,
1228
+ fix: `Remove the border from <${tag.name}>'s style (keep borderRadius if you need it). An outline chip is <Button variant="outline"> exactly as it comes; a control-like edge on your own element uses var(--ggui-color-controlOutline).`,
1229
+ line: tag.line,
1230
+ });
1231
+ }
1232
+
1172
1233
  // ── Missing Props interface ───────────────────────────────
1173
1234
  if (!sourceCode.includes('interface Props') && !sourceCode.includes('type Props')) {
1174
1235
  issues.push({
@@ -40,7 +40,7 @@ import type {
40
40
  ModelRef,
41
41
  AppGenerationProfileEffort,
42
42
  } from "@ggui-ai/protocol";
43
- import { isGeneratorId, modelRefOfRoute } from "@ggui-ai/protocol";
43
+ import { DEFAULT_MODEL as PROTOCOL_DEFAULT_MODEL, isGeneratorId, modelRefOfRoute } from "@ggui-ai/protocol";
44
44
  import type { GadgetCatalogAdapter } from "@ggui-ai/gadgets";
45
45
  import type {
46
46
  GenerationMetadata,
@@ -85,7 +85,9 @@ import type { AgentConfig } from "./harness/llm-router.js";
85
85
 
86
86
  /** The slug for the OSS default seed generator. */
87
87
  const DEFAULT_TIER: GeneratorTier = "default";
88
- const DEFAULT_MODEL: ModelRef = "anthropic/claude-haiku-4-5";
88
+ // ggui#1794 — the open-source default is protocol's, one source: a default switch moves one constant
89
+ // (`@ggui-ai/protocol` `DEFAULT_MODEL`), never two. ggui#1743 moved it to Haiku 5.5 on benchmark's receipt.
90
+ const DEFAULT_MODEL: ModelRef = PROTOCOL_DEFAULT_MODEL;
89
91
 
90
92
  export interface CreateUiGeneratorOptions {
91
93
  /**
@@ -49,6 +49,7 @@ These tokens adapt automatically to any theme (light, dark, branded). **ALWAYS u
49
49
  | link | var(--ggui-color-link) | Accent TEXT — eyebrows, taglines, links, labels, inline arrows: the brand's readable ink, derived per mode to clear 4.5:1 on the container and the ground |
50
50
  | outline | var(--ggui-color-outline) | Borders, dividers |
51
51
  | outlineVariant | var(--ggui-color-outlineVariant) | Subtle borders |
52
+ | controlOutline | var(--ggui-color-controlOutline) | A control's edge — clears 3:1 on its surface; kit controls draw it themselves |
52
53
 
53
54
  **Usage pattern:**
54
55
  - Page/section background → \`var(--ggui-color-ground)\`
@@ -173,7 +174,7 @@ Use \`var(--ggui-spacing-N)\` for all padding, gap, and margin values. **Never u
173
174
  ### Color usage guide
174
175
  - **Layering roles** (ground, container, elevated, sunken + their on* inks; outline): one kind of AREA each — use them for every background/text/border decision; they adapt to any theme
175
176
  - **primary-50/100**: Section backgrounds, highlight strips, card headers
176
- - **primary-200/300**: Borders, dividers, focus rings, input outlines
177
+ - **primary-200/300**: Dividers and section rules — never a control's border: kit controls draw their own edge (\`controlOutline\` / \`controlAccentOutline\`), so set no inline border on them
177
178
  - **primary-500/600**: Filled buttons, CTAs, badges, icon fills, active indicators — fills and borders, NOT a text color
178
179
  - **link**: Accent TEXT — eyebrows, taglines, links, labels, inline arrows; the brand's readable ink per mode (a bare \`primary-300…600\` as a text color fails tier-0: \`universal.accent_text_ink\`)
179
180
  - **primary-700/800/900**: Headings and text on light primary backgrounds
@@ -57,7 +57,7 @@ function runNoHardcodedEntities(input: AxisCheckInput): EvalIssue[] {
57
57
  mkIssue(
58
58
  "state.merge.no_hardcoded_entities",
59
59
  `Hardcoded entity array literal (multiple objects with "${idField}") in the component — entity data should come from state/props.`,
60
- `Remove the literal array. Seed state from props.{entityProp} and merge updates via stream.`,
60
+ `Remove the literal array: this data comes from ${all.filter((e) => e.idField === idField).map((e) => `props.${e.name}`).join(" / ") || "props"}. Read it there (\`?? []\` for an absent value, never a literal default), seed state from it, and merge updates via stream.`,
61
61
  ),
62
62
  );
63
63
  }
@@ -249,14 +249,128 @@ export function getInitialValuePropNames(contract?: DataContract): string[] {
249
249
  // Source scanning helpers
250
250
  // =============================================================================
251
251
 
252
+ /**
253
+ * The top-level property keys of the object literal whose `{` is at `open`, read by
254
+ * walking brackets and skipping strings, so a nested object or a `}` inside a string
255
+ * neither ends the literal early nor contributes keys. `undefined` when `open` is not a `{`.
256
+ */
257
+ function objectLiteralKeysAt(src: string, open: number): string[] | undefined {
258
+ if (src[open] !== "{") return undefined;
259
+ const keys: string[] = [];
260
+ let depth = 0;
261
+ let expectKey = true;
262
+ for (let i = open; i < src.length; i++) {
263
+ const c = src[i];
264
+ if (c === '"' || c === "'" || c === "`") {
265
+ // Skip the string; a quoted top-level key ("name": …) is a key too.
266
+ let j = i + 1;
267
+ while (j < src.length && src[j] !== c) j += src[j] === "\\" ? 2 : 1;
268
+ if (depth === 1 && expectKey && /^\s*:/.test(src.slice(j + 1))) keys.push(src.slice(i + 1, j));
269
+ i = j;
270
+ expectKey = false;
271
+ continue;
272
+ }
273
+ if (c === "{" || c === "[" || c === "(") {
274
+ depth++;
275
+ if (depth === 1) expectKey = true;
276
+ continue;
277
+ }
278
+ if (c === "}" || c === "]" || c === ")") {
279
+ depth--;
280
+ if (depth === 0) return keys;
281
+ continue;
282
+ }
283
+ if (depth !== 1) continue;
284
+ if (c === ",") {
285
+ expectKey = true;
286
+ continue;
287
+ }
288
+ if (expectKey && /[A-Za-z_$]/.test(c ?? "")) {
289
+ const m = /^([A-Za-z_$][\w$]*)\s*(:|,|\})/.exec(src.slice(i));
290
+ // `key: value`, or the shorthand `key,` / `key }`.
291
+ if (m !== null && m[1] !== undefined) keys.push(m[1]);
292
+ expectKey = false;
293
+ }
294
+ }
295
+ return keys;
296
+ }
297
+
298
+ /** The argument texts of the call whose `(` is at `open`, split on top-level commas. */
299
+ function callArgsAt(src: string, open: number): string[] {
300
+ const args: string[] = [];
301
+ let depth = 0;
302
+ let start = open + 1;
303
+ for (let i = open; i < src.length; i++) {
304
+ const c = src[i];
305
+ if (c === '"' || c === "'" || c === "`") {
306
+ let j = i + 1;
307
+ while (j < src.length && src[j] !== c) j += src[j] === "\\" ? 2 : 1;
308
+ i = j;
309
+ continue;
310
+ }
311
+ if (c === "(" || c === "{" || c === "[") depth++;
312
+ else if (c === ")" || c === "}" || c === "]") {
313
+ depth--;
314
+ if (depth === 0) {
315
+ args.push(src.slice(start, i));
316
+ return args;
317
+ }
318
+ } else if (c === "," && depth === 1) {
319
+ args.push(src.slice(start, i));
320
+ start = i + 1;
321
+ }
322
+ }
323
+ return args;
324
+ }
325
+
326
+ /**
327
+ * The keys an initial-state argument provides: an object literal (`{ … }`), a lazy
328
+ * initializer returning one (`() => ({ … })`), or an identifier bound to one in the same
329
+ * file (`const INITIAL: Form = { … }`, also behind a lazy `() => INITIAL`).
330
+ */
331
+ function initialStateKeys(src: string, argStart: number, argText: string): string[] {
332
+ const lead = argText.length - argText.trimStart().length;
333
+ let at = argStart + lead;
334
+ let text = argText.trim();
335
+ const lazy = /^\(\s*\)\s*=>\s*\(?\s*/.exec(text);
336
+ if (lazy !== null) {
337
+ at += lazy[0].length;
338
+ text = text.slice(lazy[0].length);
339
+ }
340
+ if (text.startsWith("{")) return objectLiteralKeysAt(src, at) ?? [];
341
+ const ident = /^([A-Za-z_$][\w$]*)\s*\)?\s*$/.exec(text);
342
+ if (ident === null || ident[1] === undefined) return [];
343
+ // One binding, or none: a name declared twice (shadowed, or re-declared in another scope) is
344
+ // ambiguous, and ambiguity reads as absence, so the check can never pass on the wrong binding.
345
+ const decls = [...src.matchAll(new RegExp(`(?:const|let|var)\\s+${ident[1]}\\s*(?::[^=]{1,200})?=\\s*`, "g"))];
346
+ const decl = decls.length === 1 ? decls[0] : undefined;
347
+ if (decl === undefined) return [];
348
+ return objectLiteralKeysAt(src, (decl.index ?? 0) + decl[0].length) ?? [];
349
+ }
350
+
252
351
  /**
253
352
  * Collect all key names that appear as state slots — either keys inside a
254
353
  * `useState({ ... })` object literal, or standalone state variables.
354
+ *
355
+ * ggui#1790 — also the initial-state shapes the inline-literal patterns cannot see, each
356
+ * idiomatic React: `useState(INITIAL)` with `const INITIAL = { … }` in the file, a lazy
357
+ * initializer `useState(() => ({ … }))`, and `useReducer(reducer, { … } | INITIAL)`. Missing
358
+ * them made `state.payload.covers_submit` report every payload key missing on a form that held
359
+ * them all, and the model, with nothing real to fix, spent the turn cap rewording a comment.
255
360
  */
256
361
  export function collectStateKeys(src: string): Set<string> {
257
362
  const keys = new Set<string>();
258
363
  const varRe = /const\s*\[\s*(\w+)\s*,\s*(\w+)\s*\]\s*=\s*useState/g;
259
364
  for (const m of src.matchAll(varRe)) keys.add(m[1]);
365
+ // ggui#1791 — a contextSpec slot is state too: the boilerplate binds each slot as
366
+ // `const [x, setX] = useGguiContext<T>('x')` under a DO-NOT-EDIT banner, so a form
367
+ // whose fields are context slots holds every payload key there and cannot move them
368
+ // into useState. Both the binding's name and the slot's name count.
369
+ const contextRe = /const\s*\[\s*(\w+)\s*,\s*\w+\s*\]\s*=\s*useGguiContext\s*(?:<[^>]*>)?\s*\(\s*['"`](\w+)['"`]/g;
370
+ for (const m of src.matchAll(contextRe)) {
371
+ keys.add(m[1]);
372
+ keys.add(m[2]);
373
+ }
260
374
  const objRe = /useState(?:<[^>]*>)?\s*\(\s*\{([\s\S]*?)\}\s*\)/g;
261
375
  for (const m of src.matchAll(objRe)) {
262
376
  const keyRe = /(?:^|,)\s*(\w+)\s*:/g;
@@ -267,6 +381,17 @@ export function collectStateKeys(src: string): Set<string> {
267
381
  const keyRe = /(?:^|,)\s*(\w+)\s*:/g;
268
382
  for (const km of m[1].matchAll(keyRe)) keys.add(km[1]);
269
383
  }
384
+ // ggui#1790 — the initial-state argument, resolved: useState's first, useReducer's second.
385
+ for (const m of src.matchAll(/\b(useState|useReducer)\s*(?:<[^>]*>)?\s*\(/g)) {
386
+ const open = (m.index ?? 0) + m[0].length - 1;
387
+ const args = callArgsAt(src, open);
388
+ const which = m[1] === "useReducer" ? 1 : 0;
389
+ const arg = args[which];
390
+ if (arg === undefined) continue;
391
+ let argStart = open + 1;
392
+ for (let k = 0; k < which; k++) argStart += (args[k]?.length ?? 0) + 1;
393
+ for (const key of initialStateKeys(src, argStart, arg)) keys.add(key);
394
+ }
270
395
  return keys;
271
396
  }
272
397
 
@@ -30,6 +30,16 @@ export const INSTRUMENT_BESIDE_JUDGE: Readonly<Record<string, 'edge'>> = {
30
30
  'space.edge': 'edge',
31
31
  };
32
32
 
33
+ /**
34
+ * ggui#1711 — judge rows a single pre-action frame cannot answer. Their own text makes them not-evaluated "when the
35
+ * capture has no post-action state", and every judged frame today is one capture taken before any action. Asked
36
+ * anyway, the judge answered `n/a` 94 of 94 times and left the row out 15 more times (a dropped row reads like a
37
+ * skip). So the absence is decided here, from the capture: such a row is never asked without a post-action capture,
38
+ * and it reads `n/a` with its reason, still in the tally. With one, the judge is asked as for any row.
39
+ */
40
+ export const REQUIRES_POST_ACTION_CAPTURE: ReadonlySet<string> = new Set(['state.feedback']);
41
+ const NO_POST_ACTION = 'not evaluated: no post-action capture';
42
+
33
43
  export interface CriteriaMeasurements {
34
44
  readonly overflow: boolean;
35
45
  readonly contentHeight: number | null;
@@ -40,6 +50,8 @@ export interface CriteriaMeasurements {
40
50
  * `null` when the probe ran and failed.
41
51
  */
42
52
  readonly edge?: EdgeVerdict | null;
53
+ /** ggui#1711 — the capture includes a state after an action ran. Absent = no (every capture today). */
54
+ readonly postActionCapture?: boolean;
43
55
  }
44
56
 
45
57
  function besideRead(kind: 'edge', m: CriteriaMeasurements): Read {
@@ -73,6 +85,7 @@ function instrumentRead(kind: 'fit' | 'fill', m: CriteriaMeasurements): Read {
73
85
  }
74
86
 
75
87
  function readRow(row: BankRow, answers: readonly CriteriaAnswer[][], m: CriteriaMeasurements, unansweredReason: string | undefined): Read {
88
+ if (row.evaluation === 'judge' && REQUIRES_POST_ACTION_CAPTURE.has(row.id) && m.postActionCapture !== true) return { verdict: 'n/a', evidence: NO_POST_ACTION };
76
89
  if (row.evaluation === 'judge') return majority(row.id, answers, unansweredReason);
77
90
  if (row.evaluation === 'human') return { verdict: 'n/a', evidence: `reader's column (status ${row.status})` };
78
91
  if (row.status !== 'live') return { verdict: 'n/a', evidence: `not implemented (status ${row.status})` };
@@ -147,6 +160,8 @@ export interface CriteriaJudgeInputs {
147
160
  readonly frame?: CriteriaJudgeFrame;
148
161
  /** JSON text of the props the card was rendered with; bounded by {@link CRITERIA_PROPS_MAX_CHARS}. */
149
162
  readonly propsJson?: string;
163
+ /** ggui#1711 — the capture includes a post-action state; absent = no, and rows needing one are not asked. */
164
+ readonly postActionCapture?: boolean;
150
165
  }
151
166
 
152
167
  /** Bound on the props text handed to the judge beside the frame, in characters. */
@@ -190,7 +205,10 @@ function criteriaPropsSection(propsJson: string): string {
190
205
  /** The judge's prompt block for the selected `judge` rows; empty when none is selected. */
191
206
  export function buildCriteriaJudgeBlock(bank: CriteriaBank, selected: CriteriaSelectionResult, inputs: CriteriaJudgeInputs = {}): string {
192
207
  const byId = new Map<string, BankRow>(bankRows(bank).map((r) => [r.id, r]));
193
- const asked = selected.selection.map((s) => byId.get(s.id)).filter((r): r is BankRow => r !== undefined && r.evaluation === 'judge');
208
+ const asked = selected.selection
209
+ .map((s) => byId.get(s.id))
210
+ .filter((r): r is BankRow => r !== undefined && r.evaluation === 'judge')
211
+ .filter((r) => inputs.postActionCapture === true || !REQUIRES_POST_ACTION_CAPTURE.has(r.id));
194
212
  if (asked.length === 0) return '';
195
213
  const lines = asked.map((r) => `- ${r.id} (${r.severity}): ${r.text} Evidence must name: ${r.evidence}`);
196
214
  return (
@@ -218,9 +218,9 @@ function buildEvalResult(
218
218
 
219
219
  function getDefaultEvalModel(provider: string): string {
220
220
  switch (provider) {
221
- case 'claude': return 'claude-haiku-4-5-20251001';
221
+ case 'claude': return 'claude-haiku-5-5';
222
222
  case 'openai': return 'gpt-5.6-luna';
223
223
  case 'google': return 'gemini-3.5-flash-lite';
224
- default: return 'claude-haiku-4-5-20251001';
224
+ default: return 'claude-haiku-5-5';
225
225
  }
226
226
  }
@@ -568,13 +568,13 @@ Use 'fail' only for pervasive design system violations. Use 'warn' for isolated
568
568
  function getDefaultEvalModel(provider: string): string {
569
569
  switch (provider) {
570
570
  case 'claude':
571
- return 'claude-haiku-4-5-20251001';
571
+ return 'claude-haiku-5-5';
572
572
  case 'openai':
573
573
  return 'gpt-5.6-luna';
574
574
  case 'google':
575
575
  return 'gemini-3.5-flash-lite';
576
576
  default:
577
- return 'claude-haiku-4-5-20251001';
577
+ return 'claude-haiku-5-5';
578
578
  }
579
579
  }
580
580