@ggui-ai/ui-gen 0.25.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 (106) hide show
  1. package/README.md +2 -2
  2. package/dist/{adapter-Cg_rkpGq.d.ts → adapter-BJwn74rx.d.ts} +49 -1
  3. package/dist/adapters/generation-dispatch.d.ts +8 -5
  4. package/dist/adapters/generation-dispatch.js +1118 -193
  5. package/dist/adapters/generation-dispatch.js.map +1 -1
  6. package/dist/adapters/index.js +27 -6
  7. package/dist/adapters/index.js.map +1 -1
  8. package/dist/advanced/index.d.ts +10 -9
  9. package/dist/advanced/index.js +1126 -202
  10. package/dist/advanced/index.js.map +1 -1
  11. package/dist/blueprint-validator.js +416 -92
  12. package/dist/blueprint-validator.js.map +1 -1
  13. package/dist/boilerplate.d.ts +2 -2
  14. package/dist/boilerplate.js +10 -3
  15. package/dist/boilerplate.js.map +1 -1
  16. package/dist/check/index.d.ts +3 -2
  17. package/dist/check/index.js +9 -2
  18. package/dist/check/index.js.map +1 -1
  19. package/dist/{create-ui-generator-DFnnArrv.d.ts → create-ui-generator-N4gRXpcL.d.ts} +3 -4
  20. package/dist/{design-mode-DLufhrNp.d.ts → design-mode-DGWN_Gkm.d.ts} +1 -1
  21. package/dist/{dispatch-CStl9eAw.d.ts → dispatch-BUoXRj_r.d.ts} +2 -2
  22. package/dist/evaluation/axis-checks/index.d.ts +5 -4
  23. package/dist/evaluation/axis-checks/index.js.map +1 -1
  24. package/dist/evaluation/axis-checks/registry.d.ts +4 -3
  25. package/dist/evaluation/index.d.ts +107 -539
  26. package/dist/evaluation/index.js +552 -55
  27. package/dist/evaluation/index.js.map +1 -1
  28. package/dist/evaluation/types.d.ts +17 -0
  29. package/dist/evaluation/types.js.map +1 -1
  30. package/dist/{generate-9IeNT4ZY.d.ts → generate-tSO_shQ0.d.ts} +1 -1
  31. package/dist/harness/check/runtime-render/index.d.ts +5 -5
  32. package/dist/harness/check/runtime-render/index.js +416 -92
  33. package/dist/harness/check/runtime-render/index.js.map +1 -1
  34. package/dist/harness/check/runtime-render/render-check-worker.js +393 -88
  35. package/dist/harness/check/runtime-render/render-check-worker.js.map +1 -1
  36. package/dist/harness/index.d.ts +4 -5
  37. package/dist/harness/index.js +36 -5
  38. package/dist/harness/index.js.map +1 -1
  39. package/dist/harness/prompts.d.ts +2 -2
  40. package/dist/harness/prompts.js +10 -1
  41. package/dist/harness/prompts.js.map +1 -1
  42. package/dist/harness/result-types.d.ts +17 -3
  43. package/dist/harness/result-types.js.map +1 -1
  44. package/dist/harness/runtime.d.ts +14 -6
  45. package/dist/harness/runtime.js +11 -4
  46. package/dist/harness/runtime.js.map +1 -1
  47. package/dist/harness/types-public.d.ts +4 -2
  48. package/dist/harness/types-public.js +10 -3
  49. package/dist/harness/types-public.js.map +1 -1
  50. package/dist/harness/validator-trace-sink.d.ts +4 -3
  51. package/dist/index.d.ts +46 -11
  52. package/dist/index.js +1128 -200
  53. package/dist/index.js.map +1 -1
  54. package/dist/patch.d.ts +3 -3
  55. package/dist/{prompts-YU9piOgs.d.ts → prompts-BhNUuRoZ.d.ts} +1 -1
  56. package/dist/tools/render-check-worker.js +8 -1
  57. package/dist/tools/render-check-worker.js.map +1 -1
  58. package/dist/{types-public-BJTLpzWn.d.ts → types-public-B7wn4ce5.d.ts} +384 -3
  59. package/dist/validation/index.d.ts +1 -1
  60. package/dist/validation/index.js +1 -1
  61. package/dist/validation/index.js.map +1 -1
  62. package/dist/visual-evaluator-C9L-o6ap.d.ts +639 -0
  63. package/dist/workflows.d.ts +3 -3
  64. package/package.json +12 -12
  65. package/src/adapters/generation-dispatch.ts +2 -0
  66. package/src/boilerplate/free-design-prompt.ts +3 -3
  67. package/src/boilerplate/hard-sections.ts +1 -1
  68. package/src/boilerplate/templates/layouts/chat-mobile.tsx.tmpl +3 -1
  69. package/src/boilerplate/templates/layouts/chat-universal.tsx.tmpl +3 -1
  70. package/src/boilerplate/templates/layouts/fullscreen-desktop.tsx.tmpl +3 -1
  71. package/src/boilerplate/templates/layouts/fullscreen-universal.tsx.tmpl +3 -1
  72. package/src/check/run-tier0.ts +6 -3
  73. package/src/contract-context.ts +1 -1
  74. package/src/evaluation/cost-tracker.ts +54 -6
  75. package/src/evaluation/criteria/edge-probe.ts +286 -0
  76. package/src/evaluation/criteria/edge.ts +202 -0
  77. package/src/evaluation/criteria/resolve.ts +55 -7
  78. package/src/evaluation/index.ts +5 -1
  79. package/src/evaluation/judge-spend.ts +48 -0
  80. package/src/evaluation/types-public.ts +91 -0
  81. package/src/evaluation/types.ts +10 -0
  82. package/src/evaluation/visual-evaluator.ts +236 -40
  83. package/src/harness/check/runtime-render/adapter.ts +18 -0
  84. package/src/harness/check/runtime-render/probe-pending.ts +111 -0
  85. package/src/harness/check/runtime-render/render-check-host.ts +10 -6
  86. package/src/harness/check/runtime-render/render-check-worker.ts +6 -3
  87. package/src/harness/check/runtime-render/render-check.ts +417 -133
  88. package/src/harness/check/runtime-render/walk.ts +200 -0
  89. package/src/harness/coding/assemble-result.ts +2 -0
  90. package/src/harness/coding/generate-task-runner.ts +27 -0
  91. package/src/harness/coding/init-session.ts +5 -3
  92. package/src/harness/coding/run-coding-turn.ts +5 -1
  93. package/src/harness/coding/run-eval-round.ts +63 -3
  94. package/src/harness/in-loop-evaluation.ts +39 -0
  95. package/src/harness/llm-router.ts +58 -1
  96. package/src/harness/result-types.ts +9 -0
  97. package/src/harness/runtime.ts +9 -0
  98. package/src/harness/types-public.ts +10 -1
  99. package/src/index.ts +5 -0
  100. package/src/internal/verdict-line.ts +33 -0
  101. package/src/taught-tokens.ts +32 -0
  102. package/src/tools/render-check-worker.ts +2 -1
  103. package/src/tools/render-check.ts +8 -7
  104. package/src/validation/primitives.ts +1 -1
  105. package/dist/llm-router-nfO7wPec.d.ts +0 -267
  106. package/dist/{bank-CT4eEkta.d.ts → bank-Ctdp3FUA.d.ts} +3 -3
@@ -816,6 +816,26 @@ async function collectLoginTurn(stream, opts) {
816
816
  }
817
817
 
818
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
+ }
819
839
  function addStrictSchemaConstraints(schema) {
820
840
  const result = { ...schema };
821
841
  if (result.type === "object" && result.properties) {
@@ -1156,7 +1176,9 @@ var AnthropicAgent = class extends LLMAgent {
1156
1176
  return {
1157
1177
  text,
1158
1178
  inputTokens: response.usage.input_tokens,
1159
- outputTokens: response.usage.output_tokens
1179
+ outputTokens: response.usage.output_tokens,
1180
+ ...visionFinish(anthropicFinishReason(response.stop_reason)),
1181
+ ...servedModelOf(response.model)
1160
1182
  };
1161
1183
  } catch (e) {
1162
1184
  emitLlmTraceEvent({
@@ -1767,7 +1789,9 @@ var GoogleAgent = class extends LLMAgent {
1767
1789
  text,
1768
1790
  // ggui#1186 (third site): `promptTokenCount` includes `cachedContentTokenCount` — report the non-cached input.
1769
1791
  inputTokens: splitCacheInclusiveUsage(usage?.promptTokenCount ?? 0, usage?.candidatesTokenCount ?? 0, usage?.cachedContentTokenCount ?? 0).tokens.input,
1770
- outputTokens: usage?.candidatesTokenCount ?? 0
1792
+ outputTokens: usage?.candidatesTokenCount ?? 0,
1793
+ ...visionFinish(googleFinishReason(response.candidates?.[0]?.finishReason)),
1794
+ ...servedModelOf(response.modelVersion)
1771
1795
  };
1772
1796
  const endedAt = Date.now();
1773
1797
  emitLlmTraceEvent({
@@ -2514,12 +2538,12 @@ async function runEvaluationLoop(options) {
2514
2538
  const evaluationResults = [];
2515
2539
  let currentCode = context.compiledCode;
2516
2540
  let currentSourceCode = context.sourceCode;
2517
- let round = 0;
2541
+ let round2 = 0;
2518
2542
  const maxRounds = Math.min(config.maxRounds ?? 3, MAX_EVAL_ROUNDS_HARD_LIMIT);
2519
- while (round < maxRounds) {
2520
- round++;
2521
- onProgress?.({ type: "evaluating", round });
2522
- 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...`);
2523
2547
  const evalContext = {
2524
2548
  ...context,
2525
2549
  sourceCode: currentSourceCode || context.sourceCode
@@ -2527,13 +2551,13 @@ async function runEvaluationLoop(options) {
2527
2551
  const evalResult = await runEvaluation(evalContext, config);
2528
2552
  evaluationResults.push(evalResult);
2529
2553
  console.log(
2530
- `[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}`
2531
2555
  );
2532
- if (evalResult.passed || round >= maxRounds) {
2556
+ if (evalResult.passed || round2 >= maxRounds) {
2533
2557
  break;
2534
2558
  }
2535
- onProgress?.({ type: "fixing", round });
2536
- console.log(`[eval] Round ${round}: fixing...`);
2559
+ onProgress?.({ type: "fixing", round: round2 });
2560
+ console.log(`[eval] Round ${round2}: fixing...`);
2537
2561
  const fixPrompt = buildFixPrompt(evalResult, context.originalPrompt);
2538
2562
  const cliPath = process.env.CLAUDE_WRAPPER_PATH;
2539
2563
  const env = {};
@@ -2573,7 +2597,7 @@ async function runEvaluationLoop(options) {
2573
2597
  }
2574
2598
  if (fixedCompiledCode) {
2575
2599
  currentCode = fixedCompiledCode;
2576
- console.log(`[eval] Round ${round}: fixed code captured (${currentCode.length} bytes)`);
2600
+ console.log(`[eval] Round ${round2}: fixed code captured (${currentCode.length} bytes)`);
2577
2601
  }
2578
2602
  if (fixedSourceCode) {
2579
2603
  currentSourceCode = fixedSourceCode;
@@ -4692,9 +4716,17 @@ var INSTRUMENT_BY_ID = {
4692
4716
  "floor.fit": "fit",
4693
4717
  "finish.canvas.inhabited": "fill"
4694
4718
  };
4695
- function majority(id, answers) {
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) {
4696
4728
  const votes = answers.flatMap((a) => a.filter((x) => x.id === id));
4697
- if (votes.length === 0) return { verdict: "n/a", evidence: "not answered" };
4729
+ if (votes.length === 0) return { verdict: "n/a", evidence: unansweredReason ?? "not answered" };
4698
4730
  const counts = /* @__PURE__ */ new Map();
4699
4731
  for (const v of votes) counts.set(v.verdict, (counts.get(v.verdict) ?? 0) + 1);
4700
4732
  const ranked = [...counts.entries()].sort((a, b) => b[1] - a[1]);
@@ -4711,8 +4743,8 @@ function instrumentRead(kind, m) {
4711
4743
  if (m.inkRatio === null) return { verdict: "n/a", evidence: "capture unreadable" };
4712
4744
  return { verdict: "n/a", evidence: `fill ${m.inkRatio.toFixed(3)} reported; no floor ruled` };
4713
4745
  }
4714
- function readRow(row, answers, m) {
4715
- if (row.evaluation === "judge") return majority(row.id, answers);
4746
+ function readRow(row, answers, m, unansweredReason) {
4747
+ if (row.evaluation === "judge") return majority(row.id, answers, unansweredReason);
4716
4748
  if (row.evaluation === "human") return { verdict: "n/a", evidence: `reader's column (status ${row.status})` };
4717
4749
  if (row.status !== "live") return { verdict: "n/a", evidence: `not implemented (status ${row.status})` };
4718
4750
  const kind = INSTRUMENT_BY_ID[row.id];
@@ -4724,8 +4756,18 @@ function resolveCriteriaBlock(args) {
4724
4756
  for (const s of args.selected.selection) {
4725
4757
  const row = byId.get(s.id);
4726
4758
  if (row === void 0) continue;
4727
- const read = readRow(row, args.answers, args.measurements);
4728
- verdicts.push({ id: row.id, severity: row.severity, method: row.evaluation, status: row.status, source: s.source, verdict: read.verdict, evidence: read.evidence });
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
+ });
4729
4771
  }
4730
4772
  return {
4731
4773
  criteriaSetId: args.selected.criteriaSetId,
@@ -4733,7 +4775,15 @@ function resolveCriteriaBlock(args) {
4733
4775
  selectorVersion: CRITERIA_SELECTOR_VERSION,
4734
4776
  context: args.context,
4735
4777
  selection: args.selected.selection,
4736
- verdicts
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
+ } : {}
4737
4787
  };
4738
4788
  }
4739
4789
  var CRITERIA_PROPS_MAX_CHARS = 2e3;
@@ -4938,6 +4988,356 @@ function judgeDesignIdentity(src) {
4938
4988
  function resetJudgeDesignIdentityCache() {
4939
4989
  identityCache.clear();
4940
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
4941
5341
  function canvasFitPolicy(canvas) {
4942
5342
  switch (canvas) {
4943
5343
  case "xs-chat-card":
@@ -5021,6 +5421,7 @@ function canvasOverflowIssue(canvas, viewport, contentHeight, verdict) {
5021
5421
  const box = isDeclaredViewport(canvas, viewport) ? `declared ${viewport.width}\xD7${viewport.height}; class box ${classBox.width}\xD7${classBox.height}` : `${viewport.width}\xD7${viewport.height}`;
5022
5422
  return {
5023
5423
  dimension: "canvas-overflow",
5424
+ origin: "instrument",
5024
5425
  severity: verdict === "fail" ? "critical" : "major",
5025
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"}.`,
5026
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."
@@ -5029,6 +5430,7 @@ function canvasOverflowIssue(canvas, viewport, contentHeight, verdict) {
5029
5430
  function canvasOverflowXIssue(canvas, viewport, hiddenPx, verdict) {
5030
5431
  return {
5031
5432
  dimension: "canvas-overflow-x",
5433
+ origin: "instrument",
5032
5434
  severity: verdict === "fail" ? "critical" : "major",
5033
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.`,
5034
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."
@@ -5039,6 +5441,7 @@ function canvasBlankIssue(canvas, viewport) {
5039
5441
  const box = isDeclaredViewport(canvas, viewport) ? `declared ${viewport.width}\xD7${viewport.height}; class box ${classBox.width}\xD7${classBox.height}` : `${viewport.width}\xD7${viewport.height}`;
5040
5442
  return {
5041
5443
  dimension: "canvas-blank",
5444
+ origin: "instrument",
5042
5445
  severity: "critical",
5043
5446
  description: `Nothing painted on the ${canvas} canvas (${box}): the capture is one flat colour \u2014 the component mounted and rendered no pixels.`,
5044
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."
@@ -5260,6 +5663,19 @@ async function measureContentHeight(page, expression = CONTENT_HEIGHT_EXPRESSION
5260
5663
  return null;
5261
5664
  }
5262
5665
  }
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
+ }
5263
5679
  async function captureScreenshotDetailed(html, viewport = DEFAULT_SCREENSHOT_VIEWPORT, deps = {}, capture = "full-page", natural = { padPx: JUDGE_INLINE_PAD_PX }) {
5264
5680
  try {
5265
5681
  const launchOptions = await resolveLaunchOptions(viewport, deps);
@@ -5281,12 +5697,14 @@ async function captureScreenshotDetailed(html, viewport = DEFAULT_SCREENSHOT_VIE
5281
5697
  }
5282
5698
  const framed = cardHeightPx === null ? viewport.height - 2 * natural.padPx : floorPx !== void 0 ? Math.max(cardHeightPx, floorPx) : cardHeightPx;
5283
5699
  const clip = naturalClip(viewport, framed, natural.padPx);
5700
+ const edge2 = await measureEdges(page);
5284
5701
  const screenshot2 = await page.screenshot({ type: "png", fullPage: false, clip });
5285
- return { png: Buffer.from(screenshot2), contentHeight: cardHeightPx, overflowX };
5702
+ return { png: Buffer.from(screenshot2), contentHeight: cardHeightPx, overflowX, edge: edge2 };
5286
5703
  }
5287
5704
  const contentHeight = await measureContentHeight(page);
5705
+ const edge = await measureEdges(page);
5288
5706
  const screenshot = await page.screenshot({ type: "png", fullPage: capture === "full-page" });
5289
- return { png: Buffer.from(screenshot), contentHeight };
5707
+ return { png: Buffer.from(screenshot), contentHeight, edge };
5290
5708
  } finally {
5291
5709
  await browser.close();
5292
5710
  }
@@ -5415,6 +5833,43 @@ function overflowXVerdict(canvas, frame) {
5415
5833
  if (px === void 0 || px === null || px <= OVERFLOW_X_ROUNDING_PX || frame.policy.overflow === "none") return null;
5416
5834
  return canvasOverflowXIssue(canvas, frame.viewport, px, frame.policy.overflow);
5417
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
+ }
5418
5873
  async function judgeFrame(judge, config, model, png, originalPrompt, profileBlock, canvas, k, criteriaBlock) {
5419
5874
  const [scoring, criteria] = await Promise.all([
5420
5875
  Promise.all(Array.from({ length: k }, () => judgeAndParse(judge, config, model, png, originalPrompt, profileBlock, canvas))),
@@ -5426,7 +5881,10 @@ function criteriaCallTokens(criteria) {
5426
5881
  return criteria?.kind === "ok" ? { inputTokens: criteria.inputTokens, outputTokens: criteria.outputTokens } : void 0;
5427
5882
  }
5428
5883
  function criteriaReads(criteria) {
5429
- return criteria?.kind === "ok" ? [criteria.result.criteriaAnswers ?? []] : [];
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})` } : {};
5430
5888
  }
5431
5889
  function judgeCountFor(config, canvas) {
5432
5890
  const k = Math.max(1, Math.floor(config.judgeK ?? 1));
@@ -5449,12 +5907,16 @@ async function judgeAndParse(judge, config, model, screenshot, originalPrompt, p
5449
5907
  const response = await judge(config, model, VISUAL_EVAL_PROMPT, screenshot, originalPrompt, profileBlock, criteriaBlock);
5450
5908
  try {
5451
5909
  const result = parseVisualResponse(response.text, config.passThreshold);
5452
- return { kind: "ok", result, inputTokens: response.inputTokens, outputTokens: response.outputTokens };
5910
+ return { kind: "ok", result, inputTokens: response.inputTokens, outputTokens: response.outputTokens, ...callFacts(response) };
5453
5911
  } catch (e) {
5454
5912
  const salvaged = salvageTruncatedVisualAnswer(response.text, config.passThreshold);
5455
5913
  if (salvaged !== null) {
5456
- 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`);
5457
- 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) };
5458
5920
  }
5459
5921
  const reason = `judge answer unparsable: ${e instanceof Error ? e.message : String(e)}`;
5460
5922
  if (attempt === 1) {
@@ -5533,17 +5995,11 @@ async function runVisualEvaluationDetailed(context, config, deps = {}) {
5533
5995
  const result2 = representative.result;
5534
5996
  result2.finalScore = median;
5535
5997
  result2.passed = median >= config.passThreshold;
5536
- result2.inputTokens = parsed.reduce((sum, a) => sum + a.inputTokens, 0);
5537
- 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);
5538
6000
  const response2 = { inputTokens: result2.inputTokens, outputTokens: result2.outputTokens };
5539
6001
  const criteriaTokens = criteriaCallTokens(criteriaAnswer);
5540
- const judgeRecord = {
5541
- k,
5542
- rule: "median",
5543
- samples,
5544
- sigma: populationSigma(samples),
5545
- notes: parsed.map((a) => a.result.critique ?? "")
5546
- };
6002
+ const judgeRecord = judgeRecordOf(k, parsed, samples, criteriaAnswer, model);
5547
6003
  const contentHeight = attempt2.contentHeight;
5548
6004
  const overflow = contentHeight !== null && contentHeight > viewport.height;
5549
6005
  const fitIssue = fitVerdict(canvas, frame);
@@ -5567,7 +6023,9 @@ async function runVisualEvaluationDetailed(context, config, deps = {}) {
5567
6023
  context: criteriaContext,
5568
6024
  selected: criteriaSelected,
5569
6025
  answers: criteriaReads(criteriaAnswer),
5570
- measurements: { overflow, contentHeight, viewportHeight: viewport.height, inkRatio }
6026
+ measurements: { overflow, contentHeight, viewportHeight: viewport.height, inkRatio, edge: attempt2.edge ?? null },
6027
+ ...criteriaUnansweredReason(criteriaAnswer),
6028
+ ...criteriaCallOf(criteriaAnswer)
5571
6029
  }) : void 0;
5572
6030
  perCanvasResults.push(result2);
5573
6031
  perCanvas.push({
@@ -5629,7 +6087,7 @@ function canvasScopedIssue(canvas, issue) {
5629
6087
  }
5630
6088
  function aggregateCanvasResults(perCanvas, results) {
5631
6089
  const n = results.length;
5632
- 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);
5633
6091
  const dimensions = {
5634
6092
  completeness: mean((r) => r.dimensions.completeness),
5635
6093
  visualPolish: mean((r) => r.dimensions.visualPolish),
@@ -5649,7 +6107,18 @@ function aggregateCanvasResults(perCanvas, results) {
5649
6107
  outputTokens += r.outputTokens ?? 0;
5650
6108
  });
5651
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
+ };
5652
6120
  return {
6121
+ judgeSalvage,
5653
6122
  passed: perCanvas.every((c) => c.passed),
5654
6123
  finalScore: mean((r) => r.finalScore),
5655
6124
  dimensions,
@@ -5659,8 +6128,8 @@ function aggregateCanvasResults(perCanvas, results) {
5659
6128
  outputTokens,
5660
6129
  ...criteriaSpent.length > 0 ? {
5661
6130
  criteriaTokens: {
5662
- inputTokens: criteriaSpent.reduce((sum, t) => sum + t.inputTokens, 0),
5663
- outputTokens: criteriaSpent.reduce((sum, t) => sum + t.outputTokens, 0)
6131
+ inputTokens: criteriaSpent.reduce((sum2, t) => sum2 + t.inputTokens, 0),
6132
+ outputTokens: criteriaSpent.reduce((sum2, t) => sum2 + t.outputTokens, 0)
5664
6133
  }
5665
6134
  } : {},
5666
6135
  canvases: [...perCanvas]
@@ -5746,7 +6215,9 @@ async function judgeStoredCapture(input, config, deps = {}) {
5746
6215
  context: criteriaContext,
5747
6216
  selected: criteriaSelected,
5748
6217
  answers: criteriaReads(criteriaAnswer),
5749
- measurements: { overflow, contentHeight, viewportHeight: viewport.height, inkRatio }
6218
+ measurements: { overflow, contentHeight, viewportHeight: viewport.height, inkRatio },
6219
+ ...criteriaUnansweredReason(criteriaAnswer),
6220
+ ...criteriaCallOf(criteriaAnswer)
5750
6221
  }) : void 0;
5751
6222
  const criteriaTokens = criteriaCallTokens(criteriaAnswer);
5752
6223
  return {
@@ -5757,13 +6228,13 @@ async function judgeStoredCapture(input, config, deps = {}) {
5757
6228
  score: median,
5758
6229
  passed,
5759
6230
  issues,
5760
- judge: { k, rule: "median", samples, sigma: populationSigma(samples), notes: parsed.map((a) => a.result.critique ?? "") },
6231
+ judge: judgeRecordOf(k, parsed, samples, criteriaAnswer, model),
5761
6232
  contentHeight,
5762
6233
  overflow,
5763
6234
  inkRatio,
5764
6235
  ...criteria !== void 0 ? { criteria } : {},
5765
- inputTokens: parsed.reduce((sum, a) => sum + a.inputTokens, 0),
5766
- outputTokens: parsed.reduce((sum, a) => sum + a.outputTokens, 0),
6236
+ inputTokens: parsed.reduce((sum2, a) => sum2 + a.inputTokens, 0),
6237
+ outputTokens: parsed.reduce((sum2, a) => sum2 + a.outputTokens, 0),
5767
6238
  ...criteriaTokens !== void 0 ? { criteriaTokens } : {}
5768
6239
  }
5769
6240
  };
@@ -5802,7 +6273,7 @@ function salvageTruncatedVisualAnswer(text, passThreshold) {
5802
6273
  hierarchy,
5803
6274
  aesthetics,
5804
6275
  issues,
5805
- 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]`
5806
6277
  });
5807
6278
  return parseVisualResponse(rebuilt, passThreshold);
5808
6279
  }
@@ -5831,31 +6302,43 @@ function parseVisualResponse(text, passThreshold) {
5831
6302
  };
5832
6303
  const issues = (raw.issues || []).map((i) => ({
5833
6304
  dimension: i.dimension || "visual",
6305
+ origin: "judge",
5834
6306
  severity: i.severity || "minor",
5835
6307
  description: i.description || "",
5836
6308
  fix: i.fix || ""
5837
6309
  }));
5838
- const criteriaAnswers = parseCriteriaAnswers(raw.criteria);
6310
+ const criteria = parseCriteriaAnswers(raw.criteria);
5839
6311
  return {
5840
6312
  passed: finalScore >= passThreshold,
5841
6313
  finalScore,
5842
6314
  dimensions,
5843
6315
  issues,
5844
6316
  critique: raw.critique,
5845
- ...criteriaAnswers !== void 0 ? { criteriaAnswers } : {}
6317
+ ...criteria.answers !== void 0 ? { criteriaAnswers: criteria.answers } : {},
6318
+ criteriaCounts: criteria.counts
5846
6319
  };
5847
6320
  }
5848
6321
  function parseCriteriaAnswers(raw) {
5849
- if (!Array.isArray(raw)) return void 0;
5850
- const out = [];
6322
+ if (!Array.isArray(raw)) return { counts: NO_CRITERIA_ARRAY };
6323
+ const answers = [];
6324
+ const dropped = { notObject: 0, badId: 0, badVerdict: 0 };
5851
6325
  for (const entry of raw) {
5852
- if (typeof entry !== "object" || entry === null) continue;
6326
+ if (typeof entry !== "object" || entry === null) {
6327
+ dropped.notObject += 1;
6328
+ continue;
6329
+ }
5853
6330
  const o = entry;
5854
- if (typeof o.id !== "string") continue;
5855
- if (o.verdict !== "pass" && o.verdict !== "fail" && o.verdict !== "n/a") continue;
5856
- out.push({ id: o.id, verdict: o.verdict, evidence: typeof o.evidence === "string" ? o.evidence : "" });
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 : "" });
5857
6340
  }
5858
- return out;
6341
+ return { answers, counts: { array: true, received: raw.length, kept: answers.length, dropped } };
5859
6342
  }
5860
6343
  function getDefaultVisualModel(provider) {
5861
6344
  switch (provider) {
@@ -5875,7 +6358,12 @@ async function runVisualEval(context, config, deps = {}) {
5875
6358
  const issues = (result.issues || []).map(toEvalIssue);
5876
6359
  const summary = summarizeVisualResult(result);
5877
6360
  const coverage = { status: "ran" };
5878
- 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 } : {} };
5879
6367
  }
5880
6368
  function toEvalIssue(issue) {
5881
6369
  return {
@@ -5885,7 +6373,8 @@ function toEvalIssue(issue) {
5885
6373
  subcategory: issue.dimension,
5886
6374
  severity: issue.severity === "critical" ? "critical" : "major",
5887
6375
  description: issue.description,
5888
- fix: issue.fix || ""
6376
+ fix: issue.fix || "",
6377
+ ...issue.origin !== void 0 ? { origin: issue.origin } : {}
5889
6378
  };
5890
6379
  }
5891
6380
  function cssTokensForAppTheme(theme, mode = "light", themeId) {
@@ -5925,6 +6414,14 @@ function criteriaContextFor(src) {
5925
6414
  };
5926
6415
  }
5927
6416
 
5928
- 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, HOST_COLOUR_PATTERN, HOST_FRAME_BOUNDS, INK_TOLERANCE, 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, judgeStoredCapture, matches, notApplicableCoverage, parseCriteriaBank, priorityForIssue, readInkExtent, resetJudgeDesignIdentityCache, resolveCriteriaBlock, resolveDesignPackageDir, resolveLaunchOptions, runAxisChecks, runEvaluation, runEvaluationLoop, runVisualEval, runVisualEvaluation, runVisualEvaluationDetailed, selectCriteria, summarizeVisualResult };
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
+ };
6423
+ }
6424
+
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 };
5929
6426
  //# sourceMappingURL=index.js.map
5930
6427
  //# sourceMappingURL=index.js.map