@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.
- package/README.md +2 -2
- package/dist/{adapter-Cg_rkpGq.d.ts → adapter-BJwn74rx.d.ts} +49 -1
- package/dist/adapters/generation-dispatch.d.ts +8 -5
- package/dist/adapters/generation-dispatch.js +1118 -193
- package/dist/adapters/generation-dispatch.js.map +1 -1
- package/dist/adapters/index.js +27 -6
- package/dist/adapters/index.js.map +1 -1
- package/dist/advanced/index.d.ts +10 -9
- package/dist/advanced/index.js +1126 -202
- package/dist/advanced/index.js.map +1 -1
- package/dist/blueprint-validator.js +416 -92
- package/dist/blueprint-validator.js.map +1 -1
- package/dist/boilerplate.d.ts +2 -2
- package/dist/boilerplate.js +10 -3
- package/dist/boilerplate.js.map +1 -1
- package/dist/check/index.d.ts +3 -2
- package/dist/check/index.js +9 -2
- package/dist/check/index.js.map +1 -1
- package/dist/{create-ui-generator-DFnnArrv.d.ts → create-ui-generator-N4gRXpcL.d.ts} +3 -4
- package/dist/{design-mode-DLufhrNp.d.ts → design-mode-DGWN_Gkm.d.ts} +1 -1
- package/dist/{dispatch-CStl9eAw.d.ts → dispatch-BUoXRj_r.d.ts} +2 -2
- package/dist/evaluation/axis-checks/index.d.ts +5 -4
- package/dist/evaluation/axis-checks/index.js.map +1 -1
- package/dist/evaluation/axis-checks/registry.d.ts +4 -3
- package/dist/evaluation/index.d.ts +107 -539
- package/dist/evaluation/index.js +552 -55
- package/dist/evaluation/index.js.map +1 -1
- package/dist/evaluation/types.d.ts +17 -0
- package/dist/evaluation/types.js.map +1 -1
- package/dist/{generate-9IeNT4ZY.d.ts → generate-tSO_shQ0.d.ts} +1 -1
- package/dist/harness/check/runtime-render/index.d.ts +5 -5
- package/dist/harness/check/runtime-render/index.js +416 -92
- package/dist/harness/check/runtime-render/index.js.map +1 -1
- package/dist/harness/check/runtime-render/render-check-worker.js +393 -88
- package/dist/harness/check/runtime-render/render-check-worker.js.map +1 -1
- package/dist/harness/index.d.ts +4 -5
- package/dist/harness/index.js +36 -5
- package/dist/harness/index.js.map +1 -1
- package/dist/harness/prompts.d.ts +2 -2
- package/dist/harness/prompts.js +10 -1
- package/dist/harness/prompts.js.map +1 -1
- package/dist/harness/result-types.d.ts +17 -3
- package/dist/harness/result-types.js.map +1 -1
- package/dist/harness/runtime.d.ts +14 -6
- package/dist/harness/runtime.js +11 -4
- package/dist/harness/runtime.js.map +1 -1
- package/dist/harness/types-public.d.ts +4 -2
- package/dist/harness/types-public.js +10 -3
- package/dist/harness/types-public.js.map +1 -1
- package/dist/harness/validator-trace-sink.d.ts +4 -3
- package/dist/index.d.ts +46 -11
- package/dist/index.js +1128 -200
- package/dist/index.js.map +1 -1
- package/dist/patch.d.ts +3 -3
- package/dist/{prompts-YU9piOgs.d.ts → prompts-BhNUuRoZ.d.ts} +1 -1
- package/dist/tools/render-check-worker.js +8 -1
- package/dist/tools/render-check-worker.js.map +1 -1
- package/dist/{types-public-BJTLpzWn.d.ts → types-public-B7wn4ce5.d.ts} +384 -3
- package/dist/validation/index.d.ts +1 -1
- package/dist/validation/index.js +1 -1
- package/dist/validation/index.js.map +1 -1
- package/dist/visual-evaluator-C9L-o6ap.d.ts +639 -0
- package/dist/workflows.d.ts +3 -3
- package/package.json +12 -12
- package/src/adapters/generation-dispatch.ts +2 -0
- package/src/boilerplate/free-design-prompt.ts +3 -3
- package/src/boilerplate/hard-sections.ts +1 -1
- package/src/boilerplate/templates/layouts/chat-mobile.tsx.tmpl +3 -1
- package/src/boilerplate/templates/layouts/chat-universal.tsx.tmpl +3 -1
- package/src/boilerplate/templates/layouts/fullscreen-desktop.tsx.tmpl +3 -1
- package/src/boilerplate/templates/layouts/fullscreen-universal.tsx.tmpl +3 -1
- package/src/check/run-tier0.ts +6 -3
- package/src/contract-context.ts +1 -1
- package/src/evaluation/cost-tracker.ts +54 -6
- package/src/evaluation/criteria/edge-probe.ts +286 -0
- package/src/evaluation/criteria/edge.ts +202 -0
- package/src/evaluation/criteria/resolve.ts +55 -7
- package/src/evaluation/index.ts +5 -1
- package/src/evaluation/judge-spend.ts +48 -0
- package/src/evaluation/types-public.ts +91 -0
- package/src/evaluation/types.ts +10 -0
- package/src/evaluation/visual-evaluator.ts +236 -40
- package/src/harness/check/runtime-render/adapter.ts +18 -0
- package/src/harness/check/runtime-render/probe-pending.ts +111 -0
- package/src/harness/check/runtime-render/render-check-host.ts +10 -6
- package/src/harness/check/runtime-render/render-check-worker.ts +6 -3
- package/src/harness/check/runtime-render/render-check.ts +417 -133
- package/src/harness/check/runtime-render/walk.ts +200 -0
- package/src/harness/coding/assemble-result.ts +2 -0
- package/src/harness/coding/generate-task-runner.ts +27 -0
- package/src/harness/coding/init-session.ts +5 -3
- package/src/harness/coding/run-coding-turn.ts +5 -1
- package/src/harness/coding/run-eval-round.ts +63 -3
- package/src/harness/in-loop-evaluation.ts +39 -0
- package/src/harness/llm-router.ts +58 -1
- package/src/harness/result-types.ts +9 -0
- package/src/harness/runtime.ts +9 -0
- package/src/harness/types-public.ts +10 -1
- package/src/index.ts +5 -0
- package/src/internal/verdict-line.ts +33 -0
- package/src/taught-tokens.ts +32 -0
- package/src/tools/render-check-worker.ts +2 -1
- package/src/tools/render-check.ts +8 -7
- package/src/validation/primitives.ts +1 -1
- package/dist/llm-router-nfO7wPec.d.ts +0 -267
- package/dist/{bank-CT4eEkta.d.ts → bank-Ctdp3FUA.d.ts} +3 -3
package/dist/evaluation/index.js
CHANGED
|
@@ -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
|
|
2541
|
+
let round2 = 0;
|
|
2518
2542
|
const maxRounds = Math.min(config.maxRounds ?? 3, MAX_EVAL_ROUNDS_HARD_LIMIT);
|
|
2519
|
-
while (
|
|
2520
|
-
|
|
2521
|
-
onProgress?.({ type: "evaluating", round });
|
|
2522
|
-
console.log(`[eval] Round ${
|
|
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 ${
|
|
2554
|
+
`[eval] Round ${round2}: score=${evalResult.finalScore}, passed=${evalResult.passed}, issues=${evalResult.issues.length}`
|
|
2531
2555
|
);
|
|
2532
|
-
if (evalResult.passed ||
|
|
2556
|
+
if (evalResult.passed || round2 >= maxRounds) {
|
|
2533
2557
|
break;
|
|
2534
2558
|
}
|
|
2535
|
-
onProgress?.({ type: "fixing", round });
|
|
2536
|
-
console.log(`[eval] Round ${
|
|
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 ${
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
5457
|
-
|
|
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((
|
|
5537
|
-
result2.outputTokens = parsed.reduce((
|
|
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((
|
|
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((
|
|
5663
|
-
outputTokens: criteriaSpent.reduce((
|
|
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:
|
|
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((
|
|
5766
|
-
outputTokens: parsed.reduce((
|
|
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: `[
|
|
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
|
|
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
|
-
...
|
|
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
|
|
5850
|
-
const
|
|
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)
|
|
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")
|
|
5855
|
-
|
|
5856
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|