@ggui-ai/ui-gen 0.19.0 → 0.21.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/base.d.ts +7 -0
- package/dist/adapters/base.js +1 -0
- package/dist/adapters/base.js.map +1 -1
- package/dist/adapters/generation-dispatch.d.ts +17 -3
- package/dist/adapters/generation-dispatch.js +992 -3468
- package/dist/adapters/generation-dispatch.js.map +1 -1
- package/dist/adapters/index.js +70 -13
- package/dist/adapters/index.js.map +1 -1
- package/dist/adapters/types.d.ts +1 -1
- package/dist/adapters/types.js.map +1 -1
- package/dist/advanced/index.d.ts +4 -4
- package/dist/advanced/index.js +1060 -3520
- package/dist/advanced/index.js.map +1 -1
- package/dist/blueprint-validator.js +13 -3
- package/dist/blueprint-validator.js.map +1 -1
- package/dist/boilerplate.js +2 -2
- package/dist/boilerplate.js.map +1 -1
- package/dist/check/index.d.ts +1 -1
- package/dist/check/index.js.map +1 -1
- package/dist/{create-ui-generator-CxPvudbs.d.ts → create-ui-generator-D7kOXNjJ.d.ts} +2 -2
- package/dist/{dispatch-N8wdNOX-.d.ts → dispatch-DGvhmARp.d.ts} +1 -1
- package/dist/evaluation/axis-checks/index.d.ts +3 -3
- package/dist/evaluation/axis-checks/index.js +46 -0
- package/dist/evaluation/axis-checks/index.js.map +1 -1
- package/dist/evaluation/axis-checks/registry.d.ts +1 -1
- package/dist/evaluation/axis-checks/registry.js +46 -0
- package/dist/evaluation/axis-checks/registry.js.map +1 -1
- package/dist/evaluation/index.d.ts +38 -6
- package/dist/evaluation/index.js +682 -3357
- package/dist/evaluation/index.js.map +1 -1
- package/dist/harness/check/runtime-render/index.d.ts +2 -2
- package/dist/harness/check/runtime-render/index.js +13 -3
- package/dist/harness/check/runtime-render/index.js.map +1 -1
- package/dist/harness/check/runtime-render/render-check-worker.js +8 -3
- package/dist/harness/check/runtime-render/render-check-worker.js.map +1 -1
- package/dist/harness/index.d.ts +2 -2
- package/dist/harness/index.js +562 -30
- package/dist/harness/index.js.map +1 -1
- package/dist/harness/llm-trace-sink.d.ts +7 -1
- package/dist/harness/llm-trace-sink.js.map +1 -1
- package/dist/harness/result-types.d.ts +1 -1
- package/dist/harness/runtime.d.ts +11 -2
- package/dist/harness/runtime.js +3 -3
- package/dist/harness/runtime.js.map +1 -1
- package/dist/harness/types-public.d.ts +1 -1
- package/dist/harness/types-public.js +2 -2
- package/dist/harness/types-public.js.map +1 -1
- package/dist/harness/validator-trace-sink.d.ts +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.js +1060 -3520
- package/dist/index.js.map +1 -1
- package/dist/{llm-router-D7fjBhtr.d.ts → llm-router-DwwUK1bz.d.ts} +16 -3
- package/dist/patch.d.ts +1 -1
- package/dist/providers/index.js +5 -4
- package/dist/providers/index.js.map +1 -1
- package/dist/{render-check-WfOVO9kU.d.ts → render-check-C0CqVceF.d.ts} +17 -0
- package/dist/{types-public-DKR_h70H.d.ts → types-public-CzYqZclY.d.ts} +44 -1
- package/dist/validation/index.d.ts +1 -1
- package/dist/validation/index.js +1 -1
- package/dist/validation/index.js.map +1 -1
- package/dist/workflows.d.ts +1 -1
- package/package.json +16 -12
- package/src/adapters/base.ts +8 -0
- package/src/adapters/claude/claude-code-login.ts +73 -0
- package/src/adapters/claude/sdk.ts +8 -1
- package/src/adapters/generation-dispatch.ts +19 -2
- package/src/adapters/openai/raw.ts +11 -5
- package/src/adapters/openai/sdk.ts +8 -5
- package/src/adapters/openai/tokens.ts +67 -0
- package/src/adapters/provider-router.ts +28 -0
- package/src/adapters/types.ts +1 -1
- package/src/boilerplate/generate.ts +19 -4
- package/src/boilerplate/hard-sections.ts +1 -1
- package/src/create-ui-generator.ts +19 -10
- package/src/evaluation/axis-checks/checks/universal.ts +92 -0
- package/src/evaluation/design-identity.ts +16 -5
- package/src/evaluation/types-public.ts +42 -0
- package/src/evaluation/visual-evaluator.ts +231 -24
- package/src/harness/check/runtime-render/adapter.ts +9 -0
- package/src/harness/check/runtime-render/render-check.ts +37 -1
- package/src/harness/claude-code-login-query.ts +528 -0
- package/src/harness/coding/assemble-result.ts +10 -1
- package/src/harness/coding/run-eval-round.ts +179 -34
- package/src/harness/enforced-coding.ts +20 -0
- package/src/harness/llm-router.ts +390 -55
- package/src/harness/llm-trace-sink.ts +7 -1
- package/src/harness/runtime.ts +6 -0
- package/src/providers/openai.ts +31 -19
- package/src/validation/primitives.ts +1 -1
package/dist/workflows.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { C as Classification } from './axes-CzLEMDeB.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-
|
|
5
|
+
import './types-public-CzYqZclY.js';
|
|
6
6
|
import './design-mode-DLufhrNp.js';
|
|
7
7
|
import './types-BOvHNG7K.js';
|
|
8
8
|
import './llm.js';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ggui-ai/ui-gen",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.21.0",
|
|
4
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": [
|
|
@@ -185,12 +185,13 @@
|
|
|
185
185
|
"peerDependencies": {
|
|
186
186
|
"@google/adk": "^2.0.0",
|
|
187
187
|
"@openai/agents": "^0.17.0",
|
|
188
|
+
"@sparticuz/chromium": "^149.0.0",
|
|
188
189
|
"puppeteer-core": "^24.40.0 || ^25.0.0",
|
|
189
190
|
"react": "^18.0.0 || ^19.0.0",
|
|
190
191
|
"react-dom": "^18.0.0 || ^19.0.0",
|
|
191
|
-
"@ggui-ai/
|
|
192
|
-
"@ggui-ai/
|
|
193
|
-
"@ggui-ai/
|
|
192
|
+
"@ggui-ai/protocol": "0.21.0",
|
|
193
|
+
"@ggui-ai/mcp-server-core": "0.21.0",
|
|
194
|
+
"@ggui-ai/project-config": "0.21.0"
|
|
194
195
|
},
|
|
195
196
|
"peerDependenciesMeta": {
|
|
196
197
|
"@google/adk": {
|
|
@@ -199,6 +200,9 @@
|
|
|
199
200
|
"@openai/agents": {
|
|
200
201
|
"optional": true
|
|
201
202
|
},
|
|
203
|
+
"@sparticuz/chromium": {
|
|
204
|
+
"optional": true
|
|
205
|
+
},
|
|
202
206
|
"puppeteer-core": {
|
|
203
207
|
"optional": true
|
|
204
208
|
},
|
|
@@ -229,11 +233,11 @@
|
|
|
229
233
|
"openai": "^7.9.0",
|
|
230
234
|
"typescript": "^5.0.0",
|
|
231
235
|
"zod": "^4.3.6",
|
|
232
|
-
"@ggui-ai/design": "0.
|
|
233
|
-
"@ggui-ai/gadgets": "0.
|
|
234
|
-
"@ggui-ai/sandbox": "0.
|
|
235
|
-
"@ggui-ai/
|
|
236
|
-
"@ggui-ai/
|
|
236
|
+
"@ggui-ai/design": "0.21.0",
|
|
237
|
+
"@ggui-ai/gadgets": "0.21.0",
|
|
238
|
+
"@ggui-ai/sandbox": "0.21.0",
|
|
239
|
+
"@ggui-ai/ui-visual-tester": "0.21.0",
|
|
240
|
+
"@ggui-ai/wire": "0.21.0"
|
|
237
241
|
},
|
|
238
242
|
"devDependencies": {
|
|
239
243
|
"@google/adk": "^2.0.0",
|
|
@@ -248,9 +252,9 @@
|
|
|
248
252
|
"react-dom": "^19.0.0",
|
|
249
253
|
"tsup": "^8.5.0",
|
|
250
254
|
"vitest": "^3.2.6",
|
|
251
|
-
"@ggui-ai/mcp-server-core": "0.
|
|
252
|
-
"@ggui-ai/project-config": "0.
|
|
253
|
-
"@ggui-ai/protocol": "0.
|
|
255
|
+
"@ggui-ai/mcp-server-core": "0.21.0",
|
|
256
|
+
"@ggui-ai/project-config": "0.21.0",
|
|
257
|
+
"@ggui-ai/protocol": "0.21.0"
|
|
254
258
|
},
|
|
255
259
|
"engines": {
|
|
256
260
|
"node": ">=22.0.0"
|
package/src/adapters/base.ts
CHANGED
|
@@ -14,6 +14,13 @@ export interface AdapterConfig {
|
|
|
14
14
|
apiKey?: string;
|
|
15
15
|
/** Use Bedrock instead of direct API (Claude only) */
|
|
16
16
|
useBedrock?: boolean;
|
|
17
|
+
/**
|
|
18
|
+
* ggui#1185 — run on the machine's own Claude Code login instead of a key.
|
|
19
|
+
* Claude SDK adapter only. The adapter strips every provider key from the
|
|
20
|
+
* spawned env and pins the run tool-less, config-less and non-bare (see
|
|
21
|
+
* `claude/claude-code-login.ts`). Opt-in; absent ⇒ key-based auth.
|
|
22
|
+
*/
|
|
23
|
+
claudeCodeLogin?: boolean;
|
|
17
24
|
/**
|
|
18
25
|
* Thinking level for models with extended thinking (Gemini 3.x Flash
|
|
19
26
|
* via the Interactions API). Mirrors the API's `thinking_level` enum —
|
|
@@ -129,5 +136,6 @@ export function hasCredentials(
|
|
|
129
136
|
): boolean {
|
|
130
137
|
if (config.apiKey) return true;
|
|
131
138
|
if (config.useBedrock) return true;
|
|
139
|
+
if (config.claudeCodeLogin) return true;
|
|
132
140
|
return envVarNames.some((name) => !!process.env[name]);
|
|
133
141
|
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ggui#1185 — generation through the machine's own Claude Code login.
|
|
3
|
+
*
|
|
4
|
+
* The Claude SDK adapter spawns the Claude Code binary. Given no provider
|
|
5
|
+
* key, that binary authenticates with whatever `claude` is logged in as on
|
|
6
|
+
* the machine — the operator's own plan. This module holds the three
|
|
7
|
+
* things that make such a run honest and reproducible:
|
|
8
|
+
*
|
|
9
|
+
* 1. {@link CLAUDE_CODE_LOGIN_CREDENTIAL} — the sentinel a `ProviderKeyRef`
|
|
10
|
+
* carries in place of a key, the same shape `bedrock-iam` already uses
|
|
11
|
+
* for "authenticate with ambient credentials, not a key I hold".
|
|
12
|
+
* 2. {@link stripProviderKeyEnv} — no provider key may reach the spawned
|
|
13
|
+
* process: the binary prefers an env key over its login, so a stale
|
|
14
|
+
* `ANTHROPIC_API_KEY` would silently take the run off the login path.
|
|
15
|
+
* 3. {@link claudeCodeLoginQueryOptions} — the SDK options that keep the
|
|
16
|
+
* run bounded: no built-in tool (`tools: []`), no `~/.claude` settings,
|
|
17
|
+
* hooks or MCP servers leaking in (`settingSources: []`). Not
|
|
18
|
+
* `--bare`, either: on the SDK's bundled binary `-p` reads the login
|
|
19
|
+
* store as it always has, so no flag is needed — and the flag it
|
|
20
|
+
* would be (`--no-bare`) is one that binary rejects; see the note on
|
|
21
|
+
* the function below.
|
|
22
|
+
*
|
|
23
|
+
* Opt-in, off by default; the default stays a provider key. The copy that
|
|
24
|
+
* names what this does lives with the CLI flag that enables it.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import type { Options } from "@anthropic-ai/claude-agent-sdk";
|
|
28
|
+
|
|
29
|
+
/** Sentinel credential: "no key — use the machine's Claude Code login". */
|
|
30
|
+
export const CLAUDE_CODE_LOGIN_CREDENTIAL = "claude-code-login" as const;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Every env name the spawned binary would read as a credential or an
|
|
34
|
+
* endpoint override ahead of its login. Stripped whole, never rewritten.
|
|
35
|
+
*/
|
|
36
|
+
export const PROVIDER_KEY_ENV_NAMES = [
|
|
37
|
+
"ANTHROPIC_API_KEY",
|
|
38
|
+
"CLAUDE_API_KEY",
|
|
39
|
+
"ANTHROPIC_AUTH_TOKEN",
|
|
40
|
+
"ANTHROPIC_BASE_URL",
|
|
41
|
+
] as const;
|
|
42
|
+
|
|
43
|
+
/** A copy of `env` without any provider key or endpoint override. */
|
|
44
|
+
export function stripProviderKeyEnv(
|
|
45
|
+
env: Readonly<Record<string, string | undefined>>
|
|
46
|
+
): Record<string, string> {
|
|
47
|
+
const out: Record<string, string> = {};
|
|
48
|
+
const drop: ReadonlySet<string> = new Set(PROVIDER_KEY_ENV_NAMES);
|
|
49
|
+
for (const [k, v] of Object.entries(env)) {
|
|
50
|
+
if (v !== undefined && !drop.has(k)) out[k] = v;
|
|
51
|
+
}
|
|
52
|
+
return out;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* The SDK `query()` options a login-path run is pinned to — typed as the
|
|
57
|
+
* SDK's own `Options` members, so a change in the SDK's shape is a compile
|
|
58
|
+
* error here and never a silently ignored key.
|
|
59
|
+
*
|
|
60
|
+
* NOT pinned: `--no-bare`. The SDK spawns its BUNDLED binary (2.1.229 for
|
|
61
|
+
* SDK 0.3.229), which has no bare mode and rejects the flag outright
|
|
62
|
+
* (`error: unknown option '--no-bare'`, measured 2026-09-21 on the first
|
|
63
|
+
* live turn of the login client); the machine's own `claude` (2.1.274)
|
|
64
|
+
* accepts it, which is where the earlier probe measured it. On the
|
|
65
|
+
* bundled binary `-p` reads the login store as it always has. If a later
|
|
66
|
+
* SDK bundles a binary where bare becomes the `-p` default, the flag
|
|
67
|
+
* comes back here — with the SDK version bump, measured again.
|
|
68
|
+
*/
|
|
69
|
+
export type ClaudeCodeLoginQueryOptions = Required<Pick<Options, "tools" | "settingSources">>;
|
|
70
|
+
|
|
71
|
+
export function claudeCodeLoginQueryOptions(): ClaudeCodeLoginQueryOptions {
|
|
72
|
+
return { tools: [], settingSources: [] };
|
|
73
|
+
}
|
|
@@ -13,6 +13,7 @@ import type { ClaudeSdkConfig, GenerateParams } from '../base';
|
|
|
13
13
|
import type { AdapterResult, ProviderName, AdapterMode } from '../types';
|
|
14
14
|
import { createArtifacts, processMessage } from './message-parser';
|
|
15
15
|
import { createToolMcpServer } from './tool-bridge';
|
|
16
|
+
import { claudeCodeLoginQueryOptions, stripProviderKeyEnv } from './claude-code-login';
|
|
16
17
|
|
|
17
18
|
export class ClaudeSdkAdapter extends GeneratorAdapter {
|
|
18
19
|
readonly provider: ProviderName = 'claude';
|
|
@@ -40,7 +41,11 @@ export class ClaudeSdkAdapter extends GeneratorAdapter {
|
|
|
40
41
|
const { query } = await import('@anthropic-ai/claude-agent-sdk');
|
|
41
42
|
|
|
42
43
|
// Build environment from config or process.env
|
|
43
|
-
|
|
44
|
+
// ggui#1185 — on the login path no provider key may reach the binary:
|
|
45
|
+
// it prefers an env key over its login, so a stale one would take the
|
|
46
|
+
// run off this path without a word.
|
|
47
|
+
const login = cfg.claudeCodeLogin === true;
|
|
48
|
+
const env = login ? stripProviderKeyEnv(buildEnv(cfg.env)) : buildEnv(cfg.env);
|
|
44
49
|
|
|
45
50
|
const artifacts = createArtifacts();
|
|
46
51
|
const onProgress = cfg.onProgress;
|
|
@@ -72,6 +77,8 @@ export class ClaudeSdkAdapter extends GeneratorAdapter {
|
|
|
72
77
|
allowDangerouslySkipPermissions: true,
|
|
73
78
|
...(cfg.cliPath && { pathToClaudeCodeExecutable: cfg.cliPath }),
|
|
74
79
|
...(cfg.stderr && { stderr: cfg.stderr }),
|
|
80
|
+
// ggui#1185 — tool-less, config-less, non-bare; the model is pinned above.
|
|
81
|
+
...(login ? claudeCodeLoginQueryOptions() : {}),
|
|
75
82
|
},
|
|
76
83
|
})) {
|
|
77
84
|
processMessage(
|
|
@@ -102,6 +102,12 @@ export interface GenerationDispatchParams {
|
|
|
102
102
|
* today's single screenshot at `viewport`.
|
|
103
103
|
*/
|
|
104
104
|
canvases?: readonly CanvasClass[];
|
|
105
|
+
/**
|
|
106
|
+
* ggui#1195 — per-canvas viewport override for the in-loop round: the
|
|
107
|
+
* box the composer targeted (`rendering.viewport`) so the round judges
|
|
108
|
+
* the same box. See `VisualEvalConfig.canvasViewports`.
|
|
109
|
+
*/
|
|
110
|
+
canvasViewports?: Partial<Readonly<Record<CanvasClass, { width: number; height: number }>>>;
|
|
105
111
|
};
|
|
106
112
|
/** Quality config controlling eval tiers and improvement behavior */
|
|
107
113
|
qualityConfig?: QualityConfig;
|
|
@@ -179,7 +185,12 @@ export interface GenerationDispatchParams {
|
|
|
179
185
|
* means `constrained` (byte-identical to the pre-`designMode` path).
|
|
180
186
|
*/
|
|
181
187
|
designMode?: DesignMode;
|
|
182
|
-
/**
|
|
188
|
+
/**
|
|
189
|
+
* Rendering canvas class — see `CreateHarnessInput.canvas`. Absent, the
|
|
190
|
+
* harness derives it from `shellType` × `screen` (ggui#1117), so the axis
|
|
191
|
+
* checks always judge for the canvas the generation renders on; the
|
|
192
|
+
* `free` prompt renders it, the constrained prompt ignores it.
|
|
193
|
+
*/
|
|
183
194
|
canvas?: CanvasClass;
|
|
184
195
|
/** The app's generation profile (#991). */
|
|
185
196
|
profile?: GenerationProfileInput;
|
|
@@ -296,7 +307,13 @@ export async function dispatchGeneration(
|
|
|
296
307
|
: {}),
|
|
297
308
|
// Design mode + canvas — the harness carries them to every leg.
|
|
298
309
|
...(params.designMode !== undefined ? { designMode: params.designMode } : {}),
|
|
299
|
-
|
|
310
|
+
// ggui#1117 — the canvas is ALWAYS carried: a stated one, else the class
|
|
311
|
+
// shell × screen renders on. The axis checks judge for it
|
|
312
|
+
// (`universal.root_width_cap` stands down without one — which is what every
|
|
313
|
+
// served generation got, since the serving runtime states no canvas); the
|
|
314
|
+
// constrained prompt ignores it (INVARIANT 1, `design-mode.pin.test.ts`),
|
|
315
|
+
// so the prompt digests do not move; the free prompt renders it as before.
|
|
316
|
+
canvas: params.canvas ?? canvasForRendering(params.shellType, params.screen),
|
|
300
317
|
...(params.profile !== undefined ? { profile: params.profile } : {}),
|
|
301
318
|
// Policy is pre-resolved at the dispatch boundary so `createHarness`
|
|
302
319
|
// stays free of experiment plumbing. `resolveHarnessPolicy` applies
|
|
@@ -10,6 +10,7 @@ import { GeneratorAdapter, hasCredentials } from '../base';
|
|
|
10
10
|
import type { AdapterConfig, GenerateParams } from '../base';
|
|
11
11
|
import type { AdapterResult, ToolDefinition, ProviderName, AdapterMode } from '../types';
|
|
12
12
|
import { zodToJsonSchema } from '../tool-bridge';
|
|
13
|
+
import { splitOpenAiUsage } from './tokens';
|
|
13
14
|
import {
|
|
14
15
|
createCapture,
|
|
15
16
|
captureSourceCode,
|
|
@@ -54,6 +55,9 @@ export class OpenAiRawAdapter extends GeneratorAdapter {
|
|
|
54
55
|
const capture = createCapture();
|
|
55
56
|
let totalInputTokens = 0;
|
|
56
57
|
let totalOutputTokens = 0;
|
|
58
|
+
// OpenAI's input token count INCLUDES cached tokens; track the cached
|
|
59
|
+
// subset so tokens.input reports NON-cached input (ggui#1186).
|
|
60
|
+
let totalCachedInputTokens = 0;
|
|
57
61
|
let turnsUsed = 0;
|
|
58
62
|
let allTextOutput = '';
|
|
59
63
|
|
|
@@ -74,6 +78,8 @@ export class OpenAiRawAdapter extends GeneratorAdapter {
|
|
|
74
78
|
if (response.usage) {
|
|
75
79
|
totalInputTokens += response.usage.input_tokens ?? 0;
|
|
76
80
|
totalOutputTokens += response.usage.output_tokens ?? 0;
|
|
81
|
+
totalCachedInputTokens +=
|
|
82
|
+
response.usage.input_tokens_details?.cached_tokens ?? 0;
|
|
77
83
|
}
|
|
78
84
|
|
|
79
85
|
// Process output items
|
|
@@ -156,11 +162,11 @@ export class OpenAiRawAdapter extends GeneratorAdapter {
|
|
|
156
162
|
sourceCode: capture.sourceCode,
|
|
157
163
|
stream: capture.stream,
|
|
158
164
|
generatorMeta: capture.generatorMeta,
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
165
|
+
...splitOpenAiUsage(
|
|
166
|
+
totalInputTokens,
|
|
167
|
+
totalOutputTokens,
|
|
168
|
+
totalCachedInputTokens,
|
|
169
|
+
),
|
|
164
170
|
generationTimeMs: Date.now() - startTime,
|
|
165
171
|
turnsUsed,
|
|
166
172
|
};
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import { GeneratorAdapter, hasCredentials } from '../base';
|
|
7
7
|
import type { AdapterConfig, GenerateParams } from '../base';
|
|
8
8
|
import type { AdapterResult, ProviderName, AdapterMode } from '../types';
|
|
9
|
+
import { splitOpenAiUsage } from './tokens';
|
|
9
10
|
import {
|
|
10
11
|
createCapture,
|
|
11
12
|
captureSourceCode,
|
|
@@ -87,17 +88,19 @@ export class OpenAiSdkAdapter extends GeneratorAdapter {
|
|
|
87
88
|
const usage = result.state.usage;
|
|
88
89
|
const inputTokens = usage.inputTokens;
|
|
89
90
|
const outputTokens = usage.outputTokens;
|
|
91
|
+
// OpenAI's input token count INCLUDES cached tokens; sum the cached
|
|
92
|
+
// subset across per-request details (ggui#1186).
|
|
93
|
+
const cachedInputTokens = (usage.inputTokensDetails ?? []).reduce(
|
|
94
|
+
(sum, details) => sum + (details.cached_tokens ?? 0),
|
|
95
|
+
0,
|
|
96
|
+
);
|
|
90
97
|
|
|
91
98
|
return {
|
|
92
99
|
compiledCode: capture.compiledCode,
|
|
93
100
|
sourceCode: capture.sourceCode,
|
|
94
101
|
stream: capture.stream,
|
|
95
102
|
generatorMeta: capture.generatorMeta,
|
|
96
|
-
|
|
97
|
-
input: inputTokens,
|
|
98
|
-
output: outputTokens,
|
|
99
|
-
total: inputTokens + outputTokens,
|
|
100
|
-
},
|
|
103
|
+
...splitOpenAiUsage(inputTokens, outputTokens, cachedInputTokens),
|
|
101
104
|
generationTimeMs: Date.now() - startTime,
|
|
102
105
|
turnsUsed: result.rawResponses?.length ?? 0,
|
|
103
106
|
};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { AdapterResult } from '../types';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Split an OpenAI usage report into the shared cross-adapter token
|
|
5
|
+
* convention (ggui#1186).
|
|
6
|
+
*
|
|
7
|
+
* OpenAI reports the cached prompt tokens as a SUBSET already counted
|
|
8
|
+
* inside its input token total (`usage.input_tokens` on the Responses
|
|
9
|
+
* API, `usage.inputTokens` on the Agents SDK, both INCLUDING cached
|
|
10
|
+
* tokens). Anthropic (and OpenRouter's Anthropic passthrough) instead
|
|
11
|
+
* report `input_tokens` EXCLUDING cache reads, with cache reads as a
|
|
12
|
+
* separate field.
|
|
13
|
+
*
|
|
14
|
+
* The whole fleet reads one convention: **`tokens.input` is the
|
|
15
|
+
* NON-cached input; `cacheReadTokens` is the cache-hit subset.** Cost
|
|
16
|
+
* and observability consumers price/aggregate `input` at the full input
|
|
17
|
+
* rate and `cacheReadTokens` at the cache-read rate, so the cached
|
|
18
|
+
* portion must appear in exactly one of the two — never both. Reporting
|
|
19
|
+
* OpenAI's cache-inclusive `input_tokens` as `tokens.input` while also
|
|
20
|
+
* populating `cacheReadTokens` would double-count the cached tokens (it
|
|
21
|
+
* inflates `$/gen` and breaks `render_cache_metric`'s `cacheReadPct`
|
|
22
|
+
* denominator). So the OpenAI adapters subtract the cached subset here.
|
|
23
|
+
*
|
|
24
|
+
* `total` is the full processed footprint (non-cached input + cache
|
|
25
|
+
* reads + output = `inputTokensInclCached + outputTokens`), matching the
|
|
26
|
+
* Claude adapter's `total` (input + cacheCreation + cacheRead + output);
|
|
27
|
+
* OpenAI has no explicit cache-creation step, so there is no
|
|
28
|
+
* `cacheCreationTokens` term.
|
|
29
|
+
*/
|
|
30
|
+
export function splitOpenAiUsage(
|
|
31
|
+
inputTokensInclCached: number,
|
|
32
|
+
outputTokens: number,
|
|
33
|
+
cachedTokens: number,
|
|
34
|
+
): { tokens: AdapterResult['tokens']; cacheReadTokens: number } {
|
|
35
|
+
return splitCacheInclusiveUsage(inputTokensInclCached, outputTokens, cachedTokens);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* The provider-neutral form of the split above (ggui#1186, third site): any
|
|
40
|
+
* provider whose reported input token count INCLUDES the cached part of the
|
|
41
|
+
* prompt — OpenAI's `input_tokens` + `input_tokens_details.cached_tokens`,
|
|
42
|
+
* Gemini's `total_input_tokens` + `total_cached_tokens` and
|
|
43
|
+
* `promptTokenCount` + `cachedContentTokenCount` ("the cached part of the
|
|
44
|
+
* prompt", per the SDK) — is split the same way into the non-cached input and
|
|
45
|
+
* the cached subset, so `input` is never double-counted and `total` stays the
|
|
46
|
+
* full footprint.
|
|
47
|
+
*/
|
|
48
|
+
export function splitCacheInclusiveUsage(
|
|
49
|
+
inputTokensInclCached: number,
|
|
50
|
+
outputTokens: number,
|
|
51
|
+
cachedTokens: number,
|
|
52
|
+
): { tokens: AdapterResult['tokens']; cacheReadTokens: number } {
|
|
53
|
+
// Never let a malformed usage report drive input negative.
|
|
54
|
+
const cacheRead = Math.min(
|
|
55
|
+
Math.max(cachedTokens, 0),
|
|
56
|
+
Math.max(inputTokensInclCached, 0),
|
|
57
|
+
);
|
|
58
|
+
const nonCachedInput = Math.max(inputTokensInclCached, 0) - cacheRead;
|
|
59
|
+
return {
|
|
60
|
+
tokens: {
|
|
61
|
+
input: nonCachedInput,
|
|
62
|
+
output: outputTokens,
|
|
63
|
+
total: nonCachedInput + cacheRead + outputTokens,
|
|
64
|
+
},
|
|
65
|
+
cacheReadTokens: cacheRead,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
// legacy) still needs the wire-canonical anthropic id → cross-region
|
|
13
13
|
// profile id upcast.
|
|
14
14
|
|
|
15
|
+
import { CLAUDE_CODE_LOGIN_CREDENTIAL, PROVIDER_KEY_ENV_NAMES } from './claude/claude-code-login';
|
|
15
16
|
import type { LlmRoute } from '@ggui-ai/protocol';
|
|
16
17
|
|
|
17
18
|
/**
|
|
@@ -58,6 +59,13 @@ export function getBedrockModelId(model: string): string {
|
|
|
58
59
|
}
|
|
59
60
|
|
|
60
61
|
export interface RoutingDecision {
|
|
62
|
+
/**
|
|
63
|
+
* Named only for the Claude Code login path (ggui#1185): the caller passed
|
|
64
|
+
* {@link CLAUDE_CODE_LOGIN_CREDENTIAL} as the key, every provider key is
|
|
65
|
+
* cleared from the env below, and the adapter pins the SDK options that keep
|
|
66
|
+
* the run tool-less, config-less and non-bare. Absent ⇒ key-based auth.
|
|
67
|
+
*/
|
|
68
|
+
readonly auth?: 'claude-code-login';
|
|
61
69
|
/**
|
|
62
70
|
* Upstream model id the dispatch adapter will pass to the provider
|
|
63
71
|
* SDK. For most routes this is `route.model` verbatim — the typed
|
|
@@ -143,6 +151,13 @@ function clearSiblings(
|
|
|
143
151
|
*/
|
|
144
152
|
export function resolveRoute(input: RoutingInput): RoutingDecision {
|
|
145
153
|
const { route, apiKey, env } = input;
|
|
154
|
+
if (apiKey === CLAUDE_CODE_LOGIN_CREDENTIAL && route.provider !== 'anthropic') {
|
|
155
|
+
throw new Error(
|
|
156
|
+
`${route.provider}:${route.model} cannot use the claude-code-login credential — ` +
|
|
157
|
+
`the machine's Claude Code login authenticates the Claude Code binary only; ` +
|
|
158
|
+
`pick an anthropic route or supply a ${route.provider} API key.`,
|
|
159
|
+
);
|
|
160
|
+
}
|
|
146
161
|
|
|
147
162
|
switch (route.provider) {
|
|
148
163
|
case 'openai': {
|
|
@@ -215,6 +230,19 @@ export function resolveRoute(input: RoutingInput): RoutingDecision {
|
|
|
215
230
|
}
|
|
216
231
|
|
|
217
232
|
case 'anthropic': {
|
|
233
|
+
if (apiKey === CLAUDE_CODE_LOGIN_CREDENTIAL) {
|
|
234
|
+
// ggui#1185: no key reaches the spawned binary — it authenticates with
|
|
235
|
+
// the machine's own Claude Code login. Every provider key AND every
|
|
236
|
+
// sibling is cleared, because the binary prefers an env key over its
|
|
237
|
+
// login and a stale one would silently take the run off this path.
|
|
238
|
+
const cleared: Record<string, undefined> = {};
|
|
239
|
+
for (const k of PROVIDER_KEY_ENV_NAMES) cleared[k] = undefined;
|
|
240
|
+
return {
|
|
241
|
+
model: route.model,
|
|
242
|
+
env: { ...cleared, ...clearSiblings(new Set()) },
|
|
243
|
+
auth: 'claude-code-login',
|
|
244
|
+
};
|
|
245
|
+
}
|
|
218
246
|
if (apiKey) {
|
|
219
247
|
return {
|
|
220
248
|
model: route.model,
|
package/src/adapters/types.ts
CHANGED
|
@@ -88,7 +88,7 @@ export interface AdapterResult {
|
|
|
88
88
|
subagentsUsed?: string[];
|
|
89
89
|
/** Tokens written to prompt cache (Claude-specific) */
|
|
90
90
|
cacheCreationTokens?: number;
|
|
91
|
-
/** Tokens read from prompt cache (Claude
|
|
91
|
+
/** Tokens read from prompt cache (Claude / OpenRouter / OpenAI). */
|
|
92
92
|
cacheReadTokens?: number;
|
|
93
93
|
/** SDK session ID for evaluation loop resume (Claude SDK-specific) */
|
|
94
94
|
sdkSessionId?: string;
|
|
@@ -246,8 +246,20 @@ export function generateBoilerplate(
|
|
|
246
246
|
}
|
|
247
247
|
|
|
248
248
|
const toolNote = tool ? ` (label "${label}", nextStep hint → ${tool})` : "";
|
|
249
|
+
// ggui#1190: the control's copy is the contract's label, VERBATIM — stated for
|
|
250
|
+
// EVERY action (it used to appear only beside a `nextStep` hint), because the
|
|
251
|
+
// model given `"Confirm & Schedule"` in the contract block wrote
|
|
252
|
+
// `'Confirm Schedule'`. The rule lives in DATA_PARAMETERIZATION rule 4; this is
|
|
253
|
+
// its reminder beside the action, and the axis check
|
|
254
|
+
// `universal.action_label_dropped` names the drop when both are ignored.
|
|
255
|
+
// ggui#1244: it rides the payload type's doc comment, NOT the hook line. Placed
|
|
256
|
+
// on the hook line directly before the once-hint below, the two read as the
|
|
257
|
+
// control's spec and gemini-3.5-flash-lite guarded the repeatable Save 8/8;
|
|
258
|
+
// here, with the hook line back to its earlier bytes, 5/24 — within noise of
|
|
259
|
+
// the rate before either hint moved (1/8) — while one-time commitments stayed
|
|
260
|
+
// guarded 40/40 across three providers and every label stayed verbatim (Exp 009).
|
|
249
261
|
actionTypeAliases.push(
|
|
250
|
-
`/** Action payload: ${desc || label}${toolNote} */\ntype ${typeName} = ${tsType};`
|
|
262
|
+
`/** Action payload: ${desc || label}${toolNote} — control copy: "${label}" VERBATIM */\ntype ${typeName} = ${tsType};`
|
|
251
263
|
);
|
|
252
264
|
// Inline signature tells the LLM exactly how to call this action
|
|
253
265
|
const callSig = tsType === "void" ? "() => void — fire and forget" : `(data: ${tsType}) => void`;
|
|
@@ -255,9 +267,12 @@ export function generateBoilerplate(
|
|
|
255
267
|
// ggui#1108 (mitigation): the reminder sits at the EXACT site the model
|
|
256
268
|
// writes the handler. It is a hint, not the rule — the rule is the prompt's
|
|
257
269
|
// `TERMINAL_ACTIONS` section, because only the request and the contract say
|
|
258
|
-
// which actions are meant once.
|
|
259
|
-
//
|
|
260
|
-
//
|
|
270
|
+
// which actions are meant once. It is NOT inert, whatever Exp 002b measured
|
|
271
|
+
// for a different site comment (adoption 1/36): Exp 009 (ggui#1244) removed
|
|
272
|
+
// it and one-time scheduling went armed on all three providers (guarded 9/12
|
|
273
|
+
// against 24/24), and a second hint placed beside it turned it into an
|
|
274
|
+
// instruction for every action. So it stays, ALONE on the hook line — what
|
|
275
|
+
// sits next to it is part of what it says.
|
|
261
276
|
const onceHint = " — if this is meant ONCE, disable its control after it fires";
|
|
262
277
|
actionHookCalls.push(` const ${key} = useAction<${typeName}>('${key}'); // ${callSig}${toolHint}${onceHint}`);
|
|
263
278
|
actionReturnFields.push(key);
|
|
@@ -213,7 +213,7 @@ Rules:
|
|
|
213
213
|
1. All request-specific data → props with defaults. City names, tickers, user names, dates, counts.
|
|
214
214
|
2. Layout and styling are universal. Colors, spacing, structure — these are the reusable part.
|
|
215
215
|
3. Default values come from the current request — so the component renders correctly standalone.
|
|
216
|
-
4. Copy comes from the props. NEVER invent display text the contract does not carry — no eyebrow / kicker / overline (a \`<Text caps>\` label), no helper line, no status text, no tagline. **Do NOT add a caps label above the heading** — not \`<Text caps>LET'S BEGIN</Text>\`, not \`WELCOME\`, not a section kicker like \`GETTING STARTED\` or \`OVERVIEW\` — UNLESS a prop supplies that exact text (\`<Text caps>{props.section}</Text>\`). When the request asks for a heading and a line, render the heading and the line and NOTHING above the heading. A heading's fallback is the request's own words, never a slogan of your own. Text the agent cannot change is chatter on every render, and a caps eyebrow you wrote is the most common form of it.
|
|
216
|
+
4. Copy comes from the props. NEVER invent display text the contract does not carry — no eyebrow / kicker / overline (a \`<Text caps>\` label), no helper line, no status text, no tagline. **Do NOT add a caps label above the heading** — not \`<Text caps>LET'S BEGIN</Text>\`, not \`WELCOME\`, not a section kicker like \`GETTING STARTED\` or \`OVERVIEW\` — UNLESS a prop supplies that exact text (\`<Text caps>{props.section}</Text>\`). When the request asks for a heading and a line, render the heading and the line and NOTHING above the heading. A heading's fallback is the request's own words, never a slogan of your own. Text the agent cannot change is chatter on every render, and a caps eyebrow you wrote is the most common form of it. An action's control shows the contract's \`label\` VERBATIM — every character, including \`&\`, \`/\`, \`–\` and quotes — never paraphrased, shortened or re-cased (\`"Confirm & Schedule"\` is not \`"Confirm Schedule"\`).
|
|
217
217
|
5. Props interface must be typed and exported.`;
|
|
218
218
|
|
|
219
219
|
/**
|
|
@@ -298,16 +298,25 @@ export function createUiGenerator(
|
|
|
298
298
|
// structurally into the dispatch call instead. `route.env`
|
|
299
299
|
// already carries exactly the key(s) that would have gone into
|
|
300
300
|
// `process.env`, so no extra resolution work is needed here.
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
}
|
|
310
|
-
|
|
301
|
+
//
|
|
302
|
+
// ggui#1185 — the login path has NO key to write anywhere: the
|
|
303
|
+
// route decision `auth: 'claude-code-login'` is carried as
|
|
304
|
+
// `routeOverride.claudeCodeLogin` in BOTH env modes, because the
|
|
305
|
+
// flag is the only way the decision reaches `createAgent` /
|
|
306
|
+
// `createVisionAgent` (which select the login client on it).
|
|
307
|
+
const routeOverride =
|
|
308
|
+
route.auth === 'claude-code-login'
|
|
309
|
+
? { claudeCodeLogin: true }
|
|
310
|
+
: disableEnvMutation
|
|
311
|
+
? {
|
|
312
|
+
apiKey:
|
|
313
|
+
route.env['ANTHROPIC_API_KEY'] ??
|
|
314
|
+
route.env['OPENAI_API_KEY'] ??
|
|
315
|
+
route.env['GEMINI_API_KEY'] ??
|
|
316
|
+
route.env['OPENROUTER_API_KEY'],
|
|
317
|
+
useBedrock: route.env['CLAUDE_CODE_USE_BEDROCK'] === '1',
|
|
318
|
+
}
|
|
319
|
+
: undefined;
|
|
311
320
|
|
|
312
321
|
try {
|
|
313
322
|
const tools = createGeneratorTools({
|
|
@@ -8,6 +8,7 @@ import { LUCIDE_ICON_NAMES, maxWidth as CONTAINER_MAX_WIDTH } from "@ggui-ai/des
|
|
|
8
8
|
import { CANVAS_VIEWPORTS, type CanvasClass } from "../../../design-mode.js";
|
|
9
9
|
import type { EvalIssue } from "../../types-public.js";
|
|
10
10
|
import type { AxisCheck, AxisCheckInput } from "../types.js";
|
|
11
|
+
import type { DataContract } from "@ggui-ai/protocol";
|
|
11
12
|
import {
|
|
12
13
|
getRequiredPropNames,
|
|
13
14
|
mkIssue,
|
|
@@ -499,6 +500,91 @@ function runRootWidthCap(input: AxisCheckInput): EvalIssue[] {
|
|
|
499
500
|
];
|
|
500
501
|
}
|
|
501
502
|
|
|
503
|
+
// ── universal.action_label_dropped (ggui#1190 — the transcription-drop guard) ─
|
|
504
|
+
// The GUARD half of #1190: the model transcribes an actionSpec label onto a
|
|
505
|
+
// control but DROPS a special character ("Confirm & Schedule" → "Confirm
|
|
506
|
+
// Schedule"), because the verbatim label is not at the write-site. This check
|
|
507
|
+
// NAMES that class on the bench and guards regression; the fix (carry the
|
|
508
|
+
// verbatim label / render it from data) lands separately, so #1190 stays open.
|
|
509
|
+
// WARN-class, deterministic, and tuned to flag ONLY the high-confidence drop.
|
|
510
|
+
|
|
511
|
+
const LABEL_SPECIAL_CHAR = /[^a-z0-9\s]/i;
|
|
512
|
+
const normText = (s: string): string => s.toLowerCase().replace(/\s+/g, " ").trim();
|
|
513
|
+
const alnumText = (s: string): string => s.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
514
|
+
|
|
515
|
+
/**
|
|
516
|
+
* Extract the VISIBLE rendered text of a component — JSX text nodes (with `{…}`
|
|
517
|
+
* expression containers stripped, nesting-aware) plus visible string
|
|
518
|
+
* attributes (aria-label / placeholder / title / alt). Deliberately excludes
|
|
519
|
+
* identifiers and call args: `useAction('confirmSchedule')` and the `const
|
|
520
|
+
* confirmSchedule` binding must NOT count as the label "Confirm & Schedule"
|
|
521
|
+
* appearing, or every action would false-match its own camelCase name.
|
|
522
|
+
*/
|
|
523
|
+
function visibleText(sourceCode: string): string {
|
|
524
|
+
// JSX text nodes: `>text<`. Excluding `>` from the run (`[^<>]`) skips the
|
|
525
|
+
// `=>` arrow-body conflation — an arrow's `>` is followed by code then a
|
|
526
|
+
// `>`, never text then a `<`, so only true tag-close→text→tag runs match,
|
|
527
|
+
// and identifiers (`const confirmSchedule`, `useAction('confirmSchedule')`)
|
|
528
|
+
// are never between `>` and `<`. `{…}` expression children are stripped so an
|
|
529
|
+
// expression's identifiers don't count as visible text.
|
|
530
|
+
const jsx = (sourceCode.match(/>([^<>]*)</g) ?? [])
|
|
531
|
+
.map((m) => m.slice(1, -1).replace(/\{[^}]*\}/g, " "))
|
|
532
|
+
.join(" ");
|
|
533
|
+
let attrs = "";
|
|
534
|
+
const attrRx = /\b(?:aria-label|placeholder|title|alt)\s*=\s*(?:"([^"]*)"|'([^']*)')/gi;
|
|
535
|
+
let m: RegExpExecArray | null;
|
|
536
|
+
while ((m = attrRx.exec(sourceCode)) !== null) attrs += " " + (m[1] ?? m[2] ?? "");
|
|
537
|
+
return jsx + " " + attrs;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/**
|
|
541
|
+
* Find actionSpec labels whose special-character content was dropped from the
|
|
542
|
+
* generated source. FALSE-POSITIVE-SAFE by construction — flags ONLY when the
|
|
543
|
+
* label's alphanumeric words appear CONTIGUOUSLY in the source but the full
|
|
544
|
+
* label (with its "&", "/", "+" …) does not, i.e. a clean transcription drop.
|
|
545
|
+
* Every ambiguous shape is exempt: labels rendered from data (`.label`
|
|
546
|
+
* anywhere), labels with no special char (nothing to drop), labels present
|
|
547
|
+
* verbatim, entity-encoded `&` (`&` — the words stop being contiguous, so
|
|
548
|
+
* no flag), and very short labels. Presence-ANYWHERE, not wiring-scoped, on
|
|
549
|
+
* purpose: if the label's text appears anywhere we never flag.
|
|
550
|
+
*/
|
|
551
|
+
export function findDroppedActionLabels(
|
|
552
|
+
sourceCode: string,
|
|
553
|
+
contract: DataContract | undefined,
|
|
554
|
+
): Array<{ readonly name: string; readonly label: string }> {
|
|
555
|
+
const actionSpec = contract?.actionSpec;
|
|
556
|
+
if (actionSpec === undefined) return [];
|
|
557
|
+
// Component renders any label from data → it does not transcribe → exempt all.
|
|
558
|
+
if (/\.label\b/.test(sourceCode)) return [];
|
|
559
|
+
const vis = visibleText(sourceCode);
|
|
560
|
+
const normVis = normText(vis);
|
|
561
|
+
const alnumVis = alnumText(vis);
|
|
562
|
+
const out: Array<{ name: string; label: string }> = [];
|
|
563
|
+
for (const [name, entry] of Object.entries(actionSpec)) {
|
|
564
|
+
const label = entry.label;
|
|
565
|
+
if (typeof label !== "string" || label.length === 0 || label === name) continue;
|
|
566
|
+
if (!LABEL_SPECIAL_CHAR.test(label)) continue; // no special char → nothing to drop
|
|
567
|
+
if (alnumText(label).length < 3) continue; // too short to match confidently
|
|
568
|
+
if (normVis.includes(normText(label))) continue; // present verbatim in visible text → correct
|
|
569
|
+
// The label's words appear contiguously in the VISIBLE text but the full
|
|
570
|
+
// label (with its punctuation) does not → a character was dropped.
|
|
571
|
+
if (alnumVis.includes(alnumText(label))) out.push({ name, label });
|
|
572
|
+
}
|
|
573
|
+
return out;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
function runActionLabelDropped(input: AxisCheckInput): EvalIssue[] {
|
|
577
|
+
if (input.compiledCode === null) return [];
|
|
578
|
+
return findDroppedActionLabels(input.sourceCode, input.contract).map(({ name, label }) =>
|
|
579
|
+
mkIssue(
|
|
580
|
+
"universal.action_label_dropped",
|
|
581
|
+
`The action "${name}" declares label "${label}", but the rendered control appears to drop its "${label.replace(/[a-z0-9\s]/gi, "").trim()}": the label's words are in the generated source, the full label is not — a transcription drop (ggui#1190).`,
|
|
582
|
+
`Render the label EXACTLY as the contract states it — button text should be the verbatim \`actionSpec.${name}.label\`, or render it from data (\`{x.label}\`), never a re-typed copy that drops "&", "/", "+", etc.`,
|
|
583
|
+
"warn",
|
|
584
|
+
),
|
|
585
|
+
);
|
|
586
|
+
}
|
|
587
|
+
|
|
502
588
|
export const UNIVERSAL_CHECKS: readonly AxisCheck[] = [
|
|
503
589
|
{
|
|
504
590
|
id: "universal.icon_name_known",
|
|
@@ -524,6 +610,12 @@ export const UNIVERSAL_CHECKS: readonly AxisCheck[] = [
|
|
|
524
610
|
values: ALL_RENDER_VALUES,
|
|
525
611
|
run: runRootWidthCap,
|
|
526
612
|
},
|
|
613
|
+
{
|
|
614
|
+
id: "universal.action_label_dropped",
|
|
615
|
+
axis: "render",
|
|
616
|
+
values: ALL_RENDER_VALUES,
|
|
617
|
+
run: runActionLabelDropped,
|
|
618
|
+
},
|
|
527
619
|
{
|
|
528
620
|
id: "universal.terminal_action_unguarded",
|
|
529
621
|
axis: "render",
|