@ggui-ai/ui-gen 0.18.0 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/generation-dispatch.d.ts +1 -1
- package/dist/adapters/generation-dispatch.js +248 -88
- package/dist/adapters/generation-dispatch.js.map +1 -1
- package/dist/adapters/index.js +39 -16
- 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 +2 -2
- package/dist/advanced/index.js +217 -34
- package/dist/advanced/index.js.map +1 -1
- package/dist/blueprint-validator.js +5 -2
- 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-C8Ki9Wz1.d.ts → create-ui-generator-CxPvudbs.d.ts} +1 -1
- package/dist/{dispatch-D2htzi9h.d.ts → dispatch-N8wdNOX-.d.ts} +4 -2
- package/dist/evaluation/axis-checks/index.d.ts +3 -3
- package/dist/evaluation/axis-checks/index.js +143 -3
- 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 +138 -1
- package/dist/evaluation/axis-checks/registry.js.map +1 -1
- package/dist/evaluation/index.d.ts +3 -3
- package/dist/evaluation/index.js +213 -84
- package/dist/evaluation/index.js.map +1 -1
- package/dist/harness/check/runtime-render/index.d.ts +1 -1
- package/dist/harness/check/runtime-render/index.js +5 -2
- package/dist/harness/check/runtime-render/index.js.map +1 -1
- package/dist/harness/check/runtime-render/render-check-worker.js +5 -2
- package/dist/harness/check/runtime-render/render-check-worker.js.map +1 -1
- package/dist/harness/index.d.ts +1 -1
- package/dist/harness/index.js +64 -61
- package/dist/harness/index.js.map +1 -1
- package/dist/harness/prompts.js +1 -1
- package/dist/harness/prompts.js.map +1 -1
- package/dist/harness/result-types.d.ts +1 -1
- package/dist/harness/runtime.d.ts +1 -1
- package/dist/harness/runtime.js +4 -4
- package/dist/harness/runtime.js.map +1 -1
- package/dist/harness/types-public.d.ts +8 -1
- package/dist/harness/types-public.js +64 -61
- package/dist/harness/types-public.js.map +1 -1
- package/dist/harness/validator-trace-sink.d.ts +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +217 -34
- package/dist/index.js.map +1 -1
- package/dist/patch.d.ts +1 -1
- package/dist/{types-public-DVSssdc-.d.ts → types-public-DKR_h70H.d.ts} +9 -0
- 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 +12 -12
- package/src/adapters/google/sdk.ts +11 -7
- package/src/adapters/openai/raw.ts +11 -5
- package/src/adapters/openai/sdk.ts +8 -5
- package/src/adapters/openai/tokens.ts +49 -0
- package/src/adapters/types.ts +1 -1
- package/src/boilerplate/hard-sections.ts +1 -1
- package/src/boilerplate/system-prompt.ts +1 -1
- package/src/coding-agent/tools.ts +58 -6
- package/src/contract-context.ts +1 -1
- package/src/create-harness.ts +1 -0
- package/src/evaluation/axis-checks/checks/universal.ts +207 -1
- package/src/evaluation/axis-checks/dispatch.ts +8 -1
- package/src/evaluation/types-public.ts +9 -0
- package/src/harness/check/runtime-render/render-check.ts +15 -0
- package/src/harness/coding/generate-task-runner.ts +1 -0
- package/src/harness/coding/run-coding-turn.ts +14 -0
- package/src/harness/types-public.ts +7 -0
- package/src/run-check.ts +2 -0
- package/src/tools/get-wire.ts +1 -1
- 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-DKR_h70H.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.20.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": [
|
|
@@ -188,9 +188,9 @@
|
|
|
188
188
|
"puppeteer-core": "^24.40.0 || ^25.0.0",
|
|
189
189
|
"react": "^18.0.0 || ^19.0.0",
|
|
190
190
|
"react-dom": "^18.0.0 || ^19.0.0",
|
|
191
|
-
"@ggui-ai/
|
|
192
|
-
"@ggui-ai/
|
|
193
|
-
"@ggui-ai/protocol": "0.
|
|
191
|
+
"@ggui-ai/project-config": "0.20.0",
|
|
192
|
+
"@ggui-ai/mcp-server-core": "0.20.0",
|
|
193
|
+
"@ggui-ai/protocol": "0.20.0"
|
|
194
194
|
},
|
|
195
195
|
"peerDependenciesMeta": {
|
|
196
196
|
"@google/adk": {
|
|
@@ -229,11 +229,11 @@
|
|
|
229
229
|
"openai": "^7.9.0",
|
|
230
230
|
"typescript": "^5.0.0",
|
|
231
231
|
"zod": "^4.3.6",
|
|
232
|
-
"@ggui-ai/
|
|
233
|
-
"@ggui-ai/
|
|
234
|
-
"@ggui-ai/
|
|
235
|
-
"@ggui-ai/
|
|
236
|
-
"@ggui-ai/
|
|
232
|
+
"@ggui-ai/gadgets": "0.20.0",
|
|
233
|
+
"@ggui-ai/design": "0.20.0",
|
|
234
|
+
"@ggui-ai/sandbox": "0.20.0",
|
|
235
|
+
"@ggui-ai/ui-visual-tester": "0.20.0",
|
|
236
|
+
"@ggui-ai/wire": "0.20.0"
|
|
237
237
|
},
|
|
238
238
|
"devDependencies": {
|
|
239
239
|
"@google/adk": "^2.0.0",
|
|
@@ -248,9 +248,9 @@
|
|
|
248
248
|
"react-dom": "^19.0.0",
|
|
249
249
|
"tsup": "^8.5.0",
|
|
250
250
|
"vitest": "^3.2.6",
|
|
251
|
-
"@ggui-ai/project-config": "0.
|
|
252
|
-
"@ggui-ai/protocol": "0.
|
|
253
|
-
"@ggui-ai/mcp-server-core": "0.
|
|
251
|
+
"@ggui-ai/project-config": "0.20.0",
|
|
252
|
+
"@ggui-ai/protocol": "0.20.0",
|
|
253
|
+
"@ggui-ai/mcp-server-core": "0.20.0"
|
|
254
254
|
},
|
|
255
255
|
"engines": {
|
|
256
256
|
"node": ">=22.0.0"
|
|
@@ -62,15 +62,10 @@ export async function runAdkLoop(
|
|
|
62
62
|
): Promise<AdapterResult> {
|
|
63
63
|
const startTime = Date.now();
|
|
64
64
|
|
|
65
|
-
const { FunctionTool, LlmAgent, InMemoryRunner, isFinalResponse } = await import('@google/adk');
|
|
65
|
+
const { FunctionTool, Gemini, LlmAgent, InMemoryRunner, isFinalResponse } = await import('@google/adk');
|
|
66
66
|
|
|
67
67
|
const capture = createCapture();
|
|
68
68
|
|
|
69
|
-
// Ensure ADK can find the API key
|
|
70
|
-
if (params.apiKey && !process.env.GOOGLE_GENAI_API_KEY) {
|
|
71
|
-
process.env.GOOGLE_GENAI_API_KEY = params.apiKey;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
69
|
// Build tools with capture wrappers
|
|
75
70
|
const adkTools = params.tools.map((def) =>
|
|
76
71
|
new FunctionTool({
|
|
@@ -88,9 +83,18 @@ export async function runAdkLoop(
|
|
|
88
83
|
}),
|
|
89
84
|
);
|
|
90
85
|
|
|
86
|
+
// The key the caller passed WINS, handed to the model directly (ggui#1132).
|
|
87
|
+
// The ADK resolves GOOGLE_GENAI_API_KEY || GOOGLE_API_KEY || GEMINI_API_KEY
|
|
88
|
+
// on its own when no key is given, so writing GOOGLE_GENAI_API_KEY here —
|
|
89
|
+
// a variable this adapter does not own — let a stale ambient value beat a
|
|
90
|
+
// fresh key, and no rotation could reach it. Precedence is fixed at the
|
|
91
|
+
// source: an explicit key never touches process.env; no key defers to the
|
|
92
|
+
// ADK's order untouched.
|
|
93
|
+
const model = params.apiKey ? new Gemini({ model: params.model, apiKey: params.apiKey }) : params.model;
|
|
94
|
+
|
|
91
95
|
const agent = new LlmAgent({
|
|
92
96
|
name: 'ui_generator',
|
|
93
|
-
model
|
|
97
|
+
model,
|
|
94
98
|
instruction: params.systemPrompt,
|
|
95
99
|
tools: adkTools,
|
|
96
100
|
});
|
|
@@ -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,49 @@
|
|
|
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
|
+
// Never let a malformed usage report drive input negative.
|
|
36
|
+
const cacheRead = Math.min(
|
|
37
|
+
Math.max(cachedTokens, 0),
|
|
38
|
+
Math.max(inputTokensInclCached, 0),
|
|
39
|
+
);
|
|
40
|
+
const nonCachedInput = Math.max(inputTokensInclCached, 0) - cacheRead;
|
|
41
|
+
return {
|
|
42
|
+
tokens: {
|
|
43
|
+
input: nonCachedInput,
|
|
44
|
+
output: outputTokens,
|
|
45
|
+
total: nonCachedInput + cacheRead + outputTokens,
|
|
46
|
+
},
|
|
47
|
+
cacheReadTokens: cacheRead,
|
|
48
|
+
};
|
|
49
|
+
}
|
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;
|
|
@@ -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.
|
|
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.
|
|
217
217
|
5. Props interface must be typed and exported.`;
|
|
218
218
|
|
|
219
219
|
/**
|
|
@@ -603,7 +603,7 @@ Don't render text-only metrics. A weather widget without a sun/cloud, a stock ca
|
|
|
603
603
|
</Row>
|
|
604
604
|
\`\`\`
|
|
605
605
|
|
|
606
|
-
Use \`<Icon name="..." />\` for line icons — ONLY the curated
|
|
606
|
+
Use \`<Icon name="..." />\` for line icons — ONLY the design system's curated set renders (~200 kebab-case Lucide names); a name OUTSIDE it renders an empty box and fails the check. On a single-pass render you cannot look the set up, so use ONLY a name you are sure is in it. Safe common names: \`check\` \`x\` \`plus\` \`minus\` \`search\` \`settings\` \`user\` \`users\` \`calendar\` \`clock\` \`arrow-right\` \`arrow-left\` \`chevron-right\` \`chevron-down\` \`trash-2\` \`edit\` \`info\` \`alert-circle\` \`bell\` \`home\` \`file\` \`file-text\` \`star\` \`heart\` \`mail\` \`send\` \`download\` \`upload\` \`external-link\` \`filter\` \`check-circle\`. The numbered variants (\`edit-2\`, \`edit-3\`) and synonyms (\`pencil\`, \`pen\`, \`trash\`, \`x-circle\`) are NOT in the set — use \`edit\` and \`trash-2\`. If the icon you want is not clearly one of these, OMIT it: a card with no icon beats one with an empty box. Emoji render directly for status/weather/mood. For per-stat tiny accents, use a small icon next to the label.
|
|
607
607
|
|
|
608
608
|
### Spacing — generosity beats compactness
|
|
609
609
|
|
|
@@ -12,6 +12,10 @@ import * as esbuild from 'esbuild';
|
|
|
12
12
|
import { AgentWorkspace } from './workspace';
|
|
13
13
|
import { getSoftWarnings } from './self-check';
|
|
14
14
|
import { runTier0Checks } from '../check/index.js';
|
|
15
|
+
import { runGatedAxisChecks, traceAxisChecksSkipped } from '../evaluation/axis-checks/dispatch.js';
|
|
16
|
+
import type { AxisCheck, AxisCheckInput } from '../evaluation/types-public.js';
|
|
17
|
+
import type { Classification } from '../classifier/axes.js';
|
|
18
|
+
import type { CanvasClass } from '../design-mode.js';
|
|
15
19
|
import { PRIMITIVES_DOCUMENTATION } from '../validation/index.js';
|
|
16
20
|
import type { DataContract } from '@ggui-ai/protocol';
|
|
17
21
|
import type {
|
|
@@ -46,6 +50,22 @@ function getComponentDocumentation(name: string): string {
|
|
|
46
50
|
// Auto-commit helper (shared by write and apply_changes)
|
|
47
51
|
// =============================================================================
|
|
48
52
|
|
|
53
|
+
/**
|
|
54
|
+
* The deterministic axis family at auto-commit (ggui#1122) — what a caller with a
|
|
55
|
+
* harness hands `executeTool` so the checks run on EVERY commit the coding agent
|
|
56
|
+
* makes, i.e. on the serve path, not only inside the eval round a serving deployment
|
|
57
|
+
* never enters. `checks` is the harness's pre-filtered list (`harness.check.axisChecks`)
|
|
58
|
+
* — the same list `run-check.ts` feeds the same pure runner. Absent: nothing runs.
|
|
59
|
+
*/
|
|
60
|
+
export interface AutoCommitAxisChecks {
|
|
61
|
+
readonly checks: readonly AxisCheck[];
|
|
62
|
+
readonly classification: Classification;
|
|
63
|
+
/** The ORIGINAL request — several checks read it (labels invented past the copy, terminal words). */
|
|
64
|
+
readonly originalPrompt: string;
|
|
65
|
+
/** The canvas the harness was built for (ggui#1117); absent = no rendering context, never a default. */
|
|
66
|
+
readonly canvas?: CanvasClass;
|
|
67
|
+
}
|
|
68
|
+
|
|
49
69
|
async function autoCommit(
|
|
50
70
|
workspace: AgentWorkspace,
|
|
51
71
|
commitMeta: Map<string, CommitMetadata>,
|
|
@@ -72,6 +92,8 @@ async function autoCommit(
|
|
|
72
92
|
gadgetTypes?: Readonly<Record<string, string>>,
|
|
73
93
|
/** Which tier-0 legs fire — see `runTier0Checks`'s `designMode`. */
|
|
74
94
|
designMode: DesignMode = DEFAULT_DESIGN_MODE,
|
|
95
|
+
/** ggui#1122: run the harness's axis checks here too; absent, none run. */
|
|
96
|
+
axis?: AutoCommitAxisChecks,
|
|
75
97
|
): Promise<ToolResult> {
|
|
76
98
|
const commitStart = Date.now();
|
|
77
99
|
const raw = workspace.read();
|
|
@@ -123,17 +145,45 @@ async function autoCommit(
|
|
|
123
145
|
gadgetTypes,
|
|
124
146
|
designMode,
|
|
125
147
|
);
|
|
126
|
-
|
|
148
|
+
// ggui#1122: the deterministic axis family runs HERE — on every commit the coding
|
|
149
|
+
// agent makes, which is the serve path — through the one pure runner `run-check.ts`
|
|
150
|
+
// feeds (the ggui#1046 trace line prints on both paths). Posture is the founder's:
|
|
151
|
+
// warn-as-warn, fail-as-fail. A WARN that started blocking a served generation
|
|
152
|
+
// because it moved surface would be a posture change, not a relocation, and comes
|
|
153
|
+
// to him as a named item. Only a source that BUILT is judged: on a build failure
|
|
154
|
+
// the checks are not asked (the same stand-down the gate+run wrapper makes) and
|
|
155
|
+
// the ggui#1046 trace says so — a skipped round never reads as a clean one.
|
|
156
|
+
const axisFacts = axis !== undefined
|
|
157
|
+
? {
|
|
158
|
+
sourceCode: formatted,
|
|
159
|
+
...(contract !== undefined ? { contract } : {}),
|
|
160
|
+
originalPrompt: axis.originalPrompt,
|
|
161
|
+
classification: axis.classification,
|
|
162
|
+
designMode,
|
|
163
|
+
...(axis.canvas !== undefined ? { canvas: axis.canvas } : {}),
|
|
164
|
+
}
|
|
165
|
+
: undefined;
|
|
166
|
+
if (axisFacts !== undefined && !buildSuccess) traceAxisChecksSkipped(axisFacts, 'build failed — no check ran');
|
|
167
|
+
const axisRun = axis !== undefined && axisFacts !== undefined && buildSuccess
|
|
168
|
+
? runGatedAxisChecks(axis.checks, { ...axisFacts, compiledCode } satisfies AxisCheckInput)
|
|
169
|
+
: undefined;
|
|
170
|
+
const axisIssues = axisRun?.issues ?? [];
|
|
171
|
+
const checkIssues = [...tier0Issues, ...axisIssues];
|
|
172
|
+
const tier0Fails = checkIssues.filter(i => i.result === 'fail');
|
|
127
173
|
const selfCheckPassed = tier0Fails.length === 0;
|
|
128
174
|
// When contextPolicy.labeledTier0 is set, prefix each violation with
|
|
129
175
|
// its P0/P1/P2 priority so the LLM can rank against the prompt's
|
|
130
176
|
// schema. The default (off) emits unlabeled feedback.
|
|
131
|
-
const violations =
|
|
177
|
+
const violations = checkIssues
|
|
132
178
|
.filter(i => i.result === 'fail')
|
|
133
179
|
.map(i => contextPolicy?.labeledTier0
|
|
134
180
|
? `[${i.priority ?? 'P0'}-${i.category}] ${i.description}\n Fix: ${i.fix}`
|
|
135
181
|
: `[${i.category}] ${i.description}\n Fix: ${i.fix}`);
|
|
136
|
-
|
|
182
|
+
// Axis WARNs ride the existing non-blocking channel — feedback the model sees, never a block.
|
|
183
|
+
const axisWarnings = axisIssues
|
|
184
|
+
.filter(i => i.result === 'warn')
|
|
185
|
+
.map(i => `[${i.subcategory}] ${i.description} Fix: ${i.fix}`);
|
|
186
|
+
const softWarnings = [...getSoftWarnings(raw), ...axisWarnings];
|
|
137
187
|
const selfCheckMs = Date.now() - selfCheckStart;
|
|
138
188
|
|
|
139
189
|
// Always commit (preserves history)
|
|
@@ -144,7 +194,7 @@ async function autoCommit(
|
|
|
144
194
|
|
|
145
195
|
const status = buildSuccess && selfCheckPassed ? 'PASS' : 'FAIL';
|
|
146
196
|
console.log(
|
|
147
|
-
`[coding-agent] auto-commit: ${status} | build=${buildMs}ms self-check=${selfCheckMs}ms git=${gitMs}ms total=${Date.now() - commitStart}ms | violations=${violations.length}`,
|
|
197
|
+
`[coding-agent] auto-commit: ${status} | build=${buildMs}ms self-check=${selfCheckMs}ms git=${gitMs}ms total=${Date.now() - commitStart}ms | violations=${violations.length} | axis=${axisRun === undefined ? 'off' : `${axisIssues.length - axisWarnings.length}F/${axisWarnings.length}W`}`,
|
|
148
198
|
);
|
|
149
199
|
if (violations.length > 0) {
|
|
150
200
|
for (const v of violations) {
|
|
@@ -202,6 +252,8 @@ export async function executeTool(
|
|
|
202
252
|
gadgetTypes?: Readonly<Record<string, string>>,
|
|
203
253
|
/** Which tier-0 legs fire on auto-commit — see `runTier0Checks`'s `designMode`. */
|
|
204
254
|
designMode: DesignMode = DEFAULT_DESIGN_MODE,
|
|
255
|
+
/** ggui#1122: the harness's axis checks, run at every auto-commit — see `AutoCommitAxisChecks`. */
|
|
256
|
+
axis?: AutoCommitAxisChecks,
|
|
205
257
|
): Promise<ToolResult> {
|
|
206
258
|
switch (tool) {
|
|
207
259
|
case 'write':
|
|
@@ -222,7 +274,7 @@ export async function executeTool(
|
|
|
222
274
|
const message = (input.commit_message as string) || `write ${lineCount} lines`;
|
|
223
275
|
|
|
224
276
|
console.log(`[coding-agent] write: ${lineCount} lines → auto-commit`);
|
|
225
|
-
return autoCommit(workspace, commitMeta, message, contract, contextPolicy, gadgetTypes, designMode);
|
|
277
|
+
return autoCommit(workspace, commitMeta, message, contract, contextPolicy, gadgetTypes, designMode, axis);
|
|
226
278
|
}
|
|
227
279
|
|
|
228
280
|
case 'apply_changes': {
|
|
@@ -529,7 +581,7 @@ export async function executeTool(
|
|
|
529
581
|
const message = (input.commit_message as string) || 'apply changes';
|
|
530
582
|
|
|
531
583
|
console.log(`[coding-agent] apply_changes: ${changes.length} changes applied → auto-commit`);
|
|
532
|
-
return autoCommit(workspace, commitMeta, message, contract, contextPolicy, gadgetTypes, designMode);
|
|
584
|
+
return autoCommit(workspace, commitMeta, message, contract, contextPolicy, gadgetTypes, designMode, axis);
|
|
533
585
|
}
|
|
534
586
|
|
|
535
587
|
case 'get_components_info': {
|
package/src/contract-context.ts
CHANGED
|
@@ -213,7 +213,7 @@ export function buildContractsContext(
|
|
|
213
213
|
// model reads it that way. Emitted only when true, so an absent flag
|
|
214
214
|
// leaves the prompt bytes identical to before this line existed.
|
|
215
215
|
if (entry.confirm === true) {
|
|
216
|
-
line += `\n Author flag: \`confirm\` — the author marked this action
|
|
216
|
+
line += `\n Author flag: \`confirm\` — the author marked this action as one to confirm with the user before it fires.`;
|
|
217
217
|
}
|
|
218
218
|
return line;
|
|
219
219
|
})
|
package/src/create-harness.ts
CHANGED
|
@@ -224,6 +224,7 @@ export function createHarness(input: CreateHarnessInput): Harness {
|
|
|
224
224
|
name,
|
|
225
225
|
classification,
|
|
226
226
|
designMode,
|
|
227
|
+
...(input.canvas !== undefined ? { canvas: input.canvas } : {}),
|
|
227
228
|
...(input.profile !== undefined ? { profile: input.profile } : {}),
|
|
228
229
|
how,
|
|
229
230
|
what,
|
|
@@ -4,9 +4,11 @@
|
|
|
4
4
|
// component. Dispatched via a "universal" gate that matches every render
|
|
5
5
|
// value.
|
|
6
6
|
|
|
7
|
-
import { LUCIDE_ICON_NAMES } from "@ggui-ai/design";
|
|
7
|
+
import { LUCIDE_ICON_NAMES, maxWidth as CONTAINER_MAX_WIDTH } from "@ggui-ai/design";
|
|
8
|
+
import { CANVAS_VIEWPORTS, type CanvasClass } from "../../../design-mode.js";
|
|
8
9
|
import type { EvalIssue } from "../../types-public.js";
|
|
9
10
|
import type { AxisCheck, AxisCheckInput } from "../types.js";
|
|
11
|
+
import type { DataContract } from "@ggui-ai/protocol";
|
|
10
12
|
import {
|
|
11
13
|
getRequiredPropNames,
|
|
12
14
|
mkIssue,
|
|
@@ -391,6 +393,198 @@ function runTerminalActionUnguarded(input: AxisCheckInput): EvalIssue[] {
|
|
|
391
393
|
];
|
|
392
394
|
}
|
|
393
395
|
|
|
396
|
+
// ── universal.root_width_cap (ggui#1117 — the eval leg ggui#1113 left out) ─
|
|
397
|
+
// FRAME_SIZING's width half: the outermost element FILLS the frame; a width
|
|
398
|
+
// cap belongs INSIDE it, on the column that holds text. A cap on the root is
|
|
399
|
+
// the defect on a wide canvas (the card becomes a narrow strip with the
|
|
400
|
+
// frame's ground on either side) and a no-op in a chat bubble (~400 px; a
|
|
401
|
+
// 480 px cap never binds) — so the check reads `input.canvas`: it stands
|
|
402
|
+
// down with no canvas (never a guess) and below `md`, and fires only for a
|
|
403
|
+
// cap SMALLER than the canvas. A bare `<Container>` is a cap too — its
|
|
404
|
+
// default preset is `lg`, 768 px — so it binds on `lg` / `xl` and not on
|
|
405
|
+
// `md`. A cap the check cannot resolve to pixels (a `max-w-*` class, `rem`,
|
|
406
|
+
// `%`) is reported because the prompt forbids it on the root. WARN, not
|
|
407
|
+
// fail: the prompt names the one shape never to write; whether a centred
|
|
408
|
+
// column was the right composition stays the model's call.
|
|
409
|
+
export interface RootWidthCap {
|
|
410
|
+
/** The outermost element's tag name as written. */
|
|
411
|
+
readonly element: string;
|
|
412
|
+
/** The cap as written (`maxWidth="sm"`, `maxWidth: '480px'`, `max-w-md`, or `lg (Container default)`). */
|
|
413
|
+
readonly cap: string;
|
|
414
|
+
/** The cap in pixels when it resolves (a Container preset, an `Npx` value); `null` otherwise. */
|
|
415
|
+
readonly px: number | null;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/** The opening tag of the OUTERMOST element the default export returns, as written; `null` for none or a fragment. */
|
|
419
|
+
function rootOpeningTag(sourceCode: string): { readonly name: string; readonly attrs: string } | null {
|
|
420
|
+
const start = sourceCode.indexOf("export default function");
|
|
421
|
+
const body = start >= 0 ? sourceCode.slice(start) : sourceCode;
|
|
422
|
+
const ret = /\breturn\s*\(?\s*</.exec(body);
|
|
423
|
+
if (ret === null) return null;
|
|
424
|
+
const at = ret.index + ret[0].length - 1;
|
|
425
|
+
const nameMatch = /^<([A-Za-z][\w.]*)/.exec(body.slice(at));
|
|
426
|
+
if (nameMatch === null) return null;
|
|
427
|
+
let j = at + nameMatch[0].length;
|
|
428
|
+
let depth = 0;
|
|
429
|
+
let quote: string | null = null;
|
|
430
|
+
for (; j < body.length; j++) {
|
|
431
|
+
const ch = body[j]!;
|
|
432
|
+
if (quote !== null) {
|
|
433
|
+
if (ch === quote) quote = null;
|
|
434
|
+
continue;
|
|
435
|
+
}
|
|
436
|
+
if (ch === '"' || ch === "'" || ch === "`") quote = ch;
|
|
437
|
+
else if (ch === "{") depth++;
|
|
438
|
+
else if (ch === "}") depth--;
|
|
439
|
+
else if (ch === ">" && depth === 0) break;
|
|
440
|
+
}
|
|
441
|
+
return { name: nameMatch[1]!, attrs: body.slice(at + nameMatch[0].length, j) };
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
const CONTAINER_PRESETS: Readonly<Record<string, string>> = CONTAINER_MAX_WIDTH;
|
|
445
|
+
const NOT_A_CAP = new Set(["full", "none", "100%", "unset", "initial", "inherit"]);
|
|
446
|
+
|
|
447
|
+
/** `sm` → 480; `480px` → 480; `100%` / `full` → not a cap (`undefined`); anything else → a cap of unknown size (`null`). */
|
|
448
|
+
function capPx(value: string): number | null | undefined {
|
|
449
|
+
const v = value.trim();
|
|
450
|
+
if (NOT_A_CAP.has(v)) return undefined;
|
|
451
|
+
const resolved = CONTAINER_PRESETS[v] ?? v;
|
|
452
|
+
if (NOT_A_CAP.has(resolved)) return undefined;
|
|
453
|
+
const px = /^(\d+(?:\.\d+)?)px$/.exec(resolved);
|
|
454
|
+
return px === null ? null : Number(px[1]);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/** The width cap the outermost returned element carries, or `null` when it fills the frame. */
|
|
458
|
+
export function findRootWidthCap(sourceCode: string): RootWidthCap | null {
|
|
459
|
+
const root = rootOpeningTag(sourceCode);
|
|
460
|
+
if (root === null) return null;
|
|
461
|
+
const { name, attrs } = root;
|
|
462
|
+
const prop = /\bmaxWidth\s*=\s*(?:"([^"]*)"|'([^']*)'|\{\s*(?:"([^"]*)"|'([^']*)'|(\d+(?:\.\d+)?))\s*\})/.exec(attrs);
|
|
463
|
+
if (prop !== null) {
|
|
464
|
+
const raw = prop[1] ?? prop[2] ?? prop[3] ?? prop[4] ?? `${prop[5]!}px`;
|
|
465
|
+
const px = capPx(raw);
|
|
466
|
+
return px === undefined ? null : { element: name, cap: `maxWidth="${raw}"`, px };
|
|
467
|
+
}
|
|
468
|
+
const style = /\bmaxWidth\s*:\s*(?:"([^"]*)"|'([^']*)'|`([^`]*)`|(\d+(?:\.\d+)?))/.exec(attrs);
|
|
469
|
+
if (style !== null) {
|
|
470
|
+
const raw = style[1] ?? style[2] ?? style[3] ?? `${style[4]!}px`;
|
|
471
|
+
const px = capPx(raw);
|
|
472
|
+
return px === undefined ? null : { element: name, cap: `maxWidth: '${raw}'`, px };
|
|
473
|
+
}
|
|
474
|
+
const cls = /(?<![\w-])max-w-([\w[\]%.-]+)/.exec(attrs);
|
|
475
|
+
if (cls !== null) {
|
|
476
|
+
if (cls[1] === "full" || cls[1] === "none") return null;
|
|
477
|
+
return { element: name, cap: `max-w-${cls[1]!}`, px: null };
|
|
478
|
+
}
|
|
479
|
+
if (name === "Container") return { element: name, cap: "lg (Container default)", px: capPx("lg") ?? null };
|
|
480
|
+
return null;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
const WIDE_CANVASES: ReadonlySet<CanvasClass> = new Set<CanvasClass>(["md", "lg", "xl"]);
|
|
484
|
+
|
|
485
|
+
function runRootWidthCap(input: AxisCheckInput): EvalIssue[] {
|
|
486
|
+
if (input.compiledCode === null) return [];
|
|
487
|
+
if (input.canvas === undefined || !WIDE_CANVASES.has(input.canvas)) return [];
|
|
488
|
+
const cap = findRootWidthCap(input.sourceCode);
|
|
489
|
+
if (cap === null) return [];
|
|
490
|
+
const width = CANVAS_VIEWPORTS[input.canvas].width;
|
|
491
|
+
if (cap.px !== null && cap.px >= width) return [];
|
|
492
|
+
const sized = cap.px !== null ? `${cap.px}px on a ${width}px canvas` : `on a ${width}px canvas`;
|
|
493
|
+
return [
|
|
494
|
+
mkIssue(
|
|
495
|
+
"universal.root_width_cap",
|
|
496
|
+
`The outermost element <${cap.element}> carries a width cap (${cap.cap}, ${sized}): on this canvas the card becomes a narrow strip with the frame's ground on either side. The frame owns the width; a cap belongs INSIDE the root, on the column that holds text.`,
|
|
497
|
+
'Let the root fill the frame — drop the cap on the element you return at the top (or `maxWidth="full"`) — and put the reading measure on an inner column: `<Box padding="lg"><Container maxWidth="sm">…</Container></Box>`.',
|
|
498
|
+
"warn",
|
|
499
|
+
),
|
|
500
|
+
];
|
|
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
|
+
|
|
394
588
|
export const UNIVERSAL_CHECKS: readonly AxisCheck[] = [
|
|
395
589
|
{
|
|
396
590
|
id: "universal.icon_name_known",
|
|
@@ -410,6 +604,18 @@ export const UNIVERSAL_CHECKS: readonly AxisCheck[] = [
|
|
|
410
604
|
values: ALL_RENDER_VALUES,
|
|
411
605
|
run: runViewportSized,
|
|
412
606
|
},
|
|
607
|
+
{
|
|
608
|
+
id: "universal.root_width_cap",
|
|
609
|
+
axis: "render",
|
|
610
|
+
values: ALL_RENDER_VALUES,
|
|
611
|
+
run: runRootWidthCap,
|
|
612
|
+
},
|
|
613
|
+
{
|
|
614
|
+
id: "universal.action_label_dropped",
|
|
615
|
+
axis: "render",
|
|
616
|
+
values: ALL_RENDER_VALUES,
|
|
617
|
+
run: runActionLabelDropped,
|
|
618
|
+
},
|
|
413
619
|
{
|
|
414
620
|
id: "universal.terminal_action_unguarded",
|
|
415
621
|
axis: "render",
|