@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.
Files changed (142) hide show
  1. package/README.md +2 -2
  2. package/dist/{adapter-Du7P_5Fx.d.ts → adapter-BJwn74rx.d.ts} +49 -1
  3. package/dist/adapters/generation-dispatch.d.ts +32 -7
  4. package/dist/adapters/generation-dispatch.js +1997 -216
  5. package/dist/adapters/generation-dispatch.js.map +1 -1
  6. package/dist/adapters/index.js +29 -6
  7. package/dist/adapters/index.js.map +1 -1
  8. package/dist/advanced/index.d.ts +15 -12
  9. package/dist/advanced/index.js +2013 -270
  10. package/dist/advanced/index.js.map +1 -1
  11. package/dist/{axes-CzLEMDeB.d.ts → axes-CKVCzR_x.d.ts} +1 -1
  12. package/dist/bank-Ctdp3FUA.d.ts +118 -0
  13. package/dist/blueprint-validator.js +416 -92
  14. package/dist/blueprint-validator.js.map +1 -1
  15. package/dist/boilerplate.d.ts +2 -2
  16. package/dist/boilerplate.js +34 -15
  17. package/dist/boilerplate.js.map +1 -1
  18. package/dist/check/index.d.ts +13 -5
  19. package/dist/check/index.js +56 -3
  20. package/dist/check/index.js.map +1 -1
  21. package/dist/classifier/index.d.ts +2 -2
  22. package/dist/compose.d.ts +2 -2
  23. package/dist/{create-ui-generator-GIhzx08x.d.ts → create-ui-generator-N4gRXpcL.d.ts} +11 -4
  24. package/dist/{design-mode-DLufhrNp.d.ts → design-mode-DGWN_Gkm.d.ts} +1 -1
  25. package/dist/{dispatch-BnAH56p4.d.ts → dispatch-BUoXRj_r.d.ts} +6 -4
  26. package/dist/evaluation/axis-checks/index.d.ts +7 -6
  27. package/dist/evaluation/axis-checks/index.js +90 -2
  28. package/dist/evaluation/axis-checks/index.js.map +1 -1
  29. package/dist/evaluation/axis-checks/registry.d.ts +6 -5
  30. package/dist/evaluation/axis-checks/registry.js +88 -1
  31. package/dist/evaluation/axis-checks/registry.js.map +1 -1
  32. package/dist/evaluation/index.d.ts +189 -344
  33. package/dist/evaluation/index.js +1272 -59
  34. package/dist/evaluation/index.js.map +1 -1
  35. package/dist/evaluation/types.d.ts +26 -1
  36. package/dist/evaluation/types.js.map +1 -1
  37. package/dist/fragments/index.d.ts +3 -3
  38. package/dist/{generate-9IeNT4ZY.d.ts → generate-tSO_shQ0.d.ts} +1 -1
  39. package/dist/harness/check/runtime-render/index.d.ts +7 -7
  40. package/dist/harness/check/runtime-render/index.js +416 -92
  41. package/dist/harness/check/runtime-render/index.js.map +1 -1
  42. package/dist/harness/check/runtime-render/render-check-worker.js +393 -88
  43. package/dist/harness/check/runtime-render/render-check-worker.js.map +1 -1
  44. package/dist/harness/index.d.ts +5 -6
  45. package/dist/harness/index.js +69 -20
  46. package/dist/harness/index.js.map +1 -1
  47. package/dist/harness/prompts.d.ts +2 -2
  48. package/dist/harness/prompts.js +10 -1
  49. package/dist/harness/prompts.js.map +1 -1
  50. package/dist/harness/result-types.d.ts +19 -5
  51. package/dist/harness/result-types.js.map +1 -1
  52. package/dist/harness/runtime.d.ts +21 -7
  53. package/dist/harness/runtime.js +36 -17
  54. package/dist/harness/runtime.js.map +1 -1
  55. package/dist/harness/types-public.d.ts +6 -4
  56. package/dist/harness/types-public.js +43 -18
  57. package/dist/harness/types-public.js.map +1 -1
  58. package/dist/harness/validator-trace-sink.d.ts +6 -5
  59. package/dist/hash.d.ts +1 -1
  60. package/dist/index.d.ts +64 -13
  61. package/dist/index.js +2023 -270
  62. package/dist/index.js.map +1 -1
  63. package/dist/patch.d.ts +5 -5
  64. package/dist/policy.d.ts +1 -1
  65. package/dist/{prompts-YU9piOgs.d.ts → prompts-BhNUuRoZ.d.ts} +1 -1
  66. package/dist/tools/get-primitives-ts.d.ts +1 -1
  67. package/dist/tools/get-primitives-ts.js +1 -1
  68. package/dist/tools/get-primitives-ts.js.map +1 -1
  69. package/dist/tools/render-check-worker.js +8 -1
  70. package/dist/tools/render-check-worker.js.map +1 -1
  71. package/dist/{types-BOvHNG7K.d.ts → types-Dh1s4K9j.d.ts} +1 -1
  72. package/dist/types-public-B7wn4ce5.d.ts +959 -0
  73. package/dist/validation/index.d.ts +1 -1
  74. package/dist/validation/index.js +1 -1
  75. package/dist/validation/index.js.map +1 -1
  76. package/dist/visual-evaluator-C9L-o6ap.d.ts +639 -0
  77. package/dist/workflows.d.ts +5 -5
  78. package/package.json +12 -12
  79. package/src/adapters/generation-dispatch.ts +38 -2
  80. package/src/adapters/tools.ts +2 -1
  81. package/src/boilerplate/free-design-prompt.ts +4 -4
  82. package/src/boilerplate/generate.ts +26 -1
  83. package/src/boilerplate/hard-sections.ts +14 -14
  84. package/src/boilerplate/system-prompt.ts +1 -1
  85. package/src/boilerplate/templates/layouts/chat-mobile.tsx.tmpl +3 -1
  86. package/src/boilerplate/templates/layouts/chat-universal.tsx.tmpl +3 -1
  87. package/src/boilerplate/templates/layouts/fullscreen-desktop.tsx.tmpl +3 -1
  88. package/src/boilerplate/templates/layouts/fullscreen-universal.tsx.tmpl +3 -1
  89. package/src/check/index.ts +1 -0
  90. package/src/check/run-tier0.ts +6 -3
  91. package/src/check/sample-props.ts +63 -0
  92. package/src/coding-agent/tools.ts +4 -1
  93. package/src/contract-context.ts +1 -1
  94. package/src/create-harness.ts +3 -3
  95. package/src/create-ui-generator.ts +20 -1
  96. package/src/evaluation/__fixtures__/png.ts +100 -0
  97. package/src/evaluation/axis-checks/checks/state-merge.ts +1 -1
  98. package/src/evaluation/axis-checks/checks/universal.ts +111 -0
  99. package/src/evaluation/axis-checks/dispatch.ts +4 -1
  100. package/src/evaluation/cost-tracker.ts +54 -6
  101. package/src/evaluation/criteria/bank.ts +85 -0
  102. package/src/evaluation/criteria/context.ts +52 -0
  103. package/src/evaluation/criteria/edge-probe.ts +286 -0
  104. package/src/evaluation/criteria/edge.ts +202 -0
  105. package/src/evaluation/criteria/resolve.ts +207 -0
  106. package/src/evaluation/criteria/select.ts +79 -0
  107. package/src/evaluation/index.ts +27 -0
  108. package/src/evaluation/ink-extent.ts +200 -0
  109. package/src/evaluation/judge-spend.ts +48 -0
  110. package/src/evaluation/theme-css.ts +12 -5
  111. package/src/evaluation/types-public.ts +221 -3
  112. package/src/evaluation/types.ts +19 -0
  113. package/src/evaluation/visual-evaluator.ts +853 -51
  114. package/src/harness/check/runtime-render/adapter.ts +18 -0
  115. package/src/harness/check/runtime-render/probe-pending.ts +111 -0
  116. package/src/harness/check/runtime-render/render-check-host.ts +10 -6
  117. package/src/harness/check/runtime-render/render-check-worker.ts +6 -3
  118. package/src/harness/check/runtime-render/render-check.ts +417 -133
  119. package/src/harness/check/runtime-render/walk.ts +200 -0
  120. package/src/harness/coding/assemble-result.ts +2 -0
  121. package/src/harness/coding/generate-task-runner.ts +30 -0
  122. package/src/harness/coding/init-session.ts +5 -3
  123. package/src/harness/coding/run-coding-turn.ts +12 -4
  124. package/src/harness/coding/run-eval-round.ts +82 -3
  125. package/src/harness/in-loop-evaluation.ts +39 -0
  126. package/src/harness/llm-router.ts +58 -1
  127. package/src/harness/result-types.ts +9 -0
  128. package/src/harness/runtime.ts +14 -0
  129. package/src/harness/types-public.ts +10 -1
  130. package/src/harness-tool-names.ts +38 -0
  131. package/src/index.ts +17 -0
  132. package/src/internal/verdict-line.ts +33 -0
  133. package/src/run-check.ts +2 -0
  134. package/src/taught-tokens.ts +32 -0
  135. package/src/tools/get-primitives-ts.ts +1 -1
  136. package/src/tools/get-wire.ts +1 -1
  137. package/src/tools/render-check-worker.ts +2 -1
  138. package/src/tools/render-check.ts +8 -67
  139. package/src/tools.ts +11 -1
  140. package/src/validation/primitives.ts +1 -1
  141. package/dist/llm-router-DwwUK1bz.d.ts +0 -265
  142. package/dist/types-public-Bk6d1WLN.d.ts +0 -451
@@ -0,0 +1,100 @@
1
+ /**
2
+ * A PNG writer for tests (ggui#1120): 8-bit RGB / RGBA, non-interlaced, one scanline filter for the
3
+ * whole image — the shapes the judge's browser writes, so `decodePng` is exercised on every filter it
4
+ * must undo. Test-only: nothing in `src` imports it.
5
+ */
6
+ import { crc32, deflateSync } from 'node:zlib';
7
+
8
+ export type Rgb = readonly [number, number, number];
9
+
10
+ export interface PngSpec {
11
+ readonly width: number;
12
+ readonly height: number;
13
+ readonly channels: 3 | 4;
14
+ /** The colour at (x, y). */
15
+ readonly pixel: (x: number, y: number) => Rgb;
16
+ /** The scanline filter applied to every row (0 none, 1 sub, 2 up, 3 average, 4 paeth). */
17
+ readonly filter?: 0 | 1 | 2 | 3 | 4;
18
+ /** Overrides for the malformed cases. */
19
+ readonly bitDepth?: number;
20
+ readonly colorType?: number;
21
+ readonly interlace?: number;
22
+ }
23
+
24
+ function chunk(type: string, data: Buffer): Buffer {
25
+ const head = Buffer.alloc(8);
26
+ head.writeUInt32BE(data.length, 0);
27
+ head.write(type, 4, 'latin1');
28
+ const crc = Buffer.alloc(4);
29
+ crc.writeUInt32BE(crc32(data, crc32(Buffer.from(type, 'latin1'))) >>> 0, 0);
30
+ return Buffer.concat([head, data, crc]);
31
+ }
32
+
33
+ export function encodePng(spec: PngSpec): Buffer {
34
+ const { width, height, channels } = spec;
35
+ const filter = spec.filter ?? 0;
36
+ const stride = width * channels;
37
+ const rows: number[][] = [];
38
+ for (let y = 0; y < height; y++) {
39
+ const row: number[] = [];
40
+ for (let x = 0; x < width; x++) {
41
+ const [r, g, b] = spec.pixel(x, y);
42
+ row.push(r, g, b);
43
+ if (channels === 4) row.push(255);
44
+ }
45
+ rows.push(row);
46
+ }
47
+ const raw = Buffer.alloc((stride + 1) * height);
48
+ for (let y = 0; y < height; y++) {
49
+ raw[y * (stride + 1)] = filter;
50
+ for (let i = 0; i < stride; i++) {
51
+ const x = rows[y]![i]!;
52
+ const a = i >= channels ? rows[y]![i - channels]! : 0;
53
+ const b = y > 0 ? rows[y - 1]![i]! : 0;
54
+ const c = y > 0 && i >= channels ? rows[y - 1]![i - channels]! : 0;
55
+ let out: number;
56
+ switch (filter) {
57
+ case 0:
58
+ out = x;
59
+ break;
60
+ case 1:
61
+ out = x - a;
62
+ break;
63
+ case 2:
64
+ out = x - b;
65
+ break;
66
+ case 3:
67
+ out = x - ((a + b) >> 1);
68
+ break;
69
+ case 4: {
70
+ const p = a + b - c;
71
+ const pa = Math.abs(p - a);
72
+ const pb = Math.abs(p - b);
73
+ const pc = Math.abs(p - c);
74
+ out = x - (pa <= pb && pa <= pc ? a : pb <= pc ? b : c);
75
+ break;
76
+ }
77
+ }
78
+ raw[y * (stride + 1) + 1 + i] = out & 0xff;
79
+ }
80
+ }
81
+ const ihdr = Buffer.alloc(13);
82
+ ihdr.writeUInt32BE(width, 0);
83
+ ihdr.writeUInt32BE(height, 4);
84
+ ihdr[8] = spec.bitDepth ?? 8;
85
+ ihdr[9] = spec.colorType ?? (channels === 4 ? 6 : 2);
86
+ ihdr[10] = 0;
87
+ ihdr[11] = 0;
88
+ ihdr[12] = spec.interlace ?? 0;
89
+ return Buffer.concat([
90
+ Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]),
91
+ chunk('IHDR', ihdr),
92
+ chunk('IDAT', deflateSync(raw)),
93
+ chunk('IEND', Buffer.alloc(0)),
94
+ ]);
95
+ }
96
+
97
+ /** One flat colour — the blank. */
98
+ export function flatPng(width: number, height: number, colour: Rgb, channels: 3 | 4 = 3, filter: PngSpec['filter'] = 0): Buffer {
99
+ return encodePng({ width, height, channels, pixel: () => colour, filter });
100
+ }
@@ -108,7 +108,7 @@ function runMapKeyIsId(input: AxisCheckInput): EvalIssue[] {
108
108
  mkIssue(
109
109
  "render.map_key_is_id",
110
110
  `Array key uses index variable "${key}" — reorders and stream merges will break React reconciliation.`,
111
- `Replace key={${key}} with key={item.id} (or item.symbol / whatever the entity id field is).`,
111
+ `Replace key={${key}} with key={item.id} — or, when the items carry no id, with the field that names each one (item.label / item.symbol / item.name).`,
112
112
  ),
113
113
  );
114
114
  }
@@ -585,6 +585,105 @@ function runActionLabelDropped(input: AxisCheckInput): EvalIssue[] {
585
585
  );
586
586
  }
587
587
 
588
+ // ── universal.fixture_default_echo (ggui#1285 — the fixture echo) ────────
589
+ // The probe and the judge render the source with fixture props, the judge's
590
+ // critique quotes what it saw, and an eval-fix turn can write that text back
591
+ // into the source as a default, a fallback or a placeholder. The fixture is
592
+ // judging data, not the request; a visitor then sees it whenever the agent
593
+ // leaves the prop out (guuey#1642: a builder's system-prompt line became a
594
+ // hello's baked copy). Rule 3 of Data Parameterization stays true — defaults
595
+ // come from the REQUEST — so a fixture value the request (or the contract's
596
+ // own text) states is allowed; only a fixture-only value is the echo. WARN,
597
+ // never fail: the copy is wrong, the card is not broken. Prose-like values
598
+ // only (≥ MIN_ECHO_CHARS and a space), so a city name or a number is never a
599
+ // hit.
600
+ export const MIN_ECHO_CHARS = 12;
601
+ function collectStrings(value: unknown, out: string[]): void {
602
+ if (typeof value === "string") {
603
+ out.push(value);
604
+ } else if (Array.isArray(value)) {
605
+ for (const v of value) collectStrings(v, out);
606
+ } else if (value !== null && typeof value === "object") {
607
+ for (const v of Object.values(value)) collectStrings(v, out);
608
+ }
609
+ }
610
+ /** The fixture's prose-like string values the request never stated, verbatim. */
611
+ export function fixtureOnlyStrings(input: Pick<AxisCheckInput, "fixtureProps" | "originalPrompt" | "contract">): string[] {
612
+ if (input.fixtureProps === undefined) return [];
613
+ const values: string[] = [];
614
+ collectStrings(input.fixtureProps, values);
615
+ const stated = `${input.originalPrompt}\n${input.contract === undefined ? "" : JSON.stringify(input.contract)}`;
616
+ const seen = new Set<string>();
617
+ const out: string[] = [];
618
+ for (const v of values) {
619
+ const t = v.trim();
620
+ if (t.length < MIN_ECHO_CHARS || !t.includes(" ") || seen.has(t)) continue;
621
+ seen.add(t);
622
+ if (stated.includes(t)) continue;
623
+ out.push(t);
624
+ }
625
+ return out;
626
+ }
627
+ function runFixtureDefaultEcho(input: AxisCheckInput): EvalIssue[] {
628
+ if (input.compiledCode === null) return [];
629
+ const src = input.sourceCode;
630
+ const issues: EvalIssue[] = [];
631
+ for (const value of fixtureOnlyStrings(input)) {
632
+ if (!src.includes(value)) continue;
633
+ issues.push(
634
+ mkIssue(
635
+ "universal.fixture_default_echo",
636
+ `A literal in the source equals a fixture value the request never stated: "${value.length > 60 ? `${value.slice(0, 57)}…` : value}". The fixture is judging data; a visitor sees this copy whenever the agent leaves the prop out.`,
637
+ "Take that copy from the prop it renders and drop the literal — no default, fallback or placeholder that repeats a fixture value. A default may only carry text the request itself states.",
638
+ "warn",
639
+ ),
640
+ );
641
+ }
642
+ return issues;
643
+ }
644
+
645
+ // ── universal.form_submit_control (ggui#1443 — a form whose buttons cannot submit it) ──
646
+ // The design `Button` defaults to `type="button"` (the safe default: no accidental
647
+ // submits from secondary controls), so the control that submits a `<form onSubmit>`
648
+ // must say `type="submit"`. A generated composer rendered its Send as a type-less
649
+ // `Button` inside the form: clicking it did nothing, only Enter submitted, and the
650
+ // card passed quality (#1398's dumps: 5 of 9 form cells, both arms). FAIL: a form
651
+ // with buttons and no submitting control is a control that does not work. A form
652
+ // without any button is not this check's claim; a form without `onSubmit` is not
653
+ // read (nothing to submit to).
654
+ const FORM_OPEN_RX = /<form\b[^<>]*\bonSubmit\s*=[^<>]*>/g;
655
+ const CONTROL_TAG_RX = /<(?:Button|button|input)\b[^<>]*>/g;
656
+ const SUBMIT_TYPE_RX = /\btype\s*=\s*(?:["']submit["']|\{\s*["']submit["']\s*\})/;
657
+ /** Every `<form … onSubmit=…>…</form>` block in the source, verbatim (up to the first `</form>` after each opening tag). */
658
+ export function submitForms(sourceCode: string): string[] {
659
+ const out: string[] = [];
660
+ for (const m of sourceCode.matchAll(FORM_OPEN_RX)) {
661
+ const start = m.index ?? 0;
662
+ const end = sourceCode.indexOf("</form>", start + m[0].length);
663
+ out.push(end === -1 ? sourceCode.slice(start) : sourceCode.slice(start, end + "</form>".length));
664
+ }
665
+ return out;
666
+ }
667
+ function runFormSubmitControl(input: AxisCheckInput): EvalIssue[] {
668
+ if (input.compiledCode === null) return [];
669
+ const issues: EvalIssue[] = [];
670
+ for (const form of submitForms(input.sourceCode)) {
671
+ const controls = [...form.matchAll(CONTROL_TAG_RX)].map((m) => m[0]);
672
+ if (controls.length === 0) continue;
673
+ if (controls.some((tag) => SUBMIT_TYPE_RX.test(tag))) continue;
674
+ const named = controls.find((tag) => /<Button\b/.test(tag)) ?? controls[0]!;
675
+ issues.push(
676
+ mkIssue(
677
+ "universal.form_submit_control",
678
+ `A <form onSubmit> renders ${controls.length} control(s) and none submits it: the design Button defaults to type="button", so clicking ${named.length > 60 ? `${named.slice(0, 57)}…` : named} does nothing — only Enter in a field submits.`,
679
+ 'Give the control that submits this form type="submit" — a Button inside a form defaults to type="button" and never submits on click; keep type="button" on the others.',
680
+ "fail",
681
+ ),
682
+ );
683
+ }
684
+ return issues;
685
+ }
686
+
588
687
  export const UNIVERSAL_CHECKS: readonly AxisCheck[] = [
589
688
  {
590
689
  id: "universal.icon_name_known",
@@ -622,6 +721,18 @@ export const UNIVERSAL_CHECKS: readonly AxisCheck[] = [
622
721
  values: ALL_RENDER_VALUES,
623
722
  run: runTerminalActionUnguarded,
624
723
  },
724
+ {
725
+ id: "universal.form_submit_control",
726
+ axis: "render",
727
+ values: ALL_RENDER_VALUES,
728
+ run: runFormSubmitControl,
729
+ },
730
+ {
731
+ id: "universal.fixture_default_echo",
732
+ axis: "render",
733
+ values: ALL_RENDER_VALUES,
734
+ run: runFixtureDefaultEcho,
735
+ },
625
736
  {
626
737
  id: "universal.accent_text_ink",
627
738
  axis: "render",
@@ -2,7 +2,7 @@
2
2
  // whose gate matches the classification's axis vector, and accumulates
3
3
  // issues.
4
4
 
5
- import type { DataContract } from "@ggui-ai/protocol";
5
+ import type { DataContract, JsonObject } from "@ggui-ai/protocol";
6
6
  import type { Classification } from "../../classifier/index.js";
7
7
  import type { EvalIssue } from "../types-public.js";
8
8
  import { REGISTRY } from "./registry.js";
@@ -20,6 +20,8 @@ export interface RunAxisChecksInput {
20
20
  designMode?: DesignMode;
21
21
  /** The canvas the source is judged for — see `AxisCheckInput.canvas` (ggui#1117); absent = no rendering context. */
22
22
  canvas?: CanvasClass;
23
+ /** The props the source was rendered with — see `AxisCheckInput.fixtureProps` (ggui#1285); absent = the echo check stands down. */
24
+ fixtureProps?: JsonObject;
23
25
  }
24
26
 
25
27
  export function runAxisChecks(
@@ -33,6 +35,7 @@ export function runAxisChecks(
33
35
  classification,
34
36
  ...(input.designMode !== undefined ? { designMode: input.designMode } : {}),
35
37
  ...(input.canvas !== undefined ? { canvas: input.canvas } : {}),
38
+ ...(input.fixtureProps !== undefined ? { fixtureProps: input.fixtureProps } : {}),
36
39
  };
37
40
  if (input.compiledCode === null) {
38
41
  traceAxisChecksSkipped(facts, "compiledCode null — no check ran");
@@ -31,15 +31,54 @@ function resolveRegistryId(model: string): ModelId | null {
31
31
  return keys.find((id) => id.endsWith(`/${undated}`)) ?? null;
32
32
  }
33
33
 
34
+ /**
35
+ * ggui#1524 — the largest cache-WRITE premium any registry model states (`cacheWritePer1M / inputPer1M`), never
36
+ * below 1. A cost CAP errs toward over-counting, so a model that states no write rate is priced at this bound
37
+ * times its input rate: a write costs a premium over input wherever one is charged, and pricing it at the bare
38
+ * input rate would slip that premium under the cap. Derived from the registry, so it moves with it.
39
+ */
40
+ export const CACHE_WRITE_PREMIUM_BOUND: number = Math.max(
41
+ 1,
42
+ ...Object.values(MODEL_REGISTRY).flatMap(({ costs }) =>
43
+ costs.cacheWritePer1M !== undefined && costs.inputPer1M > 0 ? [costs.cacheWritePer1M / costs.inputPer1M] : [],
44
+ ),
45
+ );
46
+
47
+ /** Per-1K USD for each kind of token a call can spend. */
48
+ export interface PricePer1k {
49
+ readonly input: number;
50
+ readonly output: number;
51
+ /** Cache reads: the model's stated rate, else its input rate — over-counts a read, the safe side for a cap. */
52
+ readonly cacheRead: number;
53
+ /** Cache writes: the model's stated rate, else its input rate × {@link CACHE_WRITE_PREMIUM_BOUND}. */
54
+ readonly cacheWrite: number;
55
+ }
56
+
34
57
  /** Fallback when a model has no registry entry — Sonnet-class rates. */
35
- const FALLBACK_PER_1K = { input: 0.003, output: 0.015 };
58
+ const FALLBACK_PER_1K: PricePer1k = {
59
+ input: 0.003,
60
+ output: 0.015,
61
+ cacheRead: 0.003,
62
+ cacheWrite: 0.003 * CACHE_WRITE_PREMIUM_BOUND,
63
+ };
36
64
 
37
- /** Per-1K input/output USD for a bare wire model id. */
38
- export function pricePer1k(model: string): { input: number; output: number } {
65
+ /** Per-1K USD for a bare wire model id: input, output, and the two cache kinds. */
66
+ export function pricePer1k(model: string): PricePer1k {
39
67
  const id = resolveRegistryId(model);
40
68
  if (id === null) return FALLBACK_PER_1K;
41
69
  const { costs } = MODEL_REGISTRY[id];
42
- return { input: costs.inputPer1M / 1000, output: costs.outputPer1M / 1000 };
70
+ return {
71
+ input: costs.inputPer1M / 1000,
72
+ output: costs.outputPer1M / 1000,
73
+ cacheRead: (costs.cacheReadPer1M ?? costs.inputPer1M) / 1000,
74
+ cacheWrite: (costs.cacheWritePer1M ?? costs.inputPer1M * CACHE_WRITE_PREMIUM_BOUND) / 1000,
75
+ };
76
+ }
77
+
78
+ /** ggui#1524 — the cache tokens a call spent, as its provider reported them; absent = unreported. */
79
+ export interface CacheSpend {
80
+ readonly read?: number;
81
+ readonly write?: number;
43
82
  }
44
83
 
45
84
  export class CostTracker {
@@ -47,9 +86,18 @@ export class CostTracker {
47
86
 
48
87
  constructor(private maxBudget: number | null) {}
49
88
 
50
- record(model: string, inputTokens: number, outputTokens: number): number {
89
+ /**
90
+ * Record one call. `inputTokens` is the provider's UNCACHED input; a call that read or wrote the prompt cache
91
+ * passes those counts in `cache` (ggui#1524), priced at the cache rates — until then a turn with a long cached
92
+ * prefix cost the cap almost nothing while the provider billed its reads and writes.
93
+ */
94
+ record(model: string, inputTokens: number, outputTokens: number, cache: CacheSpend = {}): number {
51
95
  const prices = pricePer1k(model);
52
- const cost = (inputTokens / 1000) * prices.input + (outputTokens / 1000) * prices.output;
96
+ const cost =
97
+ (inputTokens / 1000) * prices.input +
98
+ (outputTokens / 1000) * prices.output +
99
+ ((cache.read ?? 0) / 1000) * prices.cacheRead +
100
+ ((cache.write ?? 0) / 1000) * prices.cacheWrite;
53
101
  this.totalCost += cost;
54
102
  return cost;
55
103
  }
@@ -0,0 +1,85 @@
1
+ /**
2
+ * The visual criteria bank — ggui#1436 (step 1 of the founder's visual-assessment steer).
3
+ *
4
+ * The bank is DATA, never code (#1437 authors it; #1060's bar-and-judge policy): a versioned set of
5
+ * criteria the judge, an instrument or a person answers off the frame, split as the founder ruled —
6
+ * `severity` is importance, `evaluation` the method, `status` the method's readiness (the only source
7
+ * of an `n/a`), `scope` where a row applies ({} = every card; {kind} / {canvas} / {preset} = one). The
8
+ * general rows are `criteria`; a preset's own expression is a `recipes` row carrying `scope.preset`.
9
+ * This module is the ONE loader: a malformed bank refuses at launch with the offending path named,
10
+ * never mid-run. Fields the loader does not read (`examples`, `fields`, `groups`, `seed`) are page
11
+ * matter and are dropped here; an additive field tomorrow does not break today's loader.
12
+ */
13
+ import { z } from 'zod';
14
+
15
+ /** `family.property[.facet]` — `read.contrast`, `hello.field`. */
16
+ const CRITERION_ID = /^[a-z][a-z0-9-]*(?:\.[a-zA-Z][a-zA-Z0-9-]*)+$/;
17
+
18
+ /** Where a row applies; every key named must match the card's context. */
19
+ export const criteriaScopeSchema = z.object({
20
+ kind: z.string().min(1).optional(),
21
+ canvas: z.string().min(1).optional(),
22
+ preset: z.string().min(1).optional(),
23
+ });
24
+
25
+ export const bankRowSchema = z.object({
26
+ id: z.string().regex(CRITERION_ID, 'a criterion id is family.property[.facet], lower-case family'),
27
+ group: z.string().optional(),
28
+ scope: criteriaScopeSchema.default({}),
29
+ /** Importance, independent of whether anything automated checks it yet. */
30
+ severity: z.enum(['must', 'should']),
31
+ /** The method that produces the verdict. */
32
+ evaluation: z.enum(['instrument', 'judge', 'human']),
33
+ /** The method's readiness — the only source of an `n/a`. */
34
+ status: z.enum(['live', 'reference', 'planned', 'manual']),
35
+ /** The question, answerable yes/no from the frame. */
36
+ text: z.string().min(1),
37
+ /** The evidence rule: what the one-sentence evidence must name (a region or element). */
38
+ evidence: z.string().min(1),
39
+ /** The templated fix line a feedback channel may consume later; never read in v1. */
40
+ fix: z.string().optional(),
41
+ });
42
+
43
+ export const criteriaBankSchema = z
44
+ .object({
45
+ version: z.string().min(1),
46
+ applies: z
47
+ .object({
48
+ /** `["*"]` = every kind. */
49
+ kind: z.array(z.string().min(1)).optional(),
50
+ canvases: z.array(z.string().min(1)).optional(),
51
+ })
52
+ .optional(),
53
+ criteria: z.array(bankRowSchema).min(1),
54
+ recipes: z.array(bankRowSchema).optional(),
55
+ })
56
+ .superRefine((bank, ctx) => {
57
+ const seen = new Set<string>();
58
+ const check = (rows: readonly { id: string }[], key: 'criteria' | 'recipes'): void => {
59
+ rows.forEach((c, i) => {
60
+ if (seen.has(c.id)) ctx.addIssue({ code: 'custom', path: [key, i, 'id'], message: `duplicate criterion id ${c.id}` });
61
+ seen.add(c.id);
62
+ });
63
+ };
64
+ check(bank.criteria, 'criteria');
65
+ check(bank.recipes ?? [], 'recipes');
66
+ });
67
+
68
+ export type CriteriaBank = z.infer<typeof criteriaBankSchema>;
69
+ export type BankRow = z.infer<typeof bankRowSchema>;
70
+ export type CriteriaScope = z.infer<typeof criteriaScopeSchema>;
71
+
72
+ /** Every row of the bank — the general criteria, then the recipes. */
73
+ export function bankRows(bank: CriteriaBank): readonly BankRow[] {
74
+ return [...bank.criteria, ...(bank.recipes ?? [])];
75
+ }
76
+
77
+ /** The one door for bank data: refuses loudly, naming the path, before anything is judged. */
78
+ export function parseCriteriaBank(raw: unknown): CriteriaBank {
79
+ const parsed = criteriaBankSchema.safeParse(raw);
80
+ if (!parsed.success) {
81
+ const issues = parsed.error.issues.map((i) => `${i.path.map(String).join('.') || '<root>'}: ${i.message}`).join('; ');
82
+ throw new Error(`criteria bank invalid — ${issues}`);
83
+ }
84
+ return parsed.data;
85
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * The selector's inputs for one card — ggui#1436. Every key here is hashed into `criteriaSetId`
3
+ * and STORED in the block (the CTO's A1), so a stored verdict reproduces its own set from the
4
+ * record alone. `canvas` is added per canvas inside the judge; the rest is the harness's.
5
+ */
6
+ import type { DataContract } from '@ggui-ai/protocol';
7
+ import type { Classification } from '../../classifier/axes.js';
8
+ import type { GenerationProfileInput } from '../../boilerplate/styling-profile.js';
9
+ import type { CriteriaChroma, CriteriaContext } from '../types-public.js';
10
+
11
+ /** The primary at its 500 step, as the design's tokens render it (`--ggui-color-primary-500: #rrggbb;`). */
12
+ const PRIMARY_500 = /--ggui-color-primary-500:\s*(#[0-9a-fA-F]{3}|#[0-9a-fA-F]{6})\s*;/;
13
+ /** Below this max−min spread (0..1) a primary reads as achromatic — an inversion, not a hue. */
14
+ const ACHROMATIC_SPREAD = 0.08;
15
+
16
+ /** Read the theme's chroma off its tokens; `unknown` when no primary is named. */
17
+ export function chromaOfCssTokens(cssTokens: string | undefined): CriteriaChroma {
18
+ const m = cssTokens?.match(PRIMARY_500);
19
+ if (!m) return 'unknown';
20
+ const hex = m[1]!.slice(1);
21
+ const full = hex.length === 3 ? hex.split('').map((c) => c + c).join('') : hex;
22
+ const channels = [0, 2, 4].map((i) => parseInt(full.slice(i, i + 2), 16));
23
+ const spread = (Math.max(...channels) - Math.min(...channels)) / 255;
24
+ return spread < ACHROMATIC_SPREAD ? 'achromatic' : 'chromatic';
25
+ }
26
+
27
+ export interface CriteriaContextSources {
28
+ readonly classification: Classification;
29
+ readonly contract: DataContract | undefined;
30
+ readonly cssTokens: string | undefined;
31
+ readonly profile: GenerationProfileInput | undefined;
32
+ readonly shell: string;
33
+ readonly kind?: string;
34
+ }
35
+
36
+ /** The context minus `canvas` — the judge fills that per canvas. */
37
+ export type CriteriaContextInput = Omit<CriteriaContext, 'canvas'>;
38
+
39
+ export function criteriaContextFor(src: CriteriaContextSources): CriteriaContextInput {
40
+ const v = src.classification.vector;
41
+ const preset = src.profile?.aesthetic?.id;
42
+ return {
43
+ hasActions: Object.keys(src.contract?.actionSpec ?? {}).length > 0,
44
+ riskTier: src.classification.riskTier,
45
+ axes: { render: v.render, state: v.state, writes: v.writes, fetch: v.fetch, layout: v.layout },
46
+ chroma: chromaOfCssTokens(src.cssTokens),
47
+ profilePresent: src.profile !== undefined,
48
+ shell: src.shell,
49
+ ...(src.kind !== undefined ? { kind: src.kind } : {}),
50
+ ...(preset !== undefined ? { preset } : {}),
51
+ };
52
+ }