@ggui-ai/ui-gen 0.23.0 → 0.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/dist/adapter-Du7P_5Fx.d.ts +201 -0
  2. package/dist/adapters/generation-dispatch.d.ts +33 -12
  3. package/dist/adapters/generation-dispatch.js +510 -128
  4. package/dist/adapters/generation-dispatch.js.map +1 -1
  5. package/dist/adapters/index.js.map +1 -1
  6. package/dist/advanced/index.d.ts +5 -3
  7. package/dist/advanced/index.js +551 -142
  8. package/dist/advanced/index.js.map +1 -1
  9. package/dist/blueprint-validator.js +153 -59
  10. package/dist/blueprint-validator.js.map +1 -1
  11. package/dist/boilerplate.js.map +1 -1
  12. package/dist/check/index.d.ts +2 -1
  13. package/dist/check/index.js +47 -1
  14. package/dist/check/index.js.map +1 -1
  15. package/dist/{create-ui-generator-D0kbWwln.d.ts → create-ui-generator-GIhzx08x.d.ts} +34 -10
  16. package/dist/{dispatch-wxaIMwZt.d.ts → dispatch-BnAH56p4.d.ts} +1 -1
  17. package/dist/evaluation/axis-checks/index.d.ts +4 -3
  18. package/dist/evaluation/axis-checks/index.js.map +1 -1
  19. package/dist/evaluation/axis-checks/registry.d.ts +2 -1
  20. package/dist/evaluation/index.d.ts +4 -3
  21. package/dist/evaluation/index.js.map +1 -1
  22. package/dist/harness/check/runtime-render/index.d.ts +29 -10
  23. package/dist/harness/check/runtime-render/index.js +203 -60
  24. package/dist/harness/check/runtime-render/index.js.map +1 -1
  25. package/dist/harness/check/runtime-render/render-check-worker.d.ts +8 -1
  26. package/dist/harness/check/runtime-render/render-check-worker.js +72 -32
  27. package/dist/harness/check/runtime-render/render-check-worker.js.map +1 -1
  28. package/dist/harness/index.d.ts +2 -1
  29. package/dist/harness/index.js.map +1 -1
  30. package/dist/harness/prompts.js.map +1 -1
  31. package/dist/harness/result-types.d.ts +2 -1
  32. package/dist/harness/runtime.d.ts +2 -1
  33. package/dist/harness/runtime.js.map +1 -1
  34. package/dist/harness/types-public.d.ts +11 -2
  35. package/dist/harness/types-public.js.map +1 -1
  36. package/dist/harness/validator-trace-sink.d.ts +2 -1
  37. package/dist/index.d.ts +38 -3
  38. package/dist/index.js +553 -143
  39. package/dist/index.js.map +1 -1
  40. package/dist/patch.d.ts +2 -1
  41. package/dist/policy.d.ts +7 -4
  42. package/dist/policy.js.map +1 -1
  43. package/dist/tools/render-check-worker.d.ts +7 -1
  44. package/dist/tools/render-check-worker.js +55 -23
  45. package/dist/tools/render-check-worker.js.map +1 -1
  46. package/dist/{types-public-Ow-tNtFE.d.ts → types-public-Bk6d1WLN.d.ts} +77 -11
  47. package/dist/workflows.d.ts +2 -1
  48. package/package.json +12 -12
  49. package/src/adapters/generation-dispatch.ts +137 -96
  50. package/src/check/run-tier0.ts +80 -1
  51. package/src/create-ui-generator.ts +185 -95
  52. package/src/evaluation/types-public.ts +73 -11
  53. package/src/harness/check/runtime-render/adapter.ts +221 -75
  54. package/src/harness/check/runtime-render/index.ts +75 -2
  55. package/src/harness/check/runtime-render/render-check-host.ts +82 -19
  56. package/src/harness/check/runtime-render/render-check-worker.ts +83 -29
  57. package/src/harness/check/runtime-render/render-check.ts +154 -117
  58. package/src/harness/coding/generate-task-runner.ts +57 -4
  59. package/src/harness/coding/init-session.ts +13 -0
  60. package/src/harness/coding/run-eval-round.ts +238 -15
  61. package/src/harness/policy.ts +11 -0
  62. package/src/harness/primitive-slice.ts +104 -0
  63. package/src/harness/types-public.ts +9 -1
  64. package/src/index.ts +4 -0
  65. package/src/policy.ts +7 -4
  66. package/src/tools/render-check-worker.ts +74 -23
  67. package/dist/render-check-Blwj8gB6.d.ts +0 -105
@@ -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 default. The `createHarness` constructor accepts an
37
- // optional runtimeRender; this callsite injects
38
- // `DEFAULT_RUNTIME_RENDER_CHECK` so production keeps the happy-dom-
39
- // backed contract-wiring verification step.
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,10 +58,7 @@ 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";
62
63
  import type { GenerationProfileInput } from "../boilerplate/styling-profile.js";
63
64
 
@@ -118,20 +119,38 @@ export interface GenerationDispatchParams {
118
119
  */
119
120
  fixtureProps?: JsonObject;
120
121
  /**
121
- * Whether to wire `DEFAULT_RUNTIME_RENDER_CHECK` into the harness's
122
- * check leg. When `true` (default), the probe runs every coding turn
123
- * and feeds wiring-check failures back to the coding agent — useful
124
- * for quality gating but ~3-5s per call adds wall-clock per turn.
122
+ * Whether to wire the runtime-render check (`runtimeRender` when given,
123
+ * else `DEFAULT_RUNTIME_RENDER_CHECK`) into the harness's check leg.
124
+ * When `true` (default), the runtime-render probe runs ONCE
125
+ * at the exit decision, after the coding turns, in an isolated
126
+ * subprocess — never on the per-turn hot path. Without an `evaluation`
127
+ * config that round is observe + one repair turn (ggui#1380): a
128
+ * recoverable render crash buys exactly one repair turn and one
129
+ * re-probe, then the generation serves what it has; a probe that timed
130
+ * out, could not run, or had nothing to probe never blocks and never
131
+ * repairs. With an `evaluation` config the probe is the evaluation
132
+ * round's exit gate. Its status and timing land on
133
+ * `evalResult.runtimeProbe` (and on the generation's metadata through
134
+ * `createUiGenerator`).
125
135
  *
126
- * When `false`, the harness omits runtime-render from the in-loop
127
- * check pipeline. Cloud production runs with this off (per the
128
- * migration plan — probe was never in the production hot path).
129
- * Bench callers can run the probe externally as a final post-gen
130
- * check to get pass/fail visibility without slowing the loop.
136
+ * When `false`, the harness omits runtime-render entirely: no probe
137
+ * round runs, and a caller can run the probe externally as a final
138
+ * post-generation check instead.
131
139
  *
132
140
  * Default: `true`.
133
141
  */
134
142
  enableRuntimeRender?: boolean;
143
+ /**
144
+ * The runtime-render check to wire (ggui#1380) — built by the CALLER with
145
+ * `createRuntimeRenderCheck(config)` and kept for the caller's lifetime,
146
+ * because the check instance owns the probe's concurrency cap: one
147
+ * instance per generator bounds the workers a serving deployment has
148
+ * live at once; one per dispatch would give every generation its own
149
+ * free slots and bound nothing. Absent ⇒ `DEFAULT_RUNTIME_RENDER_CHECK`
150
+ * (the evaluation lane's bounds, no concurrency cap). Only read when
151
+ * `enableRuntimeRender` is on.
152
+ */
153
+ runtimeRender?: RuntimeRenderCheck;
135
154
  /**
136
155
  * Operator-registered gadget catalog forwarded to the
137
156
  * code-gen system prompt's `clientCapabilities — registered
@@ -200,7 +219,9 @@ export interface GenerationDispatchParams {
200
219
  * Map ProviderName ('claude' | 'openai' | 'google') to AgentConfig provider ('anthropic' | 'openai' | 'google').
201
220
  * The LLM router uses 'anthropic' while the rest of the codebase uses 'claude'.
202
221
  */
203
- function mapProviderForLLMRouter(provider: ProviderName): "anthropic" | "openai" | "google" | "openrouter" {
222
+ function mapProviderForLLMRouter(
223
+ provider: ProviderName
224
+ ): "anthropic" | "openai" | "google" | "openrouter" {
204
225
  return provider === "claude" ? "anthropic" : provider;
205
226
  }
206
227
 
@@ -209,7 +230,7 @@ function mapProviderForLLMRouter(provider: ProviderName): "anthropic" | "openai"
209
230
  * provider-agnostic.
210
231
  */
211
232
  export async function dispatchGeneration(
212
- params: GenerationDispatchParams,
233
+ params: GenerationDispatchParams
213
234
  ): Promise<GenerationResult> {
214
235
  // NOTE: userPrompt arrives already enriched with rendering context + contract
215
236
  // (including examples) from the caller (runner.ts / generator.ts).
@@ -294,17 +315,13 @@ export async function dispatchGeneration(
294
315
  // from '<package>'` lines for thin contract refs. The closure
295
316
  // capture below (systemPromptBuilder) wires the SAME catalog
296
317
  // into the HOW leg.
297
- ...(params.appGadgets !== undefined
298
- ? { appGadgets: params.appGadgets }
299
- : {}),
318
+ ...(params.appGadgets !== undefined ? { appGadgets: params.appGadgets } : {}),
300
319
  // Forward the third-party wrapper `.d.ts` map so
301
320
  // `WhatLeg.gadgetTypes` reaches the coding-agent's typecheck
302
321
  // overlay (autoCommit → runTier0Checks → typecheck). The
303
322
  // systemPromptBuilder closure below also captures `gadgetTypes`
304
323
  // from `params` directly for the prompt `Type:` lines.
305
- ...(params.gadgetTypes !== undefined
306
- ? { gadgetTypes: params.gadgetTypes }
307
- : {}),
324
+ ...(params.gadgetTypes !== undefined ? { gadgetTypes: params.gadgetTypes } : {}),
308
325
  // Design mode + canvas — the harness carries them to every leg.
309
326
  ...(params.designMode !== undefined ? { designMode: params.designMode } : {}),
310
327
  // ggui#1117 — the canvas is ALWAYS carried: a stated one, else the class
@@ -334,7 +351,15 @@ export async function dispatchGeneration(
334
351
  // prompt's gadget catalog renders a `Type:` line per third-party
335
352
  // gadget (the LLM sees a wrapper's real call shape it otherwise
336
353
  // can't know).
337
- systemPromptBuilder: ({ userRequest, shellType, screen, axisDelta, designMode, canvas, profile }) =>
354
+ systemPromptBuilder: ({
355
+ userRequest,
356
+ shellType,
357
+ screen,
358
+ axisDelta,
359
+ designMode,
360
+ canvas,
361
+ profile,
362
+ }) =>
338
363
  buildSystemPromptWithFunnel(
339
364
  userRequest,
340
365
  shellType,
@@ -344,29 +369,30 @@ export async function dispatchGeneration(
344
369
  params.gadgetTypes,
345
370
  designMode,
346
371
  canvas,
347
- profile,
372
+ profile
348
373
  ),
349
374
  // Pre-filtered axis-check registry. The `matches()` predicate
350
375
  // selects the checks relevant to this generation's axis vector.
351
376
  axisChecks: AXIS_CHECK_REGISTRY.filter((check) =>
352
- axisCheckMatches(classification.vector, check),
377
+ axisCheckMatches(classification.vector, check)
353
378
  ),
354
- // The default runtime-render check. Heavy deps (happy-dom + wire +
355
- // design) stay on the dispatch side of the boundary; the skeleton
356
- // createHarness defaults this to undefined.
379
+ // The runtime-render check, built under the caller's probe config.
380
+ // Heavy deps (happy-dom + wire + design) stay on the dispatch side of
381
+ // the boundary; the skeleton createHarness defaults this to undefined.
357
382
  //
358
383
  // Gated behind `enableRuntimeRender` (default true) so
359
384
  // benchmark/dev callers can opt out of in-loop probe execution and
360
385
  // run the probe externally as a final post-generation check.
361
- runtimeRender: (params.enableRuntimeRender ?? true)
362
- ? DEFAULT_RUNTIME_RENDER_CHECK
363
- : undefined,
386
+ runtimeRender:
387
+ (params.enableRuntimeRender ?? true)
388
+ ? (params.runtimeRender ?? DEFAULT_RUNTIME_RENDER_CHECK)
389
+ : undefined,
364
390
  });
365
391
  console.log(
366
- `[harness] id=${harness.id} name=${harness.name} workflow=${harness.process.workflow.name} overrides=${harness.meta.overrides.join(",") || "none"}`,
392
+ `[harness] id=${harness.id} name=${harness.name} workflow=${harness.process.workflow.name} overrides=${harness.meta.overrides.join(",") || "none"}`
367
393
  );
368
394
  console.log(
369
- `[simple] axis-vector: ${JSON.stringify(classification.vector)} risk=${classification.riskTier}`,
395
+ `[simple] axis-vector: ${JSON.stringify(classification.vector)} risk=${classification.riskTier}`
370
396
  );
371
397
 
372
398
  // ── Guard: staged workflow is not wired on the dispatch path. ──
@@ -392,7 +418,7 @@ export async function dispatchGeneration(
392
418
  `This is a misconfiguration (probably pickProcessMode returning WORKFLOWS.staged). ` +
393
419
  `For staged-like behaviour, set process.mode="staged" + workflow.name="single_pass" instead — the A1 ` +
394
420
  `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.`,
421
+ `See ralph #3 report (2026-04-14) for the full rationale.`
396
422
  );
397
423
  }
398
424
 
@@ -409,29 +435,58 @@ export async function dispatchGeneration(
409
435
  // resolveRunPolicyForProfile falls through to the identity
410
436
  // resolveRunPolicy — byte-identical to the default behavior.
411
437
  const { resolveRunPolicyForProfile } = await import("../harness/policy.js");
412
- const resolvedPolicy = resolveRunPolicyForProfile(
413
- process.env.GGUI_POLICY_PROFILE,
414
- harness,
415
- { provider: routerProvider, modelId: resolvedModel },
416
- );
438
+ const resolvedPolicy = resolveRunPolicyForProfile(process.env.GGUI_POLICY_PROFILE, harness, {
439
+ provider: routerProvider,
440
+ modelId: resolvedModel,
441
+ });
417
442
 
418
- // ── Axis-keyed primitives doc slice ──
419
- // When the resolved policy flips to axis-keyed, rebuild the first-turn
420
- // system prompt with a sliced PRIMITIVES_DOCUMENTATION derived from the
421
- // classification. The slice typically cuts the system prompt by ~80 KB
422
- // (~20K tokens). Surface awareness is preserved — the name-only primitive
423
- // catalog already lives in the system prompt above the sliced block.
424
- // Always logs `[simple] primitive-doc-bytes` telemetry so both modes
425
- // are auditable post-hoc.
443
+ // ── The primitives reference the first-turn prompt carries ──
444
+ // The system prompt embeds PRIMITIVES_DOCUMENTATION (markdown) verbatim.
445
+ // Three policy knobs decide what is SENT in its place, composed in this
446
+ // order (ggui#1324, speed/002 — the slice applies to the format in force;
447
+ // slicing the markdown before the TS swap sent a sliced markdown doc about
448
+ // the size of the full TS doc, which speed/001 measured as a trim):
449
+ // 1. primitiveDocFormat: "markdown" (the embedded doc) or "ts" (its
450
+ // TypeScript-interface form, ~55% smaller, same information);
451
+ // 2. primitiveDocSlice: "full", or an axis-keyed allowlist from the
452
+ // classification ("axis-keyed", or "axis-keyed+contract" which adds the
453
+ // inputs the contract implies) applied to THAT format's doc;
454
+ // 3. primitiveIndex: a name-only index that replaces whatever is left
455
+ // (tool-driven docs; wins when on).
456
+ // Every cell logs `[simple] primitive-doc-bytes … mode=…` (the slice) and
457
+ // `[simple] system-prompt-bytes … doc-format=… doc-bytes=…` (what is sent),
458
+ // so a prompt-size claim is auditable from the log, not inferred.
426
459
  let systemPromptOverride: string | undefined;
427
460
  {
428
461
  const { PRIMITIVES_DOCUMENTATION } = await import("../validation/index.js");
429
462
  const fullBytes = Buffer.byteLength(PRIMITIVES_DOCUMENTATION, "utf8");
463
+ const docFormat = resolvedPolicy.context.primitiveDocFormat ?? "markdown";
464
+ const indexMode = resolvedPolicy.context.primitiveIndex ?? "off";
430
465
  const mode = resolvedPolicy.context.primitiveDocSlice ?? "full";
431
- if (mode === "axis-keyed") {
432
- const { computePrimitiveAllowlist, slicePrimitiveDocumentation } =
433
- await import("../harness/primitive-slice.js");
466
+
467
+ let doc = PRIMITIVES_DOCUMENTATION;
468
+ if (docFormat === "ts" && indexMode === "off") {
469
+ const { PRIMITIVES_DOCUMENTATION_TS } = await import("../tools/get-primitives-ts.js");
470
+ doc = PRIMITIVES_DOCUMENTATION_TS;
471
+ const tsBytes = Buffer.byteLength(doc, "utf8");
472
+ console.log(
473
+ `[simple] primitive-doc-ts-bytes=${tsBytes} baseline=${fullBytes} drop=${((1 - tsBytes / fullBytes) * 100).toFixed(1)}%`
474
+ );
475
+ }
476
+ const formatBytes = Buffer.byteLength(doc, "utf8");
477
+
478
+ if (mode === "axis-keyed" || mode === "axis-keyed+contract") {
479
+ const {
480
+ computePrimitiveAllowlist,
481
+ computeContractPrimitives,
482
+ slicePrimitiveDocumentation,
483
+ slicePrimitiveDocumentationTs,
484
+ } = await import("../harness/primitive-slice.js");
434
485
  let allowlist = computePrimitiveAllowlist(classification);
486
+ if (mode === "axis-keyed+contract") {
487
+ const fromContract = computeContractPrimitives(effectiveContract);
488
+ allowlist = [...new Set([...allowlist, ...fromContract])].sort();
489
+ }
435
490
  // Apply policy-driven excludes — some policy profiles drop
436
491
  // near-synonym layout primitives (e.g. Row/Box/Spacer).
437
492
  const excludes = resolvedPolicy.context.primitiveDocExcludes ?? [];
@@ -439,61 +494,41 @@ export async function dispatchGeneration(
439
494
  const excludeSet = new Set(excludes);
440
495
  allowlist = allowlist.filter((p) => !excludeSet.has(p));
441
496
  }
442
- const sliced = slicePrimitiveDocumentation(
443
- PRIMITIVES_DOCUMENTATION,
444
- allowlist,
445
- );
446
- const slicedBytes = Buffer.byteLength(sliced, "utf8");
497
+ doc =
498
+ docFormat === "ts" && indexMode === "off"
499
+ ? slicePrimitiveDocumentationTs(doc, allowlist)
500
+ : slicePrimitiveDocumentation(doc, allowlist);
501
+ const slicedBytes = Buffer.byteLength(doc, "utf8");
447
502
  const excludeTag = excludes.length ? ` excludes=${excludes.join(",")}` : "";
448
503
  console.log(
449
- `[simple] primitive-doc-bytes=${slicedBytes} mode=axis-keyed baseline=${fullBytes} drop=${((1 - slicedBytes / fullBytes) * 100).toFixed(1)}% allowlist=${allowlist.length}/${allowlist.join(",")}${excludeTag}`,
450
- );
451
- systemPromptOverride = harness.how.systemPrompt.replace(
452
- PRIMITIVES_DOCUMENTATION,
453
- sliced,
504
+ `[simple] primitive-doc-bytes=${slicedBytes} mode=${mode} baseline=${formatBytes} drop=${((1 - slicedBytes / formatBytes) * 100).toFixed(1)}% allowlist=${allowlist.length}/${allowlist.join(",")}${excludeTag}`
454
505
  );
455
506
  } else {
456
507
  console.log(
457
- `[simple] primitive-doc-bytes=${fullBytes} mode=full baseline=${fullBytes} drop=0.0%`,
508
+ `[simple] primitive-doc-bytes=${formatBytes} mode=full baseline=${formatBytes} drop=0.0%`
458
509
  );
459
510
  }
460
511
 
461
512
  // ── Tool-driven primitive docs ──
462
- // Layers on top of axis-keyed slicing — if slicing already trimmed
463
- // the doc, the index replaces whatever's left. When both are
464
- // active, the index wins (it is a more aggressive cut). The two are
465
- // mutually exclusive in practice via profile selection; this
466
- // ordering just defines the tiebreak.
467
- const indexMode = resolvedPolicy.context.primitiveIndex ?? "off";
513
+ // A name-only index replaces whatever is left (it is the more
514
+ // aggressive cut). The two are mutually exclusive in practice via
515
+ // profile selection; this ordering just defines the tiebreak.
468
516
  if (indexMode !== "off") {
469
517
  const { buildPrimitiveIndex } = await import("../harness/primitive-index.js");
470
- const index = buildPrimitiveIndex(PRIMITIVES_DOCUMENTATION, indexMode);
471
- const indexBytes = Buffer.byteLength(index, "utf8");
518
+ doc = buildPrimitiveIndex(PRIMITIVES_DOCUMENTATION, indexMode);
519
+ const indexBytes = Buffer.byteLength(doc, "utf8");
472
520
  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,
521
+ `[simple] primitive-index-bytes=${indexBytes} mode=${indexMode} baseline=${fullBytes} drop=${((1 - indexBytes / fullBytes) * 100).toFixed(1)}%`
478
522
  );
479
523
  }
480
524
 
481
- // ── TypeScript-interface processed doc ──
482
- // Replace the markdown-table doc with the TS-interface format
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
- );
525
+ if (doc !== PRIMITIVES_DOCUMENTATION) {
526
+ systemPromptOverride = harness.how.systemPrompt.replace(PRIMITIVES_DOCUMENTATION, doc);
496
527
  }
528
+ const sentPrompt = systemPromptOverride ?? harness.how.systemPrompt;
529
+ console.log(
530
+ `[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}`
531
+ );
497
532
 
498
533
  // ── plan→impl pipeline augmentation ──
499
534
  // Append a 2-phase flow description when planFirstTurn is active so
@@ -548,7 +583,7 @@ export async function dispatchGeneration(
548
583
  console.error(
549
584
  `[dispatch] runHarness terminated: ${runResult.reason} ` +
550
585
  `(source=${runResult.finalSource ? runResult.finalSource.length + "B" : "null"}, ` +
551
- `compiled=${runResult.finalCompiled ? runResult.finalCompiled.length + "B" : "null"})`,
586
+ `compiled=${runResult.finalCompiled ? runResult.finalCompiled.length + "B" : "null"})`
552
587
  );
553
588
  }
554
589
 
@@ -560,7 +595,11 @@ export async function dispatchGeneration(
560
595
  // `assembleGenerationResult` is async — the spread below must see the
561
596
  // RESOLVED result, never the promise (a spread promise is `{}`: every
562
597
  // consumer downstream lost `compiledCode` / `tokens` on 2026-09-09).
563
- const result: GenerationResult = await assembleGenerationResult({ session, telemetry, source: finalSource });
598
+ const result: GenerationResult = await assembleGenerationResult({
599
+ session,
600
+ telemetry,
601
+ source: finalSource,
602
+ });
564
603
  // Record the arm this generation ran under (bench reports read it per
565
604
  // cell). The canvas is the one the free prompt stated: explicit when
566
605
  // passed, else the same shell × screen derivation the prompt used.
@@ -579,11 +618,13 @@ export async function dispatchGeneration(
579
618
  */
580
619
  function withArmRecord(
581
620
  result: GenerationResult,
582
- params: Pick<GenerationDispatchParams, "designMode" | "canvas" | "shellType" | "screen">,
621
+ params: Pick<GenerationDispatchParams, "designMode" | "canvas" | "shellType" | "screen">
583
622
  ): GenerationResult {
584
623
  const recordedCanvas: CanvasClass | undefined =
585
624
  params.canvas ??
586
- (params.designMode === "free" ? canvasForRendering(params.shellType, params.screen) : undefined);
625
+ (params.designMode === "free"
626
+ ? canvasForRendering(params.shellType, params.screen)
627
+ : undefined);
587
628
  return {
588
629
  ...result,
589
630
  ...(params.designMode !== undefined ? { designMode: params.designMode } : {}),
@@ -295,6 +295,85 @@ function detectCertainDoubleWiredActions(
295
295
  * fallback ban it paints nothing.
296
296
  */
297
297
  const MANIFEST_TOKENS: ReadonlySet<string> = new Set(consumedTokenManifest);
298
+
299
+ /**
300
+ * Role names retired by ggui#987 that generated code still invents, each
301
+ * with the manifest name that replaced it (ggui#1325). The self-check names
302
+ * the successor, so the model does not spend a turn guessing another name.
303
+ */
304
+ const RETIRED_TOKEN_SUCCESSORS: ReadonlyMap<string, string> = new Map([
305
+ ['--ggui-color-surface', '--ggui-color-container'],
306
+ ['--ggui-color-onSurface', '--ggui-color-onContainer'],
307
+ ['--ggui-color-surfaceVariant', '--ggui-color-sunken'],
308
+ ['--ggui-color-onSurfaceVariant', '--ggui-color-onSunken'],
309
+ ]);
310
+
311
+ /**
312
+ * The manifest names nearest an invented one (ggui#1325): a retired role's
313
+ * successor when there is one; otherwise the names sharing the invented
314
+ * name's longest family prefix at a segment boundary
315
+ * (`--ggui-motion-duration-normal` → fast, base, slow); otherwise the names
316
+ * sharing any of its segments of four letters or more
317
+ * (`--ggui-duration-slow` → the motion durations). Empty when nothing is
318
+ * near, so the hint falls back to the manifest's general shape.
319
+ */
320
+ export function suggestManifestTokens(name: string): readonly string[] {
321
+ const successor = RETIRED_TOKEN_SUCCESSORS.get(name);
322
+ if (successor !== undefined) return [successor];
323
+ // Family prefixes at the name's own segment boundaries, longest first: for
324
+ // `--ggui-motion-duration-normal` that is `--ggui-motion-duration-`, then
325
+ // `--ggui-motion-`; an empty-suffix name such as `--ggui-color-` (the model
326
+ // stopped mid-name) is its own longest prefix, so it gets the colour family.
327
+ const segments = name.split('-').filter((segment) => segment.length > 0);
328
+ const own = name.endsWith('-') ? segments.length : segments.length - 1;
329
+ for (let n = own; n >= 2; n--) {
330
+ const prefix = `--${segments.slice(0, n).join('-')}-`;
331
+ const family = consumedTokenManifest.filter((token) => token.startsWith(prefix));
332
+ if (family.length > 0) return wordHitsFirst(name, rolesFirst(family));
333
+ }
334
+ const words = new Set(
335
+ segments
336
+ .slice(1)
337
+ .filter((segment) => segment.length >= 4)
338
+ .map((segment) => segment.toLowerCase()),
339
+ );
340
+ return rolesFirst(
341
+ consumedTokenManifest.filter((token) =>
342
+ token.split('-').some((segment) => words.has(segment.toLowerCase())),
343
+ ),
344
+ );
345
+ }
346
+
347
+ /**
348
+ * Names that carry the invented name's last word lead the list: `--ggui-color-inverse` names `inverseLink` and
349
+ * `inverseOutline` before the rest of the colour family, since those are what the model was reaching for. The word is
350
+ * the last segment when it has four letters or more; shorter or empty suffixes leave the order alone.
351
+ */
352
+ function wordHitsFirst(name: string, family: readonly string[]): readonly string[] {
353
+ const word = name.slice(name.lastIndexOf('-') + 1).toLowerCase();
354
+ if (word.length < 4) return family;
355
+ const hit = (token: string): boolean => token.slice(token.lastIndexOf('-') + 1).toLowerCase().includes(word);
356
+ return [...family.filter(hit), ...family.filter((token) => !hit(token))];
357
+ }
358
+
359
+ /** Role names (a non-numeric last segment) before ramp steps, so the taught vocabulary leads a long family. */
360
+ function rolesFirst(tokens: readonly string[]): readonly string[] {
361
+ const isRole = (token: string): boolean => !/^\d+$/.test(token.slice(token.lastIndexOf('-') + 1));
362
+ return [...tokens.filter(isRole), ...tokens.filter((token) => !isRole(token))];
363
+ }
364
+
365
+ /** The sentence that names the real token first (ggui#1325); empty when nothing in the manifest is near. */
366
+ function offManifestHint(name: string): string {
367
+ const successor = RETIRED_TOKEN_SUCCESSORS.get(name);
368
+ if (successor !== undefined) {
369
+ return `"${name}" was retired (ggui#987); the current name is var(${successor}). `;
370
+ }
371
+ const near = suggestManifestTokens(name);
372
+ if (near.length === 0) return '';
373
+ const shown = near.slice(0, 8).map((token) => `var(${token})`).join(', ');
374
+ const more = near.length > 8 ? ` (and ${near.length - 8} more in that family)` : '';
375
+ return `Nearest names in the manifest: ${shown}${more}. `;
376
+ }
298
377
  const TOKEN_REF_RE = /var\(\s*(--ggui-[a-zA-Z0-9-]+)/g;
299
378
 
300
379
  /** Map wire kind to the matching `@ggui-ai/wire` hook name. */
@@ -790,7 +869,7 @@ export async function runTier0Checks(
790
869
  subcategory: 'off-manifest-token',
791
870
  severity: 'critical',
792
871
  description: `Token "${name}" is not in the design system's token manifest — no theme defines it, so it renders unset.`,
793
- fix: `Choose a token that exists: the closed vocabulary is the manifest the design-system docs list (colors: var(--ggui-color-primary-500), var(--ggui-color-onContainer), …; spacing: var(--ggui-spacing-4); type: var(--ggui-font-size-sm)). Never invent a name and never add a literal fallback.`,
872
+ fix: `${offManifestHint(name)}Choose a token that exists: the closed vocabulary is the manifest the design-system docs list (colors: var(--ggui-color-primary-500), var(--ggui-color-onContainer), …; spacing: var(--ggui-spacing-4); type: var(--ggui-font-size-sm)). Never invent a name and never add a literal fallback.`,
794
873
  line: lineNum,
795
874
  });
796
875
  }