@ggui-ai/ui-gen 0.23.0 → 0.25.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/adapter-Cg_rkpGq.d.ts +201 -0
- package/dist/adapters/generation-dispatch.d.ts +58 -15
- package/dist/adapters/generation-dispatch.js +1418 -180
- package/dist/adapters/generation-dispatch.js.map +1 -1
- package/dist/adapters/index.js +3 -1
- package/dist/adapters/index.js.map +1 -1
- package/dist/advanced/index.d.ts +12 -8
- package/dist/advanced/index.js +1467 -239
- package/dist/advanced/index.js.map +1 -1
- package/dist/{axes-CzLEMDeB.d.ts → axes-CKVCzR_x.d.ts} +1 -1
- package/dist/bank-CT4eEkta.d.ts +118 -0
- package/dist/blueprint-validator.js +153 -59
- package/dist/blueprint-validator.js.map +1 -1
- package/dist/boilerplate.js +24 -12
- package/dist/boilerplate.js.map +1 -1
- package/dist/check/index.d.ts +12 -4
- package/dist/check/index.js +94 -2
- package/dist/check/index.js.map +1 -1
- package/dist/classifier/index.d.ts +2 -2
- package/dist/compose.d.ts +2 -2
- package/dist/{create-ui-generator-D0kbWwln.d.ts → create-ui-generator-DFnnArrv.d.ts} +43 -11
- package/dist/{dispatch-wxaIMwZt.d.ts → dispatch-CStl9eAw.d.ts} +5 -3
- package/dist/evaluation/axis-checks/index.d.ts +6 -5
- package/dist/evaluation/axis-checks/index.js +90 -2
- package/dist/evaluation/axis-checks/index.js.map +1 -1
- package/dist/evaluation/axis-checks/registry.d.ts +4 -3
- package/dist/evaluation/axis-checks/registry.js +88 -1
- package/dist/evaluation/axis-checks/registry.js.map +1 -1
- package/dist/evaluation/index.d.ts +301 -23
- package/dist/evaluation/index.js +746 -30
- package/dist/evaluation/index.js.map +1 -1
- package/dist/evaluation/types.d.ts +9 -1
- package/dist/evaluation/types.js.map +1 -1
- package/dist/fragments/index.d.ts +3 -3
- package/dist/harness/check/runtime-render/index.d.ts +30 -11
- package/dist/harness/check/runtime-render/index.js +203 -60
- package/dist/harness/check/runtime-render/index.js.map +1 -1
- package/dist/harness/check/runtime-render/render-check-worker.d.ts +8 -1
- package/dist/harness/check/runtime-render/render-check-worker.js +72 -32
- package/dist/harness/check/runtime-render/render-check-worker.js.map +1 -1
- package/dist/harness/index.d.ts +5 -4
- package/dist/harness/index.js +33 -15
- package/dist/harness/index.js.map +1 -1
- package/dist/harness/prompts.js.map +1 -1
- package/dist/harness/result-types.d.ts +4 -3
- package/dist/harness/runtime.d.ts +11 -4
- package/dist/harness/runtime.js +26 -14
- package/dist/harness/runtime.js.map +1 -1
- package/dist/harness/types-public.d.ts +13 -4
- package/dist/harness/types-public.js +33 -15
- package/dist/harness/types-public.js.map +1 -1
- package/dist/harness/validator-trace-sink.d.ts +4 -3
- package/dist/hash.d.ts +1 -1
- package/dist/index.d.ts +59 -8
- package/dist/index.js +1476 -241
- package/dist/index.js.map +1 -1
- package/dist/{llm-router-DwwUK1bz.d.ts → llm-router-nfO7wPec.d.ts} +3 -1
- package/dist/patch.d.ts +4 -3
- package/dist/policy.d.ts +8 -5
- package/dist/policy.js.map +1 -1
- package/dist/tools/get-primitives-ts.d.ts +1 -1
- package/dist/tools/get-primitives-ts.js +1 -1
- package/dist/tools/get-primitives-ts.js.map +1 -1
- package/dist/tools/render-check-worker.d.ts +7 -1
- package/dist/tools/render-check-worker.js +55 -23
- package/dist/tools/render-check-worker.js.map +1 -1
- package/dist/{types-BOvHNG7K.d.ts → types-Dh1s4K9j.d.ts} +1 -1
- package/dist/{types-public-Ow-tNtFE.d.ts → types-public-BJTLpzWn.d.ts} +208 -15
- 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 +4 -3
- package/package.json +12 -12
- package/src/adapters/generation-dispatch.ts +173 -98
- package/src/adapters/tools.ts +2 -1
- package/src/boilerplate/free-design-prompt.ts +1 -1
- package/src/boilerplate/generate.ts +26 -1
- package/src/boilerplate/hard-sections.ts +13 -13
- package/src/boilerplate/system-prompt.ts +1 -1
- package/src/check/index.ts +1 -0
- package/src/check/run-tier0.ts +80 -1
- package/src/check/sample-props.ts +63 -0
- package/src/coding-agent/tools.ts +4 -1
- package/src/create-harness.ts +3 -3
- package/src/create-ui-generator.ts +204 -95
- package/src/evaluation/__fixtures__/png.ts +100 -0
- package/src/evaluation/axis-checks/checks/state-merge.ts +1 -1
- package/src/evaluation/axis-checks/checks/universal.ts +111 -0
- package/src/evaluation/axis-checks/dispatch.ts +4 -1
- package/src/evaluation/criteria/bank.ts +85 -0
- package/src/evaluation/criteria/context.ts +52 -0
- package/src/evaluation/criteria/resolve.ts +159 -0
- package/src/evaluation/criteria/select.ts +79 -0
- package/src/evaluation/index.ts +23 -0
- package/src/evaluation/ink-extent.ts +200 -0
- package/src/evaluation/theme-css.ts +12 -5
- package/src/evaluation/types-public.ts +200 -11
- package/src/evaluation/types.ts +9 -0
- package/src/evaluation/visual-evaluator.ts +636 -30
- package/src/harness/check/runtime-render/adapter.ts +221 -75
- package/src/harness/check/runtime-render/index.ts +75 -2
- package/src/harness/check/runtime-render/render-check-host.ts +82 -19
- package/src/harness/check/runtime-render/render-check-worker.ts +83 -29
- package/src/harness/check/runtime-render/render-check.ts +154 -117
- package/src/harness/coding/generate-task-runner.ts +60 -4
- package/src/harness/coding/init-session.ts +13 -0
- package/src/harness/coding/run-coding-turn.ts +7 -3
- package/src/harness/coding/run-eval-round.ts +257 -15
- package/src/harness/policy.ts +11 -0
- package/src/harness/primitive-slice.ts +104 -0
- package/src/harness/runtime.ts +5 -0
- package/src/harness/types-public.ts +9 -1
- package/src/harness-tool-names.ts +38 -0
- package/src/index.ts +16 -0
- package/src/policy.ts +7 -4
- package/src/run-check.ts +2 -0
- package/src/tools/get-primitives-ts.ts +1 -1
- package/src/tools/get-wire.ts +1 -1
- package/src/tools/render-check-worker.ts +74 -23
- package/src/tools/render-check.ts +0 -60
- package/src/tools.ts +11 -1
- package/src/validation/primitives.ts +1 -1
- package/dist/render-check-Blwj8gB6.d.ts +0 -105
package/dist/workflows.d.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { C as Classification } from './axes-
|
|
1
|
+
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-
|
|
5
|
+
import './types-public-BJTLpzWn.js';
|
|
6
|
+
import '@ggui-ai/mcp-server-core';
|
|
6
7
|
import './design-mode-DLufhrNp.js';
|
|
7
|
-
import './types-
|
|
8
|
+
import './types-Dh1s4K9j.js';
|
|
8
9
|
import './llm.js';
|
|
9
10
|
import './policy.js';
|
|
10
11
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ggui-ai/ui-gen",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.25.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": [
|
|
@@ -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/
|
|
193
|
-
"@ggui-ai/
|
|
194
|
-
"@ggui-ai/
|
|
192
|
+
"@ggui-ai/project-config": "0.25.0",
|
|
193
|
+
"@ggui-ai/mcp-server-core": "0.25.0",
|
|
194
|
+
"@ggui-ai/protocol": "0.25.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.
|
|
237
|
-
"@ggui-ai/
|
|
238
|
-
"@ggui-ai/
|
|
239
|
-
"@ggui-ai/
|
|
240
|
-
"@ggui-ai/
|
|
236
|
+
"@ggui-ai/design": "0.25.0",
|
|
237
|
+
"@ggui-ai/ui-visual-tester": "0.25.0",
|
|
238
|
+
"@ggui-ai/gadgets": "0.25.0",
|
|
239
|
+
"@ggui-ai/sandbox": "0.25.0",
|
|
240
|
+
"@ggui-ai/wire": "0.25.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/mcp-server-core": "0.
|
|
256
|
-
"@ggui-ai/project-config": "0.
|
|
257
|
-
"@ggui-ai/protocol": "0.
|
|
255
|
+
"@ggui-ai/mcp-server-core": "0.25.0",
|
|
256
|
+
"@ggui-ai/project-config": "0.25.0",
|
|
257
|
+
"@ggui-ai/protocol": "0.25.0"
|
|
258
258
|
},
|
|
259
259
|
"engines": {
|
|
260
260
|
"node": ">=22.0.0"
|
|
@@ -33,11 +33,15 @@ import { buildSystemPrompt as buildSystemPromptWithFunnel } from "../harness/run
|
|
|
33
33
|
// predicate against the full registry and passes the filtered result.
|
|
34
34
|
import { REGISTRY as AXIS_CHECK_REGISTRY } from "../evaluation/axis-checks/registry.js";
|
|
35
35
|
import { matches as axisCheckMatches } from "../evaluation/types-public.js";
|
|
36
|
-
// Runtime-render
|
|
37
|
-
// optional runtimeRender; this callsite
|
|
38
|
-
// `
|
|
39
|
-
// backed contract-wiring verification
|
|
36
|
+
// Runtime-render check. The `createHarness` constructor accepts an
|
|
37
|
+
// optional runtimeRender; this callsite wires the caller's instance
|
|
38
|
+
// (`params.runtimeRender`) or, absent one, `DEFAULT_RUNTIME_RENDER_CHECK`,
|
|
39
|
+
// so production keeps the happy-dom-backed contract-wiring verification
|
|
40
|
+
// step. The dispatch never BUILDS a check: the instance carries the
|
|
41
|
+
// concurrency cap, so its lifetime must be the caller's (one per
|
|
42
|
+
// generator), never one per generation.
|
|
40
43
|
import { DEFAULT_RUNTIME_RENDER_CHECK } from "../harness/check/runtime-render/adapter.js";
|
|
44
|
+
import type { RuntimeRenderCheck } from "../harness/types-public.js";
|
|
41
45
|
import { warmupRuntimeRenderProbe } from "../harness/check/runtime-render/index.js";
|
|
42
46
|
import { assembleGenerationResult } from "../harness/coding/assemble-result.js";
|
|
43
47
|
import {
|
|
@@ -54,13 +58,24 @@ import type {
|
|
|
54
58
|
import type { ProviderName, ToolDefinition } from "./types.js";
|
|
55
59
|
import type { QualityConfig } from "../evaluation/types-public.js";
|
|
56
60
|
import type { AgentConfig } from "../harness/llm-router.js";
|
|
57
|
-
import {
|
|
58
|
-
type GadgetDescriptor,
|
|
59
|
-
type JsonObject,
|
|
60
|
-
} from "@ggui-ai/protocol";
|
|
61
|
+
import { type GadgetDescriptor, type JsonObject } from "@ggui-ai/protocol";
|
|
61
62
|
import { canvasForRendering, type CanvasClass, type DesignMode } from "../design-mode.js";
|
|
63
|
+
import type { VisionProvider } from "../harness/llm-router.js";
|
|
64
|
+
import type { CriteriaBank } from "../evaluation/criteria/bank.js";
|
|
62
65
|
import type { GenerationProfileInput } from "../boilerplate/styling-profile.js";
|
|
63
66
|
|
|
67
|
+
/**
|
|
68
|
+
* ggui#1250 — the in-loop VISUAL judge as its own agent: a vision provider (typed so an OpenAI judge cannot
|
|
69
|
+
* be written), a model, and — when the caller resolves provider keys itself — that provider's key, which
|
|
70
|
+
* becomes the agent's own `routeOverride`. Never the coding agent's route: where this exists (an OpenAI
|
|
71
|
+
* coding lane), the coding key is another provider's.
|
|
72
|
+
*/
|
|
73
|
+
export interface VisualEvalAgentSpec {
|
|
74
|
+
readonly provider: VisionProvider;
|
|
75
|
+
readonly model: string;
|
|
76
|
+
readonly apiKey?: string;
|
|
77
|
+
}
|
|
78
|
+
|
|
64
79
|
export interface GenerationDispatchParams {
|
|
65
80
|
provider: ProviderName;
|
|
66
81
|
model: string;
|
|
@@ -108,6 +123,9 @@ export interface GenerationDispatchParams {
|
|
|
108
123
|
* the same box. See `VisualEvalConfig.canvasViewports`.
|
|
109
124
|
*/
|
|
110
125
|
canvasViewports?: Partial<Readonly<Record<CanvasClass, { width: number; height: number }>>>;
|
|
126
|
+
/** ggui#1436 — see `SingleComponentParams.visualEvaluation.criteriaBank`. */
|
|
127
|
+
criteriaBank?: CriteriaBank;
|
|
128
|
+
criteriaKind?: string;
|
|
111
129
|
};
|
|
112
130
|
/** Quality config controlling eval tiers and improvement behavior */
|
|
113
131
|
qualityConfig?: QualityConfig;
|
|
@@ -118,20 +136,38 @@ export interface GenerationDispatchParams {
|
|
|
118
136
|
*/
|
|
119
137
|
fixtureProps?: JsonObject;
|
|
120
138
|
/**
|
|
121
|
-
* Whether to wire `
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
139
|
+
* Whether to wire the runtime-render check (`runtimeRender` when given,
|
|
140
|
+
* else `DEFAULT_RUNTIME_RENDER_CHECK`) into the harness's check leg.
|
|
141
|
+
* When `true` (default), the runtime-render probe runs ONCE
|
|
142
|
+
* at the exit decision, after the coding turns, in an isolated
|
|
143
|
+
* subprocess — never on the per-turn hot path. Without an `evaluation`
|
|
144
|
+
* config that round is observe + one repair turn (ggui#1380): a
|
|
145
|
+
* recoverable render crash buys exactly one repair turn and one
|
|
146
|
+
* re-probe, then the generation serves what it has; a probe that timed
|
|
147
|
+
* out, could not run, or had nothing to probe never blocks and never
|
|
148
|
+
* repairs. With an `evaluation` config the probe is the evaluation
|
|
149
|
+
* round's exit gate. Its status and timing land on
|
|
150
|
+
* `evalResult.runtimeProbe` (and on the generation's metadata through
|
|
151
|
+
* `createUiGenerator`).
|
|
125
152
|
*
|
|
126
|
-
* When `false`, the harness omits runtime-render
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
* Bench callers can run the probe externally as a final post-gen
|
|
130
|
-
* check to get pass/fail visibility without slowing the loop.
|
|
153
|
+
* When `false`, the harness omits runtime-render entirely: no probe
|
|
154
|
+
* round runs, and a caller can run the probe externally as a final
|
|
155
|
+
* post-generation check instead.
|
|
131
156
|
*
|
|
132
157
|
* Default: `true`.
|
|
133
158
|
*/
|
|
134
159
|
enableRuntimeRender?: boolean;
|
|
160
|
+
/**
|
|
161
|
+
* The runtime-render check to wire (ggui#1380) — built by the CALLER with
|
|
162
|
+
* `createRuntimeRenderCheck(config)` and kept for the caller's lifetime,
|
|
163
|
+
* because the check instance owns the probe's concurrency cap: one
|
|
164
|
+
* instance per generator bounds the workers a serving deployment has
|
|
165
|
+
* live at once; one per dispatch would give every generation its own
|
|
166
|
+
* free slots and bound nothing. Absent ⇒ `DEFAULT_RUNTIME_RENDER_CHECK`
|
|
167
|
+
* (the evaluation lane's bounds, no concurrency cap). Only read when
|
|
168
|
+
* `enableRuntimeRender` is on.
|
|
169
|
+
*/
|
|
170
|
+
runtimeRender?: RuntimeRenderCheck;
|
|
135
171
|
/**
|
|
136
172
|
* Operator-registered gadget catalog forwarded to the
|
|
137
173
|
* code-gen system prompt's `clientCapabilities — registered
|
|
@@ -171,6 +207,13 @@ export interface GenerationDispatchParams {
|
|
|
171
207
|
* existing behavior for callers that don't set it.
|
|
172
208
|
*/
|
|
173
209
|
routeOverride?: AgentConfig["routeOverride"];
|
|
210
|
+
/**
|
|
211
|
+
* ggui#1250 — the in-loop visual judge named as its own agent (see {@link VisualEvalAgentSpec}).
|
|
212
|
+
* Absent (default, every existing caller): today's chain, `visualEval ← evaluation ← coding`, which on
|
|
213
|
+
* a lane without a vision path records the score half as skipped (#1248). Off by default; a measurement
|
|
214
|
+
* knob first, a product knob only by ruling.
|
|
215
|
+
*/
|
|
216
|
+
visualEvalAgent?: VisualEvalAgentSpec;
|
|
174
217
|
/**
|
|
175
218
|
* Retry observer (#489) — see `AgentConfig.onRetry`. Threaded onto
|
|
176
219
|
* the coding + evaluation agent specs so a rate-limited retry inside
|
|
@@ -200,7 +243,9 @@ export interface GenerationDispatchParams {
|
|
|
200
243
|
* Map ProviderName ('claude' | 'openai' | 'google') to AgentConfig provider ('anthropic' | 'openai' | 'google').
|
|
201
244
|
* The LLM router uses 'anthropic' while the rest of the codebase uses 'claude'.
|
|
202
245
|
*/
|
|
203
|
-
function mapProviderForLLMRouter(
|
|
246
|
+
function mapProviderForLLMRouter(
|
|
247
|
+
provider: ProviderName
|
|
248
|
+
): "anthropic" | "openai" | "google" | "openrouter" {
|
|
204
249
|
return provider === "claude" ? "anthropic" : provider;
|
|
205
250
|
}
|
|
206
251
|
|
|
@@ -209,7 +254,7 @@ function mapProviderForLLMRouter(provider: ProviderName): "anthropic" | "openai"
|
|
|
209
254
|
* provider-agnostic.
|
|
210
255
|
*/
|
|
211
256
|
export async function dispatchGeneration(
|
|
212
|
-
params: GenerationDispatchParams
|
|
257
|
+
params: GenerationDispatchParams
|
|
213
258
|
): Promise<GenerationResult> {
|
|
214
259
|
// NOTE: userPrompt arrives already enriched with rendering context + contract
|
|
215
260
|
// (including examples) from the caller (runner.ts / generator.ts).
|
|
@@ -247,8 +292,17 @@ export async function dispatchGeneration(
|
|
|
247
292
|
onRetry: params.onRetry,
|
|
248
293
|
}
|
|
249
294
|
: undefined;
|
|
250
|
-
//
|
|
251
|
-
//
|
|
295
|
+
// ggui#1250 — the in-loop visual judge as its own agent when named: its own provider and its own key
|
|
296
|
+
// route, never the coding agent's. Absent: today's chain, visualEval ← evaluation ← coding, inside
|
|
297
|
+
// resolveSessionAgents (ModelRoles has no visualEval slot — a role is a model, and this needs a provider).
|
|
298
|
+
const visualEvalAgent = params.visualEvalAgent
|
|
299
|
+
? {
|
|
300
|
+
provider: params.visualEvalAgent.provider,
|
|
301
|
+
model: params.visualEvalAgent.model,
|
|
302
|
+
...(params.visualEvalAgent.apiKey !== undefined ? { routeOverride: { apiKey: params.visualEvalAgent.apiKey } } : {}),
|
|
303
|
+
onRetry: params.onRetry,
|
|
304
|
+
}
|
|
305
|
+
: undefined;
|
|
252
306
|
|
|
253
307
|
// ── Contract enrichment ──────────────────────────────────────────────
|
|
254
308
|
// The contract is used as-is: `clientCapabilities.gadgets`
|
|
@@ -294,17 +348,13 @@ export async function dispatchGeneration(
|
|
|
294
348
|
// from '<package>'` lines for thin contract refs. The closure
|
|
295
349
|
// capture below (systemPromptBuilder) wires the SAME catalog
|
|
296
350
|
// into the HOW leg.
|
|
297
|
-
...(params.appGadgets !== undefined
|
|
298
|
-
? { appGadgets: params.appGadgets }
|
|
299
|
-
: {}),
|
|
351
|
+
...(params.appGadgets !== undefined ? { appGadgets: params.appGadgets } : {}),
|
|
300
352
|
// Forward the third-party wrapper `.d.ts` map so
|
|
301
353
|
// `WhatLeg.gadgetTypes` reaches the coding-agent's typecheck
|
|
302
354
|
// overlay (autoCommit → runTier0Checks → typecheck). The
|
|
303
355
|
// systemPromptBuilder closure below also captures `gadgetTypes`
|
|
304
356
|
// from `params` directly for the prompt `Type:` lines.
|
|
305
|
-
...(params.gadgetTypes !== undefined
|
|
306
|
-
? { gadgetTypes: params.gadgetTypes }
|
|
307
|
-
: {}),
|
|
357
|
+
...(params.gadgetTypes !== undefined ? { gadgetTypes: params.gadgetTypes } : {}),
|
|
308
358
|
// Design mode + canvas — the harness carries them to every leg.
|
|
309
359
|
...(params.designMode !== undefined ? { designMode: params.designMode } : {}),
|
|
310
360
|
// ggui#1117 — the canvas is ALWAYS carried: a stated one, else the class
|
|
@@ -334,7 +384,15 @@ export async function dispatchGeneration(
|
|
|
334
384
|
// prompt's gadget catalog renders a `Type:` line per third-party
|
|
335
385
|
// gadget (the LLM sees a wrapper's real call shape it otherwise
|
|
336
386
|
// can't know).
|
|
337
|
-
systemPromptBuilder: ({
|
|
387
|
+
systemPromptBuilder: ({
|
|
388
|
+
userRequest,
|
|
389
|
+
shellType,
|
|
390
|
+
screen,
|
|
391
|
+
axisDelta,
|
|
392
|
+
designMode,
|
|
393
|
+
canvas,
|
|
394
|
+
profile,
|
|
395
|
+
}) =>
|
|
338
396
|
buildSystemPromptWithFunnel(
|
|
339
397
|
userRequest,
|
|
340
398
|
shellType,
|
|
@@ -344,29 +402,30 @@ export async function dispatchGeneration(
|
|
|
344
402
|
params.gadgetTypes,
|
|
345
403
|
designMode,
|
|
346
404
|
canvas,
|
|
347
|
-
profile
|
|
405
|
+
profile
|
|
348
406
|
),
|
|
349
407
|
// Pre-filtered axis-check registry. The `matches()` predicate
|
|
350
408
|
// selects the checks relevant to this generation's axis vector.
|
|
351
409
|
axisChecks: AXIS_CHECK_REGISTRY.filter((check) =>
|
|
352
|
-
axisCheckMatches(classification.vector, check)
|
|
410
|
+
axisCheckMatches(classification.vector, check)
|
|
353
411
|
),
|
|
354
|
-
// The
|
|
355
|
-
// design) stay on the dispatch side of
|
|
356
|
-
// createHarness defaults this to undefined.
|
|
412
|
+
// The runtime-render check, built under the caller's probe config.
|
|
413
|
+
// Heavy deps (happy-dom + wire + design) stay on the dispatch side of
|
|
414
|
+
// the boundary; the skeleton createHarness defaults this to undefined.
|
|
357
415
|
//
|
|
358
416
|
// Gated behind `enableRuntimeRender` (default true) so
|
|
359
417
|
// benchmark/dev callers can opt out of in-loop probe execution and
|
|
360
418
|
// run the probe externally as a final post-generation check.
|
|
361
|
-
runtimeRender:
|
|
362
|
-
|
|
363
|
-
|
|
419
|
+
runtimeRender:
|
|
420
|
+
(params.enableRuntimeRender ?? true)
|
|
421
|
+
? (params.runtimeRender ?? DEFAULT_RUNTIME_RENDER_CHECK)
|
|
422
|
+
: undefined,
|
|
364
423
|
});
|
|
365
424
|
console.log(
|
|
366
|
-
`[harness] id=${harness.id} name=${harness.name} workflow=${harness.process.workflow.name} overrides=${harness.meta.overrides.join(",") || "none"}
|
|
425
|
+
`[harness] id=${harness.id} name=${harness.name} workflow=${harness.process.workflow.name} overrides=${harness.meta.overrides.join(",") || "none"}`
|
|
367
426
|
);
|
|
368
427
|
console.log(
|
|
369
|
-
`[simple] axis-vector: ${JSON.stringify(classification.vector)} risk=${classification.riskTier}
|
|
428
|
+
`[simple] axis-vector: ${JSON.stringify(classification.vector)} risk=${classification.riskTier}`
|
|
370
429
|
);
|
|
371
430
|
|
|
372
431
|
// ── Guard: staged workflow is not wired on the dispatch path. ──
|
|
@@ -392,7 +451,7 @@ export async function dispatchGeneration(
|
|
|
392
451
|
`This is a misconfiguration (probably pickProcessMode returning WORKFLOWS.staged). ` +
|
|
393
452
|
`For staged-like behaviour, set process.mode="staged" + workflow.name="single_pass" instead — the A1 ` +
|
|
394
453
|
`scaffold→fill phase machine lives inside the generate runner and fires without the workflow topology. ` +
|
|
395
|
-
`See ralph #3 report (2026-04-14) for the full rationale
|
|
454
|
+
`See ralph #3 report (2026-04-14) for the full rationale.`
|
|
396
455
|
);
|
|
397
456
|
}
|
|
398
457
|
|
|
@@ -400,6 +459,7 @@ export async function dispatchGeneration(
|
|
|
400
459
|
const agents = resolveSessionAgents({
|
|
401
460
|
codingAgent,
|
|
402
461
|
evaluationAgent,
|
|
462
|
+
...(visualEvalAgent !== undefined ? { visualEvalAgent } : {}),
|
|
403
463
|
});
|
|
404
464
|
|
|
405
465
|
// ── Runtime policy resolution ──
|
|
@@ -409,29 +469,58 @@ export async function dispatchGeneration(
|
|
|
409
469
|
// resolveRunPolicyForProfile falls through to the identity
|
|
410
470
|
// resolveRunPolicy — byte-identical to the default behavior.
|
|
411
471
|
const { resolveRunPolicyForProfile } = await import("../harness/policy.js");
|
|
412
|
-
const resolvedPolicy = resolveRunPolicyForProfile(
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
);
|
|
472
|
+
const resolvedPolicy = resolveRunPolicyForProfile(process.env.GGUI_POLICY_PROFILE, harness, {
|
|
473
|
+
provider: routerProvider,
|
|
474
|
+
modelId: resolvedModel,
|
|
475
|
+
});
|
|
417
476
|
|
|
418
|
-
// ──
|
|
419
|
-
//
|
|
420
|
-
//
|
|
421
|
-
//
|
|
422
|
-
//
|
|
423
|
-
//
|
|
424
|
-
//
|
|
425
|
-
//
|
|
477
|
+
// ── The primitives reference the first-turn prompt carries ──
|
|
478
|
+
// The system prompt embeds PRIMITIVES_DOCUMENTATION (markdown) verbatim.
|
|
479
|
+
// Three policy knobs decide what is SENT in its place, composed in this
|
|
480
|
+
// order (ggui#1324, speed/002 — the slice applies to the format in force;
|
|
481
|
+
// slicing the markdown before the TS swap sent a sliced markdown doc about
|
|
482
|
+
// the size of the full TS doc, which speed/001 measured as a trim):
|
|
483
|
+
// 1. primitiveDocFormat: "markdown" (the embedded doc) or "ts" (its
|
|
484
|
+
// TypeScript-interface form, ~55% smaller, same information);
|
|
485
|
+
// 2. primitiveDocSlice: "full", or an axis-keyed allowlist from the
|
|
486
|
+
// classification ("axis-keyed", or "axis-keyed+contract" which adds the
|
|
487
|
+
// inputs the contract implies) applied to THAT format's doc;
|
|
488
|
+
// 3. primitiveIndex: a name-only index that replaces whatever is left
|
|
489
|
+
// (tool-driven docs; wins when on).
|
|
490
|
+
// Every cell logs `[simple] primitive-doc-bytes … mode=…` (the slice) and
|
|
491
|
+
// `[simple] system-prompt-bytes … doc-format=… doc-bytes=…` (what is sent),
|
|
492
|
+
// so a prompt-size claim is auditable from the log, not inferred.
|
|
426
493
|
let systemPromptOverride: string | undefined;
|
|
427
494
|
{
|
|
428
495
|
const { PRIMITIVES_DOCUMENTATION } = await import("../validation/index.js");
|
|
429
496
|
const fullBytes = Buffer.byteLength(PRIMITIVES_DOCUMENTATION, "utf8");
|
|
497
|
+
const docFormat = resolvedPolicy.context.primitiveDocFormat ?? "markdown";
|
|
498
|
+
const indexMode = resolvedPolicy.context.primitiveIndex ?? "off";
|
|
430
499
|
const mode = resolvedPolicy.context.primitiveDocSlice ?? "full";
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
500
|
+
|
|
501
|
+
let doc = PRIMITIVES_DOCUMENTATION;
|
|
502
|
+
if (docFormat === "ts" && indexMode === "off") {
|
|
503
|
+
const { PRIMITIVES_DOCUMENTATION_TS } = await import("../tools/get-primitives-ts.js");
|
|
504
|
+
doc = PRIMITIVES_DOCUMENTATION_TS;
|
|
505
|
+
const tsBytes = Buffer.byteLength(doc, "utf8");
|
|
506
|
+
console.log(
|
|
507
|
+
`[simple] primitive-doc-ts-bytes=${tsBytes} baseline=${fullBytes} drop=${((1 - tsBytes / fullBytes) * 100).toFixed(1)}%`
|
|
508
|
+
);
|
|
509
|
+
}
|
|
510
|
+
const formatBytes = Buffer.byteLength(doc, "utf8");
|
|
511
|
+
|
|
512
|
+
if (mode === "axis-keyed" || mode === "axis-keyed+contract") {
|
|
513
|
+
const {
|
|
514
|
+
computePrimitiveAllowlist,
|
|
515
|
+
computeContractPrimitives,
|
|
516
|
+
slicePrimitiveDocumentation,
|
|
517
|
+
slicePrimitiveDocumentationTs,
|
|
518
|
+
} = await import("../harness/primitive-slice.js");
|
|
434
519
|
let allowlist = computePrimitiveAllowlist(classification);
|
|
520
|
+
if (mode === "axis-keyed+contract") {
|
|
521
|
+
const fromContract = computeContractPrimitives(effectiveContract);
|
|
522
|
+
allowlist = [...new Set([...allowlist, ...fromContract])].sort();
|
|
523
|
+
}
|
|
435
524
|
// Apply policy-driven excludes — some policy profiles drop
|
|
436
525
|
// near-synonym layout primitives (e.g. Row/Box/Spacer).
|
|
437
526
|
const excludes = resolvedPolicy.context.primitiveDocExcludes ?? [];
|
|
@@ -439,61 +528,41 @@ export async function dispatchGeneration(
|
|
|
439
528
|
const excludeSet = new Set(excludes);
|
|
440
529
|
allowlist = allowlist.filter((p) => !excludeSet.has(p));
|
|
441
530
|
}
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
const slicedBytes = Buffer.byteLength(
|
|
531
|
+
doc =
|
|
532
|
+
docFormat === "ts" && indexMode === "off"
|
|
533
|
+
? slicePrimitiveDocumentationTs(doc, allowlist)
|
|
534
|
+
: slicePrimitiveDocumentation(doc, allowlist);
|
|
535
|
+
const slicedBytes = Buffer.byteLength(doc, "utf8");
|
|
447
536
|
const excludeTag = excludes.length ? ` excludes=${excludes.join(",")}` : "";
|
|
448
537
|
console.log(
|
|
449
|
-
`[simple] primitive-doc-bytes=${slicedBytes} mode
|
|
450
|
-
);
|
|
451
|
-
systemPromptOverride = harness.how.systemPrompt.replace(
|
|
452
|
-
PRIMITIVES_DOCUMENTATION,
|
|
453
|
-
sliced,
|
|
538
|
+
`[simple] primitive-doc-bytes=${slicedBytes} mode=${mode} baseline=${formatBytes} drop=${((1 - slicedBytes / formatBytes) * 100).toFixed(1)}% allowlist=${allowlist.length}/${allowlist.join(",")}${excludeTag}`
|
|
454
539
|
);
|
|
455
540
|
} else {
|
|
456
541
|
console.log(
|
|
457
|
-
`[simple] primitive-doc-bytes=${
|
|
542
|
+
`[simple] primitive-doc-bytes=${formatBytes} mode=full baseline=${formatBytes} drop=0.0%`
|
|
458
543
|
);
|
|
459
544
|
}
|
|
460
545
|
|
|
461
546
|
// ── Tool-driven primitive docs ──
|
|
462
|
-
//
|
|
463
|
-
//
|
|
464
|
-
//
|
|
465
|
-
// mutually exclusive in practice via profile selection; this
|
|
466
|
-
// ordering just defines the tiebreak.
|
|
467
|
-
const indexMode = resolvedPolicy.context.primitiveIndex ?? "off";
|
|
547
|
+
// A name-only index replaces whatever is left (it is the more
|
|
548
|
+
// aggressive cut). The two are mutually exclusive in practice via
|
|
549
|
+
// profile selection; this ordering just defines the tiebreak.
|
|
468
550
|
if (indexMode !== "off") {
|
|
469
551
|
const { buildPrimitiveIndex } = await import("../harness/primitive-index.js");
|
|
470
|
-
|
|
471
|
-
const indexBytes = Buffer.byteLength(
|
|
552
|
+
doc = buildPrimitiveIndex(PRIMITIVES_DOCUMENTATION, indexMode);
|
|
553
|
+
const indexBytes = Buffer.byteLength(doc, "utf8");
|
|
472
554
|
console.log(
|
|
473
|
-
`[simple] primitive-index-bytes=${indexBytes} mode=${indexMode} baseline=${fullBytes} drop=${((1 - indexBytes / fullBytes) * 100).toFixed(1)}
|
|
474
|
-
);
|
|
475
|
-
systemPromptOverride = (systemPromptOverride ?? harness.how.systemPrompt).replace(
|
|
476
|
-
PRIMITIVES_DOCUMENTATION,
|
|
477
|
-
index,
|
|
555
|
+
`[simple] primitive-index-bytes=${indexBytes} mode=${indexMode} baseline=${fullBytes} drop=${((1 - indexBytes / fullBytes) * 100).toFixed(1)}%`
|
|
478
556
|
);
|
|
479
557
|
}
|
|
480
558
|
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
// version (~55% smaller, same info). Mutually exclusive with index
|
|
484
|
-
// modes.
|
|
485
|
-
const docFormat = resolvedPolicy.context.primitiveDocFormat ?? "markdown";
|
|
486
|
-
if (docFormat === "ts" && indexMode === "off") {
|
|
487
|
-
const { PRIMITIVES_DOCUMENTATION_TS } = await import("../tools/get-primitives-ts.js");
|
|
488
|
-
const tsBytes = Buffer.byteLength(PRIMITIVES_DOCUMENTATION_TS, "utf8");
|
|
489
|
-
console.log(
|
|
490
|
-
`[simple] primitive-doc-ts-bytes=${tsBytes} baseline=${fullBytes} drop=${((1 - tsBytes / fullBytes) * 100).toFixed(1)}%`,
|
|
491
|
-
);
|
|
492
|
-
systemPromptOverride = (systemPromptOverride ?? harness.how.systemPrompt).replace(
|
|
493
|
-
PRIMITIVES_DOCUMENTATION,
|
|
494
|
-
PRIMITIVES_DOCUMENTATION_TS,
|
|
495
|
-
);
|
|
559
|
+
if (doc !== PRIMITIVES_DOCUMENTATION) {
|
|
560
|
+
systemPromptOverride = harness.how.systemPrompt.replace(PRIMITIVES_DOCUMENTATION, doc);
|
|
496
561
|
}
|
|
562
|
+
const sentPrompt = systemPromptOverride ?? harness.how.systemPrompt;
|
|
563
|
+
console.log(
|
|
564
|
+
`[simple] system-prompt-bytes=${Buffer.byteLength(sentPrompt, "utf8")} doc-format=${docFormat === "ts" && indexMode === "off" ? "ts" : docFormat} doc-bytes=${Buffer.byteLength(doc, "utf8")} mode=${indexMode !== "off" ? `index:${indexMode}` : mode}`
|
|
565
|
+
);
|
|
497
566
|
|
|
498
567
|
// ── plan→impl pipeline augmentation ──
|
|
499
568
|
// Append a 2-phase flow description when planFirstTurn is active so
|
|
@@ -548,7 +617,7 @@ export async function dispatchGeneration(
|
|
|
548
617
|
console.error(
|
|
549
618
|
`[dispatch] runHarness terminated: ${runResult.reason} ` +
|
|
550
619
|
`(source=${runResult.finalSource ? runResult.finalSource.length + "B" : "null"}, ` +
|
|
551
|
-
`compiled=${runResult.finalCompiled ? runResult.finalCompiled.length + "B" : "null"})
|
|
620
|
+
`compiled=${runResult.finalCompiled ? runResult.finalCompiled.length + "B" : "null"})`
|
|
552
621
|
);
|
|
553
622
|
}
|
|
554
623
|
|
|
@@ -560,7 +629,11 @@ export async function dispatchGeneration(
|
|
|
560
629
|
// `assembleGenerationResult` is async — the spread below must see the
|
|
561
630
|
// RESOLVED result, never the promise (a spread promise is `{}`: every
|
|
562
631
|
// consumer downstream lost `compiledCode` / `tokens` on 2026-09-09).
|
|
563
|
-
const result: GenerationResult = await assembleGenerationResult({
|
|
632
|
+
const result: GenerationResult = await assembleGenerationResult({
|
|
633
|
+
session,
|
|
634
|
+
telemetry,
|
|
635
|
+
source: finalSource,
|
|
636
|
+
});
|
|
564
637
|
// Record the arm this generation ran under (bench reports read it per
|
|
565
638
|
// cell). The canvas is the one the free prompt stated: explicit when
|
|
566
639
|
// passed, else the same shell × screen derivation the prompt used.
|
|
@@ -579,11 +652,13 @@ export async function dispatchGeneration(
|
|
|
579
652
|
*/
|
|
580
653
|
function withArmRecord(
|
|
581
654
|
result: GenerationResult,
|
|
582
|
-
params: Pick<GenerationDispatchParams, "designMode" | "canvas" | "shellType" | "screen"
|
|
655
|
+
params: Pick<GenerationDispatchParams, "designMode" | "canvas" | "shellType" | "screen">
|
|
583
656
|
): GenerationResult {
|
|
584
657
|
const recordedCanvas: CanvasClass | undefined =
|
|
585
658
|
params.canvas ??
|
|
586
|
-
(params.designMode === "free"
|
|
659
|
+
(params.designMode === "free"
|
|
660
|
+
? canvasForRendering(params.shellType, params.screen)
|
|
661
|
+
: undefined);
|
|
587
662
|
return {
|
|
588
663
|
...result,
|
|
589
664
|
...(params.designMode !== undefined ? { designMode: params.designMode } : {}),
|
package/src/adapters/tools.ts
CHANGED
|
@@ -20,7 +20,8 @@ import { validateAllContracts } from '../check/index.js';
|
|
|
20
20
|
import { typecheck } from '../check/index.js';
|
|
21
21
|
import { lintReactHooks } from '../check/index.js';
|
|
22
22
|
import ts from 'typescript';
|
|
23
|
-
import { tryRender
|
|
23
|
+
import { tryRender } from '../tools/render-check.js';
|
|
24
|
+
import { generateSampleProps } from '../check/sample-props.js';
|
|
24
25
|
|
|
25
26
|
/**
|
|
26
27
|
* Context for creating generator tools.
|
|
@@ -212,7 +212,7 @@ With raw elements YOU own the semantics — nothing is accessible by constructio
|
|
|
212
212
|
3. **Images have \`alt\`** (\`alt=""\` for decorative). Icon-only buttons need \`aria-label\`; decorative icons and emoji are \`aria-hidden="true"\`.
|
|
213
213
|
4. **Live & streaming data** (a \`useStream\` \`.latest\` value, counters, clocks, flashing prices) sits in an \`aria-live="polite"\` (or \`role="status"\`) region so screen readers announce the change.
|
|
214
214
|
5. **Headings nest**: one \`<h1>\` per screen, \`<h2>\` for sections, \`<h3>\` below — never skipped or inverted.
|
|
215
|
-
6. **Busy state is announced**: \`<button disabled={
|
|
215
|
+
6. **Busy state is announced**: every control that dispatches an action (a button, a select whose change dispatches, an input that sends on Enter) stays pending from \`useActionPending('<action>')\` until the agent's answer repaints the card — \`<button disabled={sendPending} aria-busy={sendPending}>{sendPending ? 'Sending…' : 'Send'}</button>\`. Only a control that dispatches the action carries its busy look; while it is pending, other controls of the same record may be disabled — nothing more (controls of other records, creation controls and the action's own cancel stay enabled); busy state the card owns (\`isLoading\`) the same way.
|
|
216
216
|
7. **Stateful controls announce state**: \`role="checkbox"\` + \`aria-checked\` on a toggleable row, \`aria-pressed\` on a toggle button, \`aria-selected\` on the chosen item, \`aria-expanded\` on an open/close affordance. Styling (strikethrough, color, a check mark) is never the only carrier of state.
|
|
217
217
|
8. **Focus is visible** — style \`:focus-visible\` with a token color; never \`outline: none\` without a replacement.`;
|
|
218
218
|
|
|
@@ -230,6 +230,7 @@ export function generateBoilerplate(
|
|
|
230
230
|
const actionTypeAliases: string[] = [];
|
|
231
231
|
const actionHookCalls: string[] = [];
|
|
232
232
|
const actionReturnFields: string[] = [];
|
|
233
|
+
let hasOneShotAction = false;
|
|
233
234
|
const actionsMap: ActionSpec = contract?.actionSpec ?? {};
|
|
234
235
|
for (const [key, entry] of Object.entries(actionsMap)) {
|
|
235
236
|
const label = entry.label ?? key;
|
|
@@ -274,7 +275,29 @@ export function generateBoilerplate(
|
|
|
274
275
|
// instruction for every action. So it stays, ALONE on the hook line — what
|
|
275
276
|
// sits next to it is part of what it says.
|
|
276
277
|
const onceHint = " — if this is meant ONCE, disable its control after it fires";
|
|
277
|
-
|
|
278
|
+
// ggui#1223 (rnd reliability/013, arm C): an action the contract DECLARES
|
|
279
|
+
// `oneShot` gets its spent state as data, pre-bound beside the dispatcher —
|
|
280
|
+
// WHAT to code, beside HOW — and the generic once-hint leaves ITS line only:
|
|
281
|
+
// left there, the hint steered the model to a local `useState(false)` guard
|
|
282
|
+
// against the prompt's block (arm B: 0 of 6 declared cells used the hook).
|
|
283
|
+
// The generic hint stays verbatim on every undeclared action.
|
|
284
|
+
if (entry.oneShot === true) {
|
|
285
|
+
hasOneShotAction = true;
|
|
286
|
+
actionHookCalls.push(
|
|
287
|
+
` const ${key} = useAction<${typeName}>('${key}'); // ${callSig}${toolHint} — declared oneShot: the runtime drops a second dispatch`,
|
|
288
|
+
);
|
|
289
|
+
actionHookCalls.push(
|
|
290
|
+
` const ${key}Spent = useActionSpent('${key}'); // true once fired, also after a reload — render the control from THIS: disabled={${key}Spent} and a spent label`,
|
|
291
|
+
);
|
|
292
|
+
} else {
|
|
293
|
+
actionHookCalls.push(` const ${key} = useAction<${typeName}>('${key}'); // ${callSig}${toolHint}${onceHint}`);
|
|
294
|
+
}
|
|
295
|
+
// ggui#1398 — the dispatch's in-flight state, bound at the site the model
|
|
296
|
+
// writes the control (reliability/013: a rule alone reached 1/6, the
|
|
297
|
+
// binding 6/6).
|
|
298
|
+
actionHookCalls.push(
|
|
299
|
+
` const ${key}Pending = useActionPending('${key}'); // true from the tap until the agent's answer repaints the card — render EVERY control that dispatches '${key}' (a Button, a Select's onChange, an Input's Enter) from THIS: disabled={${key}Pending} aria-busy={${key}Pending}, and a working label on a button; no other control carries its aria-busy`,
|
|
300
|
+
);
|
|
278
301
|
actionReturnFields.push(key);
|
|
279
302
|
}
|
|
280
303
|
|
|
@@ -507,6 +530,8 @@ export function generateBoilerplate(
|
|
|
507
530
|
|
|
508
531
|
const wireHooks: string[] = [];
|
|
509
532
|
if (hasActions) wireHooks.push("useAction");
|
|
533
|
+
if (hasOneShotAction) wireHooks.push("useActionSpent");
|
|
534
|
+
if (hasActions) wireHooks.push("useActionPending");
|
|
510
535
|
if (hasStream) wireHooks.push("useStream");
|
|
511
536
|
if (hasContext) wireHooks.push("useGguiContext");
|
|
512
537
|
const wireImport = hasAnyWireFromWire
|
|
@@ -214,29 +214,29 @@ Rules:
|
|
|
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
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
|
-
5. Props interface must be typed and exported
|
|
217
|
+
5. Props interface must be typed and exported.
|
|
218
|
+
6. Keys on a mapped list come from the item's own identity: \`key={item.id}\` when the contract gives an id; when the items carry none (KPI tiles \`{label, value}\`, forecast days \`{day, high}\`), key by the field that names each one — \`key={item.label}\`, \`key={day.day}\` — never by the map index: a reorder or a stream merge re-keys by index and re-mounts the wrong rows, and the self-check refuses an index key.`;
|
|
218
219
|
|
|
219
220
|
/**
|
|
220
221
|
* A control that fires an action the user would not mean to repeat must not
|
|
221
|
-
* stay armed (ggui#1108
|
|
222
|
-
*
|
|
223
|
-
*
|
|
224
|
-
*
|
|
222
|
+
* stay armed (ggui#1108). One-shot-ness is DECLARED on the action
|
|
223
|
+
* (`actionSpec.<name>.oneShot`), enforced by the runtime, and readable by the
|
|
224
|
+
* component through `useActionSpent` (ggui#1223) — so a declared action's
|
|
225
|
+
* control renders from data that survives a reload, and the local-state guard
|
|
226
|
+
* remains only as the fallback for an action the contract failed to declare.
|
|
227
|
+
* Both arms, one HARD section: the contract says which actions are once; where
|
|
228
|
+
* it does not, the request may, and no static rule of ours can infer it from a
|
|
229
|
+
* verb.
|
|
225
230
|
*/
|
|
226
231
|
export const TERMINAL_ACTIONS = `## Actions the user means once
|
|
227
232
|
|
|
228
233
|
A second press is a mistake only when it would make a SECOND THING HAPPEN IN THE WORLD — a second booking, a second charge, a second order, a second application sent. If pressing again would only write the SAME STATE AGAIN — saving, updating, renaming, setting a preference, toggling — nothing is duplicated and the control stays armed.
|
|
229
234
|
|
|
230
|
-
|
|
235
|
+
**The contract says which actions are once.** An action the contract declares \`oneShot: true\` (in the Contract block above) is guarded by the runtime — a second dispatch is dropped — and its spent state is DATA you read, never state you hold: render its control from \`const spent = useActionSpent('<name>')\`, \`disabled={spent}\`, and say so in the label or beside it ("Submitted", "Request sent") — a control that is disabled without explanation reads as broken. The hook's answer survives a reload and a second tab; \`useState(false)\` does not, so a reloaded card built on local state re-arms a control that already fired.
|
|
231
236
|
|
|
232
|
-
**
|
|
237
|
+
**An action the contract does not declare \`oneShot\` stays armed.** Never guard one from a feeling about the verb — "confirm", "submit", "place" alone are not evidence. Guard it only where the request itself names a one-time commitment the contract failed to declare ("submit the application", "place the order"), and then with local state as the fallback: \`const [submitted, setSubmitted] = useState(false)\` set in the handler, \`disabled={submitted}\` with a label — knowing that this state lives with the component, not with the request, and starts fresh on a re-render with new props.
|
|
233
238
|
|
|
234
|
-
When
|
|
235
|
-
- Hold a small piece of state for it and set it in the handler: \`const [submitted, setSubmitted] = useState(false)\`.
|
|
236
|
-
- \`disabled={submitted}\` on the control that fires it, and say so in the label or beside it ("Submitted", "Request sent") — a control that is disabled without explanation reads as broken.
|
|
237
|
-
- A card re-rendered with new props starts fresh; the state above lives with the component, not with the request.
|
|
238
|
-
|
|
239
|
-
Nothing in the wire DECLARES one-shot-ness yet, which is why the test is what a second press would DO — not how final the word sounds.`;
|
|
239
|
+
**When in doubt, leave it armed.** A control the user cannot press a second time has lost them something that worked — save, edit, save again is ordinary. A control left armed is only what every interface does by default.`;
|
|
240
240
|
|
|
241
241
|
/**
|
|
242
242
|
* The frame owns the height — the component never sizes itself to the
|