@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.
Files changed (123) hide show
  1. package/dist/adapter-Cg_rkpGq.d.ts +201 -0
  2. package/dist/adapters/generation-dispatch.d.ts +58 -15
  3. package/dist/adapters/generation-dispatch.js +1418 -180
  4. package/dist/adapters/generation-dispatch.js.map +1 -1
  5. package/dist/adapters/index.js +3 -1
  6. package/dist/adapters/index.js.map +1 -1
  7. package/dist/advanced/index.d.ts +12 -8
  8. package/dist/advanced/index.js +1467 -239
  9. package/dist/advanced/index.js.map +1 -1
  10. package/dist/{axes-CzLEMDeB.d.ts → axes-CKVCzR_x.d.ts} +1 -1
  11. package/dist/bank-CT4eEkta.d.ts +118 -0
  12. package/dist/blueprint-validator.js +153 -59
  13. package/dist/blueprint-validator.js.map +1 -1
  14. package/dist/boilerplate.js +24 -12
  15. package/dist/boilerplate.js.map +1 -1
  16. package/dist/check/index.d.ts +12 -4
  17. package/dist/check/index.js +94 -2
  18. package/dist/check/index.js.map +1 -1
  19. package/dist/classifier/index.d.ts +2 -2
  20. package/dist/compose.d.ts +2 -2
  21. package/dist/{create-ui-generator-D0kbWwln.d.ts → create-ui-generator-DFnnArrv.d.ts} +43 -11
  22. package/dist/{dispatch-wxaIMwZt.d.ts → dispatch-CStl9eAw.d.ts} +5 -3
  23. package/dist/evaluation/axis-checks/index.d.ts +6 -5
  24. package/dist/evaluation/axis-checks/index.js +90 -2
  25. package/dist/evaluation/axis-checks/index.js.map +1 -1
  26. package/dist/evaluation/axis-checks/registry.d.ts +4 -3
  27. package/dist/evaluation/axis-checks/registry.js +88 -1
  28. package/dist/evaluation/axis-checks/registry.js.map +1 -1
  29. package/dist/evaluation/index.d.ts +301 -23
  30. package/dist/evaluation/index.js +746 -30
  31. package/dist/evaluation/index.js.map +1 -1
  32. package/dist/evaluation/types.d.ts +9 -1
  33. package/dist/evaluation/types.js.map +1 -1
  34. package/dist/fragments/index.d.ts +3 -3
  35. package/dist/harness/check/runtime-render/index.d.ts +30 -11
  36. package/dist/harness/check/runtime-render/index.js +203 -60
  37. package/dist/harness/check/runtime-render/index.js.map +1 -1
  38. package/dist/harness/check/runtime-render/render-check-worker.d.ts +8 -1
  39. package/dist/harness/check/runtime-render/render-check-worker.js +72 -32
  40. package/dist/harness/check/runtime-render/render-check-worker.js.map +1 -1
  41. package/dist/harness/index.d.ts +5 -4
  42. package/dist/harness/index.js +33 -15
  43. package/dist/harness/index.js.map +1 -1
  44. package/dist/harness/prompts.js.map +1 -1
  45. package/dist/harness/result-types.d.ts +4 -3
  46. package/dist/harness/runtime.d.ts +11 -4
  47. package/dist/harness/runtime.js +26 -14
  48. package/dist/harness/runtime.js.map +1 -1
  49. package/dist/harness/types-public.d.ts +13 -4
  50. package/dist/harness/types-public.js +33 -15
  51. package/dist/harness/types-public.js.map +1 -1
  52. package/dist/harness/validator-trace-sink.d.ts +4 -3
  53. package/dist/hash.d.ts +1 -1
  54. package/dist/index.d.ts +59 -8
  55. package/dist/index.js +1476 -241
  56. package/dist/index.js.map +1 -1
  57. package/dist/{llm-router-DwwUK1bz.d.ts → llm-router-nfO7wPec.d.ts} +3 -1
  58. package/dist/patch.d.ts +4 -3
  59. package/dist/policy.d.ts +8 -5
  60. package/dist/policy.js.map +1 -1
  61. package/dist/tools/get-primitives-ts.d.ts +1 -1
  62. package/dist/tools/get-primitives-ts.js +1 -1
  63. package/dist/tools/get-primitives-ts.js.map +1 -1
  64. package/dist/tools/render-check-worker.d.ts +7 -1
  65. package/dist/tools/render-check-worker.js +55 -23
  66. package/dist/tools/render-check-worker.js.map +1 -1
  67. package/dist/{types-BOvHNG7K.d.ts → types-Dh1s4K9j.d.ts} +1 -1
  68. package/dist/{types-public-Ow-tNtFE.d.ts → types-public-BJTLpzWn.d.ts} +208 -15
  69. package/dist/validation/index.d.ts +1 -1
  70. package/dist/validation/index.js +1 -1
  71. package/dist/validation/index.js.map +1 -1
  72. package/dist/workflows.d.ts +4 -3
  73. package/package.json +12 -12
  74. package/src/adapters/generation-dispatch.ts +173 -98
  75. package/src/adapters/tools.ts +2 -1
  76. package/src/boilerplate/free-design-prompt.ts +1 -1
  77. package/src/boilerplate/generate.ts +26 -1
  78. package/src/boilerplate/hard-sections.ts +13 -13
  79. package/src/boilerplate/system-prompt.ts +1 -1
  80. package/src/check/index.ts +1 -0
  81. package/src/check/run-tier0.ts +80 -1
  82. package/src/check/sample-props.ts +63 -0
  83. package/src/coding-agent/tools.ts +4 -1
  84. package/src/create-harness.ts +3 -3
  85. package/src/create-ui-generator.ts +204 -95
  86. package/src/evaluation/__fixtures__/png.ts +100 -0
  87. package/src/evaluation/axis-checks/checks/state-merge.ts +1 -1
  88. package/src/evaluation/axis-checks/checks/universal.ts +111 -0
  89. package/src/evaluation/axis-checks/dispatch.ts +4 -1
  90. package/src/evaluation/criteria/bank.ts +85 -0
  91. package/src/evaluation/criteria/context.ts +52 -0
  92. package/src/evaluation/criteria/resolve.ts +159 -0
  93. package/src/evaluation/criteria/select.ts +79 -0
  94. package/src/evaluation/index.ts +23 -0
  95. package/src/evaluation/ink-extent.ts +200 -0
  96. package/src/evaluation/theme-css.ts +12 -5
  97. package/src/evaluation/types-public.ts +200 -11
  98. package/src/evaluation/types.ts +9 -0
  99. package/src/evaluation/visual-evaluator.ts +636 -30
  100. package/src/harness/check/runtime-render/adapter.ts +221 -75
  101. package/src/harness/check/runtime-render/index.ts +75 -2
  102. package/src/harness/check/runtime-render/render-check-host.ts +82 -19
  103. package/src/harness/check/runtime-render/render-check-worker.ts +83 -29
  104. package/src/harness/check/runtime-render/render-check.ts +154 -117
  105. package/src/harness/coding/generate-task-runner.ts +60 -4
  106. package/src/harness/coding/init-session.ts +13 -0
  107. package/src/harness/coding/run-coding-turn.ts +7 -3
  108. package/src/harness/coding/run-eval-round.ts +257 -15
  109. package/src/harness/policy.ts +11 -0
  110. package/src/harness/primitive-slice.ts +104 -0
  111. package/src/harness/runtime.ts +5 -0
  112. package/src/harness/types-public.ts +9 -1
  113. package/src/harness-tool-names.ts +38 -0
  114. package/src/index.ts +16 -0
  115. package/src/policy.ts +7 -4
  116. package/src/run-check.ts +2 -0
  117. package/src/tools/get-primitives-ts.ts +1 -1
  118. package/src/tools/get-wire.ts +1 -1
  119. package/src/tools/render-check-worker.ts +74 -23
  120. package/src/tools/render-check.ts +0 -60
  121. package/src/tools.ts +11 -1
  122. package/src/validation/primitives.ts +1 -1
  123. package/dist/render-check-Blwj8gB6.d.ts +0 -105
@@ -0,0 +1,200 @@
1
+ /**
2
+ * The ink extent of a judge screenshot (ggui#1120): how far down the frame anything was painted, read
3
+ * from the pixels the judge already holds. `contentHeight` is the document's scroll height, floored at
4
+ * the viewport by definition — it can see overflow and never under-fill, and never a blank.
5
+ *
6
+ * The reading: the region's DOMINANT colour (its most common pixel) is the ground; a pixel is ink when
7
+ * any channel differs from it by more than {@link INK_TOLERANCE}; `lastInkRow` is the last region row
8
+ * with ink and `ratio` is `(lastInkRow + 1) / region.height`. Dominant, not the corners: an opening
9
+ * hero band makes the top corners content, and a host panel's rounded corners expose the scrim behind
10
+ * it (measured on the ggui#1083 judge frames — corner sampling read a card that fills as 1.0 and a
11
+ * panelled page as all ink).
12
+ *
13
+ * Only the ZERO is judged (`canvas-blank`, the judge's verdict): a region with no ink is one flat
14
+ * colour — the tree mounted and painted nothing, the mount-path class no compile-time check can reach
15
+ * (ggui#1104 / #1119). Every other value is reported, never scored: the corpus does not support a
16
+ * dead-space threshold (#1120).
17
+ *
18
+ * A PNG reader of exactly the judge's own captures — 8-bit RGB / RGBA, non-interlaced, `node:zlib`
19
+ * for the IDAT stream and the five scanline filters by hand — so a published package takes no image
20
+ * dependency. Any other PNG reads as unmeasurable (a reason), never as blank.
21
+ */
22
+ import { inflateSync } from 'node:zlib';
23
+
24
+ /** A channel difference at or below this is the same colour (anti-aliasing and compositing noise). */
25
+ export const INK_TOLERANCE = 8;
26
+
27
+ export interface PixelRegion {
28
+ readonly x: number;
29
+ readonly y: number;
30
+ readonly width: number;
31
+ readonly height: number;
32
+ }
33
+
34
+ export interface InkExtent {
35
+ /** `(lastInkRow + 1) / region.height`, rounded to 3 places; `0` when nothing differs from the ground. */
36
+ readonly ratio: number;
37
+ /** The last region row (0-based from the region's top) carrying ink; `null` when there is none — the blank. */
38
+ readonly lastInkRow: number | null;
39
+ /** The region read, in image px. */
40
+ readonly region: PixelRegion;
41
+ /** The dominant colour the ink was read against, `[r, g, b]`. */
42
+ readonly ground: readonly [number, number, number];
43
+ }
44
+
45
+ /** Why a capture could not be read — reported, never mistaken for a blank. */
46
+ export interface InkUnreadable {
47
+ readonly reason: string;
48
+ }
49
+
50
+ export interface DecodedImage {
51
+ readonly width: number;
52
+ readonly height: number;
53
+ readonly channels: 3 | 4;
54
+ /** Row-major, `channels` bytes per pixel, scanline filters undone. */
55
+ readonly pixels: Uint8Array;
56
+ }
57
+
58
+ const PNG_SIGNATURE = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a] as const;
59
+
60
+ /** The PNG subset the judge's browser writes: 8-bit RGB (colour type 2) or RGBA (6), not interlaced. */
61
+ export function decodePng(png: Uint8Array): DecodedImage | InkUnreadable {
62
+ const buf = Buffer.from(png.buffer, png.byteOffset, png.byteLength);
63
+ if (buf.length < 8 + 25) return { reason: `not a PNG: ${buf.length} bytes` };
64
+ for (let i = 0; i < PNG_SIGNATURE.length; i++) {
65
+ if (buf[i] !== PNG_SIGNATURE[i]) return { reason: 'not a PNG: signature mismatch' };
66
+ }
67
+ let width = 0;
68
+ let height = 0;
69
+ let channels: 3 | 4 | null = null;
70
+ const idat: Buffer[] = [];
71
+ let offset = 8;
72
+ while (offset + 8 <= buf.length) {
73
+ const length = buf.readUInt32BE(offset);
74
+ const type = buf.toString('latin1', offset + 4, offset + 8);
75
+ const start = offset + 8;
76
+ if (start + length + 4 > buf.length) return { reason: `truncated PNG: ${type} chunk runs past the end` };
77
+ const data = buf.subarray(start, start + length);
78
+ if (type === 'IHDR') {
79
+ if (length !== 13) return { reason: `malformed IHDR: ${length} bytes` };
80
+ width = data.readUInt32BE(0);
81
+ height = data.readUInt32BE(4);
82
+ const bitDepth = data[8];
83
+ const colorType = data[9];
84
+ const interlace = data[12];
85
+ if (bitDepth !== 8) return { reason: `unsupported PNG: bit depth ${bitDepth}` };
86
+ if (interlace !== 0) return { reason: 'unsupported PNG: interlaced' };
87
+ if (colorType === 2) channels = 3;
88
+ else if (colorType === 6) channels = 4;
89
+ else return { reason: `unsupported PNG: colour type ${colorType}` };
90
+ } else if (type === 'IDAT') {
91
+ idat.push(data);
92
+ } else if (type === 'IEND') {
93
+ break;
94
+ }
95
+ offset = start + length + 4;
96
+ }
97
+ if (channels === null) return { reason: 'malformed PNG: no IHDR' };
98
+ if (width === 0 || height === 0) return { reason: `empty PNG: ${width}×${height}` };
99
+ if (idat.length === 0) return { reason: 'malformed PNG: no IDAT' };
100
+ let raw: Buffer;
101
+ try {
102
+ raw = inflateSync(Buffer.concat(idat));
103
+ } catch (error) {
104
+ return { reason: `corrupt IDAT stream: ${error instanceof Error ? error.message : String(error)}` };
105
+ }
106
+ const stride = width * channels;
107
+ if (raw.length !== (stride + 1) * height) {
108
+ return { reason: `malformed PNG: ${raw.length} raw bytes for ${width}×${height}×${channels}` };
109
+ }
110
+ const pixels = new Uint8Array(stride * height);
111
+ const bpp = channels;
112
+ for (let y = 0; y < height; y++) {
113
+ const filter = raw[y * (stride + 1)]!;
114
+ const inRow = y * (stride + 1) + 1;
115
+ const outRow = y * stride;
116
+ const prevRow = outRow - stride;
117
+ for (let i = 0; i < stride; i++) {
118
+ const x = raw[inRow + i]!;
119
+ const a = i >= bpp ? pixels[outRow + i - bpp]! : 0;
120
+ const b = y > 0 ? pixels[prevRow + i]! : 0;
121
+ const c = y > 0 && i >= bpp ? pixels[prevRow + i - bpp]! : 0;
122
+ let value: number;
123
+ switch (filter) {
124
+ case 0:
125
+ value = x;
126
+ break;
127
+ case 1:
128
+ value = x + a;
129
+ break;
130
+ case 2:
131
+ value = x + b;
132
+ break;
133
+ case 3:
134
+ value = x + ((a + b) >> 1);
135
+ break;
136
+ case 4: {
137
+ const p = a + b - c;
138
+ const pa = Math.abs(p - a);
139
+ const pb = Math.abs(p - b);
140
+ const pc = Math.abs(p - c);
141
+ value = x + (pa <= pb && pa <= pc ? a : pb <= pc ? b : c);
142
+ break;
143
+ }
144
+ default:
145
+ return { reason: `malformed PNG: scanline filter ${filter} at row ${y}` };
146
+ }
147
+ pixels[outRow + i] = value & 0xff;
148
+ }
149
+ }
150
+ return { width, height, channels, pixels };
151
+ }
152
+
153
+ /**
154
+ * The ink extent of a capture, read `insetPx` in from every edge — the judge's inset skips a host
155
+ * panel's gap, rounded corners and hairline ({@link JUDGE_INK_INSET_PX}); `0` reads the whole image.
156
+ */
157
+ export function readInkExtent(png: Uint8Array, insetPx = 0): InkExtent | InkUnreadable {
158
+ const image = decodePng(png);
159
+ if ('reason' in image) return image;
160
+ const region: PixelRegion = { x: insetPx, y: insetPx, width: image.width - 2 * insetPx, height: image.height - 2 * insetPx };
161
+ if (insetPx < 0 || region.width <= 0 || region.height <= 0) {
162
+ return { reason: `an inset of ${insetPx}px leaves nothing of a ${image.width}×${image.height} image` };
163
+ }
164
+ const { channels, pixels, width } = image;
165
+ // The ground: the region's most common colour.
166
+ const counts = new Map<number, number>();
167
+ let ground = 0;
168
+ let groundCount = 0;
169
+ for (let y = region.y; y < region.y + region.height; y++) {
170
+ for (let x = region.x; x < region.x + region.width; x++) {
171
+ const i = (y * width + x) * channels;
172
+ const key = (pixels[i]! << 16) | (pixels[i + 1]! << 8) | pixels[i + 2]!;
173
+ const n = (counts.get(key) ?? 0) + 1;
174
+ counts.set(key, n);
175
+ if (n > groundCount) {
176
+ groundCount = n;
177
+ ground = key;
178
+ }
179
+ }
180
+ }
181
+ const gr = (ground >> 16) & 0xff;
182
+ const gg = (ground >> 8) & 0xff;
183
+ const gb = ground & 0xff;
184
+ let lastInkRow: number | null = null;
185
+ for (let y = region.y; y < region.y + region.height; y++) {
186
+ for (let x = region.x; x < region.x + region.width; x++) {
187
+ const i = (y * width + x) * channels;
188
+ if (
189
+ Math.abs(pixels[i]! - gr) > INK_TOLERANCE ||
190
+ Math.abs(pixels[i + 1]! - gg) > INK_TOLERANCE ||
191
+ Math.abs(pixels[i + 2]! - gb) > INK_TOLERANCE
192
+ ) {
193
+ lastInkRow = y - region.y;
194
+ break;
195
+ }
196
+ }
197
+ }
198
+ const ratio = lastInkRow === null ? 0 : Math.round(((lastInkRow + 1) / region.height) * 1000) / 1000;
199
+ return { ratio, lastInkRow, region, ground: [gr, gg, gb] };
200
+ }
@@ -5,10 +5,17 @@ import { composeThemeCss, getCssTokens } from '@ggui-ai/design/rendering';
5
5
  * The stylesheet a visual judge renders under for an app: the app's theme
6
6
  * composed the way the runtime composes it (`composeThemeCss`, layer `page`
7
7
  * — one composition, never a second), so a judged canvas paints the tokens a
8
- * visitor's page paints. Absent theme ⇒ the design's default tokens,
9
- * byte-for-byte what the judge used before.
8
+ * visitor's page paints. `themeId` is the registered ladder the app's theme
9
+ * sits on (the runtime's base layer); an unknown id composes the default
10
+ * ladder. Neither given ⇒ the design's default tokens, byte-for-byte what the
11
+ * judge used before.
10
12
  */
11
- export function cssTokensForAppTheme(theme: AppTheme | undefined, mode: 'light' | 'dark' = 'light'): string {
12
- if (theme === undefined) return getCssTokens(mode);
13
- return composeThemeCss({ layer: 'page', mode: theme.mode ?? mode, appTheme: theme });
13
+ export function cssTokensForAppTheme(theme: AppTheme | undefined, mode: 'light' | 'dark' = 'light', themeId?: string): string {
14
+ if (theme === undefined && themeId === undefined) return getCssTokens(mode);
15
+ return composeThemeCss({
16
+ layer: 'page',
17
+ mode: theme?.mode ?? mode,
18
+ ...(themeId !== undefined ? { themeId } : {}),
19
+ ...(theme !== undefined ? { appTheme: theme } : {}),
20
+ });
14
21
  }
@@ -20,6 +20,7 @@
20
20
 
21
21
  export type Priority = "P0" | "P1" | "P2";
22
22
 
23
+ import type { GenerationRuntimeProbeOutcome, GenerationRuntimeProbeStatus } from "@ggui-ai/mcp-server-core";
23
24
  import type { CanvasClass, DesignMode } from "../design-mode.js";
24
25
 
25
26
  // ─── Issue shape ───────────────────────────────────────────────────────────
@@ -63,8 +64,10 @@ export interface EvalIssue {
63
64
  }
64
65
 
65
66
  /**
66
- * Runtime-render probe execution status — three-valued so "the probe
67
- * never ran" is never conflated with "the probe ran clean".
67
+ * Runtime-render probe execution status — four-valued so "the probe
68
+ * never ran" is never conflated with "the probe ran clean". ONE union: the
69
+ * same values a generation's metadata reports (`GenerationRuntimeProbeStatus`
70
+ * in `@ggui-ai/mcp-server-core`), declared there and named here.
68
71
  *
69
72
  * - `ran` — probe executed; its findings (possibly none) are
70
73
  * in the issue stream under `runtime:*` subcategories.
@@ -83,7 +86,7 @@ export interface EvalIssue {
83
86
  * `hostLoad` say how long it ran and how busy the
84
87
  * host was, so a reader can tell the two cases apart.
85
88
  */
86
- export type RuntimeProbeStatus = "ran" | "infra-skipped" | "not-applicable" | "timed-out";
89
+ export type RuntimeProbeStatus = GenerationRuntimeProbeStatus;
87
90
 
88
91
  /**
89
92
  * The host's 1-minute load average at a probe's start and end, beside the
@@ -98,21 +101,76 @@ export interface ProbeHostLoad {
98
101
  }
99
102
 
100
103
  /**
101
- * Probe execution meta stamped onto an `EvalResult` at the exit-decision
102
- * points that invoke the probe. Absence of this field means the eval
103
- * path never reached a probe invocation at all — consumers treat that
104
- * the same as not-run, never as a pass.
104
+ * The engine's own fields on a probe's meta, beside the outcome a
105
+ * generation's metadata reports: why a probe did not finish, its render
106
+ * time, the host's load around it. Present on every arm of
107
+ * {@link RuntimeProbeMeta}; each is absent when the probe had nothing to
108
+ * say about it.
105
109
  */
106
- export interface RuntimeProbeMeta {
107
- readonly status: RuntimeProbeStatus;
110
+ export interface RuntimeProbeMetaDetail {
108
111
  /** Populated for `infra-skipped` / `not-applicable` / `timed-out` — why the probe didn't finish. */
109
112
  readonly reason?: string;
110
- /** `timed-out` only: how long the probe ran before it was stopped. */
111
- readonly elapsedMs?: number;
113
+ /** `ran` only: the check's own render time, inside `elapsedMs`. */
114
+ readonly renderMs?: number;
112
115
  /** Host load around the probe, when it ran isolated (absent for an in-process probe). */
113
116
  readonly hostLoad?: ProbeHostLoad;
117
+ /** Time the check waited for a probe slot before it started, ms; present only when > 0 (ggui#1380 C2b). */
118
+ readonly queuedMs?: number;
114
119
  }
115
120
 
121
+ /**
122
+ * Probe execution meta stamped onto an `EvalResult` at the exit-decision
123
+ * points that invoke the probe. Absence of this field means the eval
124
+ * path never reached a probe invocation at all — consumers treat that
125
+ * the same as not-run, never as a pass.
126
+ *
127
+ * ONE union with the generation metadata's probe outcome
128
+ * (`GenerationRuntimeProbeOutcome` in `@ggui-ai/mcp-server-core`, declared
129
+ * there and carried here with {@link RuntimeProbeMetaDetail} on every arm),
130
+ * so the round's stamp and the metadata's record cannot say different
131
+ * things (ggui#1380):
132
+ *
133
+ * - `ran` + `verdict: 'pass'` — the probe executed and no check failed;
134
+ * - `ran` + `verdict: 'fail'` — ANY check failed; `failChecks` lists the
135
+ * distinct check kinds that did (`RenderCheckKind`, the engine's name
136
+ * for the same union), never empty, in the order `RENDER_CHECK_KINDS`
137
+ * declares them, parsed from the probe's own
138
+ * `runtime:<check>[:<subject>]` issues;
139
+ * - `timed-out` / `infra-skipped` / `not-applicable` — no verdict, and the
140
+ * type carries none. `elapsedMs` (on every status that reached the
141
+ * check) and `hostLoad` say how long it ran and how busy the host was.
142
+ *
143
+ * The verdict and the repair trigger are two different questions. The
144
+ * verdict is `fail` on any failing check. The one repair turn a probe-only
145
+ * round buys fires on a `render-no-throw` fail the engine recognises
146
+ * (`isRecoverableRenderCrash`) and on nothing else: on the PROBE-ONLY round a
147
+ * `fail` verdict whose checks are `prop-sensitivity`, `action-wiring` or any
148
+ * other kind is recorded here and never fed back. The same meta is stamped on
149
+ * the EVALUATION lane, where a `prop-sensitivity` fail buys that lane's own
150
+ * contract-feedback round (ggui#1261, `FEEDBACK_PROBE_CHECKS`) — a different
151
+ * turn under a different rule, not the repair turn.
152
+ */
153
+ export type RuntimeProbeMeta = GenerationRuntimeProbeOutcome & RuntimeProbeMetaDetail;
154
+
155
+ /**
156
+ * The one repair turn a probe-only round buys (ggui#1380). A serving
157
+ * deployment that wires the runtime probe and configures no evaluator runs
158
+ * the probe once after the coding turns; a render crash the engine
159
+ * recognises buys exactly one repair turn, then one re-probe. This records
160
+ * what came of it:
161
+ * - `compiled: false` — the repair turn's code did not pass self-check, so
162
+ * no re-probe ran (there is no `after`) and the pre-repair card is the
163
+ * one served;
164
+ * - `compiled: true` — the re-probe ran and `after` is its whole meta:
165
+ * its status, its verdict when it ran, and the checks that still fail.
166
+ * Whether the crash class the turn was bought for is still there is
167
+ * `after.verdict === 'fail' && after.failChecks.includes('render-no-throw')`.
168
+ * The two arms are exclusive by type.
169
+ */
170
+ export type RuntimeProbeRepair =
171
+ | { readonly attempted: true; readonly compiled: false; readonly trigger?: never }
172
+ | { readonly attempted: true; readonly compiled: true; readonly trigger: RuntimeProbeMeta };
173
+
116
174
  /**
117
175
  * Per-criterion execution status — three-valued so "the criterion
118
176
  * silently produced no verdict" is never conflated with "the criterion
@@ -226,6 +284,13 @@ export interface EvalResult {
226
284
  * probe runner; absent on eval paths that never invoke the probe.
227
285
  */
228
286
  runtimeProbe?: RuntimeProbeMeta;
287
+ /**
288
+ * The probe-only round's repair record (ggui#1380) — present only when a
289
+ * recoverable render crash bought the one repair turn. See
290
+ * {@link RuntimeProbeRepair}. `runtimeProbe` beside it is the re-probe's
291
+ * verdict when the repair compiled, else the pre-repair probe's.
292
+ */
293
+ runtimeProbeRepair?: RuntimeProbeRepair;
229
294
  /**
230
295
  * The contract-feedback round, when one fired this generation (ggui#1261):
231
296
  * which exit-probe findings bought it and the source as it stood when the
@@ -271,6 +336,108 @@ export interface CanvasJudgeRecord {
271
336
  readonly notes: string[];
272
337
  }
273
338
 
339
+ // ─── Visual criteria (ggui#1436) ──────────────────────────────────────────
340
+ // The judge's typed criteria block per canvas — report-only beside the score
341
+ // until a later word flips a gate to it. Every field below is optional on the
342
+ // stamps so yesterday's `eval.json` and an old judge answer still parse (N−1).
343
+ import type { FetchShape, LayoutShape, RenderShape, RiskTier, StateShape, WriteShape } from "../classifier/axes.js";
344
+
345
+ export type CriteriaSeverity = "must" | "should";
346
+ export type CriteriaVerdict = "pass" | "fail" | "n/a";
347
+ /** The method that produces a verdict: an instrument's measurement, the vision judge off the frame, or a person's eye. */
348
+ export type CriteriaMethod = "instrument" | "judge" | "human";
349
+ /** The method's readiness, as the bank's author wrote it — the only source of an `n/a` that is not the judge's own. */
350
+ export type CriteriaStatus = "live" | "reference" | "planned" | "manual";
351
+ /**
352
+ * How a criterion entered the selection: bank-level (`static`, an empty scope) or by the selector's
353
+ * rules for this card (`context`). A model PROPOSING a criterion at judge time is RESERVED and not a
354
+ * member until a consumer exists — it would be `should`-only, recorded verbatim, never a gate.
355
+ */
356
+ export type CriteriaSource = "static" | "context";
357
+ export type CriteriaChroma = "achromatic" | "chromatic" | "unknown";
358
+ /** The classifier's shape axes, as hashed into the set id. */
359
+ export interface CriteriaAxes {
360
+ render: RenderShape;
361
+ state: StateShape;
362
+ writes: WriteShape;
363
+ fetch: FetchShape;
364
+ layout: LayoutShape;
365
+ }
366
+ /** The selector's inputs, stored with the verdict so a reader recomputes `criteriaSetId` from the record alone. */
367
+ export interface CriteriaContext {
368
+ canvas: CanvasClass;
369
+ hasActions: boolean;
370
+ riskTier: RiskTier;
371
+ axes: CriteriaAxes;
372
+ chroma: CriteriaChroma;
373
+ profilePresent: boolean;
374
+ /** The harness's shell as named; `unknown` when the round carries none. */
375
+ shell: string;
376
+ /** The caller's item kind (a bootstrap mint's `hello`); hashed when present. */
377
+ kind?: string;
378
+ /** The styling preset in force (the profile's aesthetic id); a recipe is selected only under its preset. */
379
+ preset?: string;
380
+ }
381
+ /** What was asked and why — recorded BEFORE the judge runs. Omission is the selector's decision. */
382
+ export interface CriteriaSelection {
383
+ id: string;
384
+ source: CriteriaSource;
385
+ reason: string;
386
+ }
387
+ /** One criterion's verdict; `n/a` is the judge's answer ("cannot read this off the frame"), an unanswered id, a tie, or a method whose `status` is not live. */
388
+ export interface CriterionVerdict {
389
+ id: string;
390
+ severity: CriteriaSeverity;
391
+ method: CriteriaMethod;
392
+ status: CriteriaStatus;
393
+ source: CriteriaSource;
394
+ verdict: CriteriaVerdict;
395
+ evidence: string;
396
+ }
397
+ export interface CriteriaBlock {
398
+ /** sha256(bank version | selector version | canonical context), 16 hex — the block's own digest. */
399
+ criteriaSetId: string;
400
+ bankVersion: string;
401
+ selectorVersion: string;
402
+ context: CriteriaContext;
403
+ selection: CriteriaSelection[];
404
+ verdicts: CriterionVerdict[];
405
+ }
406
+ /** The roll-up across canvases; a `must` at `n/a` is listed, never counted as a pass. */
407
+ export interface CriteriaRollup {
408
+ mustFailed: string[];
409
+ shouldFailed: string[];
410
+ mustNa: string[];
411
+ }
412
+
413
+ /** ggui#1492 — a host"s frame round the inline card, as data. Colours are `#rrggbb`; the ring"s `alpha` is 0..1. */
414
+ export interface HostInlineFrame {
415
+ /** The frame"s surface — what shows behind a transparent card, and under the floor"s residual. */
416
+ readonly surface: string;
417
+ /** The room the frame sits in (the page ground round it). */
418
+ readonly ground: string;
419
+ readonly ring: { readonly widthPx: number; readonly color: string; readonly alpha: number };
420
+ readonly radiusPx: number;
421
+ /** The host"s minimum frame height: a card shorter than it sits in a frame this tall, and the visitor sees the rest as void. */
422
+ readonly minHeightPx?: number;
423
+ }
424
+ /** ggui#1492 — how a host presents one canvas: the label the judge is told, the page ground, and (the inline card only) its frame. */
425
+ export interface CanvasHostPresentation {
426
+ readonly label: string;
427
+ /** The page ground under a fill canvas"s panel; the inline card"s ground lives on its {@link HostInlineFrame}. */
428
+ readonly ground?: string;
429
+ readonly frame?: HostInlineFrame;
430
+ }
431
+ /** ggui#1492 — why the judge did not draw a canvas's host presentation. The codes are the lane reader's own. */
432
+ export type HostPresentationIgnoredReason = "label_missing" | "color_not_hex" | "frame_off_inline" | "frame_malformed" | "ground_on_inline";
433
+ /**
434
+ * ggui#1492 — what the judge DID with a canvas's host presentation, read from the judge itself: `applied` carries
435
+ * the presentation it drew, `ignored` the reason it drew none. Absent when the caller supplied no presentation.
436
+ */
437
+ export type CanvasPresentationOutcome =
438
+ | { readonly status: "applied"; readonly applied: CanvasHostPresentation }
439
+ | { readonly status: "ignored"; readonly reason: HostPresentationIgnoredReason };
440
+
274
441
  export interface CanvasVisualSummary {
275
442
  canvas: CanvasClass;
276
443
  viewport: { width: number; height: number };
@@ -282,8 +449,19 @@ export interface CanvasVisualSummary {
282
449
  contentHeight: number | null;
283
450
  /** `contentHeight > viewport.height` — measured on every canvas, judged per `canvasFitPolicy`. */
284
451
  overflow: boolean;
452
+ /**
453
+ * ggui#1120 — how far down the capture anything was painted: the last inked row over the measured
454
+ * region's height (inside a drawn host panel's chrome on md/lg/xl), read from the screenshot's
455
+ * pixels against their dominant colour. `0` is the blank the judge fails (`canvas-blank`); every
456
+ * other value is reported, never scored. `null` when the capture could not be read.
457
+ */
458
+ inkRatio: number | null;
285
459
  /** How `score` was reached — always present (ggui#1072). */
286
460
  judge: CanvasJudgeRecord;
461
+ /** ggui#1436 — the typed criteria block, when a bank was configured; report-only. */
462
+ criteria?: CriteriaBlock;
463
+ /** ggui#1492 — what the judge did with this canvas's host presentation, when one was supplied. */
464
+ presentation?: CanvasPresentationOutcome;
287
465
  /**
288
466
  * How the judge composed the mount (ggui#1100): `'fill'` on every
289
467
  * fullscreen canvas — the served runtime's fit, the root stretched to
@@ -320,6 +498,8 @@ export interface VisualEvalSummary {
320
498
  design?: { readonly src: string; readonly srcSha256: string };
321
499
  /** The mode the judge's tokens were composed in, when the caller said (ggui#1076). */
322
500
  themeMode?: 'light' | 'dark';
501
+ /** ggui#1436 — the criteria roll-up across canvases, when any canvas carries a block; report-only. */
502
+ criteria?: CriteriaRollup;
323
503
  }
324
504
 
325
505
  // ─── Quality mode ──────────────────────────────────────────────────────────
@@ -345,6 +525,7 @@ export const DEFAULT_QUALITY_CONFIG: QualityConfig = {
345
525
  // `./axis-checks/`.
346
526
 
347
527
  import type { DataContract } from "@ggui-ai/protocol";
528
+ import type { JsonObject } from "@ggui-ai/protocol";
348
529
  import type { AxisVector, Classification } from "../classifier/axes.js";
349
530
  import type { AxisKey } from "../fragments/index.js";
350
531
 
@@ -371,6 +552,14 @@ export interface AxisCheckInput {
371
552
  * default (a silent `lg` would be the same guess with a nicer face).
372
553
  */
373
554
  canvas?: CanvasClass;
555
+ /**
556
+ * The props the probe and the judge RENDERED the source with (ggui#1285) — a bootstrap order's
557
+ * `sampleProps`, the bench commit's fixture. They are judging data, not the request: a literal in
558
+ * the source that equals one of these values, and that the request never stated, is baked copy a
559
+ * visitor sees whenever the agent leaves the prop out. Populated on the served loop and at
560
+ * auto-commit; ABSENT where the caller has none — the check stands down, it never guesses.
561
+ */
562
+ fixtureProps?: JsonObject;
374
563
  }
375
564
 
376
565
  /**
@@ -33,6 +33,13 @@ export interface EvaluationIssue {
33
33
  /**
34
34
  * Result of a single evaluation round.
35
35
  */
36
+ /** One judge answer to a criterion (ggui#1436), as parsed — before the K-majority. */
37
+ export interface CriteriaAnswer {
38
+ id: string;
39
+ verdict: 'pass' | 'fail' | 'n/a';
40
+ evidence: string;
41
+ }
42
+
36
43
  export interface EvaluationResult {
37
44
  /** Whether the component passed the quality threshold */
38
45
  passed: boolean;
@@ -48,6 +55,8 @@ export interface EvaluationResult {
48
55
  inputTokens?: number;
49
56
  /** Output tokens consumed by the evaluation LLM call */
50
57
  outputTokens?: number;
58
+ /** ggui#1436 — the judge's criteria answers, when the prompt asked for them; never part of the score. */
59
+ criteriaAnswers?: CriteriaAnswer[];
51
60
  }
52
61
 
53
62
  /**