@ggui-ai/ui-gen 0.24.0 → 0.26.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/README.md +2 -2
- package/dist/{adapter-Du7P_5Fx.d.ts → adapter-BJwn74rx.d.ts} +49 -1
- package/dist/adapters/generation-dispatch.d.ts +32 -7
- package/dist/adapters/generation-dispatch.js +1997 -216
- package/dist/adapters/generation-dispatch.js.map +1 -1
- package/dist/adapters/index.js +29 -6
- package/dist/adapters/index.js.map +1 -1
- package/dist/advanced/index.d.ts +15 -12
- package/dist/advanced/index.js +2013 -270
- package/dist/advanced/index.js.map +1 -1
- package/dist/{axes-CzLEMDeB.d.ts → axes-CKVCzR_x.d.ts} +1 -1
- package/dist/bank-Ctdp3FUA.d.ts +118 -0
- package/dist/blueprint-validator.js +416 -92
- package/dist/blueprint-validator.js.map +1 -1
- package/dist/boilerplate.d.ts +2 -2
- package/dist/boilerplate.js +34 -15
- package/dist/boilerplate.js.map +1 -1
- package/dist/check/index.d.ts +13 -5
- package/dist/check/index.js +56 -3
- 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-GIhzx08x.d.ts → create-ui-generator-N4gRXpcL.d.ts} +11 -4
- package/dist/{design-mode-DLufhrNp.d.ts → design-mode-DGWN_Gkm.d.ts} +1 -1
- package/dist/{dispatch-BnAH56p4.d.ts → dispatch-BUoXRj_r.d.ts} +6 -4
- package/dist/evaluation/axis-checks/index.d.ts +7 -6
- 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 +6 -5
- 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 +189 -344
- package/dist/evaluation/index.js +1272 -59
- package/dist/evaluation/index.js.map +1 -1
- package/dist/evaluation/types.d.ts +26 -1
- package/dist/evaluation/types.js.map +1 -1
- package/dist/fragments/index.d.ts +3 -3
- package/dist/{generate-9IeNT4ZY.d.ts → generate-tSO_shQ0.d.ts} +1 -1
- package/dist/harness/check/runtime-render/index.d.ts +7 -7
- package/dist/harness/check/runtime-render/index.js +416 -92
- package/dist/harness/check/runtime-render/index.js.map +1 -1
- package/dist/harness/check/runtime-render/render-check-worker.js +393 -88
- package/dist/harness/check/runtime-render/render-check-worker.js.map +1 -1
- package/dist/harness/index.d.ts +5 -6
- package/dist/harness/index.js +69 -20
- package/dist/harness/index.js.map +1 -1
- package/dist/harness/prompts.d.ts +2 -2
- package/dist/harness/prompts.js +10 -1
- package/dist/harness/prompts.js.map +1 -1
- package/dist/harness/result-types.d.ts +19 -5
- package/dist/harness/result-types.js.map +1 -1
- package/dist/harness/runtime.d.ts +21 -7
- package/dist/harness/runtime.js +36 -17
- package/dist/harness/runtime.js.map +1 -1
- package/dist/harness/types-public.d.ts +6 -4
- package/dist/harness/types-public.js +43 -18
- package/dist/harness/types-public.js.map +1 -1
- package/dist/harness/validator-trace-sink.d.ts +6 -5
- package/dist/hash.d.ts +1 -1
- package/dist/index.d.ts +64 -13
- package/dist/index.js +2023 -270
- package/dist/index.js.map +1 -1
- package/dist/patch.d.ts +5 -5
- package/dist/policy.d.ts +1 -1
- package/dist/{prompts-YU9piOgs.d.ts → prompts-BhNUuRoZ.d.ts} +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.js +8 -1
- 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-B7wn4ce5.d.ts +959 -0
- package/dist/validation/index.d.ts +1 -1
- package/dist/validation/index.js +1 -1
- package/dist/validation/index.js.map +1 -1
- package/dist/visual-evaluator-C9L-o6ap.d.ts +639 -0
- package/dist/workflows.d.ts +5 -5
- package/package.json +12 -12
- package/src/adapters/generation-dispatch.ts +38 -2
- package/src/adapters/tools.ts +2 -1
- package/src/boilerplate/free-design-prompt.ts +4 -4
- package/src/boilerplate/generate.ts +26 -1
- package/src/boilerplate/hard-sections.ts +14 -14
- package/src/boilerplate/system-prompt.ts +1 -1
- package/src/boilerplate/templates/layouts/chat-mobile.tsx.tmpl +3 -1
- package/src/boilerplate/templates/layouts/chat-universal.tsx.tmpl +3 -1
- package/src/boilerplate/templates/layouts/fullscreen-desktop.tsx.tmpl +3 -1
- package/src/boilerplate/templates/layouts/fullscreen-universal.tsx.tmpl +3 -1
- package/src/check/index.ts +1 -0
- package/src/check/run-tier0.ts +6 -3
- package/src/check/sample-props.ts +63 -0
- package/src/coding-agent/tools.ts +4 -1
- package/src/contract-context.ts +1 -1
- package/src/create-harness.ts +3 -3
- package/src/create-ui-generator.ts +20 -1
- 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/cost-tracker.ts +54 -6
- package/src/evaluation/criteria/bank.ts +85 -0
- package/src/evaluation/criteria/context.ts +52 -0
- package/src/evaluation/criteria/edge-probe.ts +286 -0
- package/src/evaluation/criteria/edge.ts +202 -0
- package/src/evaluation/criteria/resolve.ts +207 -0
- package/src/evaluation/criteria/select.ts +79 -0
- package/src/evaluation/index.ts +27 -0
- package/src/evaluation/ink-extent.ts +200 -0
- package/src/evaluation/judge-spend.ts +48 -0
- package/src/evaluation/theme-css.ts +12 -5
- package/src/evaluation/types-public.ts +221 -3
- package/src/evaluation/types.ts +19 -0
- package/src/evaluation/visual-evaluator.ts +853 -51
- package/src/harness/check/runtime-render/adapter.ts +18 -0
- package/src/harness/check/runtime-render/probe-pending.ts +111 -0
- package/src/harness/check/runtime-render/render-check-host.ts +10 -6
- package/src/harness/check/runtime-render/render-check-worker.ts +6 -3
- package/src/harness/check/runtime-render/render-check.ts +417 -133
- package/src/harness/check/runtime-render/walk.ts +200 -0
- package/src/harness/coding/assemble-result.ts +2 -0
- package/src/harness/coding/generate-task-runner.ts +30 -0
- package/src/harness/coding/init-session.ts +5 -3
- package/src/harness/coding/run-coding-turn.ts +12 -4
- package/src/harness/coding/run-eval-round.ts +82 -3
- package/src/harness/in-loop-evaluation.ts +39 -0
- package/src/harness/llm-router.ts +58 -1
- package/src/harness/result-types.ts +9 -0
- package/src/harness/runtime.ts +14 -0
- package/src/harness/types-public.ts +10 -1
- package/src/harness-tool-names.ts +38 -0
- package/src/index.ts +17 -0
- package/src/internal/verdict-line.ts +33 -0
- package/src/run-check.ts +2 -0
- package/src/taught-tokens.ts +32 -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 +2 -1
- package/src/tools/render-check.ts +8 -67
- package/src/tools.ts +11 -1
- package/src/validation/primitives.ts +1 -1
- package/dist/llm-router-DwwUK1bz.d.ts +0 -265
- package/dist/types-public-Bk6d1WLN.d.ts +0 -451
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* From K judge answers and the capture's measurements to one `CriteriaBlock` — ggui#1436.
|
|
3
|
+
*
|
|
4
|
+
* The judge is asked every `evaluation: judge` row (this block IS their implementation; the row's
|
|
5
|
+
* `status` is carried on the verdict as the author wrote it). An `instrument` row is answered by a
|
|
6
|
+
* measurement the capture already ran when its status is `live` and a binding exists; otherwise it
|
|
7
|
+
* reads `n/a` with the reason from `status`. A `human` row reads `n/a` for the judge. K-majority per
|
|
8
|
+
* id; a tie or an unanswered id reads `n/a` (the CTO's A2) — a `must` at `n/a` is never a pass when
|
|
9
|
+
* a gate later reads the set.
|
|
10
|
+
*/
|
|
11
|
+
import type { CriteriaAnswer } from '../types.js';
|
|
12
|
+
import type { CriteriaAnswerCounts, CriteriaBlock, CriteriaContext, CriteriaVerdict, CriterionVerdict } from '../types-public.js';
|
|
13
|
+
import type { VisionFinishReason } from '../../harness/llm-router.js';
|
|
14
|
+
import { bankRows, type BankRow, type CriteriaBank } from './bank.js';
|
|
15
|
+
import { CRITERIA_SELECTOR_VERSION, type CriteriaSelectionResult } from './select.js';
|
|
16
|
+
import type { EdgeVerdict } from './edge.js';
|
|
17
|
+
|
|
18
|
+
/** The instruments the capture runs today, by the bank ids they answer (v1 binding; a bank field may replace it). */
|
|
19
|
+
export const INSTRUMENT_BY_ID: Readonly<Record<string, 'fit' | 'fill'>> = {
|
|
20
|
+
'comp.fit': 'fit',
|
|
21
|
+
'floor.fit': 'fit',
|
|
22
|
+
'finish.canvas.inhabited': 'fill',
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* ggui#1663 — instruments that read BESIDE a judge row, report-only: the row keeps the judge's verdict and gains the
|
|
27
|
+
* measurement as `instrument`, so the two can be compared before any verdict moves.
|
|
28
|
+
*/
|
|
29
|
+
export const INSTRUMENT_BESIDE_JUDGE: Readonly<Record<string, 'edge'>> = {
|
|
30
|
+
'space.edge': 'edge',
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export interface CriteriaMeasurements {
|
|
34
|
+
readonly overflow: boolean;
|
|
35
|
+
readonly contentHeight: number | null;
|
|
36
|
+
readonly viewportHeight: number;
|
|
37
|
+
readonly inkRatio: number | null;
|
|
38
|
+
/**
|
|
39
|
+
* ggui#1663 — the edge rule on the capture's DOM: absent when this capture had no page to read (a stored frame),
|
|
40
|
+
* `null` when the probe ran and failed.
|
|
41
|
+
*/
|
|
42
|
+
readonly edge?: EdgeVerdict | null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function besideRead(kind: 'edge', m: CriteriaMeasurements): Read {
|
|
46
|
+
if (m.edge === undefined) return { verdict: 'n/a', evidence: 'not measured: this capture had no page to read' };
|
|
47
|
+
if (m.edge === null) return { verdict: 'n/a', evidence: 'edge probe failed on this capture' };
|
|
48
|
+
return { verdict: m.edge.verdict, evidence: m.edge.evidence };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
type Read = { verdict: CriteriaVerdict; evidence: string };
|
|
52
|
+
|
|
53
|
+
function majority(id: string, answers: readonly CriteriaAnswer[][], unansweredReason: string | undefined): Read {
|
|
54
|
+
const votes = answers.flatMap((a) => a.filter((x) => x.id === id));
|
|
55
|
+
if (votes.length === 0) return { verdict: 'n/a', evidence: unansweredReason ?? 'not answered' };
|
|
56
|
+
const counts = new Map<CriteriaVerdict, number>();
|
|
57
|
+
for (const v of votes) counts.set(v.verdict, (counts.get(v.verdict) ?? 0) + 1);
|
|
58
|
+
const ranked = [...counts.entries()].sort((a, b) => b[1] - a[1]);
|
|
59
|
+
const top = ranked[0]!;
|
|
60
|
+
if (ranked.length > 1 && ranked[1]![1] === top[1]) return { verdict: 'n/a', evidence: 'no majority' };
|
|
61
|
+
const evidence = votes.find((v) => v.verdict === top[0])?.evidence ?? '';
|
|
62
|
+
return { verdict: top[0], evidence };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function instrumentRead(kind: 'fit' | 'fill', m: CriteriaMeasurements): Read {
|
|
66
|
+
if (kind === 'fit') {
|
|
67
|
+
if (m.contentHeight === null) return { verdict: 'n/a', evidence: 'content height unmeasurable' };
|
|
68
|
+
return { verdict: m.overflow ? 'fail' : 'pass', evidence: `content ${m.contentHeight}px against a ${m.viewportHeight}px box` };
|
|
69
|
+
}
|
|
70
|
+
if (m.inkRatio === null) return { verdict: 'n/a', evidence: 'capture unreadable' };
|
|
71
|
+
// Reported, never gated: no fill floor is ruled (hello-finish property 1).
|
|
72
|
+
return { verdict: 'n/a', evidence: `fill ${m.inkRatio.toFixed(3)} reported; no floor ruled` };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function readRow(row: BankRow, answers: readonly CriteriaAnswer[][], m: CriteriaMeasurements, unansweredReason: string | undefined): Read {
|
|
76
|
+
if (row.evaluation === 'judge') return majority(row.id, answers, unansweredReason);
|
|
77
|
+
if (row.evaluation === 'human') return { verdict: 'n/a', evidence: `reader's column (status ${row.status})` };
|
|
78
|
+
if (row.status !== 'live') return { verdict: 'n/a', evidence: `not implemented (status ${row.status})` };
|
|
79
|
+
const kind = INSTRUMENT_BY_ID[row.id];
|
|
80
|
+
return kind === undefined ? { verdict: 'n/a', evidence: 'instrument not wired (status live, no binding)' } : instrumentRead(kind, m);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function resolveCriteriaBlock(args: {
|
|
84
|
+
readonly bank: CriteriaBank;
|
|
85
|
+
readonly context: CriteriaContext;
|
|
86
|
+
readonly selected: CriteriaSelectionResult;
|
|
87
|
+
readonly answers: readonly CriteriaAnswer[][];
|
|
88
|
+
readonly measurements: CriteriaMeasurements;
|
|
89
|
+
/**
|
|
90
|
+
* ggui#1127 — why a judge row has no answer, when the caller knows: a salvaged criteria answer keeps no criteria,
|
|
91
|
+
* so its rows say so instead of reading as a clean `n/a`. Absent ⇒ "not answered".
|
|
92
|
+
*/
|
|
93
|
+
readonly unansweredReason?: string;
|
|
94
|
+
/** ggui#1687 — the call that answered, as it came back: its answer's counts and its stop reason. */
|
|
95
|
+
readonly call?: { readonly answer: CriteriaAnswerCounts; readonly finishReason?: VisionFinishReason };
|
|
96
|
+
}): CriteriaBlock {
|
|
97
|
+
const byId = new Map<string, BankRow>(bankRows(args.bank).map((r) => [r.id, r]));
|
|
98
|
+
const verdicts: CriterionVerdict[] = [];
|
|
99
|
+
for (const s of args.selected.selection) {
|
|
100
|
+
const row = byId.get(s.id);
|
|
101
|
+
if (row === undefined) continue;
|
|
102
|
+
const read = readRow(row, args.answers, args.measurements, args.unansweredReason);
|
|
103
|
+
const beside = INSTRUMENT_BESIDE_JUDGE[row.id];
|
|
104
|
+
verdicts.push({
|
|
105
|
+
id: row.id,
|
|
106
|
+
severity: row.severity,
|
|
107
|
+
method: row.evaluation,
|
|
108
|
+
status: row.status,
|
|
109
|
+
source: s.source,
|
|
110
|
+
verdict: read.verdict,
|
|
111
|
+
evidence: read.evidence,
|
|
112
|
+
...(beside !== undefined && row.evaluation === 'judge' ? { instrument: besideRead(beside, args.measurements) } : {}),
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
return {
|
|
116
|
+
criteriaSetId: args.selected.criteriaSetId,
|
|
117
|
+
bankVersion: args.bank.version,
|
|
118
|
+
selectorVersion: CRITERIA_SELECTOR_VERSION,
|
|
119
|
+
context: args.context,
|
|
120
|
+
selection: args.selected.selection,
|
|
121
|
+
verdicts,
|
|
122
|
+
...(args.call !== undefined
|
|
123
|
+
? {
|
|
124
|
+
call: {
|
|
125
|
+
answer: args.call.answer,
|
|
126
|
+
// Kept answers whose id names no row of the bank: the judge answered something nobody asked.
|
|
127
|
+
unknownIds: args.answers.flat().filter((a) => !byId.has(a.id)).length,
|
|
128
|
+
...(args.call.finishReason !== undefined ? { finishReason: args.call.finishReason } : {}),
|
|
129
|
+
},
|
|
130
|
+
}
|
|
131
|
+
: {})
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** The judge's prompt block for the selected `judge` rows; empty when none is selected. */
|
|
136
|
+
/** The capture the judge is handed: its canvas class and the box it was taken at (the class box, or a declared one). */
|
|
137
|
+
export interface CriteriaJudgeFrame {
|
|
138
|
+
readonly canvas: CriteriaContext['canvas'];
|
|
139
|
+
readonly width: number;
|
|
140
|
+
readonly height: number;
|
|
141
|
+
/** ggui#1492 — the host whose presentation framed this canvas (e.g. "a chat widget, inline, reference theme"); absent ⇒ the judge's own stand-in. */
|
|
142
|
+
readonly hostLabel?: string;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** What the block carries besides the selected rows: the frame's box, and the props the card was rendered with. */
|
|
146
|
+
export interface CriteriaJudgeInputs {
|
|
147
|
+
readonly frame?: CriteriaJudgeFrame;
|
|
148
|
+
/** JSON text of the props the card was rendered with; bounded by {@link CRITERIA_PROPS_MAX_CHARS}. */
|
|
149
|
+
readonly propsJson?: string;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Bound on the props text handed to the judge beside the frame, in characters. */
|
|
153
|
+
export const CRITERIA_PROPS_MAX_CHARS = 2000;
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* The frame's box and how the card occupies it, named so the judge reads the capture it was handed and
|
|
157
|
+
* never guesses it: without it, phone rows described a 390×844 frame as a 768×1024 tablet, and the
|
|
158
|
+
* inline card's verdicts flipped between near-identical frames (the evidence audit on ggui#1438).
|
|
159
|
+
* The inline chat card is sized to its content by the host (the box grows and shrinks with the card; a
|
|
160
|
+
* `maxHeight` caps and scrolls), so the judge captures it at its natural height on the host's ground
|
|
161
|
+
* (ggui#1475) and composition is judged within the card's extent; every other canvas is a fixed box the
|
|
162
|
+
* card fills.
|
|
163
|
+
*/
|
|
164
|
+
export function criteriaFrameLine(frame: CriteriaJudgeFrame): string {
|
|
165
|
+
const box = `${frame.width}×${frame.height}`;
|
|
166
|
+
if (frame.canvas === 'xs-chat-card') {
|
|
167
|
+
const framedBy =
|
|
168
|
+
frame.hostLabel !== undefined
|
|
169
|
+
? `inside the host's own frame (${frame.hostLabel}), on the host's ground`
|
|
170
|
+
: "inside a generic stand-in for the host's frame — the card's surface with a thin ring and rounded corners, on the page ground";
|
|
171
|
+
return `This frame is an inline chat card captured at its natural height (at most ${box}, the host's ceiling), ${framedBy}: the host sizes the card to its content and draws that frame, so judge composition and space within the card's own extent, and read the frame and the ground round it as the host's, not the card's.`;
|
|
172
|
+
}
|
|
173
|
+
return `This frame is ${box}, a fixed full-screen box the card fills${frame.hostLabel !== undefined ? ` (${frame.hostLabel})` : ''}: judge composition and space against the whole box, edge to edge.`;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* The one declared input besides the frame. Rows whose evidence names props (whether a rendered string
|
|
178
|
+
* came from content or props, whether the primary action is the one the props and contract name) cannot
|
|
179
|
+
* be answered from pixels alone; every other row stays frame-only.
|
|
180
|
+
*/
|
|
181
|
+
function criteriaPropsSection(propsJson: string): string {
|
|
182
|
+
const text = propsJson.length > CRITERIA_PROPS_MAX_CHARS ? `${propsJson.slice(0, CRITERIA_PROPS_MAX_CHARS)}… (truncated)` : propsJson;
|
|
183
|
+
return (
|
|
184
|
+
'The one input besides the frame — the props this card was rendered with. Read them ONLY for rows whose evidence names props ' +
|
|
185
|
+
'(for example, whether a rendered string came from them); every other row is read off the frame alone:\n' +
|
|
186
|
+
`${text}\n`
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** The judge's prompt block for the selected `judge` rows; empty when none is selected. */
|
|
191
|
+
export function buildCriteriaJudgeBlock(bank: CriteriaBank, selected: CriteriaSelectionResult, inputs: CriteriaJudgeInputs = {}): string {
|
|
192
|
+
const byId = new Map<string, BankRow>(bankRows(bank).map((r) => [r.id, r]));
|
|
193
|
+
const asked = selected.selection.map((s) => byId.get(s.id)).filter((r): r is BankRow => r !== undefined && r.evaluation === 'judge');
|
|
194
|
+
if (asked.length === 0) return '';
|
|
195
|
+
const lines = asked.map((r) => `- ${r.id} (${r.severity}): ${r.text} Evidence must name: ${r.evidence}`);
|
|
196
|
+
return (
|
|
197
|
+
'## Criteria — answer each one from the FRAME only\n' +
|
|
198
|
+
'Read these off the screenshot. Never infer them from the request, the styling profile or any direction text above.\n' +
|
|
199
|
+
(inputs.frame !== undefined ? `${criteriaFrameLine(inputs.frame)}\n` : '') +
|
|
200
|
+
(inputs.propsJson !== undefined ? criteriaPropsSection(inputs.propsJson) : '') +
|
|
201
|
+
`${lines.join('\n')}\n` +
|
|
202
|
+
'Add to your JSON a top-level "criteria" array with exactly these ids: ' +
|
|
203
|
+
'[{ "id": "<id>", "verdict": "pass" | "fail" | "n/a", "evidence": "<one sentence naming the region or element you read it from>" }]. ' +
|
|
204
|
+
'Every row here was selected as applicable to this card, so "n/a" never means "does not apply": ' +
|
|
205
|
+
'use it ONLY when this frame cannot show the criterion — it is then recorded as not evaluated.'
|
|
206
|
+
);
|
|
207
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The selector — ggui#1436's "dynamic" layer, deterministic and recorded: a pure function of the
|
|
3
|
+
* bank and the card's context. Same context → same `criteriaSetId` → same selection, so verdicts
|
|
4
|
+
* pair across arms and replay from the record. A row the selector excludes is absent from the
|
|
5
|
+
* block (the selector's decision); a selected row the judge cannot read comes back `n/a` (the
|
|
6
|
+
* judge's answer). The two are different signals.
|
|
7
|
+
*/
|
|
8
|
+
import { createHash } from 'node:crypto';
|
|
9
|
+
import type { CriteriaContext, CriteriaSelection } from '../types-public.js';
|
|
10
|
+
import { bankRows, type BankRow, type CriteriaBank } from './bank.js';
|
|
11
|
+
|
|
12
|
+
/** Bump when a selection rule changes: it is hashed into every `criteriaSetId`. */
|
|
13
|
+
export const CRITERIA_SELECTOR_VERSION = 'selector@2';
|
|
14
|
+
|
|
15
|
+
/** The context as one string, keys sorted at every level — what the id hashes and what the record stores. */
|
|
16
|
+
export function canonicalCriteriaContext(ctx: CriteriaContext): string {
|
|
17
|
+
const sortKeys = (value: unknown): unknown =>
|
|
18
|
+
value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
19
|
+
? Object.fromEntries(
|
|
20
|
+
Object.entries(value as Record<string, unknown>)
|
|
21
|
+
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
|
|
22
|
+
.map(([k, v]) => [k, sortKeys(v)]),
|
|
23
|
+
)
|
|
24
|
+
: value;
|
|
25
|
+
return JSON.stringify(sortKeys(ctx));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** sha256(bank version | selector version | canonical context), 16 hex — a label at this scale, not a security digest. */
|
|
29
|
+
export function criteriaSetIdFor(bankVersion: string, ctx: CriteriaContext): string {
|
|
30
|
+
return createHash('sha256')
|
|
31
|
+
.update(`${bankVersion}|${CRITERIA_SELECTOR_VERSION}|${canonicalCriteriaContext(ctx)}`, 'utf8')
|
|
32
|
+
.digest('hex')
|
|
33
|
+
.slice(0, 16);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** The scope keys that hold for this context, or `null` when one fails; `[]` for an empty scope (static). */
|
|
37
|
+
function matchedScope(row: BankRow, ctx: CriteriaContext): string[] | null {
|
|
38
|
+
const s = row.scope;
|
|
39
|
+
const matched: string[] = [];
|
|
40
|
+
if (s.kind !== undefined) {
|
|
41
|
+
if (ctx.kind !== s.kind) return null;
|
|
42
|
+
matched.push(`kind=${s.kind}`);
|
|
43
|
+
}
|
|
44
|
+
if (s.canvas !== undefined) {
|
|
45
|
+
if (ctx.canvas !== s.canvas) return null;
|
|
46
|
+
matched.push(`canvas=${s.canvas}`);
|
|
47
|
+
}
|
|
48
|
+
if (s.preset !== undefined) {
|
|
49
|
+
if (ctx.preset !== s.preset) return null;
|
|
50
|
+
matched.push(`preset=${s.preset}`);
|
|
51
|
+
}
|
|
52
|
+
return matched;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface CriteriaSelectionResult {
|
|
56
|
+
readonly criteriaSetId: string;
|
|
57
|
+
readonly selection: CriteriaSelection[];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Select the bank's rows for one card. The bank-level `applies` gate is read first (canvases always;
|
|
62
|
+
* kind only when the caller named one and the bank names kinds other than `*`); then each row's
|
|
63
|
+
* `scope`. An empty scope is `static`; a matched scope is `context` with the keys that matched.
|
|
64
|
+
*/
|
|
65
|
+
export function selectCriteria(bank: CriteriaBank, ctx: CriteriaContext): CriteriaSelectionResult {
|
|
66
|
+
const criteriaSetId = criteriaSetIdFor(bank.version, ctx);
|
|
67
|
+
const applies = bank.applies;
|
|
68
|
+
if (applies?.canvases !== undefined && !applies.canvases.includes(ctx.canvas)) return { criteriaSetId, selection: [] };
|
|
69
|
+
if (applies?.kind !== undefined && ctx.kind !== undefined && !applies.kind.includes('*') && !applies.kind.includes(ctx.kind)) {
|
|
70
|
+
return { criteriaSetId, selection: [] };
|
|
71
|
+
}
|
|
72
|
+
const selection: CriteriaSelection[] = [];
|
|
73
|
+
for (const row of bankRows(bank)) {
|
|
74
|
+
const matched = matchedScope(row, ctx);
|
|
75
|
+
if (matched === null) continue;
|
|
76
|
+
selection.push(matched.length === 0 ? { id: row.id, source: 'static', reason: 'static' } : { id: row.id, source: 'context', reason: `context: ${matched.join(', ')}` });
|
|
77
|
+
}
|
|
78
|
+
return { criteriaSetId, selection };
|
|
79
|
+
}
|
package/src/evaluation/index.ts
CHANGED
|
@@ -26,6 +26,10 @@ export type {
|
|
|
26
26
|
VisualEvalSummary,
|
|
27
27
|
CanvasJudgeRecord,
|
|
28
28
|
CanvasVisualSummary,
|
|
29
|
+
CanvasHostPresentation,
|
|
30
|
+
HostInlineFrame,
|
|
31
|
+
CanvasPresentationOutcome,
|
|
32
|
+
HostPresentationIgnoredReason,
|
|
29
33
|
EvalCriterion,
|
|
30
34
|
CriterionCoverage,
|
|
31
35
|
CriterionRunStatus,
|
|
@@ -103,10 +107,18 @@ export {
|
|
|
103
107
|
captureScreenshotDetailed,
|
|
104
108
|
canvasFitPolicy,
|
|
105
109
|
canvasOverflowIssue,
|
|
110
|
+
canvasBlankIssue,
|
|
111
|
+
JUDGE_INK_INSET_PX,
|
|
112
|
+
JUDGE_GROUND_MARGIN_PX,
|
|
106
113
|
CONTENT_HEIGHT_EXPRESSION,
|
|
114
|
+
CARD_HEIGHT_EXPRESSION,
|
|
115
|
+
HOST_COLOUR_PATTERN,
|
|
116
|
+
HOST_FRAME_BOUNDS,
|
|
107
117
|
runVisualEval,
|
|
108
118
|
summarizeVisualResult,
|
|
109
119
|
} from './visual-evaluator.js';
|
|
120
|
+
export { readInkExtent, decodePng, INK_TOLERANCE } from './ink-extent.js';
|
|
121
|
+
export type { InkExtent, InkUnreadable, PixelRegion, DecodedImage } from './ink-extent.js';
|
|
110
122
|
export type {
|
|
111
123
|
VisualEvalConfig,
|
|
112
124
|
VisualEvalContext,
|
|
@@ -116,12 +128,16 @@ export type {
|
|
|
116
128
|
VisualEvalDetailed,
|
|
117
129
|
ScreenshotAttempt,
|
|
118
130
|
CanvasVisualResult,
|
|
131
|
+
CriteriaCallTokens,
|
|
132
|
+
VisualLegTokens,
|
|
119
133
|
CanvasFitPolicy,
|
|
120
134
|
CaptureMode,
|
|
121
135
|
ScreenshotDeps,
|
|
122
136
|
ScreenshotLauncher,
|
|
123
137
|
ScreenshotBrowser,
|
|
124
138
|
ScreenshotPage,
|
|
139
|
+
ScreenshotClip,
|
|
140
|
+
NaturalCaptureOptions,
|
|
125
141
|
ChromiumProvider,
|
|
126
142
|
} from './visual-evaluator.js';
|
|
127
143
|
|
|
@@ -148,3 +164,14 @@ export type {
|
|
|
148
164
|
} from '../design-mode.js';
|
|
149
165
|
export { cssTokensForAppTheme } from './theme-css.js';
|
|
150
166
|
export { designTreeSha256, judgeDesignIdentity, resetJudgeDesignIdentityCache, type JudgeDesignIdentity } from './design-identity.js';
|
|
167
|
+
|
|
168
|
+
// ggui#1436 — the visual criteria: the bank loader (data), the selector, the resolver.
|
|
169
|
+
export { bankRows, criteriaBankSchema, parseCriteriaBank, type BankRow, type CriteriaBank, type CriteriaScope } from './criteria/bank.js';
|
|
170
|
+
export { CRITERIA_SELECTOR_VERSION, canonicalCriteriaContext, criteriaSetIdFor, selectCriteria, type CriteriaSelectionResult } from './criteria/select.js';
|
|
171
|
+
export { chromaOfCssTokens, criteriaContextFor, type CriteriaContextInput, type CriteriaContextSources } from './criteria/context.js';
|
|
172
|
+
export { INSTRUMENT_BESIDE_JUDGE, INSTRUMENT_BY_ID, buildCriteriaJudgeBlock, resolveCriteriaBlock, type CriteriaMeasurements } from './criteria/resolve.js';
|
|
173
|
+
export { EDGE_TOLERANCE_PX, judgeEdges, type EdgeBlock, type EdgeOffset, type EdgeVerdict } from './criteria/edge.js';
|
|
174
|
+
export { EDGE_BLOCKS_EXPRESSION, parseEdgeProbe, type EdgeProbe } from './criteria/edge-probe.js';
|
|
175
|
+
export { judgeStoredCapture, type StoredCaptureInput, type StoredCaptureOutcome, type StoredCaptureVerdict } from './visual-evaluator.js';
|
|
176
|
+
export { judgedFrameTokens, type JudgeSpend, type JudgedTokens } from './judge-spend.js';
|
|
177
|
+
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ggui#1640 — one judged frame's (or one visual evaluation's) spend, summed over EVERY leg the judge reports.
|
|
3
|
+
*
|
|
4
|
+
* The judge reports its legs apart: the scoring calls' `inputTokens` / `outputTokens`, and, since ggui#1436, the
|
|
5
|
+
* report-only criteria call's `criteriaTokens`. A caller that sums only the first leg under-counts, and a dollar
|
|
6
|
+
* ceiling built on that sum stops late. {@link judgedFrameTokens} is the one sum. The two `satisfies` tables below list
|
|
7
|
+
* every `*Tokens` member of the results it reads, so a leg added to either result type fails the build here until the
|
|
8
|
+
* sum counts it.
|
|
9
|
+
*/
|
|
10
|
+
import type { CriteriaCallTokens, StoredCaptureVerdict, VisualEvaluationResult } from './visual-evaluator.js';
|
|
11
|
+
|
|
12
|
+
/** A spend in tokens: input and output, over every leg. */
|
|
13
|
+
export interface JudgeSpend {
|
|
14
|
+
readonly inputTokens: number;
|
|
15
|
+
readonly outputTokens: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** The members of `T` whose names end in `Tokens`. */
|
|
19
|
+
type TokenMember<T> = { [K in keyof T]-?: K extends `${string}Tokens` ? K : never }[keyof T];
|
|
20
|
+
|
|
21
|
+
/** Which leg each `*Tokens` member belongs to. A member missing here, or listed and gone, is a compile error. */
|
|
22
|
+
const STORED_VERDICT_LEGS = {
|
|
23
|
+
inputTokens: 'scoring',
|
|
24
|
+
outputTokens: 'scoring',
|
|
25
|
+
criteriaTokens: 'criteria',
|
|
26
|
+
} as const satisfies Record<TokenMember<StoredCaptureVerdict>, 'scoring' | 'criteria'>;
|
|
27
|
+
const VISUAL_RESULT_LEGS = {
|
|
28
|
+
inputTokens: 'scoring',
|
|
29
|
+
outputTokens: 'scoring',
|
|
30
|
+
criteriaTokens: 'criteria',
|
|
31
|
+
} as const satisfies Record<TokenMember<VisualEvaluationResult>, 'scoring' | 'criteria'>;
|
|
32
|
+
void STORED_VERDICT_LEGS;
|
|
33
|
+
void VISUAL_RESULT_LEGS;
|
|
34
|
+
|
|
35
|
+
/** The legs {@link judgedFrameTokens} reads, as both result types carry them. */
|
|
36
|
+
export interface JudgedTokens {
|
|
37
|
+
readonly inputTokens?: number;
|
|
38
|
+
readonly outputTokens?: number;
|
|
39
|
+
readonly criteriaTokens?: CriteriaCallTokens;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** A judged frame's spend over every leg: the scoring calls plus the report-only criteria call, when one ran. */
|
|
43
|
+
export function judgedFrameTokens(result: JudgedTokens): JudgeSpend {
|
|
44
|
+
return {
|
|
45
|
+
inputTokens: (result.inputTokens ?? 0) + (result.criteriaTokens?.inputTokens ?? 0),
|
|
46
|
+
outputTokens: (result.outputTokens ?? 0) + (result.criteriaTokens?.outputTokens ?? 0),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
@@ -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.
|
|
9
|
-
*
|
|
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({
|
|
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
|
}
|