niceeval 0.6.1 → 0.6.2

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 (296) hide show
  1. package/dist/agents/types.d.ts +67 -5
  2. package/dist/context/types.d.ts +32 -12
  3. package/dist/i18n/en.d.ts +54 -0
  4. package/dist/i18n/zh-CN.d.ts +55 -1
  5. package/dist/o11y/types.d.ts +16 -2
  6. package/dist/report/aggregate.d.ts +5 -3
  7. package/dist/report/aggregate.js +32 -5
  8. package/dist/report/built-ins/experiment-comparison.d.ts +39 -1
  9. package/dist/report/built-ins/experiment-comparison.js +116 -10
  10. package/dist/report/built-ins/index.d.ts +1 -0
  11. package/dist/report/built-ins/index.js +1 -1
  12. package/dist/report/components.d.ts +8 -2
  13. package/dist/report/components.js +3 -3
  14. package/dist/report/compute.d.ts +11 -18
  15. package/dist/report/compute.js +54 -34
  16. package/dist/report/flag.d.ts +16 -1
  17. package/dist/report/flag.js +19 -1
  18. package/dist/report/format.d.ts +16 -8
  19. package/dist/report/format.js +27 -12
  20. package/dist/report/index.d.ts +4 -3
  21. package/dist/report/index.js +5 -4
  22. package/dist/report/locale.d.ts +11 -2
  23. package/dist/report/locale.js +23 -5
  24. package/dist/report/metrics.d.ts +13 -1
  25. package/dist/report/metrics.js +65 -14
  26. package/dist/report/primitives.d.ts +6 -0
  27. package/dist/report/react/AttemptList.d.ts +2 -2
  28. package/dist/report/react/AttemptList.js +5 -6
  29. package/dist/report/react/EvalList.d.ts +1 -1
  30. package/dist/report/react/EvalList.js +0 -0
  31. package/dist/report/react/ExperimentComparison.d.ts +8 -0
  32. package/dist/report/react/ExperimentComparison.js +11 -0
  33. package/dist/report/react/ExperimentList.d.ts +2 -1
  34. package/dist/report/react/ExperimentList.js +8 -10
  35. package/dist/report/react/MetricScatter.js +5 -11
  36. package/dist/report/react/chart-math.d.ts +23 -6
  37. package/dist/report/react/chart-math.js +71 -19
  38. package/dist/report/react/fixtures.d.ts +3 -3
  39. package/dist/report/react/fixtures.js +21 -14
  40. package/dist/report/report.d.ts +5 -1
  41. package/dist/report/report.js +6 -2
  42. package/dist/report/text/faces.d.ts +1 -1
  43. package/dist/report/text/faces.js +42 -41
  44. package/dist/report/text/table.js +36 -5
  45. package/dist/report/types.d.ts +39 -21
  46. package/dist/results/types.d.ts +11 -0
  47. package/dist/runner/feedback/sink.d.ts +110 -0
  48. package/dist/runner/types.d.ts +513 -22
  49. package/dist/sandbox/docker.d.ts +23 -2
  50. package/dist/sandbox/e2b.d.ts +15 -1
  51. package/dist/sandbox/errors.d.ts +30 -3
  52. package/dist/sandbox/io-retry.d.ts +17 -0
  53. package/dist/sandbox/registry.d.ts +2 -0
  54. package/dist/sandbox/resolve.d.ts +18 -5
  55. package/dist/sandbox/retry.d.ts +11 -1
  56. package/dist/sandbox/types.d.ts +39 -5
  57. package/dist/sandbox/vercel.d.ts +7 -1
  58. package/dist/scoring/coverage.d.ts +30 -0
  59. package/dist/scoring/display.d.ts +21 -0
  60. package/dist/scoring/display.js +120 -0
  61. package/dist/scoring/types.d.ts +103 -20
  62. package/dist/shared/aggregate.d.ts +1 -0
  63. package/dist/shared/aggregate.js +3 -3
  64. package/dist/shared/types.d.ts +28 -0
  65. package/dist/tty-line.d.ts +0 -4
  66. package/dist/util.d.ts +23 -0
  67. package/docs-site/zh/concepts/adapter.mdx +22 -4
  68. package/docs-site/zh/concepts/experiment.mdx +1 -1
  69. package/docs-site/zh/concepts/overview.mdx +6 -6
  70. package/docs-site/zh/guides/agent-feedback-loop.mdx +28 -26
  71. package/docs-site/zh/guides/authoring.mdx +33 -0
  72. package/docs-site/zh/guides/ci-integration.mdx +23 -12
  73. package/docs-site/zh/guides/connect-your-agent.mdx +29 -3
  74. package/docs-site/zh/guides/custom-reports.mdx +29 -34
  75. package/docs-site/zh/guides/dataset-fanout.mdx +25 -3
  76. package/docs-site/zh/guides/debug-sandbox.mdx +57 -0
  77. package/docs-site/zh/guides/debugging.mdx +210 -0
  78. package/docs-site/zh/guides/experiments.mdx +10 -3
  79. package/docs-site/zh/guides/official-adapters.mdx +26 -2
  80. package/docs-site/zh/guides/publish-report.mdx +30 -16
  81. package/docs-site/zh/guides/report-components.mdx +42 -30
  82. package/docs-site/zh/guides/reporters.mdx +2 -2
  83. package/docs-site/zh/guides/results-data.mdx +17 -9
  84. package/docs-site/zh/guides/runner.mdx +17 -7
  85. package/docs-site/zh/guides/sandbox-agent.mdx +56 -7
  86. package/docs-site/zh/guides/sandbox-providers.mdx +257 -9
  87. package/docs-site/zh/guides/scoring-guide.mdx +4 -4
  88. package/docs-site/zh/guides/viewing-results.mdx +79 -36
  89. package/docs-site/zh/guides/write-experiment.mdx +5 -3
  90. package/docs-site/zh/guides/write-send.mdx +17 -1
  91. package/docs-site/zh/index.mdx +1 -1
  92. package/docs-site/zh/reference/builtin-agents.mdx +27 -0
  93. package/docs-site/zh/reference/capabilities.mdx +2 -2
  94. package/docs-site/zh/reference/cli.mdx +33 -7
  95. package/docs-site/zh/reference/define-agent.mdx +57 -4
  96. package/docs-site/zh/reference/define-config.mdx +1 -1
  97. package/docs-site/zh/reference/define-eval.mdx +42 -9
  98. package/docs-site/zh/reference/expect.mdx +26 -1
  99. package/package.json +5 -1
  100. package/src/agents/ai-sdk-otel.test.ts +1 -0
  101. package/src/agents/ai-sdk.test.ts +3 -0
  102. package/src/agents/ai-sdk.ts +3 -0
  103. package/src/agents/bub-install-spec.test.ts +34 -0
  104. package/src/agents/bub-install-spec.ts +32 -0
  105. package/src/agents/bub.ts +31 -32
  106. package/src/agents/claude-code.test.ts +130 -9
  107. package/src/agents/claude-code.ts +76 -4
  108. package/src/agents/codex.test.ts +189 -40
  109. package/src/agents/codex.ts +155 -14
  110. package/src/agents/coding-cli-versions.test.ts +15 -0
  111. package/src/agents/coding-cli-versions.ts +3 -0
  112. package/src/agents/index.ts +11 -0
  113. package/src/agents/langgraph.test.ts +204 -0
  114. package/src/agents/langgraph.ts +495 -0
  115. package/src/agents/marketplace.ts +85 -0
  116. package/src/agents/native-config.test.ts +179 -0
  117. package/src/agents/native-config.ts +267 -0
  118. package/src/agents/openai-compat.test.ts +1 -0
  119. package/src/agents/openclaw.test.ts +31 -0
  120. package/src/agents/openclaw.ts +171 -0
  121. package/src/agents/plugin-config.test.ts +1 -0
  122. package/src/agents/sdk-streams.test.ts +79 -0
  123. package/src/agents/sdk-streams.ts +55 -10
  124. package/src/agents/skills.test.ts +1 -0
  125. package/src/agents/streaming.test.ts +3 -9
  126. package/src/agents/types.ts +68 -5
  127. package/src/agents/ui-message-stream.test.ts +3 -0
  128. package/src/cli.ts +411 -108
  129. package/src/context/context.test.ts +51 -12
  130. package/src/context/context.ts +161 -29
  131. package/src/context/session.test.ts +1 -0
  132. package/src/context/session.ts +114 -6
  133. package/src/context/types.ts +30 -12
  134. package/src/define.test.ts +13 -8
  135. package/src/define.ts +25 -4
  136. package/src/expect/index.ts +53 -23
  137. package/src/i18n/en.ts +64 -2
  138. package/src/i18n/zh-CN.ts +65 -3
  139. package/src/o11y/cost.test.ts +1 -0
  140. package/src/o11y/execution-tree.test.ts +1 -20
  141. package/src/o11y/otlp/mappers/claude-code.test.ts +1 -0
  142. package/src/o11y/otlp/parse.test.ts +1 -0
  143. package/src/o11y/otlp/turn-otel.test.ts +1 -0
  144. package/src/o11y/parsers/bub.test.ts +1 -0
  145. package/src/o11y/parsers/claude-code.test.ts +1 -34
  146. package/src/o11y/parsers/openclaw.test.ts +154 -0
  147. package/src/o11y/parsers/openclaw.ts +310 -0
  148. package/src/o11y/prices.json +746 -311
  149. package/src/o11y/tool-names.test.ts +1 -0
  150. package/src/o11y/types.ts +16 -2
  151. package/src/report/aggregate.ts +34 -5
  152. package/src/report/built-in-user-parity.test.tsx +110 -153
  153. package/src/report/built-ins/experiment-comparison.tsx +173 -13
  154. package/src/report/built-ins/index.ts +6 -1
  155. package/src/report/components.tsx +9 -3
  156. package/src/report/compute.ts +70 -40
  157. package/src/report/dual-render.test.tsx +194 -67
  158. package/src/report/flag.ts +30 -2
  159. package/src/report/format.ts +35 -11
  160. package/src/report/index.ts +22 -4
  161. package/src/report/locale.ts +25 -5
  162. package/src/report/metrics.ts +67 -14
  163. package/src/report/primitives.tsx +6 -0
  164. package/src/report/react/AttemptList.tsx +6 -31
  165. package/src/report/react/EvalList.tsx +0 -0
  166. package/src/report/react/ExperimentComparison.tsx +68 -0
  167. package/src/report/react/ExperimentList.tsx +15 -9
  168. package/src/report/react/MetricScatter.tsx +12 -14
  169. package/src/report/react/chart-math.test.ts +85 -0
  170. package/src/report/react/chart-math.ts +101 -22
  171. package/src/report/react/enhance.js +33 -1
  172. package/src/report/react/fixtures.ts +24 -17
  173. package/src/report/react/render.test.tsx +9 -64
  174. package/src/report/react/styles.css +73 -2
  175. package/src/report/report.test.ts +306 -98
  176. package/src/report/report.ts +6 -2
  177. package/src/report/text/faces.ts +47 -43
  178. package/src/report/text/table.ts +42 -5
  179. package/src/report/types.ts +41 -21
  180. package/src/results/annotated-source.test.ts +62 -9
  181. package/src/results/annotated-source.ts +64 -6
  182. package/src/results/attempt-evidence.test.ts +9 -7
  183. package/src/results/attempt-evidence.ts +15 -8
  184. package/src/results/attempt-source.ts +6 -3
  185. package/src/results/copy.ts +145 -55
  186. package/src/results/host-equivalence.test.ts +8 -6
  187. package/src/results/index.ts +2 -0
  188. package/src/results/locator.test.ts +1 -22
  189. package/src/results/open.ts +7 -1
  190. package/src/results/publish.ts +149 -0
  191. package/src/results/results.test.ts +85 -51
  192. package/src/results/truncate.ts +90 -0
  193. package/src/results/types.ts +7 -0
  194. package/src/results/writer.ts +31 -13
  195. package/src/runner/attempt.test.ts +138 -7
  196. package/src/runner/attempt.ts +603 -104
  197. package/src/runner/discover.test.ts +47 -0
  198. package/src/runner/discover.ts +36 -2
  199. package/src/runner/eval-source.test.ts +1 -27
  200. package/src/runner/feedback/agent.test.ts +504 -0
  201. package/src/runner/feedback/agent.ts +409 -0
  202. package/src/runner/feedback/ci.test.ts +562 -0
  203. package/src/runner/feedback/ci.ts +401 -0
  204. package/src/runner/feedback/coordinator.test.ts +317 -0
  205. package/src/runner/feedback/coordinator.ts +397 -0
  206. package/src/runner/feedback/failure.ts +40 -0
  207. package/src/runner/feedback/human.test.ts +616 -0
  208. package/src/runner/feedback/human.ts +535 -0
  209. package/src/runner/feedback/index.ts +66 -0
  210. package/src/runner/feedback/io.ts +78 -0
  211. package/src/runner/feedback/profile.test.ts +50 -0
  212. package/src/runner/feedback/profile.ts +58 -0
  213. package/src/runner/feedback/reducer.test.ts +395 -0
  214. package/src/runner/feedback/reducer.ts +260 -0
  215. package/src/runner/feedback/renderer.ts +82 -0
  216. package/src/runner/feedback/sink.ts +203 -0
  217. package/src/runner/feedback/testing.ts +106 -0
  218. package/src/runner/ledger.test.ts +230 -0
  219. package/src/runner/ledger.ts +329 -0
  220. package/src/runner/report.test.ts +128 -3
  221. package/src/runner/report.ts +33 -9
  222. package/src/runner/reporters/artifacts.ts +8 -2
  223. package/src/runner/reporters/braintrust.test.ts +8 -7
  224. package/src/runner/reporters/braintrust.ts +9 -2
  225. package/src/runner/reporters/index.ts +2 -2
  226. package/src/runner/reporters/json.test.ts +162 -0
  227. package/src/runner/reporters/json.ts +35 -8
  228. package/src/runner/reporters/shared.ts +1 -5
  229. package/src/runner/run.test.ts +760 -3
  230. package/src/runner/run.ts +242 -36
  231. package/src/runner/sandbox-prep.ts +3 -42
  232. package/src/runner/timing.ts +158 -0
  233. package/src/runner/types.ts +518 -22
  234. package/src/sandbox/checkpoint.test.ts +55 -0
  235. package/src/sandbox/checkpoint.ts +29 -8
  236. package/src/sandbox/cli-commands.ts +407 -0
  237. package/src/sandbox/docker.ts +115 -16
  238. package/src/sandbox/e2b-agent-template.test.ts +56 -0
  239. package/src/sandbox/e2b-agent-template.ts +94 -0
  240. package/src/sandbox/e2b.ts +74 -9
  241. package/src/sandbox/errors.ts +111 -4
  242. package/src/sandbox/index.ts +2 -0
  243. package/src/sandbox/io-retry.test.ts +58 -0
  244. package/src/sandbox/io-retry.ts +45 -0
  245. package/src/sandbox/keep-registry.test.ts +86 -0
  246. package/src/sandbox/keep-registry.ts +142 -0
  247. package/src/sandbox/keep.ts +178 -0
  248. package/src/sandbox/paths.test.ts +1 -0
  249. package/src/sandbox/paths.ts +19 -8
  250. package/src/sandbox/registry.ts +20 -3
  251. package/src/sandbox/resolve.ts +76 -11
  252. package/src/sandbox/retry.test.ts +70 -0
  253. package/src/sandbox/retry.ts +46 -4
  254. package/src/sandbox/types.ts +44 -6
  255. package/src/sandbox/vercel.ts +43 -20
  256. package/src/scoring/collector.ts +60 -17
  257. package/src/scoring/coverage.ts +95 -0
  258. package/src/scoring/diff.ts +81 -0
  259. package/src/scoring/display.test.ts +121 -0
  260. package/src/scoring/display.ts +133 -0
  261. package/src/scoring/evidence.test.ts +189 -0
  262. package/src/scoring/judge.test.ts +142 -0
  263. package/src/scoring/judge.ts +15 -18
  264. package/src/scoring/scoped.ts +217 -50
  265. package/src/scoring/types.ts +117 -20
  266. package/src/scoring/verdict.ts +16 -4
  267. package/src/shared/aggregate.ts +3 -2
  268. package/src/shared/types.ts +31 -0
  269. package/src/show/compose.ts +2 -2
  270. package/src/show/index.ts +21 -1
  271. package/src/show/render.ts +619 -104
  272. package/src/show/show.test.ts +235 -19
  273. package/src/tty-line.ts +8 -26
  274. package/src/util.test.ts +1 -0
  275. package/src/util.ts +41 -0
  276. package/src/view/app/components/AttemptModal.tsx +153 -2
  277. package/src/view/app/components/CodeView.tsx +32 -11
  278. package/src/view/app/components/CopyControls.tsx +2 -2
  279. package/src/view/app/i18n.ts +6 -0
  280. package/src/view/app/lib/attempt-route.test.ts +1 -0
  281. package/src/view/app/lib/verdict.ts +7 -9
  282. package/src/view/artifact-serving.test.ts +2 -1
  283. package/src/view/client-dist/app.css +1 -1
  284. package/src/view/client-dist/app.js +17 -17
  285. package/src/view/data.test.ts +1 -0
  286. package/src/view/data.ts +11 -1
  287. package/src/view/index.ts +11 -0
  288. package/src/view/server.ts +2 -0
  289. package/src/view/styles.css +3 -0
  290. package/src/view/view-report.test.ts +6 -5
  291. package/src/runner/reporters/console.ts +0 -70
  292. package/src/runner/reporters/live.test.ts +0 -56
  293. package/src/runner/reporters/live.ts +0 -247
  294. package/src/runner/reporters/quiet.test.ts +0 -66
  295. package/src/runner/reporters/quiet.ts +0 -49
  296. package/src/runner/reporters/table.ts +0 -277
@@ -1,5 +1,4 @@
1
- // 通用图表数值工具:round-number 刻度生成与标签防撞布局,零依赖纯函数。
2
- // 从 src/view/app/lib/chart.ts 迁来(view 侧从这里 re-export),报告组件与 view 共用同一份。
1
+ // 通用图表数值工具:round-number 刻度生成与点标签布局,零依赖纯函数。
3
2
 
4
3
  function niceNumber(range: number, round: boolean): number {
5
4
  if (range <= 0) return 1;
@@ -35,34 +34,114 @@ export function niceTicks(min: number, max: number, count = 5): number[] {
35
34
  return ticks;
36
35
  }
37
36
 
38
- export interface LabelInput {
37
+ export interface PointLabelInput {
39
38
  cx: number;
40
39
  cy: number;
40
+ /** 标签文本的估算宽度(px)。 */
41
41
  width: number;
42
- /** true = 文字锚在点左侧(text-anchor: end),false = 锚在右侧(text-anchor: start)。 */
43
- anchorLeft: boolean;
42
+ }
43
+
44
+ /** 标签允许占用的画布范围(含坐标轴边距,标签可以借用空白边距)。 */
45
+ export interface LabelBounds {
46
+ x0: number;
47
+ y0: number;
48
+ x1: number;
49
+ y1: number;
50
+ }
51
+
52
+ export interface PlacedPointLabel {
53
+ /** <text> 的锚点坐标(y 为文字基线)。 */
54
+ x: number;
55
+ y: number;
56
+ anchor: "start" | "end" | "middle";
57
+ /** 标签不在点左右紧邻位:渲染层补 leader line 连回原点。 */
58
+ leader: boolean;
59
+ }
60
+
61
+ /** 标签框高度(基线上方 10px、下方 2px,对应 11px 字号)。 */
62
+ const LABEL_ASCENT = 10;
63
+ const LABEL_DESCENT = 2;
64
+ /** 数据点视为边长 2×DOT_R 的方形障碍(点半径 4.5 + 命中余量)。 */
65
+ const DOT_R = 6;
66
+ /** 紧邻位与点的水平间距;候选环按它逐环外扩。 */
67
+ const NEAR = 10;
68
+ const RINGS = [NEAR, NEAR + 12, NEAR + 26];
69
+
70
+ interface Box {
71
+ x0: number;
72
+ y0: number;
73
+ x1: number;
74
+ y1: number;
75
+ }
76
+
77
+ function labelBoxAt(x: number, y: number, anchor: PlacedPointLabel["anchor"], width: number): Box {
78
+ const x0 = anchor === "start" ? x : anchor === "end" ? x - width : x - width / 2;
79
+ return { x0, y0: y - LABEL_ASCENT, x1: x0 + width, y1: y + LABEL_DESCENT };
80
+ }
81
+
82
+ function overlapArea(a: Box, b: Box): number {
83
+ const w = Math.min(a.x1, b.x1) - Math.max(a.x0, b.x0);
84
+ const h = Math.min(a.y1, b.y1) - Math.max(a.y0, b.y0);
85
+ return w > 0 && h > 0 ? w * h : 0;
86
+ }
87
+
88
+ function outOfBoundsArea(box: Box, bounds: LabelBounds): number {
89
+ const total = (box.x1 - box.x0) * (box.y1 - box.y0);
90
+ const w = Math.min(box.x1, bounds.x1) - Math.max(box.x0, bounds.x0);
91
+ const h = Math.min(box.y1, bounds.y1) - Math.max(box.y0, bounds.y0);
92
+ const inside = w > 0 && h > 0 ? w * h : 0;
93
+ return total - inside;
94
+ }
95
+
96
+ /** 一个点在距离 d 的候选环:左右紧邻、四个斜角、正上正下。顺序即同代价时的偏好序。 */
97
+ function ringCandidates(p: PointLabelInput, d: number): Omit<PlacedPointLabel, "leader">[] {
98
+ const diag = d * 0.75;
99
+ return [
100
+ { x: p.cx + d, y: p.cy + 4, anchor: "start" },
101
+ { x: p.cx - d, y: p.cy + 4, anchor: "end" },
102
+ { x: p.cx + diag, y: p.cy - diag - 2, anchor: "start" },
103
+ { x: p.cx - diag, y: p.cy - diag - 2, anchor: "end" },
104
+ { x: p.cx, y: p.cy - d - LABEL_DESCENT - 2, anchor: "middle" },
105
+ { x: p.cx + diag, y: p.cy + diag + LABEL_ASCENT, anchor: "start" },
106
+ { x: p.cx - diag, y: p.cy + diag + LABEL_ASCENT, anchor: "end" },
107
+ { x: p.cx, y: p.cy + d + LABEL_ASCENT + 2, anchor: "middle" },
108
+ ];
44
109
  }
45
110
 
46
111
  /**
47
- * 贪心地把互相重叠的直接标签往下推开(按 y 排序、逐个查重叠再下移),点本身位置不变。
48
- * 散点常见同分不同价的聚簇,这一步避免标签叠成一团糊字。
112
+ * 散点直接标签的候选位择优布局(docs/feature/reports/library.md「MetricScatter」):
113
+ * 每个标签在点四周由近及远的候选环上打分——与已放置标签的重叠、与任何数据点的重叠、
114
+ * 越出画布的面积、离点距离逐项累加,取代价最小的候选。存在无冲突候选时标签必不遮点、
115
+ * 不叠标签、不越界;全候选冲突时取重叠最小者,绝不丢标签。重合点簇因此向不同方向散开,
116
+ * 而不是单向堆叠压到下方的数据点上。
49
117
  */
50
- export function layoutLabelOffsets(items: LabelInput[], lineHeight = 14, padX = 4): number[] {
51
- const offsets = new Array(items.length).fill(0);
52
- const order = [...items.keys()].sort((a, b) => items[a].cy - items[b].cy || items[a].cx - items[b].cx);
53
- const placed: { x0: number; x1: number; y: number }[] = [];
118
+ export function placePointLabels(points: PointLabelInput[], bounds: LabelBounds): PlacedPointLabel[] {
119
+ const dots: Box[] = points.map((p) => ({ x0: p.cx - DOT_R, y0: p.cy - DOT_R, x1: p.cx + DOT_R, y1: p.cy + DOT_R }));
120
+ const placedBoxes: Box[] = [];
121
+ const result: PlacedPointLabel[] = new Array(points.length);
122
+ // 由上到下、由左到右逐个放置,布局确定可复现
123
+ const order = [...points.keys()].sort((a, b) => points[a].cy - points[b].cy || points[a].cx - points[b].cx);
124
+
54
125
  for (const i of order) {
55
- const it = items[i];
56
- const x0 = it.anchorLeft ? it.cx - it.width : it.cx;
57
- const x1 = it.anchorLeft ? it.cx : it.cx + it.width;
58
- let y = it.cy;
59
- let guard = 0;
60
- while (guard < 40 && placed.some((p) => x0 < p.x1 + padX && x1 > p.x0 - padX && Math.abs(y - p.y) < lineHeight)) {
61
- y += lineHeight;
62
- guard++;
126
+ const p = points[i];
127
+ let best: { candidate: Omit<PlacedPointLabel, "leader">; box: Box; cost: number; leader: boolean } | null = null;
128
+ for (let ring = 0; ring < RINGS.length; ring++) {
129
+ const candidates = ringCandidates(p, RINGS[ring]);
130
+ for (let c = 0; c < candidates.length; c++) {
131
+ const candidate = candidates[c];
132
+ const box = labelBoxAt(candidate.x, candidate.y, candidate.anchor, p.width);
133
+ let cost = 0;
134
+ for (const placed of placedBoxes) cost += overlapArea(box, placed) * 4;
135
+ for (const dot of dots) cost += overlapArea(box, dot) * 3;
136
+ cost += outOfBoundsArea(box, bounds) * 6;
137
+ cost += ring * 8 + c * 0.5; // 近环、偏好序靠前者优先,只作同冲突程度下的排序
138
+ if (best === null || cost < best.cost) {
139
+ best = { candidate, box, cost, leader: !(ring === 0 && c <= 1) };
140
+ }
141
+ }
63
142
  }
64
- placed.push({ x0, x1, y });
65
- offsets[i] = y - it.cy;
143
+ placedBoxes.push(best!.box);
144
+ result[i] = { ...best!.candidate, leader: best!.leader };
66
145
  }
67
- return offsets;
146
+ return result;
68
147
  }
@@ -1,5 +1,5 @@
1
1
  // niceeval 报告的渐进增强 runtime:纯 vanilla JS、零依赖、IIFE、幂等。
2
- // 只作用于 .nre DOM 与 data-nre-* 属性;三个行为——表格排序、行过滤、SVG 点 tooltip。
2
+ // 只作用于 .nre DOM 与 data-nre-* 属性;四个行为——实验组切换、表格排序、行过滤、SVG 点 tooltip。
3
3
  // 静态 HTML 无 JS 时内容完整可读是硬约束:排序有数据侧预排、tooltip 退化为原生
4
4
  // <title>、过滤输入框静默无功能。全部经 document 级事件委托绑定,重复注入本文件
5
5
  // 只在首次生效(window.__nreEnhanced 守卫),DOM 被搬动(如 view 把 <template> 内容
@@ -14,6 +14,38 @@
14
14
  return target && target.closest ? target.closest(selector) : null;
15
15
  }
16
16
 
17
+ // ───────────────────────── 实验组:[data-nre-experiment-group-select] ─────────────────────────
18
+ // 数据侧已经把每组的摘要、散点和列表分别算好;这里仅在同一报告节点内切换 <details>,
19
+ // 不重新请求、不重算,也不触碰 Runs / Traces / Attempt 证据室。
20
+
21
+ function selectExperimentGroup(control) {
22
+ var root = control.closest("[data-nre-experiment-groups]");
23
+ if (!root) return;
24
+ var selected = control.getAttribute("data-nre-experiment-group-select");
25
+ var controls = root.querySelectorAll("[data-nre-experiment-group-select]");
26
+ var panels = root.querySelectorAll("[data-nre-experiment-group-panel]");
27
+ for (var i = 0; i < controls.length; i++) {
28
+ var active = controls[i].getAttribute("data-nre-experiment-group-select") === selected;
29
+ controls[i].setAttribute("aria-selected", active ? "true" : "false");
30
+ controls[i].tabIndex = active ? 0 : -1;
31
+ }
32
+ for (var j = 0; j < panels.length; j++) {
33
+ panels[j].open = panels[j].getAttribute("data-nre-experiment-group-panel") === selected;
34
+ }
35
+ }
36
+
37
+ document.addEventListener("click", function (e) {
38
+ var control = closest(e.target, "[data-nre-experiment-group-select]");
39
+ if (control) selectExperimentGroup(control);
40
+ });
41
+
42
+ document.addEventListener("keydown", function (e) {
43
+ var control = closest(e.target, "[data-nre-experiment-group-select]");
44
+ if (!control || (e.key !== "Enter" && e.key !== " ")) return;
45
+ e.preventDefault();
46
+ selectExperimentGroup(control);
47
+ });
48
+
17
49
  // ───────────────────────── 排序:th[data-nre-sort] ─────────────────────────
18
50
  // 点击按该列排序 tbody 行(td/th 的 data-sort-value,数值优先、退回字符串;
19
51
  // 空值恒沉底),再点反向;方向指示由 th 上的 nre-sort-asc / nre-sort-desc 类驱动。
@@ -21,7 +21,7 @@ import type { AttemptLocator } from "../../results/locator.ts";
21
21
 
22
22
  const locator = (s: string): AttemptLocator => s as AttemptLocator;
23
23
 
24
- export const passRateColumn: MetricColumn = { key: "pass-rate", label: "pass rate", unit: "%", better: "higher" };
24
+ export const passRateColumn: MetricColumn = { key: "task-pass-rate", label: "pass rate", unit: "%", better: "higher" };
25
25
  export const codeLinesColumn: MetricColumn = { key: "code-lines", label: "code lines", unit: "lines", better: "lower" };
26
26
  export const costColumn: MetricColumn = { key: "cost", label: "cost", unit: "$", better: "lower" };
27
27
 
@@ -73,7 +73,7 @@ export const groupSummaryData: GroupSummaryData = {
73
73
  lastRunAt: "2026-07-01T11:30:00Z",
74
74
  };
75
75
 
76
- export const tableData: TableData<"pass-rate" | "code-lines"> = {
76
+ export const tableData: TableData<"task-pass-rate" | "code-lines"> = {
77
77
  dimension: "agent",
78
78
  // 行顺序故意不按 passRate 排:组件必须按传入顺序渲染,不重排
79
79
  columns: [passRateColumn, codeLinesColumn],
@@ -81,7 +81,7 @@ export const tableData: TableData<"pass-rate" | "code-lines"> = {
81
81
  {
82
82
  key: "codex",
83
83
  cells: {
84
- "pass-rate": { value: 0.5, display: "50%", samples: 6, total: 6, refs: [] },
84
+ "task-pass-rate": { value: 0.5, display: "50%", samples: 6, total: 6, refs: [] },
85
85
  // 全 null:一个有效样本都没有 → 缺数据文案,绝不画 0
86
86
  "code-lines": { value: null, display: "—", samples: 0, total: 6, refs: [] },
87
87
  },
@@ -89,7 +89,7 @@ export const tableData: TableData<"pass-rate" | "code-lines"> = {
89
89
  {
90
90
  key: "bub",
91
91
  cells: {
92
- "pass-rate": {
92
+ "task-pass-rate": {
93
93
  value: 0.87,
94
94
  display: "87%",
95
95
  samples: 6,
@@ -104,13 +104,13 @@ export const tableData: TableData<"pass-rate" | "code-lines"> = {
104
104
  };
105
105
 
106
106
  /** rows: "experiment" 的榜单形态:行携带 agent/model 元信息与 eval 级折叠计票。 */
107
- export const tableDataWithMeta: TableData<"pass-rate"> = {
107
+ export const tableDataWithMeta: TableData<"task-pass-rate"> = {
108
108
  dimension: "experiment",
109
109
  columns: [passRateColumn],
110
110
  rows: [
111
111
  {
112
112
  key: "compare/bub",
113
- cells: { "pass-rate": { value: 0.5, display: "50%", samples: 2, total: 2, refs: [] } },
113
+ cells: { "task-pass-rate": { value: 0.5, display: "50%", samples: 2, total: 2, refs: [] } },
114
114
  meta: {
115
115
  agent: "bub",
116
116
  model: "gpt-5.4",
@@ -119,7 +119,7 @@ export const tableDataWithMeta: TableData<"pass-rate"> = {
119
119
  },
120
120
  {
121
121
  key: "compare/codex",
122
- cells: { "pass-rate": { value: 1, display: "100%", samples: 2, total: 2, refs: [] } },
122
+ cells: { "task-pass-rate": { value: 1, display: "100%", samples: 2, total: 2, refs: [] } },
123
123
  meta: {
124
124
  agent: "codex",
125
125
  verdicts: { passed: 2, failed: 0, errored: 0, skipped: 0 },
@@ -261,7 +261,7 @@ export const lineData: LineData = {
261
261
  ],
262
262
  };
263
263
 
264
- export const deltaData: DeltaData<"pass-rate" | "cost"> = {
264
+ export const deltaData: DeltaData<"task-pass-rate" | "cost"> = {
265
265
  columns: [passRateColumn, costColumn],
266
266
  rows: [
267
267
  {
@@ -270,7 +270,7 @@ export const deltaData: DeltaData<"pass-rate" | "cost"> = {
270
270
  b: { experimentId: "compare/bub--agents-md" },
271
271
  cells: {
272
272
  // 通过率 +12pp:better higher → 好(绿)
273
- "pass-rate": {
273
+ "task-pass-rate": {
274
274
  a: { value: 0.5, display: "50%", samples: 6, total: 6, refs: [] },
275
275
  b: { value: 0.62, display: "62%", samples: 6, total: 6, refs: [] },
276
276
  delta: 0.12,
@@ -290,7 +290,7 @@ export const deltaData: DeltaData<"pass-rate" | "cost"> = {
290
290
  a: { experimentId: "compare/codex" },
291
291
  b: { experimentId: "compare/codex--agents-md" },
292
292
  cells: {
293
- "pass-rate": {
293
+ "task-pass-rate": {
294
294
  a: { value: 0.4, display: "40%", samples: 6, total: 6, refs: [] },
295
295
  b: { value: 0.4, display: "40%", samples: 6, total: 6, refs: [] },
296
296
  delta: 0,
@@ -322,8 +322,9 @@ const failedAttempt: AttemptListItem = {
322
322
  name: "roots-correct",
323
323
  severity: "gate",
324
324
  score: 0,
325
- passed: false,
326
- detail: "expected x=2, got x=3",
325
+ outcome: "failed" as const,
326
+ expected: "x=2",
327
+ received: "x=3",
327
328
  evidence: "judge: sign flipped when substituting into the quadratic formula",
328
329
  },
329
330
  ],
@@ -338,7 +339,17 @@ const erroredAttempt: AttemptListItem = {
338
339
  attempt: 0,
339
340
  agent: "codex",
340
341
  verdict: "errored",
341
- error: "TypeError: cannot read properties of undefined (reading 'foo')",
342
+ // 结构化 error:列表只渲染 message 一层摘要;cause/stack diagnostics 是下钻详情,随数据携带
343
+ error: {
344
+ code: "unexpected-error",
345
+ message: "TypeError: cannot read properties of undefined (reading 'foo')",
346
+ phase: "eval.run",
347
+ stack: "TypeError: cannot read properties of undefined (reading 'foo')\n at run (adapter.ts:42:7)",
348
+ cause: { name: "TypeError", message: "cannot read properties of undefined (reading 'foo')" },
349
+ },
350
+ diagnostics: [
351
+ { code: "sandbox-teardown-failed", level: "warning", message: "sandbox teardown timed out", phase: "sandbox.teardown" },
352
+ ],
342
353
  assertions: [],
343
354
  durationMs: 4_500,
344
355
  locator: locator("@1c1c1c1"),
@@ -358,7 +369,6 @@ export const evalListItems: EvalListItem[] = [
358
369
  evalId: "algebra/quadratic",
359
370
  experimentId: "compare/bub",
360
371
  verdict: "failed",
361
- reason: "roots-correct: expected x=2, got x=3",
362
372
  score: { value: 0, display: "0%", samples: 1, total: 1, refs: [failedAttempt.locator] },
363
373
  duration: { value: 32_000, display: "32.0s", samples: 1, total: 1, refs: [failedAttempt.locator] },
364
374
  cost: { value: 0.12, display: "$0.12", samples: 1, total: 1, refs: [failedAttempt.locator] },
@@ -368,7 +378,6 @@ export const evalListItems: EvalListItem[] = [
368
378
  evalId: "geometry/angles",
369
379
  experimentId: "compare/codex",
370
380
  verdict: "errored",
371
- reason: erroredAttempt.error,
372
381
  score: { value: 0, display: "0%", samples: 1, total: 1, refs: [erroredAttempt.locator] },
373
382
  duration: { value: 4_500, display: "4.5s", samples: 1, total: 1, refs: [erroredAttempt.locator] },
374
383
  cost: { value: null, display: "—", samples: 0, total: 1, refs: [] },
@@ -394,7 +403,6 @@ export const experimentListItems: ExperimentListItem[] = [
394
403
  {
395
404
  evalId: "algebra/quadratic",
396
405
  verdict: "failed",
397
- reason: "roots-correct: expected x=2, got x=3",
398
406
  duration: { value: 32_000, display: "32.0s", samples: 1, total: 1, refs: [failedAttempt.locator] },
399
407
  cost: { value: 0.12, display: "$0.12", samples: 1, total: 1, refs: [failedAttempt.locator] },
400
408
  attempts: [failedAttempt, failedRetryAttempt],
@@ -423,7 +431,6 @@ export const experimentListItems: ExperimentListItem[] = [
423
431
  {
424
432
  evalId: "geometry/angles",
425
433
  verdict: "errored",
426
- reason: erroredAttempt.error,
427
434
  duration: { value: 4_500, display: "4.5s", samples: 1, total: 1, refs: [erroredAttempt.locator] },
428
435
  cost: { value: null, display: "—", samples: 0, total: 1, refs: [] },
429
436
  attempts: [erroredAttempt],
@@ -1,11 +1,9 @@
1
+ // cases: docs/engineering/unit-tests/reports/cases.md
1
2
  // 「不 hydrate 也完整」的验收测试:每个组件过 renderToStaticMarkup,
2
3
  // 断言纯静态 HTML 里就有全部关键内容——数字、覆盖率角标、缺数据文案、
3
4
  // 散点的 SVG 与系列名、truncated 行、attemptHref 链接。
4
- // 另外锁两条契约:源码零 hooks;维度键跨组件同色。
5
+ // 另外锁一条契约:维度键跨组件同色。
5
6
 
6
- import { readFileSync, readdirSync } from "node:fs";
7
- import { dirname, join } from "node:path";
8
- import { fileURLToPath } from "node:url";
9
7
  import { renderToStaticMarkup } from "react-dom/server";
10
8
  import { describe, expect, it } from "vitest";
11
9
  import type { AttemptLocator } from "../../results/locator.ts";
@@ -72,12 +70,6 @@ describe("RunOverview", () => {
72
70
  describe("MetricTable", () => {
73
71
  const html = renderToStaticMarkup(<MetricTable data={tableData} attemptHref={attemptHref} />);
74
72
 
75
- it("按传入顺序渲染行,不重排(排序在数据侧)", () => {
76
- // fixture 里 codex(50%)在 bub(87%)前面,输出必须保持
77
- expect(html.indexOf(">codex<")).toBeGreaterThan(-1);
78
- expect(html.indexOf(">codex<")).toBeLessThan(html.indexOf(">bub<"));
79
- });
80
-
81
73
  it("列头带 label、unit 与 better 方向", () => {
82
74
  expect(html).toContain("pass rate");
83
75
  expect(html).toContain("(%)");
@@ -157,21 +149,12 @@ describe("MetricMatrix", () => {
157
149
  expect(html).toContain("eval × agent");
158
150
  });
159
151
 
160
- it("稀疏格子:没有样本的格子空着(恰好一个)", () => {
161
- expect(html.match(/nre-td-empty/g)).toHaveLength(1);
162
- });
163
-
164
152
  it("格子数字与 refs 下钻链接", () => {
165
153
  expect(html).toContain("100%");
166
154
  expect(html).toContain("0%");
167
155
  expect(html).toContain('href="/attempts/@1b3b3b3"');
168
156
  expect(html).toContain('href="/attempts/@1b7b7b7"');
169
157
  });
170
-
171
- it("列头(维度键)带稳定散列配色 class", () => {
172
- expect(html).toContain(colorClassForKey("bub"));
173
- expect(html).toContain(colorClassForKey("codex"));
174
- });
175
158
  });
176
159
 
177
160
  describe("Scoreboard", () => {
@@ -243,11 +226,6 @@ describe("MetricScatter", () => {
243
226
  expect(html).toContain(">codex-mid</text>");
244
227
  });
245
228
 
246
- it("null 点不画,底部注脚如实报数", () => {
247
- expect(html).not.toContain('data-key="compare/codex-broken"');
248
- expect(html).toContain("1 point missing data");
249
- });
250
-
251
229
  it("hover 退化为 <title>:display 与 samples/total", () => {
252
230
  expect(html).toContain("<title>");
253
231
  expect(html).toContain("50%(6/6)");
@@ -304,11 +282,6 @@ describe("DeltaTable", () => {
304
282
  expect(html).toContain("nre-delta-flat");
305
283
  });
306
284
 
307
- it("任一侧缺数据:Δ 显示为缺,不硬算", () => {
308
- expect(html).toContain("nre-delta-missing");
309
- expect(html).toContain("no data");
310
- });
311
-
312
285
  it("每行标出 A → B 的 experimentId", () => {
313
286
  expect(html).toContain("compare/bub → compare/bub--agents-md");
314
287
  });
@@ -322,14 +295,11 @@ describe("DeltaTable", () => {
322
295
  describe("AttemptList", () => {
323
296
  const html = renderToStaticMarkup(<AttemptList items={attemptListItems} />);
324
297
 
325
- it("逐条断言:name、score、detail、evidence", () => {
298
+ it("只显示一条主失败摘要,不内联完整 evidence", () => {
326
299
  expect(html).toContain("roots-correct");
327
- expect(html).toContain("expected x=2, got x=3");
328
- expect(html).toContain("judge: sign flipped when substituting into the quadratic formula");
329
- });
330
-
331
- it("errored 的 error 摘要", () => {
332
- expect(html).toContain("TypeError: cannot read properties of undefined");
300
+ expect(html).toContain("expected x=2");
301
+ expect(html).toContain("received x=3");
302
+ expect(html).not.toContain("judge: sign flipped when substituting into the quadratic formula");
333
303
  });
334
304
 
335
305
  it("total > items.length 时如实报「还有 n 条没列」", () => {
@@ -339,28 +309,14 @@ describe("AttemptList", () => {
339
309
  expect(html).not.toContain("more not shown");
340
310
  });
341
311
 
342
- it("每条 attempt 带 locator + 默认证据室深链,不追加字母缩写", () => {
343
- expect(html).toContain('href="#/attempt/@1a4a4a4"');
344
- expect(html).toContain('href="#/attempt/@1c1c1c1"');
345
- expect(html).not.toMatch(/\[[EXD⏱,]+\]/);
346
- });
347
-
348
- it("长文本(evidence)收进 <details>,零 JS 可展开", () => {
349
- expect(html).toContain("<details");
350
- expect(html).toContain("<summary>");
312
+ it("AttemptList 自身不展开断言详情", () => {
313
+ expect(html).not.toContain("<details");
351
314
  });
352
315
  });
353
316
 
354
317
  describe("EvalList", () => {
355
318
  const html = renderToStaticMarkup(<EvalList items={evalListItems} />);
356
319
 
357
- it("每项一个 experimentId + evalId,判定与分数在场", () => {
358
- expect(html).toContain("algebra/quadratic");
359
- expect(html).toContain("geometry/angles");
360
- expect(html).toContain("compare/bub");
361
- expect(html).toContain("compare/codex");
362
- });
363
-
364
320
  it("展开到这道题的 Attempt(与 AttemptList 同一套 AttemptRow 渲染)", () => {
365
321
  expect(html).toContain('href="#/attempt/@1a4a4a4"');
366
322
  expect(html).toContain("roots-correct");
@@ -377,7 +333,7 @@ describe("ExperimentList", () => {
377
333
 
378
334
  it("web 面是带过滤框与八个固定列头的 experiment 比较表", () => {
379
335
  expect(html).toContain('data-nre-experiment-filter=""');
380
- for (const header of ["Experiment", "Model", "Agent", "Avg duration", "Pass rate", "Tokens", "Est. cost", "Result"]) {
336
+ for (const header of ["Experiment", "Model", "Agent", "Avg duration", "End-to-end pass rate", "Tokens", "Est. cost", "Result"]) {
381
337
  expect(html).toContain(`>${header}</button>`);
382
338
  }
383
339
  expect(html).toContain('data-nre-experiment-sort="4" class="nre-sort-desc"');
@@ -438,15 +394,4 @@ describe("跨组件契约", () => {
438
394
  ].join("");
439
395
  expect(all).not.toContain("<script");
440
396
  });
441
-
442
- it("组件源码零 hooks(本实验的「不 hydrate 也完整」用最笨的方式保证)", () => {
443
- const dir = dirname(fileURLToPath(import.meta.url));
444
- const sources = readdirSync(dir)
445
- .filter((f) => (f.endsWith(".tsx") || f.endsWith(".ts")) && !f.includes(".test."))
446
- .map((f) => readFileSync(join(dir, f), "utf8"));
447
- expect(sources.length).toBeGreaterThanOrEqual(12);
448
- for (const src of sources) {
449
- expect(src).not.toMatch(/\buse[A-Z][A-Za-z]*\s*\(/);
450
- }
451
- });
452
397
  });
@@ -61,6 +61,7 @@
61
61
  .nre-attempt-list .nre-attempt-experiment,
62
62
  .nre-attempt-list .nre-attempt-duration,
63
63
  .nre-attempt-list .nre-attempt-cost { color:var(--muted); font-size:.9em; font-variant-numeric:tabular-nums; }
64
+ .nre-attempt-list .nre-attempt-result { display:-webkit-box; overflow:hidden; -webkit-box-orient:vertical; -webkit-line-clamp:2; margin:.4rem 0 0; color:color-mix(in oklch,var(--muted),var(--text) 12%); overflow-wrap:anywhere; }
64
65
  .nre-attempt-list .nre-attempt-error { margin:.4rem 0 0; padding:6px 10px; border:1px solid color-mix(in oklch,var(--bad),var(--line) 50%); border-radius:7px; background:color-mix(in oklch,var(--bad),var(--panel) 92%); color:var(--bad); font-family:ui-monospace,SFMono-Regular,Menlo,monospace; font-size:.85em; white-space:pre-wrap; word-break:break-word; }
65
66
  .nre-attempt-list .nre-assertions { list-style:none; margin:.4rem 0 0; padding:0; display:grid; gap:6px; }
66
67
  .nre-attempt-list .nre-assertion { padding-left:.25rem; }
@@ -75,6 +76,77 @@
75
76
  .nre-attempt-list .nre-truncated { color:var(--warn); font-size:.9em; }
76
77
  .nre-attempt-list .nre-attempt-list-empty { border:1px dashed var(--line-strong); border-radius:8px; padding:18px; color:var(--muted); text-align:center; background:var(--panel); }
77
78
 
79
+ /* ---- ExperimentComparison:完整组索引 + 当前组独立 panel ---- */
80
+ .nre-experiment-comparison .nre-experiment-group-tabs {
81
+ display:grid;
82
+ grid-template-columns:repeat(auto-fit,minmax(320px,1fr));
83
+ gap:12px;
84
+ margin-bottom:20px;
85
+ }
86
+ .nre-experiment-comparison .nre-experiment-group-tab {
87
+ min-width:0;
88
+ border:1px solid var(--line);
89
+ border-radius:9px;
90
+ padding:14px 16px 8px;
91
+ background:var(--panel);
92
+ cursor:pointer;
93
+ transition:border-color .12s,background .12s;
94
+ }
95
+ .nre-experiment-comparison .nre-experiment-group-tab:hover {
96
+ border-color:var(--line-strong);
97
+ background:color-mix(in oklch,var(--panel-2),var(--panel) 35%);
98
+ }
99
+ .nre-experiment-comparison .nre-experiment-group-tab:focus-visible {
100
+ outline:2px solid var(--nre-c0);
101
+ outline-offset:2px;
102
+ }
103
+ .nre-experiment-comparison .nre-experiment-group-tab[aria-selected="true"] {
104
+ border-color:var(--nre-c0);
105
+ box-shadow:inset 3px 0 0 var(--nre-c0);
106
+ }
107
+ .nre-experiment-comparison .nre-experiment-group-name {
108
+ display:block;
109
+ margin-bottom:10px;
110
+ color:var(--text);
111
+ font-size:15px;
112
+ overflow-wrap:anywhere;
113
+ }
114
+ .nre-experiment-comparison .nre-experiment-group-tab .nre-group-kpis {
115
+ column-gap:16px;
116
+ }
117
+ .nre-experiment-comparison .nre-experiment-group-tab .nre-group-kpi {
118
+ min-width:58px;
119
+ }
120
+ .nre-experiment-comparison .nre-experiment-group-panel {
121
+ border:1px solid var(--line);
122
+ border-radius:9px;
123
+ padding:16px;
124
+ background:var(--panel);
125
+ }
126
+ .nre-experiment-comparison .nre-experiment-group-panel > summary {
127
+ cursor:pointer;
128
+ font-size:16px;
129
+ font-weight:700;
130
+ overflow-wrap:anywhere;
131
+ }
132
+ .nre-experiment-comparison .nre-experiment-group-panel[open] > summary {
133
+ margin-bottom:16px;
134
+ }
135
+ .nre-experiment-comparison .nre-experiment-group-panel > .nre {
136
+ margin:0 0 18px;
137
+ }
138
+ .nre-experiment-comparison .nre-experiment-group-panel > .nre:last-child {
139
+ margin-bottom:0;
140
+ }
141
+ .nre-experiment-comparison .nre-experiment-groups-empty {
142
+ border:1px dashed var(--line-strong);
143
+ border-radius:8px;
144
+ padding:18px;
145
+ color:var(--muted);
146
+ text-align:center;
147
+ background:var(--panel);
148
+ }
149
+
78
150
  /* ---- EvalList:实体列表的第二级,一条 <details> 一个 experimentId × evalId ---- */
79
151
  .nre-eval-list .nre-evals { list-style:none; margin:0; padding:0; }
80
152
  .nre-eval-list .nre-eval { border-bottom:1px solid var(--line); }
@@ -88,7 +160,6 @@
88
160
  .nre-eval-list .nre-eval-attempts-count,
89
161
  .nre-eval-list .nre-eval-duration,
90
162
  .nre-eval-list .nre-eval-cost { color:var(--muted); font-size:.9em; font-variant-numeric:tabular-nums; }
91
- .nre-eval-list .nre-eval-reason { color:color-mix(in oklch,var(--muted),var(--text) 6%); }
92
163
  .nre-eval-list .nre-attempts { list-style:none; margin:0 0 12px; padding:0 0 0 20px; border-left:2px solid color-mix(in oklch,var(--nre-c0),transparent 55%); }
93
164
  .nre-eval-list .nre-eval-list-empty { border:1px dashed var(--line-strong); border-radius:8px; padding:18px; color:var(--muted); text-align:center; background:var(--panel); }
94
165
 
@@ -132,7 +203,7 @@
132
203
  .nre-experiment-list .nre-experiment-attempt-row { padding:5px 0 5px 20px; color:var(--muted); }
133
204
  .nre-experiment-list .nre-eval-id { font-weight:560; overflow-wrap:anywhere; }
134
205
  .nre-experiment-list .nre-eval-attempt-count { color:var(--soft); font-size:12px; }
135
- .nre-experiment-list .nre-eval-summary { color:var(--muted); font-size:.92em; overflow-wrap:anywhere; }
206
+ .nre-experiment-list .nre-eval-rollup { color:var(--muted); font-size:.92em; overflow-wrap:anywhere; }
136
207
  .nre-experiment-list .nre-attempt-branch { color:var(--line-strong); font-variant-numeric:tabular-nums; }
137
208
  .nre-experiment-list .nre-eval-attempt-badges { display:inline-flex; flex-wrap:wrap; gap:.6em; }
138
209
  .nre-experiment-list .nre-eval-attempt-metrics,