@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
@@ -5,7 +5,8 @@ import { z } from 'zod';
5
5
  import { LUCIDE_ICON_NAMES, maxWidth } from '@ggui-ai/design';
6
6
  import { createHash } from 'crypto';
7
7
  import { build } from 'esbuild';
8
- import { getCssTokens, composeThemeCss, fillFitRule } from '@ggui-ai/design/rendering';
8
+ import { EXPANDED_FRAME, getCssTokens, composeThemeCss, expandedFrameScrimDecls, fillFitRule, expandedFramePanelRule } from '@ggui-ai/design/rendering';
9
+ import { inflateSync } from 'zlib';
9
10
  import { readFileSync, readdirSync, statSync, mkdtempSync, writeFileSync, rmSync, existsSync } from 'fs';
10
11
  import { resolve, relative, dirname } from 'path';
11
12
  import { fileURLToPath } from 'url';
@@ -815,6 +816,26 @@ async function collectLoginTurn(stream, opts) {
815
816
  }
816
817
 
817
818
  // src/harness/llm-router.ts
819
+ function servedModelOf(name) {
820
+ return typeof name === "string" && name.length > 0 ? { servedModel: name } : {};
821
+ }
822
+ function anthropicFinishReason(stopReason) {
823
+ if (stopReason === null || stopReason === void 0) return void 0;
824
+ if (stopReason === "end_turn" || stopReason === "stop_sequence") return "stop";
825
+ if (stopReason === "max_tokens") return "length";
826
+ if (stopReason === "refusal") return "content-filter";
827
+ return "other";
828
+ }
829
+ function googleFinishReason(finishReason) {
830
+ if (finishReason === null || finishReason === void 0) return void 0;
831
+ if (finishReason === "STOP") return "stop";
832
+ if (finishReason === "MAX_TOKENS") return "length";
833
+ if (["SAFETY", "RECITATION", "BLOCKLIST", "PROHIBITED_CONTENT", "SPII", "IMAGE_SAFETY"].includes(finishReason)) return "content-filter";
834
+ return "other";
835
+ }
836
+ function visionFinish(finishReason) {
837
+ return finishReason !== void 0 ? { finishReason } : {};
838
+ }
818
839
  function addStrictSchemaConstraints(schema) {
819
840
  const result = { ...schema };
820
841
  if (result.type === "object" && result.properties) {
@@ -1155,7 +1176,9 @@ var AnthropicAgent = class extends LLMAgent {
1155
1176
  return {
1156
1177
  text,
1157
1178
  inputTokens: response.usage.input_tokens,
1158
- outputTokens: response.usage.output_tokens
1179
+ outputTokens: response.usage.output_tokens,
1180
+ ...visionFinish(anthropicFinishReason(response.stop_reason)),
1181
+ ...servedModelOf(response.model)
1159
1182
  };
1160
1183
  } catch (e) {
1161
1184
  emitLlmTraceEvent({
@@ -1766,7 +1789,9 @@ var GoogleAgent = class extends LLMAgent {
1766
1789
  text,
1767
1790
  // ggui#1186 (third site): `promptTokenCount` includes `cachedContentTokenCount` — report the non-cached input.
1768
1791
  inputTokens: splitCacheInclusiveUsage(usage?.promptTokenCount ?? 0, usage?.candidatesTokenCount ?? 0, usage?.cachedContentTokenCount ?? 0).tokens.input,
1769
- outputTokens: usage?.candidatesTokenCount ?? 0
1792
+ outputTokens: usage?.candidatesTokenCount ?? 0,
1793
+ ...visionFinish(googleFinishReason(response.candidates?.[0]?.finishReason)),
1794
+ ...servedModelOf(response.modelVersion)
1770
1795
  };
1771
1796
  const endedAt = Date.now();
1772
1797
  emitLlmTraceEvent({
@@ -2513,12 +2538,12 @@ async function runEvaluationLoop(options) {
2513
2538
  const evaluationResults = [];
2514
2539
  let currentCode = context.compiledCode;
2515
2540
  let currentSourceCode = context.sourceCode;
2516
- let round = 0;
2541
+ let round2 = 0;
2517
2542
  const maxRounds = Math.min(config.maxRounds ?? 3, MAX_EVAL_ROUNDS_HARD_LIMIT);
2518
- while (round < maxRounds) {
2519
- round++;
2520
- onProgress?.({ type: "evaluating", round });
2521
- console.log(`[eval] Round ${round}: evaluating...`);
2543
+ while (round2 < maxRounds) {
2544
+ round2++;
2545
+ onProgress?.({ type: "evaluating", round: round2 });
2546
+ console.log(`[eval] Round ${round2}: evaluating...`);
2522
2547
  const evalContext = {
2523
2548
  ...context,
2524
2549
  sourceCode: currentSourceCode || context.sourceCode
@@ -2526,13 +2551,13 @@ async function runEvaluationLoop(options) {
2526
2551
  const evalResult = await runEvaluation(evalContext, config);
2527
2552
  evaluationResults.push(evalResult);
2528
2553
  console.log(
2529
- `[eval] Round ${round}: score=${evalResult.finalScore}, passed=${evalResult.passed}, issues=${evalResult.issues.length}`
2554
+ `[eval] Round ${round2}: score=${evalResult.finalScore}, passed=${evalResult.passed}, issues=${evalResult.issues.length}`
2530
2555
  );
2531
- if (evalResult.passed || round >= maxRounds) {
2556
+ if (evalResult.passed || round2 >= maxRounds) {
2532
2557
  break;
2533
2558
  }
2534
- onProgress?.({ type: "fixing", round });
2535
- console.log(`[eval] Round ${round}: fixing...`);
2559
+ onProgress?.({ type: "fixing", round: round2 });
2560
+ console.log(`[eval] Round ${round2}: fixing...`);
2536
2561
  const fixPrompt = buildFixPrompt(evalResult, context.originalPrompt);
2537
2562
  const cliPath = process.env.CLAUDE_WRAPPER_PATH;
2538
2563
  const env = {};
@@ -2572,7 +2597,7 @@ async function runEvaluationLoop(options) {
2572
2597
  }
2573
2598
  if (fixedCompiledCode) {
2574
2599
  currentCode = fixedCompiledCode;
2575
- console.log(`[eval] Round ${round}: fixed code captured (${currentCode.length} bytes)`);
2600
+ console.log(`[eval] Round ${round2}: fixed code captured (${currentCode.length} bytes)`);
2576
2601
  }
2577
2602
  if (fixedSourceCode) {
2578
2603
  currentSourceCode = fixedSourceCode;
@@ -3295,6 +3320,81 @@ function runActionLabelDropped(input) {
3295
3320
  )
3296
3321
  );
3297
3322
  }
3323
+ var MIN_ECHO_CHARS = 12;
3324
+ function collectStrings(value, out) {
3325
+ if (typeof value === "string") {
3326
+ out.push(value);
3327
+ } else if (Array.isArray(value)) {
3328
+ for (const v of value) collectStrings(v, out);
3329
+ } else if (value !== null && typeof value === "object") {
3330
+ for (const v of Object.values(value)) collectStrings(v, out);
3331
+ }
3332
+ }
3333
+ function fixtureOnlyStrings(input) {
3334
+ if (input.fixtureProps === void 0) return [];
3335
+ const values = [];
3336
+ collectStrings(input.fixtureProps, values);
3337
+ const stated = `${input.originalPrompt}
3338
+ ${input.contract === void 0 ? "" : JSON.stringify(input.contract)}`;
3339
+ const seen = /* @__PURE__ */ new Set();
3340
+ const out = [];
3341
+ for (const v of values) {
3342
+ const t = v.trim();
3343
+ if (t.length < MIN_ECHO_CHARS || !t.includes(" ") || seen.has(t)) continue;
3344
+ seen.add(t);
3345
+ if (stated.includes(t)) continue;
3346
+ out.push(t);
3347
+ }
3348
+ return out;
3349
+ }
3350
+ function runFixtureDefaultEcho(input) {
3351
+ if (input.compiledCode === null) return [];
3352
+ const src = input.sourceCode;
3353
+ const issues = [];
3354
+ for (const value of fixtureOnlyStrings(input)) {
3355
+ if (!src.includes(value)) continue;
3356
+ issues.push(
3357
+ mkIssue(
3358
+ "universal.fixture_default_echo",
3359
+ `A literal in the source equals a fixture value the request never stated: "${value.length > 60 ? `${value.slice(0, 57)}\u2026` : value}". The fixture is judging data; a visitor sees this copy whenever the agent leaves the prop out.`,
3360
+ "Take that copy from the prop it renders and drop the literal \u2014 no default, fallback or placeholder that repeats a fixture value. A default may only carry text the request itself states.",
3361
+ "warn"
3362
+ )
3363
+ );
3364
+ }
3365
+ return issues;
3366
+ }
3367
+ var FORM_OPEN_RX = /<form\b[^<>]*\bonSubmit\s*=[^<>]*>/g;
3368
+ var CONTROL_TAG_RX = /<(?:Button|button|input)\b[^<>]*>/g;
3369
+ var SUBMIT_TYPE_RX = /\btype\s*=\s*(?:["']submit["']|\{\s*["']submit["']\s*\})/;
3370
+ function submitForms(sourceCode) {
3371
+ const out = [];
3372
+ for (const m of sourceCode.matchAll(FORM_OPEN_RX)) {
3373
+ const start = m.index ?? 0;
3374
+ const end = sourceCode.indexOf("</form>", start + m[0].length);
3375
+ out.push(end === -1 ? sourceCode.slice(start) : sourceCode.slice(start, end + "</form>".length));
3376
+ }
3377
+ return out;
3378
+ }
3379
+ function runFormSubmitControl(input) {
3380
+ if (input.compiledCode === null) return [];
3381
+ const issues = [];
3382
+ for (const form of submitForms(input.sourceCode)) {
3383
+ const controls = [...form.matchAll(CONTROL_TAG_RX)].map((m) => m[0]);
3384
+ if (controls.length === 0) continue;
3385
+ if (controls.some((tag) => SUBMIT_TYPE_RX.test(tag))) continue;
3386
+ const named = controls.find((tag) => /<Button\b/.test(tag)) ?? controls[0];
3387
+ issues.push(
3388
+ mkIssue(
3389
+ "universal.form_submit_control",
3390
+ `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)}\u2026` : named} does nothing \u2014 only Enter in a field submits.`,
3391
+ 'Give the control that submits this form type="submit" \u2014 a Button inside a form defaults to type="button" and never submits on click; keep type="button" on the others.',
3392
+ "fail"
3393
+ )
3394
+ );
3395
+ }
3396
+ return issues;
3397
+ }
3298
3398
  var UNIVERSAL_CHECKS = [
3299
3399
  {
3300
3400
  id: "universal.icon_name_known",
@@ -3332,6 +3432,18 @@ var UNIVERSAL_CHECKS = [
3332
3432
  values: ALL_RENDER_VALUES,
3333
3433
  run: runTerminalActionUnguarded
3334
3434
  },
3435
+ {
3436
+ id: "universal.form_submit_control",
3437
+ axis: "render",
3438
+ values: ALL_RENDER_VALUES,
3439
+ run: runFormSubmitControl
3440
+ },
3441
+ {
3442
+ id: "universal.fixture_default_echo",
3443
+ axis: "render",
3444
+ values: ALL_RENDER_VALUES,
3445
+ run: runFixtureDefaultEcho
3446
+ },
3335
3447
  {
3336
3448
  id: "universal.accent_text_ink",
3337
3449
  axis: "render",
@@ -3455,7 +3567,7 @@ function runMapKeyIsId(input) {
3455
3567
  mkIssue(
3456
3568
  "render.map_key_is_id",
3457
3569
  `Array key uses index variable "${key}" \u2014 reorders and stream merges will break React reconciliation.`,
3458
- `Replace key={${key}} with key={item.id} (or item.symbol / whatever the entity id field is).`
3570
+ `Replace key={${key}} with key={item.id} \u2014 or, when the items carry no id, with the field that names each one (item.label / item.symbol / item.name).`
3459
3571
  )
3460
3572
  );
3461
3573
  }
@@ -4391,7 +4503,8 @@ function runAxisChecks(classification, input) {
4391
4503
  originalPrompt: input.originalPrompt,
4392
4504
  classification,
4393
4505
  ...input.designMode !== void 0 ? { designMode: input.designMode } : {},
4394
- ...input.canvas !== void 0 ? { canvas: input.canvas } : {}
4506
+ ...input.canvas !== void 0 ? { canvas: input.canvas } : {},
4507
+ ...input.fixtureProps !== void 0 ? { fixtureProps: input.fixtureProps } : {}
4395
4508
  };
4396
4509
  if (input.compiledCode === null) {
4397
4510
  traceAxisChecksSkipped(facts, "compiledCode null \u2014 no check ran");
@@ -4498,6 +4611,337 @@ function buildStylingProfileJudgeBlock(profile) {
4498
4611
  if (quote.length === 0) return "";
4499
4612
  return ["## Declared styling profile", "", STYLING_PROFILE_JUDGE_FRAME, "", quote].join("\n");
4500
4613
  }
4614
+ var CRITERION_ID = /^[a-z][a-z0-9-]*(?:\.[a-zA-Z][a-zA-Z0-9-]*)+$/;
4615
+ var criteriaScopeSchema = z.object({
4616
+ kind: z.string().min(1).optional(),
4617
+ canvas: z.string().min(1).optional(),
4618
+ preset: z.string().min(1).optional()
4619
+ });
4620
+ var bankRowSchema = z.object({
4621
+ id: z.string().regex(CRITERION_ID, "a criterion id is family.property[.facet], lower-case family"),
4622
+ group: z.string().optional(),
4623
+ scope: criteriaScopeSchema.default({}),
4624
+ /** Importance, independent of whether anything automated checks it yet. */
4625
+ severity: z.enum(["must", "should"]),
4626
+ /** The method that produces the verdict. */
4627
+ evaluation: z.enum(["instrument", "judge", "human"]),
4628
+ /** The method's readiness — the only source of an `n/a`. */
4629
+ status: z.enum(["live", "reference", "planned", "manual"]),
4630
+ /** The question, answerable yes/no from the frame. */
4631
+ text: z.string().min(1),
4632
+ /** The evidence rule: what the one-sentence evidence must name (a region or element). */
4633
+ evidence: z.string().min(1),
4634
+ /** The templated fix line a feedback channel may consume later; never read in v1. */
4635
+ fix: z.string().optional()
4636
+ });
4637
+ var criteriaBankSchema = z.object({
4638
+ version: z.string().min(1),
4639
+ applies: z.object({
4640
+ /** `["*"]` = every kind. */
4641
+ kind: z.array(z.string().min(1)).optional(),
4642
+ canvases: z.array(z.string().min(1)).optional()
4643
+ }).optional(),
4644
+ criteria: z.array(bankRowSchema).min(1),
4645
+ recipes: z.array(bankRowSchema).optional()
4646
+ }).superRefine((bank, ctx) => {
4647
+ const seen = /* @__PURE__ */ new Set();
4648
+ const check = (rows, key) => {
4649
+ rows.forEach((c, i) => {
4650
+ if (seen.has(c.id)) ctx.addIssue({ code: "custom", path: [key, i, "id"], message: `duplicate criterion id ${c.id}` });
4651
+ seen.add(c.id);
4652
+ });
4653
+ };
4654
+ check(bank.criteria, "criteria");
4655
+ check(bank.recipes ?? [], "recipes");
4656
+ });
4657
+ function bankRows(bank) {
4658
+ return [...bank.criteria, ...bank.recipes ?? []];
4659
+ }
4660
+ function parseCriteriaBank(raw) {
4661
+ const parsed = criteriaBankSchema.safeParse(raw);
4662
+ if (!parsed.success) {
4663
+ const issues = parsed.error.issues.map((i) => `${i.path.map(String).join(".") || "<root>"}: ${i.message}`).join("; ");
4664
+ throw new Error(`criteria bank invalid \u2014 ${issues}`);
4665
+ }
4666
+ return parsed.data;
4667
+ }
4668
+
4669
+ // src/evaluation/criteria/select.ts
4670
+ var CRITERIA_SELECTOR_VERSION = "selector@2";
4671
+ function canonicalCriteriaContext(ctx) {
4672
+ const sortKeys = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? Object.fromEntries(
4673
+ Object.entries(value).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([k, v]) => [k, sortKeys(v)])
4674
+ ) : value;
4675
+ return JSON.stringify(sortKeys(ctx));
4676
+ }
4677
+ function criteriaSetIdFor(bankVersion, ctx) {
4678
+ return createHash("sha256").update(`${bankVersion}|${CRITERIA_SELECTOR_VERSION}|${canonicalCriteriaContext(ctx)}`, "utf8").digest("hex").slice(0, 16);
4679
+ }
4680
+ function matchedScope(row, ctx) {
4681
+ const s = row.scope;
4682
+ const matched = [];
4683
+ if (s.kind !== void 0) {
4684
+ if (ctx.kind !== s.kind) return null;
4685
+ matched.push(`kind=${s.kind}`);
4686
+ }
4687
+ if (s.canvas !== void 0) {
4688
+ if (ctx.canvas !== s.canvas) return null;
4689
+ matched.push(`canvas=${s.canvas}`);
4690
+ }
4691
+ if (s.preset !== void 0) {
4692
+ if (ctx.preset !== s.preset) return null;
4693
+ matched.push(`preset=${s.preset}`);
4694
+ }
4695
+ return matched;
4696
+ }
4697
+ function selectCriteria(bank, ctx) {
4698
+ const criteriaSetId = criteriaSetIdFor(bank.version, ctx);
4699
+ const applies = bank.applies;
4700
+ if (applies?.canvases !== void 0 && !applies.canvases.includes(ctx.canvas)) return { criteriaSetId, selection: [] };
4701
+ if (applies?.kind !== void 0 && ctx.kind !== void 0 && !applies.kind.includes("*") && !applies.kind.includes(ctx.kind)) {
4702
+ return { criteriaSetId, selection: [] };
4703
+ }
4704
+ const selection = [];
4705
+ for (const row of bankRows(bank)) {
4706
+ const matched = matchedScope(row, ctx);
4707
+ if (matched === null) continue;
4708
+ selection.push(matched.length === 0 ? { id: row.id, source: "static", reason: "static" } : { id: row.id, source: "context", reason: `context: ${matched.join(", ")}` });
4709
+ }
4710
+ return { criteriaSetId, selection };
4711
+ }
4712
+
4713
+ // src/evaluation/criteria/resolve.ts
4714
+ var INSTRUMENT_BY_ID = {
4715
+ "comp.fit": "fit",
4716
+ "floor.fit": "fit",
4717
+ "finish.canvas.inhabited": "fill"
4718
+ };
4719
+ var INSTRUMENT_BESIDE_JUDGE = {
4720
+ "space.edge": "edge"
4721
+ };
4722
+ function besideRead(kind, m) {
4723
+ if (m.edge === void 0) return { verdict: "n/a", evidence: "not measured: this capture had no page to read" };
4724
+ if (m.edge === null) return { verdict: "n/a", evidence: "edge probe failed on this capture" };
4725
+ return { verdict: m.edge.verdict, evidence: m.edge.evidence };
4726
+ }
4727
+ function majority(id, answers, unansweredReason) {
4728
+ const votes = answers.flatMap((a) => a.filter((x) => x.id === id));
4729
+ if (votes.length === 0) return { verdict: "n/a", evidence: unansweredReason ?? "not answered" };
4730
+ const counts = /* @__PURE__ */ new Map();
4731
+ for (const v of votes) counts.set(v.verdict, (counts.get(v.verdict) ?? 0) + 1);
4732
+ const ranked = [...counts.entries()].sort((a, b) => b[1] - a[1]);
4733
+ const top = ranked[0];
4734
+ if (ranked.length > 1 && ranked[1][1] === top[1]) return { verdict: "n/a", evidence: "no majority" };
4735
+ const evidence = votes.find((v) => v.verdict === top[0])?.evidence ?? "";
4736
+ return { verdict: top[0], evidence };
4737
+ }
4738
+ function instrumentRead(kind, m) {
4739
+ if (kind === "fit") {
4740
+ if (m.contentHeight === null) return { verdict: "n/a", evidence: "content height unmeasurable" };
4741
+ return { verdict: m.overflow ? "fail" : "pass", evidence: `content ${m.contentHeight}px against a ${m.viewportHeight}px box` };
4742
+ }
4743
+ if (m.inkRatio === null) return { verdict: "n/a", evidence: "capture unreadable" };
4744
+ return { verdict: "n/a", evidence: `fill ${m.inkRatio.toFixed(3)} reported; no floor ruled` };
4745
+ }
4746
+ function readRow(row, answers, m, unansweredReason) {
4747
+ if (row.evaluation === "judge") return majority(row.id, answers, unansweredReason);
4748
+ if (row.evaluation === "human") return { verdict: "n/a", evidence: `reader's column (status ${row.status})` };
4749
+ if (row.status !== "live") return { verdict: "n/a", evidence: `not implemented (status ${row.status})` };
4750
+ const kind = INSTRUMENT_BY_ID[row.id];
4751
+ return kind === void 0 ? { verdict: "n/a", evidence: "instrument not wired (status live, no binding)" } : instrumentRead(kind, m);
4752
+ }
4753
+ function resolveCriteriaBlock(args) {
4754
+ const byId = new Map(bankRows(args.bank).map((r) => [r.id, r]));
4755
+ const verdicts = [];
4756
+ for (const s of args.selected.selection) {
4757
+ const row = byId.get(s.id);
4758
+ if (row === void 0) continue;
4759
+ const read = readRow(row, args.answers, args.measurements, args.unansweredReason);
4760
+ const beside = INSTRUMENT_BESIDE_JUDGE[row.id];
4761
+ verdicts.push({
4762
+ id: row.id,
4763
+ severity: row.severity,
4764
+ method: row.evaluation,
4765
+ status: row.status,
4766
+ source: s.source,
4767
+ verdict: read.verdict,
4768
+ evidence: read.evidence,
4769
+ ...beside !== void 0 && row.evaluation === "judge" ? { instrument: besideRead(beside, args.measurements) } : {}
4770
+ });
4771
+ }
4772
+ return {
4773
+ criteriaSetId: args.selected.criteriaSetId,
4774
+ bankVersion: args.bank.version,
4775
+ selectorVersion: CRITERIA_SELECTOR_VERSION,
4776
+ context: args.context,
4777
+ selection: args.selected.selection,
4778
+ verdicts,
4779
+ ...args.call !== void 0 ? {
4780
+ call: {
4781
+ answer: args.call.answer,
4782
+ // Kept answers whose id names no row of the bank: the judge answered something nobody asked.
4783
+ unknownIds: args.answers.flat().filter((a) => !byId.has(a.id)).length,
4784
+ ...args.call.finishReason !== void 0 ? { finishReason: args.call.finishReason } : {}
4785
+ }
4786
+ } : {}
4787
+ };
4788
+ }
4789
+ var CRITERIA_PROPS_MAX_CHARS = 2e3;
4790
+ function criteriaFrameLine(frame) {
4791
+ const box = `${frame.width}\xD7${frame.height}`;
4792
+ if (frame.canvas === "xs-chat-card") {
4793
+ const framedBy = frame.hostLabel !== void 0 ? `inside the host's own frame (${frame.hostLabel}), on the host's ground` : "inside a generic stand-in for the host's frame \u2014 the card's surface with a thin ring and rounded corners, on the page ground";
4794
+ 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.`;
4795
+ }
4796
+ return `This frame is ${box}, a fixed full-screen box the card fills${frame.hostLabel !== void 0 ? ` (${frame.hostLabel})` : ""}: judge composition and space against the whole box, edge to edge.`;
4797
+ }
4798
+ function criteriaPropsSection(propsJson) {
4799
+ const text = propsJson.length > CRITERIA_PROPS_MAX_CHARS ? `${propsJson.slice(0, CRITERIA_PROPS_MAX_CHARS)}\u2026 (truncated)` : propsJson;
4800
+ return `The one input besides the frame \u2014 the props this card was rendered with. Read them ONLY for rows whose evidence names props (for example, whether a rendered string came from them); every other row is read off the frame alone:
4801
+ ${text}
4802
+ `;
4803
+ }
4804
+ function buildCriteriaJudgeBlock(bank, selected, inputs = {}) {
4805
+ const byId = new Map(bankRows(bank).map((r) => [r.id, r]));
4806
+ const asked = selected.selection.map((s) => byId.get(s.id)).filter((r) => r !== void 0 && r.evaluation === "judge");
4807
+ if (asked.length === 0) return "";
4808
+ const lines = asked.map((r) => `- ${r.id} (${r.severity}): ${r.text} Evidence must name: ${r.evidence}`);
4809
+ return "## Criteria \u2014 answer each one from the FRAME only\nRead these off the screenshot. Never infer them from the request, the styling profile or any direction text above.\n" + (inputs.frame !== void 0 ? `${criteriaFrameLine(inputs.frame)}
4810
+ ` : "") + (inputs.propsJson !== void 0 ? criteriaPropsSection(inputs.propsJson) : "") + `${lines.join("\n")}
4811
+ Add to your JSON a top-level "criteria" array with exactly these ids: [{ "id": "<id>", "verdict": "pass" | "fail" | "n/a", "evidence": "<one sentence naming the region or element you read it from>" }]. Every row here was selected as applicable to this card, so "n/a" never means "does not apply": use it ONLY when this frame cannot show the criterion \u2014 it is then recorded as not evaluated.`;
4812
+ }
4813
+ var INK_TOLERANCE = 8;
4814
+ var PNG_SIGNATURE = [137, 80, 78, 71, 13, 10, 26, 10];
4815
+ function decodePng(png) {
4816
+ const buf = Buffer.from(png.buffer, png.byteOffset, png.byteLength);
4817
+ if (buf.length < 8 + 25) return { reason: `not a PNG: ${buf.length} bytes` };
4818
+ for (let i = 0; i < PNG_SIGNATURE.length; i++) {
4819
+ if (buf[i] !== PNG_SIGNATURE[i]) return { reason: "not a PNG: signature mismatch" };
4820
+ }
4821
+ let width = 0;
4822
+ let height = 0;
4823
+ let channels = null;
4824
+ const idat = [];
4825
+ let offset = 8;
4826
+ while (offset + 8 <= buf.length) {
4827
+ const length = buf.readUInt32BE(offset);
4828
+ const type = buf.toString("latin1", offset + 4, offset + 8);
4829
+ const start = offset + 8;
4830
+ if (start + length + 4 > buf.length) return { reason: `truncated PNG: ${type} chunk runs past the end` };
4831
+ const data = buf.subarray(start, start + length);
4832
+ if (type === "IHDR") {
4833
+ if (length !== 13) return { reason: `malformed IHDR: ${length} bytes` };
4834
+ width = data.readUInt32BE(0);
4835
+ height = data.readUInt32BE(4);
4836
+ const bitDepth = data[8];
4837
+ const colorType = data[9];
4838
+ const interlace = data[12];
4839
+ if (bitDepth !== 8) return { reason: `unsupported PNG: bit depth ${bitDepth}` };
4840
+ if (interlace !== 0) return { reason: "unsupported PNG: interlaced" };
4841
+ if (colorType === 2) channels = 3;
4842
+ else if (colorType === 6) channels = 4;
4843
+ else return { reason: `unsupported PNG: colour type ${colorType}` };
4844
+ } else if (type === "IDAT") {
4845
+ idat.push(data);
4846
+ } else if (type === "IEND") {
4847
+ break;
4848
+ }
4849
+ offset = start + length + 4;
4850
+ }
4851
+ if (channels === null) return { reason: "malformed PNG: no IHDR" };
4852
+ if (width === 0 || height === 0) return { reason: `empty PNG: ${width}\xD7${height}` };
4853
+ if (idat.length === 0) return { reason: "malformed PNG: no IDAT" };
4854
+ let raw;
4855
+ try {
4856
+ raw = inflateSync(Buffer.concat(idat));
4857
+ } catch (error) {
4858
+ return { reason: `corrupt IDAT stream: ${error instanceof Error ? error.message : String(error)}` };
4859
+ }
4860
+ const stride = width * channels;
4861
+ if (raw.length !== (stride + 1) * height) {
4862
+ return { reason: `malformed PNG: ${raw.length} raw bytes for ${width}\xD7${height}\xD7${channels}` };
4863
+ }
4864
+ const pixels = new Uint8Array(stride * height);
4865
+ const bpp = channels;
4866
+ for (let y = 0; y < height; y++) {
4867
+ const filter = raw[y * (stride + 1)];
4868
+ const inRow = y * (stride + 1) + 1;
4869
+ const outRow = y * stride;
4870
+ const prevRow = outRow - stride;
4871
+ for (let i = 0; i < stride; i++) {
4872
+ const x = raw[inRow + i];
4873
+ const a = i >= bpp ? pixels[outRow + i - bpp] : 0;
4874
+ const b = y > 0 ? pixels[prevRow + i] : 0;
4875
+ const c = y > 0 && i >= bpp ? pixels[prevRow + i - bpp] : 0;
4876
+ let value;
4877
+ switch (filter) {
4878
+ case 0:
4879
+ value = x;
4880
+ break;
4881
+ case 1:
4882
+ value = x + a;
4883
+ break;
4884
+ case 2:
4885
+ value = x + b;
4886
+ break;
4887
+ case 3:
4888
+ value = x + (a + b >> 1);
4889
+ break;
4890
+ case 4: {
4891
+ const p = a + b - c;
4892
+ const pa = Math.abs(p - a);
4893
+ const pb = Math.abs(p - b);
4894
+ const pc = Math.abs(p - c);
4895
+ value = x + (pa <= pb && pa <= pc ? a : pb <= pc ? b : c);
4896
+ break;
4897
+ }
4898
+ default:
4899
+ return { reason: `malformed PNG: scanline filter ${filter} at row ${y}` };
4900
+ }
4901
+ pixels[outRow + i] = value & 255;
4902
+ }
4903
+ }
4904
+ return { width, height, channels, pixels };
4905
+ }
4906
+ function readInkExtent(png, insetPx = 0) {
4907
+ const image = decodePng(png);
4908
+ if ("reason" in image) return image;
4909
+ const region = { x: insetPx, y: insetPx, width: image.width - 2 * insetPx, height: image.height - 2 * insetPx };
4910
+ if (insetPx < 0 || region.width <= 0 || region.height <= 0) {
4911
+ return { reason: `an inset of ${insetPx}px leaves nothing of a ${image.width}\xD7${image.height} image` };
4912
+ }
4913
+ const { channels, pixels, width } = image;
4914
+ const counts = /* @__PURE__ */ new Map();
4915
+ let ground = 0;
4916
+ let groundCount = 0;
4917
+ for (let y = region.y; y < region.y + region.height; y++) {
4918
+ for (let x = region.x; x < region.x + region.width; x++) {
4919
+ const i = (y * width + x) * channels;
4920
+ const key = pixels[i] << 16 | pixels[i + 1] << 8 | pixels[i + 2];
4921
+ const n = (counts.get(key) ?? 0) + 1;
4922
+ counts.set(key, n);
4923
+ if (n > groundCount) {
4924
+ groundCount = n;
4925
+ ground = key;
4926
+ }
4927
+ }
4928
+ }
4929
+ const gr = ground >> 16 & 255;
4930
+ const gg = ground >> 8 & 255;
4931
+ const gb = ground & 255;
4932
+ let lastInkRow = null;
4933
+ for (let y = region.y; y < region.y + region.height; y++) {
4934
+ for (let x = region.x; x < region.x + region.width; x++) {
4935
+ const i = (y * width + x) * channels;
4936
+ if (Math.abs(pixels[i] - gr) > INK_TOLERANCE || Math.abs(pixels[i + 1] - gg) > INK_TOLERANCE || Math.abs(pixels[i + 2] - gb) > INK_TOLERANCE) {
4937
+ lastInkRow = y - region.y;
4938
+ break;
4939
+ }
4940
+ }
4941
+ }
4942
+ const ratio = lastInkRow === null ? 0 : Math.round((lastInkRow + 1) / region.height * 1e3) / 1e3;
4943
+ return { ratio, lastInkRow, region, ground: [gr, gg, gb] };
4944
+ }
4501
4945
  function isDesignRenderingInput(relativePath) {
4502
4946
  const rel = relativePath.split("\\").join("/");
4503
4947
  if (rel.startsWith("__tests__/") || rel.includes("/__tests__/")) return false;
@@ -4544,10 +4988,360 @@ function judgeDesignIdentity(src) {
4544
4988
  function resetJudgeDesignIdentityCache() {
4545
4989
  identityCache.clear();
4546
4990
  }
4991
+
4992
+ // src/evaluation/criteria/edge.ts
4993
+ var EDGE_TOLERANCE_PX = 2;
4994
+ var CARD = "card";
4995
+ var same = (a, b) => Math.abs(a - b) <= EDGE_TOLERANCE_PX;
4996
+ var overlaps = (a, b) => a.left < b.right && b.left < a.right;
4997
+ var axis = (b) => (b.left + b.right) / 2;
4998
+ function contentEdge(surface, blocks) {
4999
+ const texts = blocks.filter((b) => b.within === surface.id && b.kind !== "surface" && b.role !== "centred");
5000
+ if (texts.length === 0) return void 0;
5001
+ const first = texts.reduce((a, b) => b.top < a.top ? b : a);
5002
+ return first.textEdge ?? first.edge;
5003
+ }
5004
+ function columns(direct) {
5005
+ const all = direct.filter((b) => b.kind !== "surface");
5006
+ const bridges = (b) => {
5007
+ const under = all.filter((x) => x !== b && overlaps(x, b));
5008
+ return under.some((x, i) => under.slice(i + 1).some((y) => !overlaps(x, y)));
5009
+ };
5010
+ const flow = all.filter((b) => !bridges(b));
5011
+ const parentPool = all.filter((b) => bridges(b));
5012
+ const parent = flow.map((_, i) => i);
5013
+ const find = (i) => parent[i] === i ? i : parent[i] = find(parent[i]);
5014
+ for (let i = 0; i < flow.length; i += 1) for (let j = i + 1; j < flow.length; j += 1) if (overlaps(flow[i], flow[j])) parent[find(i)] = find(j);
5015
+ const groups = /* @__PURE__ */ new Map();
5016
+ flow.forEach((b, i) => groups.set(find(i), [...groups.get(find(i)) ?? [], b]));
5017
+ const cols = [...groups.values()];
5018
+ for (const s of direct.filter((b) => b.kind === "surface")) {
5019
+ const hit = cols.filter((c) => c.some((b) => b.kind !== "surface" && overlaps(b, s)));
5020
+ if (hit.length === 1) hit[0].push(s);
5021
+ else parentPool.push(s);
5022
+ }
5023
+ const lp = parentPool.map((_, i) => i);
5024
+ const lfind = (i) => lp[i] === i ? i : lp[i] = lfind(lp[i]);
5025
+ for (let i = 0; i < parentPool.length; i += 1) for (let j = i + 1; j < parentPool.length; j += 1) if (overlaps(parentPool[i], parentPool[j])) lp[lfind(i)] = lfind(j);
5026
+ const lgroups = /* @__PURE__ */ new Map();
5027
+ parentPool.forEach((b, i) => lgroups.set(lfind(i), [...lgroups.get(lfind(i)) ?? [], b]));
5028
+ cols.push(...lgroups.values());
5029
+ return cols;
5030
+ }
5031
+ function checkColumn(container, col, blocks) {
5032
+ const surfaces = col.filter((b) => b.kind === "surface");
5033
+ const grounds = surfaces.flatMap((s) => {
5034
+ const c = s.role === "table" ? void 0 : contentEdge(s, blocks);
5035
+ return c === void 0 ? [] : [c];
5036
+ });
5037
+ const flow = col.filter((b) => b.kind !== "surface" && b.role !== "centred" && b.role !== "list-item");
5038
+ const listItems = col.filter((b) => b.role === "list-item");
5039
+ const candidates = [...flow.flatMap((b) => [b.edge, ...b.textEdge !== void 0 ? [b.textEdge] : []]), ...surfaces.flatMap((s) => [s.edge]), ...grounds];
5040
+ const blockEdges = (b) => [b.edge, ...b.textEdge !== void 0 ? [b.textEdge] : []];
5041
+ const endEdgesBesides = (b) => [...surfaces.map((s) => s.right), ...flow.filter((x) => x !== b && x.kind === "controls").map((x) => x.right)];
5042
+ let best = candidates.length === 0 ? [] : null;
5043
+ for (const E of candidates) {
5044
+ const allowed = [E, ...grounds];
5045
+ const off = [];
5046
+ for (const b of flow) {
5047
+ if (blockEdges(b).some((x) => allowed.some((a) => same(x, a)))) continue;
5048
+ if (b.kind === "controls" && endEdgesBesides(b).some((r) => same(b.right, r))) continue;
5049
+ const nearest = allowed.reduce((n, a) => Math.abs(a - b.edge) < Math.abs(n - b.edge) ? a : n);
5050
+ off.push({ container, block: b.id, at: b.edge, nearest, offPx: Math.abs(b.edge - nearest) });
5051
+ }
5052
+ for (const s of surfaces) {
5053
+ const c = s.role === "table" ? void 0 : contentEdge(s, blocks);
5054
+ if (same(s.edge, E) || c !== void 0 && same(c, E)) continue;
5055
+ const nearest = c !== void 0 && Math.abs(c - E) < Math.abs(s.edge - E) ? c : s.edge;
5056
+ off.push({ container, block: s.id, at: nearest, nearest: E, offPx: Math.abs(nearest - E) });
5057
+ }
5058
+ if (best === null || off.length < best.length || off.length === best.length && sum(off) < sum(best)) best = off;
5059
+ if (best.length === 0) break;
5060
+ }
5061
+ const offsets = best ?? [];
5062
+ if (listItems.length > 1) {
5063
+ const e0 = listItems[0].edge;
5064
+ for (const li of listItems.slice(1)) if (!same(li.edge, e0)) offsets.push({ container, block: li.id, at: li.edge, nearest: e0, offPx: Math.abs(li.edge - e0) });
5065
+ }
5066
+ const centred = col.filter((b) => b.role === "centred");
5067
+ const edgeAligned = col.filter((b) => b.role !== "centred");
5068
+ const groupTop = centred.length > 0 ? Math.min(...centred.map((c) => c.top)) : 0;
5069
+ const groupEndsColumn = centred.length > 1 && edgeAligned.every((b) => b.top < groupTop);
5070
+ if (centred.length > 0 && edgeAligned.some((b) => b.kind !== "surface") && !groupEndsColumn) {
5071
+ const E = flow[0]?.edge ?? edgeAligned[0].edge;
5072
+ for (const c of centred) offsets.push({ container, block: c.id, at: c.edge, nearest: E, offPx: Math.abs(c.edge - E), reason: "a centred block in an edge-aligned column" });
5073
+ } else if (centred.length > 1) {
5074
+ const a0 = axis(centred[0]);
5075
+ for (const c of centred.slice(1)) if (!same(axis(c), a0)) offsets.push({ container, block: c.id, at: axis(c), nearest: a0, offPx: Math.abs(axis(c) - a0) });
5076
+ }
5077
+ return offsets;
5078
+ }
5079
+ var sum = (o) => o.reduce((s, x) => s + x.offPx, 0);
5080
+ function judgeEdges(measured, opts = {}) {
5081
+ if (opts.placeholdersOnly === true) return { verdict: "n/a", offsets: [], evidence: "only loading placeholders are shown; not evaluated" };
5082
+ let blocks = measured;
5083
+ const kept = blocks.filter((b) => b.role !== "placeholder");
5084
+ const content = kept.filter((b) => b.kind !== "surface").length;
5085
+ if (content < 2) return { verdict: "n/a", offsets: [], evidence: kept.length === 0 ? "no blocks were measured" : "fewer than two blocks once placeholders are set aside; not evaluated" };
5086
+ blocks = kept;
5087
+ const offsets = [];
5088
+ const containers = [CARD, ...blocks.filter((b) => b.kind === "surface" && b.role !== "table").map((b) => b.id)];
5089
+ for (const container of containers) {
5090
+ const direct = blocks.filter((b) => (b.within ?? CARD) === container);
5091
+ for (const col of columns(direct)) offsets.push(...checkColumn(container, col, blocks));
5092
+ }
5093
+ if (offsets.length === 0) return { verdict: "pass", offsets, evidence: "every column shares its edge, or sits on a stacked surface\u2019s content edge, within 2 px" };
5094
+ const worst = offsets.reduce((a, b) => b.offPx > a.offPx ? b : a);
5095
+ const lines = offsets.map((o) => `${o.block} at x${round(o.at)} is ${round(o.offPx)} px from x${round(o.nearest)} (in ${o.container}${o.reason !== void 0 ? `; ${o.reason}` : ""})`);
5096
+ return { verdict: "fail", offsets, evidence: `${offsets.length} block(s) off the allowed edges, largest ${round(worst.offPx)} px: ${lines.join("; ")}` };
5097
+ }
5098
+ var round = (n) => (Math.round(n * 10) / 10).toString();
5099
+
5100
+ // src/evaluation/criteria/edge-probe.ts
5101
+ var EDGE_BLOCKS_EXPRESSION = String.raw`(() => {
5102
+ const root = document.getElementById('root');
5103
+ if (!root) return null;
5104
+ const rootBox = root.getBoundingClientRect();
5105
+ const x0 = rootBox.left;
5106
+ const px = (v) => Math.round((v - x0) * 2) / 2;
5107
+ const CONTROL = 'button,input,select,textarea,[role="button"],[role="tab"],[role="switch"],[role="checkbox"],[role="radio"],[role="option"],[role="link"],a[href]';
5108
+ const ALNUM = /[\p{L}\p{N}]/u;
5109
+ const css = (el) => getComputedStyle(el);
5110
+ const clear = (c) => !c || c === 'transparent' || /rgba\([^)]*,\s*0(\.0+)?\)$/.test(c);
5111
+ // Visible through every ancestor (an ancestor at opacity 0 or visibility hidden hides it), and not degenerate.
5112
+ const shown = (el) => {
5113
+ const s = css(el);
5114
+ if (s.display === 'none' || s.visibility === 'hidden' || Number(s.opacity) === 0) return false;
5115
+ if (typeof el.checkVisibility === 'function' && !el.checkVisibility({ opacityProperty: true, visibilityProperty: true })) return false;
5116
+ const r = el.getBoundingClientRect();
5117
+ return r.width > 1 && r.height > 1;
5118
+ };
5119
+ const inline = (s) => /^inline/.test(s.display) && s.display !== 'inline-block' && s.display !== 'inline-flex' && s.display !== 'inline-grid';
5120
+ const groundOf = (el) => {
5121
+ for (let p = el.parentElement; p; p = p.parentElement) { const b = css(p).backgroundColor; if (!clear(b)) return b; }
5122
+ return 'rgb(255, 255, 255)';
5123
+ };
5124
+ const sides = (s) => ['Top', 'Right', 'Bottom', 'Left'].filter((k) => parseFloat(s['border' + k + 'Width']) > 0 && s['border' + k + 'Style'] !== 'none' && !clear(s['border' + k + 'Color'])).length;
5125
+ const bounded = (el) => { const s = css(el); return sides(s) >= 1 || (!clear(s.backgroundColor) && s.backgroundColor !== groundOf(el)); };
5126
+ // A control: a native or ARIA one, or a bounded box the visitor clicks (a pointer cursor or a tab stop) that holds no
5127
+ // control of its own and is not the whole card. Its contents are not blocks of the column (decided 2026-10-02).
5128
+ const isControl = (el) => {
5129
+ const s = css(el);
5130
+ if (inline(s)) return false;
5131
+ if (el.matches(CONTROL)) return true;
5132
+ if (el === root) return false;
5133
+ const clickable = s.cursor === 'pointer' || (el.hasAttribute('tabindex') && el.tabIndex >= 0);
5134
+ if (!clickable || !bounded(el) || el.querySelector(CONTROL)) return false;
5135
+ const r = el.getBoundingClientRect();
5136
+ return r.width * r.height < 0.6 * rootBox.width * rootBox.height;
5137
+ };
5138
+ const isTable = (el) => el.matches('table,[role="table"],[role="grid"]');
5139
+ const isSurface = (el) => {
5140
+ if (el === root || isControl(el)) return false;
5141
+ const s = css(el);
5142
+ if (/^inline/.test(s.display) || s.display === 'contents') return false;
5143
+ if (isTable(el)) return true;
5144
+ return sides(s) >= 3 || (!clear(s.backgroundColor) && s.backgroundColor !== groundOf(el)) || (s.boxShadow && s.boxShadow !== 'none');
5145
+ };
5146
+ const path = (el) => {
5147
+ const parts = [];
5148
+ for (let e = el; e && e !== root && parts.length < 4; e = e.parentElement) {
5149
+ const i = e.parentElement ? Array.prototype.indexOf.call(e.parentElement.children, e) + 1 : 1;
5150
+ parts.unshift(e.tagName.toLowerCase() + ':' + i);
5151
+ }
5152
+ return parts.join('>');
5153
+ };
5154
+ const label = (el) => { const t = (el.innerText || '').replace(/\s+/g, ' ').trim(); return path(el) + (t ? ' "' + t.slice(0, 24) + '"' : ''); };
5155
+ const surfaces = [];
5156
+ const surfaceOf = (el, self) => { for (let p = self ? el : el.parentElement; p && p !== root; p = p.parentElement) if (surfaces.indexOf(p) >= 0) return p; return null; };
5157
+ const inTable = (el) => { for (let p = el.parentElement; p && p !== root; p = p.parentElement) if (isTable(p)) return true; return false; };
5158
+ const inControl = (el) => { for (let p = el; p && p !== root; p = p.parentElement) if (isControl(p)) return p; return null; };
5159
+ const all = Array.prototype.slice.call(root.querySelectorAll('*')).filter(shown);
5160
+ for (const el of all) if (isSurface(el) && !inTable(el) && !inControl(el)) surfaces.push(el);
5161
+ const ids = new Map();
5162
+ const idOf = (el) => { if (!ids.has(el)) ids.set(el, label(el)); return ids.get(el); };
5163
+ const blocks = [];
5164
+ // Per block: the element it stands for and its first line's box, for rows.
5165
+ const owners = [];
5166
+ const lines = [];
5167
+ // Per block: its raw line boxes and left, so a row reads the line two items share, not a wrapped item's widest line.
5168
+ const rects = [];
5169
+ const rawLeft = [];
5170
+ const words = [];
5171
+ const within = (el, self) => { const s = surfaceOf(el, self); return s ? { within: idOf(s) } : {}; };
5172
+ for (const s of surfaces) {
5173
+ const r = s.getBoundingClientRect();
5174
+ owners.push(s); lines.push({ top: r.top, bottom: r.bottom }); rects.push([r]); rawLeft.push(r.left); words.push('');
5175
+ // A placeholder: a box with no text, no control and no image or icon inside it (a loading bar). Not a block.
5176
+ const holds = Array.prototype.some.call(s.querySelectorAll('*'), (el) => shown(el) && (el.matches(CONTROL) || el.tagName === 'IMG' || el.tagName.toLowerCase() === 'svg')) || ALNUM.test(s.innerText || '');
5177
+ blocks.push(Object.assign({ id: idOf(s), kind: 'surface', edge: px(r.left), left: px(r.left), right: px(r.right), top: Math.round(r.top) }, within(s, false), isTable(s) ? { role: 'table' } : holds ? {} : { role: 'placeholder' }));
5178
+ }
5179
+ const blockOf = (node) => { for (let p = node.parentElement; p && p !== root; p = p.parentElement) if (!inline(css(p))) return p; return root; };
5180
+ const groups = new Map();
5181
+ const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
5182
+ for (let n = walker.nextNode(); n; n = walker.nextNode()) {
5183
+ if (!/\S/.test(n.data) || !n.parentElement || n.parentElement.closest('style,script,noscript,template') || !shown(n.parentElement) || inControl(n.parentElement) || inTable(n.parentElement)) continue;
5184
+ const b = blockOf(n);
5185
+ if (!groups.has(b)) groups.set(b, []);
5186
+ groups.get(b).push(n);
5187
+ }
5188
+ // Ink: text whose own colour is not transparent. The edge is the first visible character in ink (decided 2026-10-02):
5189
+ // leading invisible text, private-use icon-font glyphs and formatting characters are not characters of the block,
5190
+ // while a currency sign or a quotation mark is.
5191
+ const inked = (n) => !clear(css(n.parentElement).color) && ALNUM.test(n.data);
5192
+ const textRects = (nodes) => {
5193
+ const rs = [];
5194
+ for (const n of nodes) { if (!inked(n)) continue; const range = document.createRange(); range.selectNodeContents(n); for (const r of range.getClientRects()) if (r.width > 0 && r.height > 0) rs.push(r); }
5195
+ return rs;
5196
+ };
5197
+ const glyphAt = (n, i) => { const range = document.createRange(); range.setStart(n, i); range.setEnd(n, i + 1); const r = range.getClientRects()[0]; return r || null; };
5198
+ const firstGlyph = (nodes) => {
5199
+ for (const n of nodes) { if (!inked(n)) continue; const i = n.data.search(/[^\s\p{Co}\p{Cf}]/u); if (i < 0) continue; const r = glyphAt(n, i); if (r) return r; }
5200
+ return null;
5201
+ };
5202
+ // A marker paints: a loaded image, an SVG with a filled or stroked shape, or an element with its own fill, border
5203
+ // or visible text. An element that paints nothing is not a marker.
5204
+ const painted = (m) => {
5205
+ if (!shown(m)) return false;
5206
+ if (m.tagName === 'IMG') return m.complete && m.naturalWidth > 0;
5207
+ if (m.tagName.toLowerCase() === 'svg' || m.closest('svg')) {
5208
+ const shapes = m.querySelectorAll('path,circle,rect,line,polyline,polygon,ellipse,use,text');
5209
+ return Array.prototype.some.call(shapes, (sh) => { const st = css(sh); return (st.fill !== 'none' && !clear(st.fill)) || (st.stroke !== 'none' && !clear(st.stroke)); });
5210
+ }
5211
+ const s = css(m);
5212
+ if (!clear(s.backgroundColor) || sides(s) >= 1 || ((m.textContent || '').trim().length > 0 && !clear(s.color))) return true;
5213
+ // A wrapper that paints nothing itself is a marker when an image or icon inside it paints.
5214
+ return Array.prototype.some.call(m.querySelectorAll('svg,img'), (k) => painted(k));
5215
+ };
5216
+ const markerBefore = (b, textLeft, lineTop, lineBottom) => {
5217
+ const cands = [];
5218
+ const consider = (m) => { if (!painted(m)) return; const r = m.getBoundingClientRect(); if (r.left < textLeft - 2 && r.bottom > lineTop && r.top < lineBottom) cands.push(r.left); };
5219
+ for (const m of b.querySelectorAll('svg,img,[aria-hidden="true"],i')) if ((m.textContent || '').trim().length <= 2) consider(m);
5220
+ const p = b.parentElement;
5221
+ if (p && p !== root && /flex/.test(css(p).display) && !/column/.test(css(p).flexDirection)) for (const sib of p.children) { if (sib === b) break; if (sib.matches('svg,img,i,[aria-hidden="true"]') || !(sib.textContent || '').trim()) consider(sib); }
5222
+ return cands.length ? Math.min.apply(null, cands) : null;
5223
+ };
5224
+ let texts = 0;
5225
+ let ornaments = 0;
5226
+ for (const [b, nodes] of groups) {
5227
+ if (!nodes.some((n) => ALNUM.test(n.data))) { ornaments += 1; continue; }
5228
+ const rs = textRects(nodes);
5229
+ const g = firstGlyph(nodes);
5230
+ if (!rs.length || g === null) continue;
5231
+ texts += 1;
5232
+ const first = g.left;
5233
+ const left = Math.min.apply(null, rs.map((r) => r.left));
5234
+ const right = Math.max.apply(null, rs.map((r) => r.right));
5235
+ const top = Math.min.apply(null, rs.map((r) => r.top));
5236
+ const marker = markerBefore(b, first, g.top, g.bottom);
5237
+ const s = css(b);
5238
+ const box = b.getBoundingClientRect();
5239
+ const parent = b.parentElement ? css(b.parentElement) : null;
5240
+ const centredText = s.textAlign === 'center' && right - left < box.width * 0.9;
5241
+ const centredBox = parent && /flex/.test(parent.display) && /column/.test(parent.flexDirection) && parent.alignItems === 'center';
5242
+ const list = b.matches('li') || s.display === 'list-item' ? b.closest('ul,ol') : null;
5243
+ const inProse = list && list.previousElementSibling && list.previousElementSibling.matches('p');
5244
+ const block = { id: idOf(b), kind: 'text', edge: px(marker !== null ? marker : first), left: px(Math.min(left, marker !== null ? marker : left)), right: px(right), top: Math.round(top) };
5245
+ if (marker !== null) block.textEdge = px(first);
5246
+ if (centredText || centredBox) block.role = 'centred';
5247
+ else if (inProse) block.role = 'list-item';
5248
+ owners.push(b); lines.push({ top: g.top, bottom: g.bottom }); rects.push(rs); rawLeft.push(Math.min(left, marker !== null ? marker : left)); words.push(nodes.map((n) => n.data).join('').trim());
5249
+ blocks.push(Object.assign(block, within(b, true)));
5250
+ }
5251
+ const ctlGroups = new Map();
5252
+ for (const el of all) { if (!isControl(el) || inTable(el) || (el.parentElement && inControl(el.parentElement))) continue; const p = el.parentElement || root; if (!ctlGroups.has(p)) ctlGroups.set(p, []); ctlGroups.get(p).push(el); }
5253
+ let controls = 0;
5254
+ for (const [p, els] of ctlGroups) {
5255
+ const rs = els.map((e) => e.getBoundingClientRect());
5256
+ const topRow = Math.min.apply(null, rs.map((r) => r.top));
5257
+ let firstEl = null; let firstR = null;
5258
+ els.forEach((e, i) => { const r = rs[i]; if (r.top - topRow < r.height / 2 && (!firstR || r.left < firstR.left)) { firstEl = e; firstR = r; } });
5259
+ let edge = firstR.left;
5260
+ if (!bounded(firstEl)) { const tw = document.createTreeWalker(firstEl, NodeFilter.SHOW_TEXT); const ns = []; for (let n = tw.nextNode(); n; n = tw.nextNode()) ns.push(n); const g = firstGlyph(ns); if (g) edge = g.left; }
5261
+ controls += 1;
5262
+ const ps = css(p);
5263
+ const centred = ps.justifyContent === 'center' || (ps.textAlign === 'center' && !/flex|grid/.test(ps.display));
5264
+ const block = { id: idOf(p) + ' [controls]', kind: 'controls', edge: px(edge), left: px(Math.min.apply(null, rs.map((r) => r.left))), right: px(Math.max.apply(null, rs.map((r) => r.right))), top: Math.round(topRow) };
5265
+ if (centred) block.role = 'centred';
5266
+ owners.push(firstEl); lines.push({ top: firstR.top, bottom: firstR.bottom }); rects.push(rs); rawLeft.push(Math.min.apply(null, rs.map((r) => r.left))); words.push('');
5267
+ blocks.push(Object.assign(block, within(firstEl, true)));
5268
+ }
5269
+ // Rows (decided 2026-10-01, read by geometry 2026-10-02): items set on one line, however the CSS sets them. An item
5270
+ // after another measured item on its line that holds one block (a text, a control, or a box holding at most one
5271
+ // text) belongs to the row and is not measured; an item with a stack inside it is a column.
5272
+ const dropped = new Set();
5273
+ const sameLine = (a, b) => Math.min(a.bottom, b.bottom) - Math.max(a.top, b.top) > Math.min(a.bottom - a.top, b.bottom - b.top) / 2;
5274
+ const contains = (o, el) => o === el || o.contains(el);
5275
+ const innerTexts = (i) => owners.map((el, k) => k).filter((k) => k !== i && blocks[k].kind !== 'surface' && contains(owners[i], owners[k]));
5276
+ const isPill = (i) => blocks[i].kind === 'surface' && innerTexts(i).length <= 1 && !owners.some((el, k) => k !== i && blocks[k].kind === 'surface' && contains(owners[i], el));
5277
+ const itemOf = (el, other) => { let e = el; while (e.parentElement && e.parentElement !== root && !e.parentElement.contains(other)) e = e.parentElement; return e; };
5278
+ for (let i = 0; i < blocks.length; i += 1) {
5279
+ if (blocks[i].kind === 'surface' && !isPill(i)) continue;
5280
+ const before = blocks.findIndex((bj, j) => j !== i && bj.within === blocks[i].within && !(bj.kind === 'surface' && !isPill(j)) && !contains(owners[j], owners[i]) && !contains(owners[i], owners[j]) && rects[j].some((r) => sameLine(r, lines[i]) && r.right <= rawLeft[i] + 1));
5281
+ if (before < 0) continue;
5282
+ // A number or bullet in front of a text on its line is a hanging marker, not a row: the pair is one block with two
5283
+ // edges, the marker's and the text's.
5284
+ if (blocks[i].kind === 'text' && blocks[before].kind === 'text' && /^[\p{N}\p{P}\p{S}]{1,3}$/u.test(words[before])) {
5285
+ blocks[i] = Object.assign({}, blocks[i], { edge: blocks[before].edge, left: Math.min(blocks[i].left, blocks[before].left), textEdge: blocks[i].edge });
5286
+ delete blocks[i].role;
5287
+ dropped.add(before);
5288
+ continue;
5289
+ }
5290
+ const item = itemOf(owners[i], owners[before]);
5291
+ const inside = owners.map((el, k) => k).filter((k) => contains(item, owners[k]) && blocks[k].kind !== 'surface');
5292
+ const boxes = owners.map((el, k) => k).filter((k) => contains(item, owners[k]) && blocks[k].kind === 'surface' && k !== i);
5293
+ if (blocks[i].kind === 'surface') { if (boxes.length === 0) { dropped.add(i); for (const k of innerTexts(i)) dropped.add(k); } continue; }
5294
+ if (inside.length === 1 && boxes.length === 0) dropped.add(i);
5295
+ }
5296
+ const kept = blocks.filter((_, i) => !dropped.has(i));
5297
+ // Placeholders: a frame with no text in ink shows nothing to evaluate, whatever controls or boxes it draws.
5298
+ return { blocks: kept, placeholdersOnly: texts === 0, skipped: { rowItems: dropped.size, ornaments } };
5299
+ })()`;
5300
+ var KINDS = ["text", "controls", "surface"];
5301
+ var ROLES = ["list-item", "table", "centred", "placeholder"];
5302
+ var isKind = (v) => KINDS.some((k) => k === v);
5303
+ var isRole = (v) => ROLES.some((r) => r === v);
5304
+ var num = (v) => typeof v === "number" && Number.isFinite(v);
5305
+ var field = (o, key) => Reflect.get(o, key);
5306
+ function parseEdgeProbe(value) {
5307
+ if (typeof value !== "object" || value === null) return null;
5308
+ const raw = field(value, "blocks");
5309
+ const placeholdersOnly = field(value, "placeholdersOnly");
5310
+ if (!Array.isArray(raw) || typeof placeholdersOnly !== "boolean") return null;
5311
+ const skippedRaw = field(value, "skipped");
5312
+ if (typeof skippedRaw !== "object" || skippedRaw === null) return null;
5313
+ const rowItems = field(skippedRaw, "rowItems");
5314
+ const ornaments = field(skippedRaw, "ornaments");
5315
+ if (!num(rowItems) || !num(ornaments)) return null;
5316
+ const blocks = [];
5317
+ for (const entry of raw) {
5318
+ if (typeof entry !== "object" || entry === null) return null;
5319
+ const [id, kind, edge, left, right, top] = ["id", "kind", "edge", "left", "right", "top"].map((k) => field(entry, k));
5320
+ const [within, textEdge, role] = ["within", "textEdge", "role"].map((k) => field(entry, k));
5321
+ if (typeof id !== "string" || !isKind(kind) || !num(edge) || !num(left) || !num(right) || !num(top)) return null;
5322
+ if (within !== void 0 && typeof within !== "string") return null;
5323
+ if (textEdge !== void 0 && !num(textEdge)) return null;
5324
+ if (role !== void 0 && !isRole(role)) return null;
5325
+ blocks.push({
5326
+ id,
5327
+ kind,
5328
+ edge,
5329
+ left,
5330
+ right,
5331
+ top,
5332
+ ...within !== void 0 ? { within } : {},
5333
+ ...textEdge !== void 0 ? { textEdge } : {},
5334
+ ...role !== void 0 ? { role } : {}
5335
+ });
5336
+ }
5337
+ return { blocks, placeholdersOnly, skipped: { rowItems, ornaments } };
5338
+ }
5339
+
5340
+ // src/evaluation/visual-evaluator.ts
4547
5341
  function canvasFitPolicy(canvas) {
4548
5342
  switch (canvas) {
4549
5343
  case "xs-chat-card":
4550
- return { capture: "viewport", overflow: "fail" };
5344
+ return { capture: "natural", overflow: "fail" };
4551
5345
  case "mobile-fullscreen-small":
4552
5346
  return { capture: "full-page", overflow: "warn" };
4553
5347
  default:
@@ -4555,9 +5349,65 @@ function canvasFitPolicy(canvas) {
4555
5349
  }
4556
5350
  }
4557
5351
  var JUDGE_SCOPE_CLASS = "ggui-judge-scope";
5352
+ var JUDGE_PANEL_CLASS = "ggui-judge-panel";
5353
+ var JUDGE_INK_INSET_PX = EXPANDED_FRAME.insetPx + 24 + 1;
4558
5354
  function canvasFit(canvas) {
4559
5355
  return displayModeForCanvas(canvas) === "fullscreen" ? "fill" : void 0;
4560
5356
  }
5357
+ var JUDGE_GROUND_MARGIN_PX = EXPANDED_FRAME.insetPx;
5358
+ var JUDGE_GROUND_CLASS = "ggui-judge-ground";
5359
+ var JUDGE_INLINE_FRAME_CLASS = "ggui-judge-inline-frame";
5360
+ var JUDGE_INLINE_FRAME_BORDER_PX = 1;
5361
+ var JUDGE_INLINE_FRAME_RADIUS_PX = 16;
5362
+ var JUDGE_INLINE_PAD_PX = JUDGE_GROUND_MARGIN_PX + JUDGE_INLINE_FRAME_BORDER_PX;
5363
+ var JUDGE_INLINE_INK_INSET_PX = JUDGE_INLINE_PAD_PX + JUDGE_INLINE_FRAME_RADIUS_PX;
5364
+ var HOST_COLOUR_PATTERN = /^#[0-9a-f]{6}$/;
5365
+ var HOST_FRAME_BOUNDS = {
5366
+ ringWidthPx: { min: 0, max: 8 },
5367
+ ringAlpha: { min: 0, max: 1 },
5368
+ radiusPx: { min: 0, max: 64 },
5369
+ minHeightPx: { min: 0, max: 2560 }
5370
+ };
5371
+ function resolvePresentation(canvas, p) {
5372
+ if (p === void 0) return void 0;
5373
+ const ignored = (reason, why) => {
5374
+ console.warn(`[visual-eval] host presentation for ${canvas} ignored (${reason}): ${why}`);
5375
+ return { status: "ignored", reason };
5376
+ };
5377
+ const inline = canvasFitPolicy(canvas).capture === "natural";
5378
+ if (typeof p.label !== "string" || p.label.length === 0) return ignored("label_missing", "no label");
5379
+ if (p.ground !== void 0 && !HOST_COLOUR_PATTERN.test(p.ground)) return ignored("color_not_hex", "ground is not #rrggbb");
5380
+ if (inline && p.ground !== void 0) return ignored("ground_on_inline", "the inline card's room is its frame's ground");
5381
+ const f = p.frame;
5382
+ if (f !== void 0) {
5383
+ if (!inline) return ignored("frame_off_inline", "a frame on a canvas that is not the inline card");
5384
+ if (![f.surface, f.ground, f.ring.color].every((c) => HOST_COLOUR_PATTERN.test(c))) return ignored("color_not_hex", "a frame colour is not #rrggbb");
5385
+ const B = HOST_FRAME_BOUNDS;
5386
+ const inRange = (n, b) => Number.isFinite(n) && n >= b.min && n <= b.max;
5387
+ if (!inRange(f.ring.widthPx, B.ringWidthPx) || !inRange(f.ring.alpha, B.ringAlpha) || !inRange(f.radiusPx, B.radiusPx)) return ignored("frame_malformed", "a frame number out of range");
5388
+ if (f.minHeightPx !== void 0 && !inRange(f.minHeightPx, B.minHeightPx)) return ignored("frame_malformed", "minHeightPx out of range");
5389
+ }
5390
+ return { status: "applied", applied: p };
5391
+ }
5392
+ function inlineGeometry(frame) {
5393
+ if (frame === void 0) return { padPx: JUDGE_INLINE_PAD_PX, inkInsetPx: JUDGE_INLINE_INK_INSET_PX, floorPx: void 0 };
5394
+ const padPx = JUDGE_GROUND_MARGIN_PX + frame.ring.widthPx;
5395
+ return { padPx, inkInsetPx: padPx + frame.radiusPx, floorPx: frame.minHeightPx };
5396
+ }
5397
+ function canvasChrome(canvas) {
5398
+ if (canvasFitPolicy(canvas).capture === "natural") return "ground";
5399
+ return canvasFit(canvas) === "fill" && canvas !== "mobile-fullscreen-small" ? "panel" : void 0;
5400
+ }
5401
+ function judgeWindow(viewport, chrome, inlinePadPx = JUDGE_INLINE_PAD_PX) {
5402
+ if (chrome === void 0) return viewport;
5403
+ const gap = 2 * (chrome === "ground" ? inlinePadPx : EXPANDED_FRAME.insetPx);
5404
+ return { width: viewport.width + gap, height: viewport.height + gap };
5405
+ }
5406
+ function cardHeight(documentHeight, chrome) {
5407
+ if (documentHeight === null || chrome === void 0 || chrome === "ground") return documentHeight;
5408
+ const gap = 2 * EXPANDED_FRAME.insetPx;
5409
+ return documentHeight < gap ? null : documentHeight - gap;
5410
+ }
4561
5411
  function integerBox(viewport) {
4562
5412
  return { width: Math.round(viewport.width), height: Math.round(viewport.height) };
4563
5413
  }
@@ -4571,11 +5421,32 @@ function canvasOverflowIssue(canvas, viewport, contentHeight, verdict) {
4571
5421
  const box = isDeclaredViewport(canvas, viewport) ? `declared ${viewport.width}\xD7${viewport.height}; class box ${classBox.width}\xD7${classBox.height}` : `${viewport.width}\xD7${viewport.height}`;
4572
5422
  return {
4573
5423
  dimension: "canvas-overflow",
5424
+ origin: "instrument",
4574
5425
  severity: verdict === "fail" ? "critical" : "major",
4575
5426
  description: `Rendered content is ${contentHeight}px tall on the ${canvas} canvas (${box}) \u2014 ${hidden}px ${verdict === "fail" ? "is cut off: the inline card does not scroll" : "sits below the first screen"}.`,
4576
5427
  fix: "Fit the composition to the canvas: fewer and shorter sections, one compact row of chips, no hero taller than a third of the box, no fixed min-heights or tall paddings \u2014 measure against the canvas, not the page."
4577
5428
  };
4578
5429
  }
5430
+ function canvasOverflowXIssue(canvas, viewport, hiddenPx, verdict) {
5431
+ return {
5432
+ dimension: "canvas-overflow-x",
5433
+ origin: "instrument",
5434
+ severity: verdict === "fail" ? "critical" : "major",
5435
+ description: `Content runs ${hiddenPx}px past the ${canvas} card's ${viewport.width}px width \u2014 the card's frame clips it, so a visitor sees a cut-off row or a sideways scroll.`,
5436
+ fix: "Keep every row within the card: wrap long text (overflow-wrap: anywhere), let a wide table or code block scroll inside its own container, and give no child a fixed width wider than the card."
5437
+ };
5438
+ }
5439
+ function canvasBlankIssue(canvas, viewport) {
5440
+ const classBox = CANVAS_VIEWPORTS[canvas];
5441
+ const box = isDeclaredViewport(canvas, viewport) ? `declared ${viewport.width}\xD7${viewport.height}; class box ${classBox.width}\xD7${classBox.height}` : `${viewport.width}\xD7${viewport.height}`;
5442
+ return {
5443
+ dimension: "canvas-blank",
5444
+ origin: "instrument",
5445
+ severity: "critical",
5446
+ description: `Nothing painted on the ${canvas} canvas (${box}): the capture is one flat colour \u2014 the component mounted and rendered no pixels.`,
5447
+ fix: "Return and paint the component tree: a root that renders nothing is a blank, not an empty state. Check that the default export returns its JSX, that no early return yields null on the sample props, and that the first paint shows visible content."
5448
+ };
5449
+ }
4579
5450
  function resolveUiGenPackageDir() {
4580
5451
  const selfDir = dirname(fileURLToPath(import.meta.url));
4581
5452
  return resolve(selfDir, "..", "..");
@@ -4691,7 +5562,16 @@ try {
4691
5562
  rmSync(tmpDir, { recursive: true, force: true });
4692
5563
  }
4693
5564
  }
4694
- function buildRenderHTML(bundledCode, cssTokens, fit) {
5565
+ function inlineFrameDecls(frame) {
5566
+ if (frame === void 0) {
5567
+ return `background: var(--ggui-color-container); border: ${JUDGE_INLINE_FRAME_BORDER_PX}px solid color-mix(in srgb, var(--ggui-color-onContainer) 8%, transparent); border-radius: ${JUDGE_INLINE_FRAME_RADIUS_PX}px; overflow: hidden;`;
5568
+ }
5569
+ const alphaPct = Math.round(frame.ring.alpha * 1e3) / 10;
5570
+ return `background: ${frame.surface}; border: ${frame.ring.widthPx}px solid color-mix(in srgb, ${frame.ring.color} ${alphaPct}%, transparent); border-radius: ${frame.radiusPx}px; overflow: hidden;`;
5571
+ }
5572
+ function buildRenderHTML(bundledCode, cssTokens, fit, chrome, presentation) {
5573
+ if (chrome === "panel" && fit !== "fill") throw new Error("the judge's panel frames a fill canvas only (ggui#1083 cut 3)");
5574
+ if (chrome === "ground" && fit === "fill") throw new Error("the host ground frames an inline card only \u2014 a fill canvas has no natural height (ggui#1475)");
4695
5575
  const tokens = cssTokens ?? getCssTokens();
4696
5576
  return `<!DOCTYPE html>
4697
5577
  <html>
@@ -4706,15 +5586,16 @@ function buildRenderHTML(bundledCode, cssTokens, fit) {
4706
5586
  /* ggui#1083 \u2014 the host stand-in (neutral-50) under the theme's scrim (its tint at
4707
5587
  its opacity): the ground a frosted host puts under the card, so the judge scores
4708
5588
  the card where the visitor sees it. */
4709
- background: color-mix(in oklch, var(--ggui-scrim-tint, var(--ggui-color-ground, #ffffff)) calc(var(--ggui-scrim-opacity, 0.45) * 100%), var(--ggui-color-neutral-50, #ffffff));
5589
+ ${presentation?.frame !== void 0 ? `background: ${presentation.frame.ground};` : presentation?.ground !== void 0 ? `background: ${presentation.ground};` : expandedFrameScrimDecls()}
4710
5590
  color: var(--ggui-color-neutral-900, #111827);
4711
5591
  }
4712
5592
  .error { color: #dc2626; padding: 16px; font-family: monospace; white-space: pre-wrap; }
4713
- ${fit === "fill" ? fillFitRule(JUDGE_SCOPE_CLASS) : ""}
5593
+ ${fit === "fill" ? fillFitRule(JUDGE_SCOPE_CLASS) + (chrome === "panel" ? expandedFramePanelRule(JUDGE_PANEL_CLASS, JUDGE_SCOPE_CLASS) : "") : ""}
5594
+ ${chrome === "ground" ? `.${JUDGE_GROUND_CLASS} { padding: ${JUDGE_GROUND_MARGIN_PX}px; } .${JUDGE_INLINE_FRAME_CLASS} { ${inlineFrameDecls(presentation?.frame)} }` : ""}
4714
5595
  </style>
4715
5596
  </head>
4716
5597
  <body>
4717
- <div id="root"${fit === "fill" ? ` class="${JUDGE_SCOPE_CLASS}"` : ""}></div>
5598
+ ${chrome === "panel" ? `<div class="${JUDGE_PANEL_CLASS}">` : chrome === "ground" ? `<div class="${JUDGE_GROUND_CLASS}"><div class="${JUDGE_INLINE_FRAME_CLASS}">` : ""}<div id="root"${fit === "fill" ? ` class="${JUDGE_SCOPE_CLASS}"` : ""}></div>${chrome === "panel" ? "</div>" : chrome === "ground" ? "</div></div>" : ""}
4718
5599
  <script type="importmap">
4719
5600
  {
4720
5601
  "imports": {
@@ -4762,16 +5643,40 @@ async function launchWithPuppeteer(options) {
4762
5643
  return puppeteer.default.launch(options);
4763
5644
  }
4764
5645
  var CONTENT_HEIGHT_EXPRESSION = "Math.max(document.documentElement.scrollHeight, document.body ? document.body.scrollHeight : 0)";
4765
- async function measureContentHeight(page) {
5646
+ function cardHeightExpression(padPx) {
5647
+ return `(() => { const el = document.documentElement; const prev = el.style.height; el.style.height = 'max-content'; const h = Math.ceil(el.getBoundingClientRect().height); el.style.height = prev; return h - ${2 * padPx}; })()`;
5648
+ }
5649
+ var CARD_HEIGHT_EXPRESSION = cardHeightExpression(JUDGE_INLINE_PAD_PX);
5650
+ var OVERFLOW_X_ROUNDING_PX = 1;
5651
+ var CARD_OVERFLOW_X_EXPRESSION = "(() => { const r = document.getElementById('root'); return r ? Math.max(0, Math.ceil(r.scrollWidth - r.clientWidth)) : null; })()";
5652
+ function naturalClip(window, cardHeightPx, padPx = JUDGE_INLINE_PAD_PX) {
5653
+ const m = padPx;
5654
+ const box = window.height - 2 * m;
5655
+ return { x: 0, y: 0, width: window.width, height: Math.max(1, Math.min(Math.ceil(cardHeightPx), box)) + 2 * m };
5656
+ }
5657
+ async function measureContentHeight(page, expression = CONTENT_HEIGHT_EXPRESSION) {
4766
5658
  try {
4767
- const h = await page.evaluate(CONTENT_HEIGHT_EXPRESSION);
5659
+ const h = await page.evaluate(expression);
4768
5660
  return typeof h === "number" && Number.isFinite(h) ? Math.round(h) : null;
4769
5661
  } catch (e) {
4770
5662
  console.warn(`[visual-eval] content height unavailable: ${e instanceof Error ? e.message : String(e)}`);
4771
5663
  return null;
4772
5664
  }
4773
5665
  }
4774
- async function captureScreenshotDetailed(html, viewport = DEFAULT_SCREENSHOT_VIEWPORT, deps = {}, capture = "full-page") {
5666
+ async function measureEdges(page) {
5667
+ try {
5668
+ const probe = parseEdgeProbe(await page.evaluate(EDGE_BLOCKS_EXPRESSION));
5669
+ if (probe === null) {
5670
+ console.warn("[visual-eval] edge probe unavailable: the page returned no readable blocks");
5671
+ return null;
5672
+ }
5673
+ return judgeEdges(probe.blocks, { placeholdersOnly: probe.placeholdersOnly });
5674
+ } catch (e) {
5675
+ console.warn(`[visual-eval] edge probe unavailable: ${e instanceof Error ? e.message : String(e)}`);
5676
+ return null;
5677
+ }
5678
+ }
5679
+ async function captureScreenshotDetailed(html, viewport = DEFAULT_SCREENSHOT_VIEWPORT, deps = {}, capture = "full-page", natural = { padPx: JUDGE_INLINE_PAD_PX }) {
4775
5680
  try {
4776
5681
  const launchOptions = await resolveLaunchOptions(viewport, deps);
4777
5682
  const browser = await (deps.launch ?? launchWithPuppeteer)(launchOptions);
@@ -4783,9 +5688,23 @@ async function captureScreenshotDetailed(html, viewport = DEFAULT_SCREENSHOT_VIE
4783
5688
  await page.waitForSelector("#root > *", { timeout: 1e4 }).catch(() => {
4784
5689
  });
4785
5690
  await new Promise((r) => setTimeout(r, deps.settleMs ?? 1e3));
5691
+ if (capture === "natural") {
5692
+ const cardHeightPx = await measureContentHeight(page, natural.padPx === JUDGE_INLINE_PAD_PX ? CARD_HEIGHT_EXPRESSION : cardHeightExpression(natural.padPx));
5693
+ const overflowX = await measureContentHeight(page, CARD_OVERFLOW_X_EXPRESSION);
5694
+ const floorPx = natural.floorPx;
5695
+ if (floorPx !== void 0 && cardHeightPx !== null && cardHeightPx < floorPx) {
5696
+ await page.evaluate(`(() => { const f = document.querySelector('.${JUDGE_INLINE_FRAME_CLASS}'); if (f) f.style.minHeight = '${floorPx}px'; })()`);
5697
+ }
5698
+ const framed = cardHeightPx === null ? viewport.height - 2 * natural.padPx : floorPx !== void 0 ? Math.max(cardHeightPx, floorPx) : cardHeightPx;
5699
+ const clip = naturalClip(viewport, framed, natural.padPx);
5700
+ const edge2 = await measureEdges(page);
5701
+ const screenshot2 = await page.screenshot({ type: "png", fullPage: false, clip });
5702
+ return { png: Buffer.from(screenshot2), contentHeight: cardHeightPx, overflowX, edge: edge2 };
5703
+ }
4786
5704
  const contentHeight = await measureContentHeight(page);
5705
+ const edge = await measureEdges(page);
4787
5706
  const screenshot = await page.screenshot({ type: "png", fullPage: capture === "full-page" });
4788
- return { png: Buffer.from(screenshot), contentHeight };
5707
+ return { png: Buffer.from(screenshot), contentHeight, edge };
4789
5708
  } finally {
4790
5709
  await browser.close();
4791
5710
  }
@@ -4841,12 +5760,14 @@ Respond with ONLY a JSON object (no markdown, no explanation):
4841
5760
  var VISUAL_JUDGE_MAX_OUTPUT_TOKENS = 4096;
4842
5761
  var VISUAL_JUDGE_PROMPT_VERSION = "v2-bounded-issues";
4843
5762
  var VISUAL_JUDGE_PROMPT_DIGEST = createHash("sha256").update(VISUAL_EVAL_PROMPT, "utf8").digest("hex");
4844
- async function callMultimodalLLM(config, model, prompt, screenshot, originalPrompt, profileBlock = "") {
5763
+ async function callMultimodalLLM(config, model, prompt, screenshot, originalPrompt, profileBlock = "", criteriaBlock = "") {
4845
5764
  const userPrompt = `## Original Request
4846
5765
  ${originalPrompt}
4847
5766
 
4848
5767
  ` + (profileBlock.length > 0 ? `${profileBlock}
4849
5768
 
5769
+ ` : "") + (criteriaBlock.length > 0 ? `${criteriaBlock}
5770
+
4850
5771
  ` : "") + "Evaluate the screenshot of the generated component.";
4851
5772
  const agent = createVisionAgent({
4852
5773
  provider: config.provider === "claude" ? "anthropic" : config.provider,
@@ -4861,20 +5782,110 @@ ${originalPrompt}
4861
5782
  VISUAL_JUDGE_MAX_OUTPUT_TOKENS
4862
5783
  );
4863
5784
  }
4864
- async function frameCanvas(bundledCode, context, html, canvas, canvasViewports, deps) {
5785
+ function readInk(png, chrome, canvas, inlineInkInsetPx = JUDGE_INLINE_INK_INSET_PX) {
5786
+ if (png === null) return null;
5787
+ const ink = readInkExtent(png, chrome === "panel" ? JUDGE_INK_INSET_PX : chrome === "ground" ? inlineInkInsetPx : 0);
5788
+ if ("reason" in ink) {
5789
+ console.warn(`[visual-eval] ink extent unreadable at canvas ${canvas}: ${ink.reason}`);
5790
+ return null;
5791
+ }
5792
+ return ink;
5793
+ }
5794
+ function blankVerdict(canvas, frame) {
5795
+ if (frame.chrome === "ground" && frame.attempt.contentHeight !== null && frame.attempt.contentHeight <= 0) {
5796
+ return canvasBlankIssue(canvas, frame.viewport);
5797
+ }
5798
+ return frame.ink !== null && frame.ink.lastInkRow === null ? canvasBlankIssue(canvas, frame.viewport) : null;
5799
+ }
5800
+ async function frameCanvas(bundledCode, context, html, canvas, canvasViewports, deps, hostPresentations) {
4865
5801
  const declaredBox = canvasViewports?.[canvas];
4866
5802
  const viewport = declaredBox !== void 0 ? integerBox(declaredBox) : CANVAS_VIEWPORTS[canvas];
4867
5803
  const policy = canvasFitPolicy(canvas);
4868
5804
  const fit = canvasFit(canvas);
4869
- const canvasHtml = fit !== void 0 ? buildRenderHTML(bundledCode, context.cssTokens, fit) : html;
4870
- const attempt = await captureScreenshotDetailed(canvasHtml, viewport, deps, policy.capture);
4871
- return { viewport, policy, fit, declared: declaredBox !== void 0, attempt };
5805
+ const chrome = canvasChrome(canvas);
5806
+ const outcome = resolvePresentation(canvas, hostPresentations?.[canvas]);
5807
+ const presentation = outcome?.status === "applied" ? outcome.applied : void 0;
5808
+ const geometry = inlineGeometry(presentation?.frame);
5809
+ const canvasHtml = fit !== void 0 || chrome !== void 0 ? buildRenderHTML(bundledCode, context.cssTokens, fit, chrome, presentation) : html;
5810
+ const captured = await captureScreenshotDetailed(canvasHtml, judgeWindow(viewport, chrome, geometry.padPx), deps, policy.capture, {
5811
+ padPx: geometry.padPx,
5812
+ ...geometry.floorPx !== void 0 ? { floorPx: geometry.floorPx } : {}
5813
+ });
5814
+ const attempt = { ...captured, contentHeight: cardHeight(captured.contentHeight, chrome) };
5815
+ return {
5816
+ viewport,
5817
+ policy,
5818
+ fit,
5819
+ chrome,
5820
+ declared: declaredBox !== void 0,
5821
+ attempt,
5822
+ ink: readInk(captured.png, chrome, canvas, geometry.inkInsetPx),
5823
+ ...outcome !== void 0 ? { presentation: outcome } : {}
5824
+ };
4872
5825
  }
4873
5826
  function fitVerdict(canvas, frame) {
4874
5827
  const contentHeight = frame.attempt.contentHeight;
4875
5828
  if (contentHeight === null || contentHeight <= frame.viewport.height || frame.policy.overflow === "none") return null;
4876
5829
  return canvasOverflowIssue(canvas, frame.viewport, contentHeight, frame.policy.overflow);
4877
5830
  }
5831
+ function overflowXVerdict(canvas, frame) {
5832
+ const px = frame.attempt.overflowX;
5833
+ if (px === void 0 || px === null || px <= OVERFLOW_X_ROUNDING_PX || frame.policy.overflow === "none") return null;
5834
+ return canvasOverflowXIssue(canvas, frame.viewport, px, frame.policy.overflow);
5835
+ }
5836
+ function judgeSalvageOf(error, text, finishReason) {
5837
+ const cause = finishReason === void 0 ? "unknown" : finishReason === "length" ? "output-cap" : finishReason === "stop" ? "malformed" : "stopped";
5838
+ const message = error instanceof Error ? error.message : String(error);
5839
+ const position = /position (\d+)/.exec(message);
5840
+ const start = text.indexOf("{");
5841
+ const offset = position !== null ? (start > 0 ? start : 0) + Number(position[1]) : void 0;
5842
+ const parseError = message.startsWith("Visual evaluator did not return valid JSON") ? "no complete JSON object in the answer" : message.replace(/"(?:[^"\\]|\\.)*"/g, '"\u2026"').slice(0, 160);
5843
+ return { cause, ...finishReason !== void 0 ? { finishReason } : {}, parseError, ...offset !== void 0 ? { offset } : {}, chars: text.length };
5844
+ }
5845
+ function judgeRecordOf(k, parsed, samples, criteria, requestedModel) {
5846
+ const salvaged = parsed.flatMap((a) => a.salvage !== void 0 ? [a.salvage.cause] : []);
5847
+ const answered = [...parsed, ...criteria?.kind === "ok" ? [criteria] : []];
5848
+ const served = [...new Set(answered.flatMap((a) => a.servedModel !== void 0 ? [a.servedModel] : []))];
5849
+ const criteriaSalvaged = criteria?.kind === "ok" ? criteria.salvage?.cause : void 0;
5850
+ return {
5851
+ k,
5852
+ rule: "median",
5853
+ samples,
5854
+ sigma: populationSigma(samples),
5855
+ notes: parsed.map((a) => a.result.critique ?? ""),
5856
+ ...salvaged.length > 0 ? { salvaged } : {},
5857
+ ...criteriaSalvaged !== void 0 ? { criteriaSalvaged } : {},
5858
+ models: { requested: requestedModel, served }
5859
+ };
5860
+ }
5861
+ function callFacts(response) {
5862
+ return {
5863
+ ...response.finishReason !== void 0 ? { finishReason: response.finishReason } : {},
5864
+ ...response.servedModel !== void 0 ? { servedModel: response.servedModel } : {}
5865
+ };
5866
+ }
5867
+ var NO_CRITERIA_ARRAY = { array: false, received: 0, kept: 0, dropped: { notObject: 0, badId: 0, badVerdict: 0 } };
5868
+ function criteriaCallOf(criteria) {
5869
+ if (criteria?.kind !== "ok") return {};
5870
+ const answer = criteria.result.criteriaCounts ?? NO_CRITERIA_ARRAY;
5871
+ return { call: { answer, ...criteria.finishReason !== void 0 ? { finishReason: criteria.finishReason } : {} } };
5872
+ }
5873
+ async function judgeFrame(judge, config, model, png, originalPrompt, profileBlock, canvas, k, criteriaBlock) {
5874
+ const [scoring, criteria] = await Promise.all([
5875
+ Promise.all(Array.from({ length: k }, () => judgeAndParse(judge, config, model, png, originalPrompt, profileBlock, canvas))),
5876
+ criteriaBlock.length > 0 ? judgeAndParse(judge, config, model, png, originalPrompt, profileBlock, canvas, criteriaBlock) : void 0
5877
+ ]);
5878
+ return { scoring, criteria };
5879
+ }
5880
+ function criteriaCallTokens(criteria) {
5881
+ return criteria?.kind === "ok" ? { inputTokens: criteria.inputTokens, outputTokens: criteria.outputTokens } : void 0;
5882
+ }
5883
+ function criteriaReads(criteria) {
5884
+ return criteria?.kind === "ok" && criteria.salvage === void 0 ? [criteria.result.criteriaAnswers ?? []] : [];
5885
+ }
5886
+ function criteriaUnansweredReason(criteria) {
5887
+ return criteria?.kind === "ok" && criteria.salvage !== void 0 ? { unansweredReason: `not answered: the criteria answer was salvaged (${criteria.salvage.cause})` } : {};
5888
+ }
4878
5889
  function judgeCountFor(config, canvas) {
4879
5890
  const k = Math.max(1, Math.floor(config.judgeK ?? 1));
4880
5891
  if (k === 1) return 1;
@@ -4890,18 +5901,22 @@ function populationSigma(values) {
4890
5901
  const variance = values.reduce((a, b) => a + (b - mean) ** 2, 0) / values.length;
4891
5902
  return Math.round(Math.sqrt(variance) * 10) / 10;
4892
5903
  }
4893
- async function judgeAndParse(judge, config, model, screenshot, originalPrompt, profileBlock, canvas) {
5904
+ async function judgeAndParse(judge, config, model, screenshot, originalPrompt, profileBlock, canvas, criteriaBlock = "") {
4894
5905
  let firstReason;
4895
5906
  for (let attempt = 1; attempt <= 2; attempt++) {
4896
- const response = await judge(config, model, VISUAL_EVAL_PROMPT, screenshot, originalPrompt, profileBlock);
5907
+ const response = await judge(config, model, VISUAL_EVAL_PROMPT, screenshot, originalPrompt, profileBlock, criteriaBlock);
4897
5908
  try {
4898
5909
  const result = parseVisualResponse(response.text, config.passThreshold);
4899
- return { kind: "ok", result, inputTokens: response.inputTokens, outputTokens: response.outputTokens };
5910
+ return { kind: "ok", result, inputTokens: response.inputTokens, outputTokens: response.outputTokens, ...callFacts(response) };
4900
5911
  } catch (e) {
4901
5912
  const salvaged = salvageTruncatedVisualAnswer(response.text, config.passThreshold);
4902
5913
  if (salvaged !== null) {
4903
- console.warn(`[visual-eval] visual_judge_truncated${canvas ? ` canvas=${canvas}` : ""}: answer cut after ${response.text.length} chars \u2014 score recovered from the closed prefix, issues list partial`);
4904
- return { kind: "ok", result: salvaged, inputTokens: response.inputTokens, outputTokens: response.outputTokens };
5914
+ const salvage = judgeSalvageOf(e, response.text, response.finishReason);
5915
+ const token = criteriaBlock.length > 0 ? "visual_criteria_salvaged" : "visual_judge_salvaged";
5916
+ console.warn(
5917
+ `[visual-eval] ${token}${canvas ? ` canvas=${canvas}` : ""} cause=${salvage.cause} finish=${salvage.finishReason ?? "unreported"} at=${salvage.offset ?? "?"}/${salvage.chars} (${salvage.parseError}): score recovered from the closed prefix, issues list partial`
5918
+ );
5919
+ return { kind: "ok", result: salvaged, inputTokens: response.inputTokens, outputTokens: response.outputTokens, salvage, ...callFacts(response) };
4905
5920
  }
4906
5921
  const reason = `judge answer unparsable: ${e instanceof Error ? e.message : String(e)}`;
4907
5922
  if (attempt === 1) {
@@ -4942,7 +5957,7 @@ async function runVisualEvaluationDetailed(context, config, deps = {}) {
4942
5957
  const perCanvas = [];
4943
5958
  const perCanvasResults = [];
4944
5959
  for (const canvas of config.canvases) {
4945
- const frame = await frameCanvas(bundledCode, context, html, canvas, config.canvasViewports, deps);
5960
+ const frame = await frameCanvas(bundledCode, context, html, canvas, config.canvasViewports, deps, config.hostPresentations);
4946
5961
  const { viewport, policy, fit, attempt: attempt2 } = frame;
4947
5962
  const screenshot2 = attempt2.png;
4948
5963
  if (!screenshot2) {
@@ -4952,8 +5967,22 @@ async function runVisualEvaluationDetailed(context, config, deps = {}) {
4952
5967
  }
4953
5968
  const k = judgeCountFor(config, canvas);
4954
5969
  const profileBlock = buildStylingProfileJudgeBlock(context.profile);
4955
- const answers = await Promise.all(
4956
- Array.from({ length: k }, () => judgeAndParse(judge, config, model, screenshot2, context.originalPrompt, profileBlock, canvas))
5970
+ const criteriaContext = context.criteria !== void 0 ? { ...context.criteria.context, canvas } : void 0;
5971
+ const criteriaSelected = context.criteria !== void 0 && criteriaContext !== void 0 ? selectCriteria(context.criteria.bank, criteriaContext) : void 0;
5972
+ const criteriaBlock = context.criteria !== void 0 && criteriaSelected !== void 0 ? buildCriteriaJudgeBlock(context.criteria.bank, criteriaSelected, {
5973
+ frame: { canvas, width: viewport.width, height: viewport.height, ...frame.presentation?.status === "applied" ? { hostLabel: frame.presentation.applied.label } : {} },
5974
+ ...config.sampleProps !== void 0 ? { propsJson: JSON.stringify(config.sampleProps) } : {}
5975
+ }) : "";
5976
+ const { scoring: answers, criteria: criteriaAnswer } = await judgeFrame(
5977
+ judge,
5978
+ config,
5979
+ model,
5980
+ screenshot2,
5981
+ context.originalPrompt,
5982
+ profileBlock,
5983
+ canvas,
5984
+ k,
5985
+ criteriaBlock
4957
5986
  );
4958
5987
  const parsed = answers.filter((a) => a.kind === "ok");
4959
5988
  if (parsed.length === 0) {
@@ -4966,16 +5995,11 @@ async function runVisualEvaluationDetailed(context, config, deps = {}) {
4966
5995
  const result2 = representative.result;
4967
5996
  result2.finalScore = median;
4968
5997
  result2.passed = median >= config.passThreshold;
4969
- result2.inputTokens = parsed.reduce((sum, a) => sum + a.inputTokens, 0);
4970
- result2.outputTokens = parsed.reduce((sum, a) => sum + a.outputTokens, 0);
5998
+ result2.inputTokens = parsed.reduce((sum2, a) => sum2 + a.inputTokens, 0);
5999
+ result2.outputTokens = parsed.reduce((sum2, a) => sum2 + a.outputTokens, 0);
4971
6000
  const response2 = { inputTokens: result2.inputTokens, outputTokens: result2.outputTokens };
4972
- const judgeRecord = {
4973
- k,
4974
- rule: "median",
4975
- samples,
4976
- sigma: populationSigma(samples),
4977
- notes: parsed.map((a) => a.result.critique ?? "")
4978
- };
6001
+ const criteriaTokens = criteriaCallTokens(criteriaAnswer);
6002
+ const judgeRecord = judgeRecordOf(k, parsed, samples, criteriaAnswer, model);
4979
6003
  const contentHeight = attempt2.contentHeight;
4980
6004
  const overflow = contentHeight !== null && contentHeight > viewport.height;
4981
6005
  const fitIssue = fitVerdict(canvas, frame);
@@ -4983,6 +6007,26 @@ async function runVisualEvaluationDetailed(context, config, deps = {}) {
4983
6007
  result2.issues.push(fitIssue);
4984
6008
  if (policy.overflow === "fail") result2.passed = false;
4985
6009
  }
6010
+ const overflowXIssue = overflowXVerdict(canvas, frame);
6011
+ if (overflowXIssue !== null) {
6012
+ result2.issues.push(overflowXIssue);
6013
+ if (policy.overflow === "fail") result2.passed = false;
6014
+ }
6015
+ const blankIssue = blankVerdict(canvas, frame);
6016
+ if (blankIssue !== null) {
6017
+ result2.issues.push(blankIssue);
6018
+ result2.passed = false;
6019
+ }
6020
+ const inkRatio = frame.ink?.ratio ?? null;
6021
+ const criteria = context.criteria !== void 0 && criteriaContext !== void 0 && criteriaSelected !== void 0 ? resolveCriteriaBlock({
6022
+ bank: context.criteria.bank,
6023
+ context: criteriaContext,
6024
+ selected: criteriaSelected,
6025
+ answers: criteriaReads(criteriaAnswer),
6026
+ measurements: { overflow, contentHeight, viewportHeight: viewport.height, inkRatio, edge: attempt2.edge ?? null },
6027
+ ...criteriaUnansweredReason(criteriaAnswer),
6028
+ ...criteriaCallOf(criteriaAnswer)
6029
+ }) : void 0;
4986
6030
  perCanvasResults.push(result2);
4987
6031
  perCanvas.push({
4988
6032
  canvas,
@@ -4992,12 +6036,16 @@ async function runVisualEvaluationDetailed(context, config, deps = {}) {
4992
6036
  screenshotPng: screenshot2,
4993
6037
  contentHeight,
4994
6038
  overflow,
6039
+ inkRatio,
4995
6040
  judge: judgeRecord,
4996
6041
  ...fit !== void 0 ? { fit } : {},
4997
- ...frame.declared ? { declared: true } : {}
6042
+ ...frame.declared ? { declared: true } : {},
6043
+ ...criteria !== void 0 ? { criteria } : {},
6044
+ ...criteriaTokens !== void 0 ? { criteriaTokens } : {},
6045
+ ...frame.presentation !== void 0 ? { presentation: frame.presentation } : {}
4998
6046
  });
4999
6047
  console.log(
5000
- `[visual-eval] canvas=${canvas} ${viewport.width}\xD7${viewport.height} score=${result2.finalScore}${k > 1 ? ` (median of ${judgeRecord.samples.length}/${k}: ${judgeRecord.samples.join(",")} \u03C3=${judgeRecord.sigma})` : ""} ${result2.passed ? "pass" : "FAIL"} | content=${contentHeight ?? "?"}px${overflow ? ` OVERFLOW (${policy.overflow})` : ""} | in=${response2.inputTokens} out=${response2.outputTokens}`
6048
+ `[visual-eval] canvas=${canvas} ${viewport.width}\xD7${viewport.height} score=${result2.finalScore}${k > 1 ? ` (median of ${judgeRecord.samples.length}/${k}: ${judgeRecord.samples.join(",")} \u03C3=${judgeRecord.sigma})` : ""} ${result2.passed ? "pass" : "FAIL"} | content=${contentHeight ?? "?"}px${overflow ? ` OVERFLOW (${policy.overflow})` : ""} ink=${inkRatio ?? "?"}${blankIssue !== null ? " BLANK" : ""} | in=${response2.inputTokens} out=${response2.outputTokens}` + (criteriaTokens !== void 0 ? ` | criteria in=${criteriaTokens.inputTokens} out=${criteriaTokens.outputTokens}` : "")
5001
6049
  );
5002
6050
  }
5003
6051
  const aggregate = aggregateCanvasResults(perCanvas, perCanvasResults);
@@ -5039,7 +6087,7 @@ function canvasScopedIssue(canvas, issue) {
5039
6087
  }
5040
6088
  function aggregateCanvasResults(perCanvas, results) {
5041
6089
  const n = results.length;
5042
- const mean = (pick) => Math.round(results.reduce((sum, r) => sum + pick(r), 0) / n);
6090
+ const mean = (pick) => Math.round(results.reduce((sum2, r) => sum2 + pick(r), 0) / n);
5043
6091
  const dimensions = {
5044
6092
  completeness: mean((r) => r.dimensions.completeness),
5045
6093
  visualPolish: mean((r) => r.dimensions.visualPolish),
@@ -5058,7 +6106,19 @@ function aggregateCanvasResults(perCanvas, results) {
5058
6106
  inputTokens += r.inputTokens ?? 0;
5059
6107
  outputTokens += r.outputTokens ?? 0;
5060
6108
  });
6109
+ const criteriaSpent = perCanvas.flatMap((c) => c.criteriaTokens !== void 0 ? [c.criteriaTokens] : []);
6110
+ const judgeSalvage = {
6111
+ scoring: {
6112
+ salvaged: perCanvas.reduce((sum2, c) => sum2 + (c.judge.salvaged?.length ?? 0), 0),
6113
+ answers: perCanvas.reduce((sum2, c) => sum2 + c.judge.samples.length, 0)
6114
+ },
6115
+ criteria: {
6116
+ salvaged: perCanvas.filter((c) => c.judge.criteriaSalvaged !== void 0).length,
6117
+ answered: perCanvas.filter((c) => c.criteriaTokens !== void 0).length
6118
+ }
6119
+ };
5061
6120
  return {
6121
+ judgeSalvage,
5062
6122
  passed: perCanvas.every((c) => c.passed),
5063
6123
  finalScore: mean((r) => r.finalScore),
5064
6124
  dimensions,
@@ -5066,6 +6126,12 @@ function aggregateCanvasResults(perCanvas, results) {
5066
6126
  ...critiques.length > 0 ? { critique: critiques.join("\n") } : {},
5067
6127
  inputTokens,
5068
6128
  outputTokens,
6129
+ ...criteriaSpent.length > 0 ? {
6130
+ criteriaTokens: {
6131
+ inputTokens: criteriaSpent.reduce((sum2, t) => sum2 + t.inputTokens, 0),
6132
+ outputTokens: criteriaSpent.reduce((sum2, t) => sum2 + t.outputTokens, 0)
6133
+ }
6134
+ } : {},
5069
6135
  canvases: [...perCanvas]
5070
6136
  };
5071
6137
  }
@@ -5078,9 +6144,17 @@ function summarizeVisualResult(result) {
5078
6144
  passed: c.passed,
5079
6145
  contentHeight: c.contentHeight,
5080
6146
  overflow: c.overflow,
6147
+ inkRatio: c.inkRatio,
5081
6148
  judge: c.judge,
5082
- ...c.fit !== void 0 ? { fit: c.fit } : {}
6149
+ ...c.fit !== void 0 ? { fit: c.fit } : {},
6150
+ ...c.criteria !== void 0 ? { criteria: c.criteria } : {},
6151
+ ...c.presentation !== void 0 ? { presentation: c.presentation } : {}
5083
6152
  }));
6153
+ const blocks = result.canvases.map((c) => c.criteria).filter((b) => b !== void 0);
6154
+ const idsWhere = (severity, verdict) => [
6155
+ ...new Set(blocks.flatMap((b) => b.verdicts.filter((v) => v.severity === severity && v.verdict === verdict).map((v) => v.id)))
6156
+ ];
6157
+ const rollup = blocks.length > 0 ? { mustFailed: idsWhere("must", "fail"), shouldFailed: idsWhere("should", "fail"), mustNa: idsWhere("must", "n/a") } : void 0;
5084
6158
  const fitCanvas = result.canvases.find((c) => canvasFitPolicy(c.canvas).overflow === "fail" && c.contentHeight !== null);
5085
6159
  const fit = fitCanvas !== void 0 && fitCanvas.contentHeight !== null ? {
5086
6160
  canvas: fitCanvas.canvas,
@@ -5095,7 +6169,74 @@ function summarizeVisualResult(result) {
5095
6169
  canvases,
5096
6170
  ...fit !== void 0 ? { fit } : {},
5097
6171
  ...result.design !== void 0 ? { design: result.design } : {},
5098
- ...result.themeMode !== void 0 ? { themeMode: result.themeMode } : {}
6172
+ ...result.themeMode !== void 0 ? { themeMode: result.themeMode } : {},
6173
+ ...rollup !== void 0 ? { criteria: rollup } : {}
6174
+ };
6175
+ }
6176
+ async function judgeStoredCapture(input, config, deps = {}) {
6177
+ const judge = deps.judge ?? callMultimodalLLM;
6178
+ const model = config.model ?? getDefaultVisualModel(config.provider);
6179
+ const { canvas, viewport, png } = input;
6180
+ const chrome = canvasChrome(canvas);
6181
+ const ink = readInk(png, chrome, canvas);
6182
+ const inkRatio = ink?.ratio ?? null;
6183
+ const k = judgeCountFor(config, canvas);
6184
+ const profileBlock = buildStylingProfileJudgeBlock(input.profile);
6185
+ const criteriaContext = input.criteria !== void 0 ? { ...input.criteria.context, canvas } : void 0;
6186
+ const criteriaSelected = input.criteria !== void 0 && criteriaContext !== void 0 ? selectCriteria(input.criteria.bank, criteriaContext) : void 0;
6187
+ const criteriaBlock = input.criteria !== void 0 && criteriaSelected !== void 0 ? buildCriteriaJudgeBlock(input.criteria.bank, criteriaSelected, {
6188
+ frame: { canvas, width: viewport.width, height: viewport.height },
6189
+ ...input.sampleProps !== void 0 ? { propsJson: JSON.stringify(input.sampleProps) } : {}
6190
+ }) : "";
6191
+ const { scoring: answers, criteria: criteriaAnswer } = await judgeFrame(judge, config, model, png, input.originalPrompt, profileBlock, canvas, k, criteriaBlock);
6192
+ const parsed = answers.filter((a) => a.kind === "ok");
6193
+ if (parsed.length === 0) {
6194
+ const first = answers.find((a) => a.kind === "unparsable");
6195
+ return { kind: "unavailable", reason: first?.reason ?? "judge answer unparsable" };
6196
+ }
6197
+ const samples = parsed.map((a) => a.result.finalScore);
6198
+ const median = medianOf(samples);
6199
+ const result = parsed[samples.indexOf(median)].result;
6200
+ const issues = [...result.issues];
6201
+ let passed = median >= config.passThreshold;
6202
+ const contentHeight = input.contentHeight;
6203
+ const overflow = contentHeight !== null && contentHeight > viewport.height;
6204
+ const policy = canvasFitPolicy(canvas);
6205
+ if (overflow && contentHeight !== null && policy.overflow !== "none") {
6206
+ issues.push(canvasOverflowIssue(canvas, viewport, contentHeight, policy.overflow));
6207
+ if (policy.overflow === "fail") passed = false;
6208
+ }
6209
+ if (ink !== null && ink.lastInkRow === null) {
6210
+ issues.push(canvasBlankIssue(canvas, viewport));
6211
+ passed = false;
6212
+ }
6213
+ const criteria = input.criteria !== void 0 && criteriaContext !== void 0 && criteriaSelected !== void 0 ? resolveCriteriaBlock({
6214
+ bank: input.criteria.bank,
6215
+ context: criteriaContext,
6216
+ selected: criteriaSelected,
6217
+ answers: criteriaReads(criteriaAnswer),
6218
+ measurements: { overflow, contentHeight, viewportHeight: viewport.height, inkRatio },
6219
+ ...criteriaUnansweredReason(criteriaAnswer),
6220
+ ...criteriaCallOf(criteriaAnswer)
6221
+ }) : void 0;
6222
+ const criteriaTokens = criteriaCallTokens(criteriaAnswer);
6223
+ return {
6224
+ kind: "ok",
6225
+ verdict: {
6226
+ canvas,
6227
+ viewport,
6228
+ score: median,
6229
+ passed,
6230
+ issues,
6231
+ judge: judgeRecordOf(k, parsed, samples, criteriaAnswer, model),
6232
+ contentHeight,
6233
+ overflow,
6234
+ inkRatio,
6235
+ ...criteria !== void 0 ? { criteria } : {},
6236
+ inputTokens: parsed.reduce((sum2, a) => sum2 + a.inputTokens, 0),
6237
+ outputTokens: parsed.reduce((sum2, a) => sum2 + a.outputTokens, 0),
6238
+ ...criteriaTokens !== void 0 ? { criteriaTokens } : {}
6239
+ }
5099
6240
  };
5100
6241
  }
5101
6242
  function salvageTruncatedVisualAnswer(text, passThreshold) {
@@ -5132,7 +6273,7 @@ function salvageTruncatedVisualAnswer(text, passThreshold) {
5132
6273
  hierarchy,
5133
6274
  aesthetics,
5134
6275
  issues,
5135
- critique: `[truncated judge answer \u2014 score recovered from the closed prefix; ${issues.length} issue(s) kept, the rest and the critique were cut]`
6276
+ critique: `[salvaged judge answer \u2014 it did not parse; score recovered from the closed prefix; ${issues.length} issue(s) kept, the rest and the critique lost]`
5136
6277
  });
5137
6278
  return parseVisualResponse(rebuilt, passThreshold);
5138
6279
  }
@@ -5161,18 +6302,44 @@ function parseVisualResponse(text, passThreshold) {
5161
6302
  };
5162
6303
  const issues = (raw.issues || []).map((i) => ({
5163
6304
  dimension: i.dimension || "visual",
6305
+ origin: "judge",
5164
6306
  severity: i.severity || "minor",
5165
6307
  description: i.description || "",
5166
6308
  fix: i.fix || ""
5167
6309
  }));
6310
+ const criteria = parseCriteriaAnswers(raw.criteria);
5168
6311
  return {
5169
6312
  passed: finalScore >= passThreshold,
5170
6313
  finalScore,
5171
6314
  dimensions,
5172
6315
  issues,
5173
- critique: raw.critique
6316
+ critique: raw.critique,
6317
+ ...criteria.answers !== void 0 ? { criteriaAnswers: criteria.answers } : {},
6318
+ criteriaCounts: criteria.counts
5174
6319
  };
5175
6320
  }
6321
+ function parseCriteriaAnswers(raw) {
6322
+ if (!Array.isArray(raw)) return { counts: NO_CRITERIA_ARRAY };
6323
+ const answers = [];
6324
+ const dropped = { notObject: 0, badId: 0, badVerdict: 0 };
6325
+ for (const entry of raw) {
6326
+ if (typeof entry !== "object" || entry === null) {
6327
+ dropped.notObject += 1;
6328
+ continue;
6329
+ }
6330
+ const o = entry;
6331
+ if (typeof o.id !== "string") {
6332
+ dropped.badId += 1;
6333
+ continue;
6334
+ }
6335
+ if (o.verdict !== "pass" && o.verdict !== "fail" && o.verdict !== "n/a") {
6336
+ dropped.badVerdict += 1;
6337
+ continue;
6338
+ }
6339
+ answers.push({ id: o.id, verdict: o.verdict, evidence: typeof o.evidence === "string" ? o.evidence : "" });
6340
+ }
6341
+ return { answers, counts: { array: true, received: raw.length, kept: answers.length, dropped } };
6342
+ }
5176
6343
  function getDefaultVisualModel(provider) {
5177
6344
  switch (provider) {
5178
6345
  case "claude":
@@ -5191,7 +6358,12 @@ async function runVisualEval(context, config, deps = {}) {
5191
6358
  const issues = (result.issues || []).map(toEvalIssue);
5192
6359
  const summary = summarizeVisualResult(result);
5193
6360
  const coverage = { status: "ran" };
5194
- return summary === void 0 ? { issues, coverage } : { issues, summary, coverage };
6361
+ const tokens = result.inputTokens !== void 0 && result.outputTokens !== void 0 ? {
6362
+ inputTokens: result.inputTokens,
6363
+ outputTokens: result.outputTokens,
6364
+ ...result.criteriaTokens !== void 0 ? { criteria: result.criteriaTokens } : {}
6365
+ } : void 0;
6366
+ return { issues, ...summary !== void 0 ? { summary } : {}, coverage, ...tokens !== void 0 ? { tokens } : {} };
5195
6367
  }
5196
6368
  function toEvalIssue(issue) {
5197
6369
  return {
@@ -5201,14 +6373,55 @@ function toEvalIssue(issue) {
5201
6373
  subcategory: issue.dimension,
5202
6374
  severity: issue.severity === "critical" ? "critical" : "major",
5203
6375
  description: issue.description,
5204
- fix: issue.fix || ""
6376
+ fix: issue.fix || "",
6377
+ ...issue.origin !== void 0 ? { origin: issue.origin } : {}
5205
6378
  };
5206
6379
  }
5207
- function cssTokensForAppTheme(theme, mode = "light") {
5208
- if (theme === void 0) return getCssTokens(mode);
5209
- return composeThemeCss({ layer: "page", mode: theme.mode ?? mode, appTheme: theme });
6380
+ function cssTokensForAppTheme(theme, mode = "light", themeId) {
6381
+ if (theme === void 0 && themeId === void 0) return getCssTokens(mode);
6382
+ return composeThemeCss({
6383
+ layer: "page",
6384
+ mode: theme?.mode ?? mode,
6385
+ ...themeId !== void 0 ? { themeId } : {},
6386
+ ...theme !== void 0 ? { appTheme: theme } : {}
6387
+ });
6388
+ }
6389
+
6390
+ // src/evaluation/criteria/context.ts
6391
+ var PRIMARY_500 = /--ggui-color-primary-500:\s*(#[0-9a-fA-F]{3}|#[0-9a-fA-F]{6})\s*;/;
6392
+ var ACHROMATIC_SPREAD = 0.08;
6393
+ function chromaOfCssTokens(cssTokens) {
6394
+ const m = cssTokens?.match(PRIMARY_500);
6395
+ if (!m) return "unknown";
6396
+ const hex = m[1].slice(1);
6397
+ const full = hex.length === 3 ? hex.split("").map((c) => c + c).join("") : hex;
6398
+ const channels = [0, 2, 4].map((i) => parseInt(full.slice(i, i + 2), 16));
6399
+ const spread = (Math.max(...channels) - Math.min(...channels)) / 255;
6400
+ return spread < ACHROMATIC_SPREAD ? "achromatic" : "chromatic";
6401
+ }
6402
+ function criteriaContextFor(src) {
6403
+ const v = src.classification.vector;
6404
+ const preset = src.profile?.aesthetic?.id;
6405
+ return {
6406
+ hasActions: Object.keys(src.contract?.actionSpec ?? {}).length > 0,
6407
+ riskTier: src.classification.riskTier,
6408
+ axes: { render: v.render, state: v.state, writes: v.writes, fetch: v.fetch, layout: v.layout },
6409
+ chroma: chromaOfCssTokens(src.cssTokens),
6410
+ profilePresent: src.profile !== void 0,
6411
+ shell: src.shell,
6412
+ ...src.kind !== void 0 ? { kind: src.kind } : {},
6413
+ ...preset !== void 0 ? { preset } : {}
6414
+ };
6415
+ }
6416
+
6417
+ // src/evaluation/judge-spend.ts
6418
+ function judgedFrameTokens(result) {
6419
+ return {
6420
+ inputTokens: (result.inputTokens ?? 0) + (result.criteriaTokens?.inputTokens ?? 0),
6421
+ outputTokens: (result.outputTokens ?? 0) + (result.criteriaTokens?.outputTokens ?? 0)
6422
+ };
5210
6423
  }
5211
6424
 
5212
- export { CANVAS_BREAKPOINTS, CANVAS_CLASSES, CANVAS_DESCRIPTORS, CANVAS_VIEWPORTS, CONTENT_HEIGHT_EXPRESSION, CRITERIA, DEFAULT_EVAL_MAX_ROUNDS, DEFAULT_QUALITY_CONFIG, LLM_EVAL_STATIC_CRITERIA, MAX_EVAL_ROUNDS_HARD_LIMIT, VISUAL_EVAL_PROMPT, VISUAL_JUDGE_MAX_OUTPUT_TOKENS, VISUAL_JUDGE_PROMPT_DIGEST, VISUAL_JUDGE_PROMPT_VERSION, buildCodingCriteriaSummary, buildFixPrompt, callMultimodalLLM, canvasDisplayModeForShell, canvasFitPolicy, canvasForRendering, canvasForViewportWidth, canvasOverflowIssue, captureScreenshot, captureScreenshotDetailed, computeEvaluationScore, createEvaluationToolsServer, criteriaFor, cssTokensForAppTheme, describeCanvas, designTreeSha256, extractCompiledCode, extractCompiledCodeFromMessage, extractEvalResult, extractSourceCode, extractSourceCodeFromMessage, extractToolResultTexts, getActionableIssues, getCriteriaByPriority, getCriterionById, getEvaluatorSystemPrompt, getLLMCriteria, isBlocked, judgeDesignIdentity, matches, notApplicableCoverage, priorityForIssue, resetJudgeDesignIdentityCache, resolveDesignPackageDir, resolveLaunchOptions, runAxisChecks, runEvaluation, runEvaluationLoop, runVisualEval, runVisualEvaluation, runVisualEvaluationDetailed, summarizeVisualResult };
6425
+ export { CANVAS_BREAKPOINTS, CANVAS_CLASSES, CANVAS_DESCRIPTORS, CANVAS_VIEWPORTS, CARD_HEIGHT_EXPRESSION, CONTENT_HEIGHT_EXPRESSION, CRITERIA, CRITERIA_SELECTOR_VERSION, DEFAULT_EVAL_MAX_ROUNDS, DEFAULT_QUALITY_CONFIG, EDGE_BLOCKS_EXPRESSION, EDGE_TOLERANCE_PX, HOST_COLOUR_PATTERN, HOST_FRAME_BOUNDS, INK_TOLERANCE, INSTRUMENT_BESIDE_JUDGE, INSTRUMENT_BY_ID, JUDGE_GROUND_MARGIN_PX, JUDGE_INK_INSET_PX, LLM_EVAL_STATIC_CRITERIA, MAX_EVAL_ROUNDS_HARD_LIMIT, VISUAL_EVAL_PROMPT, VISUAL_JUDGE_MAX_OUTPUT_TOKENS, VISUAL_JUDGE_PROMPT_DIGEST, VISUAL_JUDGE_PROMPT_VERSION, bankRows, buildCodingCriteriaSummary, buildCriteriaJudgeBlock, buildFixPrompt, callMultimodalLLM, canonicalCriteriaContext, canvasBlankIssue, canvasDisplayModeForShell, canvasFitPolicy, canvasForRendering, canvasForViewportWidth, canvasOverflowIssue, captureScreenshot, captureScreenshotDetailed, chromaOfCssTokens, computeEvaluationScore, createEvaluationToolsServer, criteriaBankSchema, criteriaContextFor, criteriaFor, criteriaSetIdFor, cssTokensForAppTheme, decodePng, describeCanvas, designTreeSha256, extractCompiledCode, extractCompiledCodeFromMessage, extractEvalResult, extractSourceCode, extractSourceCodeFromMessage, extractToolResultTexts, getActionableIssues, getCriteriaByPriority, getCriterionById, getEvaluatorSystemPrompt, getLLMCriteria, isBlocked, judgeDesignIdentity, judgeEdges, judgeStoredCapture, judgedFrameTokens, matches, notApplicableCoverage, parseCriteriaBank, parseEdgeProbe, priorityForIssue, readInkExtent, resetJudgeDesignIdentityCache, resolveCriteriaBlock, resolveDesignPackageDir, resolveLaunchOptions, runAxisChecks, runEvaluation, runEvaluationLoop, runVisualEval, runVisualEvaluation, runVisualEvaluationDetailed, selectCriteria, summarizeVisualResult };
5213
6426
  //# sourceMappingURL=index.js.map
5214
6427
  //# sourceMappingURL=index.js.map