niceeval 0.6.0 → 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 (314) hide show
  1. package/dist/agents/types.d.ts +72 -6
  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 -0
  9. package/dist/report/built-ins/experiment-comparison.js +119 -0
  10. package/dist/report/built-ins/index.d.ts +2 -1
  11. package/dist/report/built-ins/index.js +2 -2
  12. package/dist/report/components.d.ts +10 -2
  13. package/dist/report/components.js +3 -3
  14. package/dist/report/compute.d.ts +11 -18
  15. package/dist/report/compute.js +68 -66
  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 -14
  19. package/dist/report/format.js +28 -30
  20. package/dist/report/index.d.ts +5 -4
  21. package/dist/report/index.js +6 -5
  22. package/dist/report/locale.d.ts +23 -3
  23. package/dist/report/locale.js +47 -6
  24. package/dist/report/metrics.d.ts +13 -1
  25. package/dist/report/metrics.js +66 -15
  26. package/dist/report/primitives.d.ts +6 -0
  27. package/dist/report/react/AttemptList.d.ts +4 -4
  28. package/dist/report/react/AttemptList.js +8 -10
  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 +4 -2
  34. package/dist/report/react/ExperimentList.js +57 -7
  35. package/dist/report/react/MetricScatter.js +6 -16
  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 +30 -18
  40. package/dist/report/react/format.d.ts +1 -1
  41. package/dist/report/react/format.js +1 -1
  42. package/dist/report/react/index.d.ts +1 -1
  43. package/dist/report/report.d.ts +5 -1
  44. package/dist/report/report.js +6 -2
  45. package/dist/report/text/faces.d.ts +1 -1
  46. package/dist/report/text/faces.js +100 -61
  47. package/dist/report/text/table.js +36 -5
  48. package/dist/report/types.d.ts +40 -34
  49. package/dist/results/types.d.ts +11 -0
  50. package/dist/runner/feedback/sink.d.ts +110 -0
  51. package/dist/runner/types.d.ts +513 -22
  52. package/dist/sandbox/docker.d.ts +23 -2
  53. package/dist/sandbox/e2b.d.ts +15 -1
  54. package/dist/sandbox/errors.d.ts +30 -3
  55. package/dist/sandbox/io-retry.d.ts +17 -0
  56. package/dist/sandbox/registry.d.ts +2 -0
  57. package/dist/sandbox/resolve.d.ts +18 -5
  58. package/dist/sandbox/retry.d.ts +11 -1
  59. package/dist/sandbox/types.d.ts +39 -5
  60. package/dist/sandbox/vercel.d.ts +7 -1
  61. package/dist/scoring/coverage.d.ts +30 -0
  62. package/dist/scoring/display.d.ts +21 -0
  63. package/dist/scoring/display.js +120 -0
  64. package/dist/scoring/types.d.ts +103 -20
  65. package/dist/shared/aggregate.d.ts +1 -0
  66. package/dist/shared/aggregate.js +3 -3
  67. package/dist/shared/types.d.ts +28 -0
  68. package/dist/tty-line.d.ts +0 -4
  69. package/dist/util.d.ts +23 -0
  70. package/docs-site/zh/concepts/adapter.mdx +24 -6
  71. package/docs-site/zh/concepts/assert.mdx +11 -10
  72. package/docs-site/zh/concepts/evals.mdx +7 -6
  73. package/docs-site/zh/concepts/experiment.mdx +1 -1
  74. package/docs-site/zh/concepts/overview.mdx +7 -7
  75. package/docs-site/zh/guides/agent-feedback-loop.mdx +35 -31
  76. package/docs-site/zh/guides/authoring.mdx +33 -0
  77. package/docs-site/zh/guides/ci-integration.mdx +23 -12
  78. package/docs-site/zh/guides/connect-your-agent.mdx +29 -3
  79. package/docs-site/zh/guides/custom-reports.mdx +29 -34
  80. package/docs-site/zh/guides/dataset-fanout.mdx +25 -3
  81. package/docs-site/zh/guides/debug-sandbox.mdx +57 -0
  82. package/docs-site/zh/guides/debugging.mdx +210 -0
  83. package/docs-site/zh/guides/experiments.mdx +10 -3
  84. package/docs-site/zh/guides/fixtures.mdx +3 -1
  85. package/docs-site/zh/guides/official-adapters.mdx +27 -3
  86. package/docs-site/zh/guides/publish-report.mdx +30 -16
  87. package/docs-site/zh/guides/report-components.mdx +49 -37
  88. package/docs-site/zh/guides/reporters.mdx +2 -2
  89. package/docs-site/zh/guides/results-data.mdx +42 -8
  90. package/docs-site/zh/guides/runner.mdx +17 -7
  91. package/docs-site/zh/guides/sandbox-agent.mdx +57 -7
  92. package/docs-site/zh/guides/sandbox-providers.mdx +258 -10
  93. package/docs-site/zh/guides/scoring-guide.mdx +4 -4
  94. package/docs-site/zh/guides/viewing-results.mdx +85 -41
  95. package/docs-site/zh/guides/write-experiment.mdx +5 -3
  96. package/docs-site/zh/guides/write-send.mdx +19 -2
  97. package/docs-site/zh/index.mdx +1 -1
  98. package/docs-site/zh/reference/builtin-agents.mdx +27 -0
  99. package/docs-site/zh/reference/capabilities.mdx +2 -2
  100. package/docs-site/zh/reference/cli.mdx +35 -9
  101. package/docs-site/zh/reference/define-agent.mdx +60 -5
  102. package/docs-site/zh/reference/define-config.mdx +1 -1
  103. package/docs-site/zh/reference/define-eval.mdx +42 -9
  104. package/docs-site/zh/reference/events.mdx +2 -2
  105. package/docs-site/zh/reference/expect.mdx +36 -6
  106. package/package.json +5 -1
  107. package/src/agents/ai-sdk-otel.test.ts +1 -0
  108. package/src/agents/ai-sdk.test.ts +3 -0
  109. package/src/agents/ai-sdk.ts +3 -0
  110. package/src/agents/bub-install-spec.test.ts +34 -0
  111. package/src/agents/bub-install-spec.ts +32 -0
  112. package/src/agents/bub.ts +31 -32
  113. package/src/agents/claude-code.test.ts +130 -9
  114. package/src/agents/claude-code.ts +76 -4
  115. package/src/agents/codex.test.ts +189 -40
  116. package/src/agents/codex.ts +155 -14
  117. package/src/agents/coding-cli-versions.test.ts +15 -0
  118. package/src/agents/coding-cli-versions.ts +3 -0
  119. package/src/agents/index.ts +11 -0
  120. package/src/agents/langgraph.test.ts +204 -0
  121. package/src/agents/langgraph.ts +495 -0
  122. package/src/agents/marketplace.ts +85 -0
  123. package/src/agents/native-config.test.ts +179 -0
  124. package/src/agents/native-config.ts +267 -0
  125. package/src/agents/openai-compat.test.ts +1 -0
  126. package/src/agents/openclaw.test.ts +31 -0
  127. package/src/agents/openclaw.ts +171 -0
  128. package/src/agents/plugin-config.test.ts +1 -0
  129. package/src/agents/sdk-streams.test.ts +79 -0
  130. package/src/agents/sdk-streams.ts +55 -10
  131. package/src/agents/skills.test.ts +1 -0
  132. package/src/agents/streaming.test.ts +3 -9
  133. package/src/agents/types.ts +73 -6
  134. package/src/agents/ui-message-stream.test.ts +3 -0
  135. package/src/cli.ts +411 -108
  136. package/src/context/context.test.ts +51 -12
  137. package/src/context/context.ts +161 -29
  138. package/src/context/session.test.ts +1 -0
  139. package/src/context/session.ts +114 -6
  140. package/src/context/types.ts +30 -12
  141. package/src/define.test.ts +13 -8
  142. package/src/define.ts +25 -4
  143. package/src/expect/index.ts +53 -23
  144. package/src/i18n/en.ts +65 -4
  145. package/src/i18n/zh-CN.ts +66 -4
  146. package/src/o11y/cost.test.ts +1 -0
  147. package/src/o11y/execution-tree.test.ts +1 -20
  148. package/src/o11y/otlp/mappers/claude-code.test.ts +1 -0
  149. package/src/o11y/otlp/parse.test.ts +1 -0
  150. package/src/o11y/otlp/turn-otel.test.ts +1 -0
  151. package/src/o11y/parsers/bub.test.ts +1 -0
  152. package/src/o11y/parsers/claude-code.test.ts +1 -34
  153. package/src/o11y/parsers/openclaw.test.ts +154 -0
  154. package/src/o11y/parsers/openclaw.ts +310 -0
  155. package/src/o11y/prices.json +746 -311
  156. package/src/o11y/tool-names.test.ts +1 -0
  157. package/src/o11y/types.ts +16 -2
  158. package/src/report/aggregate.ts +34 -5
  159. package/src/report/built-in-user-parity.test.tsx +127 -173
  160. package/src/report/built-ins/experiment-comparison.tsx +179 -0
  161. package/src/report/built-ins/index.ts +7 -2
  162. package/src/report/components.tsx +11 -3
  163. package/src/report/compute.ts +80 -74
  164. package/src/report/dual-render.test.tsx +222 -91
  165. package/src/report/flag.ts +30 -2
  166. package/src/report/format.ts +36 -27
  167. package/src/report/index.ts +23 -6
  168. package/src/report/locale.ts +49 -6
  169. package/src/report/metrics.ts +68 -15
  170. package/src/report/primitives.tsx +6 -0
  171. package/src/report/react/AttemptList.tsx +9 -36
  172. package/src/report/react/EvalList.tsx +0 -0
  173. package/src/report/react/ExperimentComparison.tsx +68 -0
  174. package/src/report/react/ExperimentList.tsx +173 -55
  175. package/src/report/react/MetricScatter.tsx +13 -25
  176. package/src/report/react/chart-math.test.ts +85 -0
  177. package/src/report/react/chart-math.ts +101 -22
  178. package/src/report/react/enhance.js +72 -1
  179. package/src/report/react/fixtures.ts +34 -21
  180. package/src/report/react/format.ts +1 -1
  181. package/src/report/react/index.tsx +0 -1
  182. package/src/report/react/render.test.tsx +30 -69
  183. package/src/report/react/styles.css +112 -14
  184. package/src/report/report.test.ts +308 -105
  185. package/src/report/report.ts +6 -2
  186. package/src/report/text/faces.ts +111 -67
  187. package/src/report/text/table.ts +42 -5
  188. package/src/report/types.ts +42 -34
  189. package/src/results/annotated-source.test.ts +62 -9
  190. package/src/results/annotated-source.ts +64 -6
  191. package/src/results/attempt-evidence.test.ts +9 -7
  192. package/src/results/attempt-evidence.ts +15 -8
  193. package/src/results/attempt-source.ts +6 -3
  194. package/src/results/copy.ts +145 -55
  195. package/src/results/host-equivalence.test.ts +11 -9
  196. package/src/results/index.ts +2 -0
  197. package/src/results/locator.test.ts +1 -22
  198. package/src/results/open.ts +7 -1
  199. package/src/results/publish.ts +149 -0
  200. package/src/results/results.test.ts +85 -51
  201. package/src/results/truncate.ts +90 -0
  202. package/src/results/types.ts +7 -0
  203. package/src/results/writer.ts +31 -13
  204. package/src/runner/attempt.test.ts +138 -7
  205. package/src/runner/attempt.ts +603 -104
  206. package/src/runner/discover.test.ts +47 -0
  207. package/src/runner/discover.ts +36 -2
  208. package/src/runner/eval-source.test.ts +1 -27
  209. package/src/runner/feedback/agent.test.ts +504 -0
  210. package/src/runner/feedback/agent.ts +409 -0
  211. package/src/runner/feedback/ci.test.ts +562 -0
  212. package/src/runner/feedback/ci.ts +401 -0
  213. package/src/runner/feedback/coordinator.test.ts +317 -0
  214. package/src/runner/feedback/coordinator.ts +397 -0
  215. package/src/runner/feedback/failure.ts +40 -0
  216. package/src/runner/feedback/human.test.ts +616 -0
  217. package/src/runner/feedback/human.ts +535 -0
  218. package/src/runner/feedback/index.ts +66 -0
  219. package/src/runner/feedback/io.ts +78 -0
  220. package/src/runner/feedback/profile.test.ts +50 -0
  221. package/src/runner/feedback/profile.ts +58 -0
  222. package/src/runner/feedback/reducer.test.ts +395 -0
  223. package/src/runner/feedback/reducer.ts +260 -0
  224. package/src/runner/feedback/renderer.ts +82 -0
  225. package/src/runner/feedback/sink.ts +203 -0
  226. package/src/runner/feedback/testing.ts +106 -0
  227. package/src/runner/ledger.test.ts +230 -0
  228. package/src/runner/ledger.ts +329 -0
  229. package/src/runner/report.test.ts +128 -3
  230. package/src/runner/report.ts +33 -9
  231. package/src/runner/reporters/artifacts.ts +8 -2
  232. package/src/runner/reporters/braintrust.test.ts +8 -7
  233. package/src/runner/reporters/braintrust.ts +9 -2
  234. package/src/runner/reporters/index.ts +2 -2
  235. package/src/runner/reporters/json.test.ts +162 -0
  236. package/src/runner/reporters/json.ts +35 -8
  237. package/src/runner/reporters/shared.ts +1 -5
  238. package/src/runner/run.test.ts +760 -3
  239. package/src/runner/run.ts +242 -36
  240. package/src/runner/sandbox-prep.ts +3 -42
  241. package/src/runner/timing.ts +158 -0
  242. package/src/runner/types.ts +518 -22
  243. package/src/sandbox/checkpoint.test.ts +55 -0
  244. package/src/sandbox/checkpoint.ts +29 -8
  245. package/src/sandbox/cli-commands.ts +407 -0
  246. package/src/sandbox/docker.ts +115 -16
  247. package/src/sandbox/e2b-agent-template.test.ts +56 -0
  248. package/src/sandbox/e2b-agent-template.ts +94 -0
  249. package/src/sandbox/e2b.ts +74 -9
  250. package/src/sandbox/errors.ts +111 -4
  251. package/src/sandbox/index.ts +2 -0
  252. package/src/sandbox/io-retry.test.ts +58 -0
  253. package/src/sandbox/io-retry.ts +45 -0
  254. package/src/sandbox/keep-registry.test.ts +86 -0
  255. package/src/sandbox/keep-registry.ts +142 -0
  256. package/src/sandbox/keep.ts +178 -0
  257. package/src/sandbox/paths.test.ts +1 -0
  258. package/src/sandbox/paths.ts +19 -8
  259. package/src/sandbox/registry.ts +20 -3
  260. package/src/sandbox/resolve.ts +76 -11
  261. package/src/sandbox/retry.test.ts +70 -0
  262. package/src/sandbox/retry.ts +46 -4
  263. package/src/sandbox/types.ts +44 -6
  264. package/src/sandbox/vercel.ts +43 -20
  265. package/src/scoring/collector.ts +60 -17
  266. package/src/scoring/coverage.ts +95 -0
  267. package/src/scoring/diff.ts +81 -0
  268. package/src/scoring/display.test.ts +121 -0
  269. package/src/scoring/display.ts +133 -0
  270. package/src/scoring/evidence.test.ts +189 -0
  271. package/src/scoring/judge.test.ts +142 -0
  272. package/src/scoring/judge.ts +15 -18
  273. package/src/scoring/scoped.ts +217 -50
  274. package/src/scoring/types.ts +117 -20
  275. package/src/scoring/verdict.ts +16 -4
  276. package/src/shared/aggregate.ts +3 -2
  277. package/src/shared/types.ts +31 -0
  278. package/src/show/compose.ts +2 -2
  279. package/src/show/index.ts +29 -16
  280. package/src/show/render.ts +626 -308
  281. package/src/show/show.test.ts +251 -36
  282. package/src/tty-line.ts +8 -26
  283. package/src/util.test.ts +1 -0
  284. package/src/util.ts +41 -0
  285. package/src/view/app/components/AttemptModal.tsx +153 -2
  286. package/src/view/app/components/CodeView.tsx +32 -11
  287. package/src/view/app/components/CopyControls.tsx +2 -2
  288. package/src/view/app/i18n.ts +6 -0
  289. package/src/view/app/lib/attempt-route.test.ts +1 -0
  290. package/src/view/app/lib/verdict.ts +7 -9
  291. package/src/view/artifact-serving.test.ts +2 -1
  292. package/src/view/client-dist/app.css +1 -1
  293. package/src/view/client-dist/app.js +17 -17
  294. package/src/view/data.test.ts +2 -1
  295. package/src/view/data.ts +17 -7
  296. package/src/view/index.ts +12 -1
  297. package/src/view/server.ts +2 -0
  298. package/src/view/shared/types.ts +1 -1
  299. package/src/view/styles.css +3 -0
  300. package/src/view/view-report.test.ts +11 -10
  301. package/dist/o11y/execution-tree.d.ts +0 -103
  302. package/dist/o11y/otlp/select.d.ts +0 -22
  303. package/dist/report/built-ins/cost-pass-rate-comparison.d.ts +0 -1
  304. package/dist/report/built-ins/cost-pass-rate-comparison.js +0 -17
  305. package/dist/results/annotated-source.d.ts +0 -61
  306. package/dist/results/attempt-evidence.d.ts +0 -69
  307. package/dist/results/attempt-source.d.ts +0 -15
  308. package/src/report/built-ins/cost-pass-rate-comparison.tsx +0 -23
  309. package/src/runner/reporters/console.ts +0 -70
  310. package/src/runner/reporters/live.test.ts +0 -56
  311. package/src/runner/reporters/live.ts +0 -247
  312. package/src/runner/reporters/quiet.test.ts +0 -66
  313. package/src/runner/reporters/quiet.ts +0 -49
  314. package/src/runner/reporters/table.ts +0 -277
@@ -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 类驱动。
@@ -73,6 +105,45 @@
73
105
  }
74
106
  });
75
107
 
108
+ // ExperimentList 的 web 面以原生 details 表达「八列主行 + 展开明细」,不是 table/tbody。
109
+ // 单独按 summary 的列排序、按整条 details 文本过滤;无 JS 时保持数据侧成功率降序。
110
+ document.addEventListener("click", function (e) {
111
+ var control = closest(e.target, "[data-nre-experiment-sort]");
112
+ if (!control) return;
113
+ var board = control.closest(".nre-experiment-table");
114
+ if (!board) return;
115
+ var index = Number(control.getAttribute("data-nre-experiment-sort"));
116
+ var dir = control.classList.contains("nre-sort-asc") ? "desc" : "asc";
117
+ var controls = board.querySelectorAll("[data-nre-experiment-sort]");
118
+ for (var i = 0; i < controls.length; i++) controls[i].classList.remove("nre-sort-asc", "nre-sort-desc");
119
+ control.classList.add(dir === "asc" ? "nre-sort-asc" : "nre-sort-desc");
120
+ var entries = Array.prototype.slice.call(board.querySelectorAll(":scope > .nre-experiment-entry"));
121
+ entries.sort(function (a, b) {
122
+ var ac = a.querySelector(".nre-experiment-summary").children[index];
123
+ var bc = b.querySelector(".nre-experiment-summary").children[index];
124
+ var av = ac ? ac.getAttribute("data-sort-value") || ac.textContent.trim() : "";
125
+ var bv = bc ? bc.getAttribute("data-sort-value") || bc.textContent.trim() : "";
126
+ if (av === "" && bv === "") return 0;
127
+ if (av === "") return 1;
128
+ if (bv === "") return -1;
129
+ var an = Number(av), bn = Number(bv);
130
+ var out = !isNaN(an) && !isNaN(bn) ? an - bn : String(av).localeCompare(String(bv));
131
+ return dir === "asc" ? out : -out;
132
+ });
133
+ for (var j = 0; j < entries.length; j++) board.appendChild(entries[j]);
134
+ });
135
+
136
+ document.addEventListener("input", function (e) {
137
+ var input = closest(e.target, "input[data-nre-experiment-filter]");
138
+ if (!input) return;
139
+ var scope = input.parentElement;
140
+ var entries = scope ? scope.querySelectorAll(".nre-experiment-entry") : [];
141
+ var query = input.value.trim().toLowerCase();
142
+ for (var i = 0; i < entries.length; i++) {
143
+ entries[i].classList.toggle("nre-row-hidden", query !== "" && entries[i].textContent.toLowerCase().indexOf(query) === -1);
144
+ }
145
+ });
146
+
76
147
  // ───────────────────────── tooltip:.nre-scatter-point / .nre-line-point ─────────────────────────
77
148
  // 首次 hover 时把点内 <title> 的内容搬进 data-nre-title(避免与原生 tooltip 重影),
78
149
  // 渲染样式化 tooltip div(定位在点上方,挂在所属 figure 里)。无 JS 时 <title> 原样生效。
@@ -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,15 +322,15 @@ 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
  ],
330
331
  durationMs: 32_000,
331
332
  costUSD: 0.12,
332
333
  locator: locator("@1a4a4a4"),
333
- capabilities: { eval: true, execution: true, timing: true, diff: false },
334
334
  };
335
335
 
336
336
  const erroredAttempt: AttemptListItem = {
@@ -339,11 +339,27 @@ const erroredAttempt: AttemptListItem = {
339
339
  attempt: 0,
340
340
  agent: "codex",
341
341
  verdict: "errored",
342
- 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
+ ],
343
353
  assertions: [],
344
354
  durationMs: 4_500,
345
355
  locator: locator("@1c1c1c1"),
346
- capabilities: { eval: false, execution: true, timing: false, diff: false },
356
+ };
357
+
358
+ const failedRetryAttempt: AttemptListItem = {
359
+ ...failedAttempt,
360
+ attempt: 4,
361
+ durationMs: 35_000,
362
+ locator: locator("@1b5b5b5"),
347
363
  };
348
364
 
349
365
  export const attemptListItems: AttemptListItem[] = [failedAttempt, erroredAttempt];
@@ -353,7 +369,6 @@ export const evalListItems: EvalListItem[] = [
353
369
  evalId: "algebra/quadratic",
354
370
  experimentId: "compare/bub",
355
371
  verdict: "failed",
356
- reason: "roots-correct: expected x=2, got x=3",
357
372
  score: { value: 0, display: "0%", samples: 1, total: 1, refs: [failedAttempt.locator] },
358
373
  duration: { value: 32_000, display: "32.0s", samples: 1, total: 1, refs: [failedAttempt.locator] },
359
374
  cost: { value: 0.12, display: "$0.12", samples: 1, total: 1, refs: [failedAttempt.locator] },
@@ -363,7 +378,6 @@ export const evalListItems: EvalListItem[] = [
363
378
  evalId: "geometry/angles",
364
379
  experimentId: "compare/codex",
365
380
  verdict: "errored",
366
- reason: erroredAttempt.error,
367
381
  score: { value: 0, display: "0%", samples: 1, total: 1, refs: [erroredAttempt.locator] },
368
382
  duration: { value: 4_500, display: "4.5s", samples: 1, total: 1, refs: [erroredAttempt.locator] },
369
383
  cost: { value: null, display: "—", samples: 0, total: 1, refs: [] },
@@ -376,22 +390,22 @@ export const experimentListItems: ExperimentListItem[] = [
376
390
  experimentId: "compare/bub",
377
391
  agent: "bub",
378
392
  model: "gpt-5.4",
393
+ flags: { memory: true },
379
394
  verdicts: { passed: 1, failed: 1, errored: 0, skipped: 0 },
380
395
  passRate: { value: 0.5, display: "50%", samples: 2, total: 2, refs: [] },
381
396
  cost: { value: 0.12, display: "$0.12", samples: 1, total: 2, refs: [failedAttempt.locator] },
382
397
  duration: { value: 32_000, display: "32.0s", samples: 2, total: 2, refs: [] },
383
398
  tokens: { value: null, display: "—", samples: 0, total: 2, refs: [] },
384
399
  evals: 2,
385
- attempts: 2,
400
+ attempts: 3,
386
401
  lastRunAt: "2026-07-01T10:00:00Z",
387
402
  evalRows: [
388
403
  {
389
404
  evalId: "algebra/quadratic",
390
405
  verdict: "failed",
391
- reason: "roots-correct: expected x=2, got x=3",
392
406
  duration: { value: 32_000, display: "32.0s", samples: 1, total: 1, refs: [failedAttempt.locator] },
393
407
  cost: { value: 0.12, display: "$0.12", samples: 1, total: 1, refs: [failedAttempt.locator] },
394
- attempts: [failedAttempt],
408
+ attempts: [failedAttempt, failedRetryAttempt],
395
409
  },
396
410
  {
397
411
  evalId: "algebra/simple",
@@ -417,7 +431,6 @@ export const experimentListItems: ExperimentListItem[] = [
417
431
  {
418
432
  evalId: "geometry/angles",
419
433
  verdict: "errored",
420
- reason: erroredAttempt.error,
421
434
  duration: { value: 4_500, display: "4.5s", samples: 1, total: 1, refs: [erroredAttempt.locator] },
422
435
  cost: { value: null, display: "—", samples: 0, total: 1, refs: [] },
423
436
  attempts: [erroredAttempt],
@@ -1,7 +1,7 @@
1
1
  // web 面的小工具:展示格式化统一住在计算侧的 ../format.ts(两个渲染面同一份),
2
2
  // 这里 re-export 并补 class 名拼接。MetricCell 一律自带 display,组件不重算。
3
3
 
4
- export { MISSING_TEXT, capabilityBadge, formatDurationMs, formatPercent, formatUSD, verdictMark } from "../format.ts";
4
+ export { MISSING_TEXT, formatDurationMs, formatPercent, formatUSD, verdictMark } from "../format.ts";
5
5
 
6
6
  /** 拼 class 名:过滤空值,末尾接使用者透传的 className。 */
7
7
  export function cx(...parts: (string | undefined | false)[]): string {
@@ -38,7 +38,6 @@ export type {
38
38
 
39
39
  // 数据契约类型(家在 ../types.ts,「算」与「画」两侧共用同一份)
40
40
  export type {
41
- AttemptEvidenceCapabilities,
42
41
  AttemptListItem,
43
42
  AttemptLocator,
44
43
  DeltaData,
@@ -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)");
@@ -271,7 +249,7 @@ describe("MetricScatter", () => {
271
249
  expect(empty).toContain("4 points missing data");
272
250
  });
273
251
 
274
- it("恰好 1 个可画点:比较至少要两个实验,不画孤点图", () => {
252
+ it("恰好 1 个可画点:照常画出单点", () => {
275
253
  const single = renderToStaticMarkup(
276
254
  <MetricScatter
277
255
  data={{
@@ -281,8 +259,8 @@ describe("MetricScatter", () => {
281
259
  }}
282
260
  />,
283
261
  );
284
- expect(single).not.toContain("<svg");
285
- expect(single).toContain("At least 2 experiments needed to compare");
262
+ expect(single).toContain("<svg");
263
+ expect(single).toContain("nre-scatter-point");
286
264
  });
287
265
  });
288
266
 
@@ -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).toContain("[E,X,⏱]"); // failedAttempt: eval + execution + timing
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");
@@ -373,18 +329,34 @@ describe("EvalList", () => {
373
329
  });
374
330
 
375
331
  describe("ExperimentList", () => {
376
- const html = renderToStaticMarkup(<ExperimentList items={experimentListItems} />);
332
+ const html = renderToStaticMarkup(<ExperimentList items={experimentListItems} filter />);
333
+
334
+ it("web 面是带过滤框与八个固定列头的 experiment 比较表", () => {
335
+ expect(html).toContain('data-nre-experiment-filter=""');
336
+ for (const header of ["Experiment", "Model", "Agent", "Avg duration", "End-to-end pass rate", "Tokens", "Est. cost", "Result"]) {
337
+ expect(html).toContain(`>${header}</button>`);
338
+ }
339
+ expect(html).toContain('data-nre-experiment-sort="4" class="nre-sort-desc"');
340
+ });
377
341
 
378
- it("主行:身份、agent/model、官方两级聚合指标", () => {
342
+ it("主行:身份、agent/model、官方两级聚合指标与判定构成", () => {
379
343
  expect(html).toContain("compare/bub");
380
344
  expect(html).toContain("compare/codex");
381
345
  expect(html).toContain("gpt-5.4");
382
346
  expect(html).toContain("50%"); // passRate.display
347
+ expect(html).toContain("1 passed / 1 failed");
348
+ expect(html).toContain("memory=true");
383
349
  });
384
350
 
385
- it("展开到 Eval:判定符 + 该题 Attempt 的 locator 徽标内联", () => {
351
+ it("展开到 Eval:同一道题的全部 Attempt 逐条显示", () => {
386
352
  expect(html).toContain("algebra/quadratic");
387
353
  expect(html).toContain('href="#/attempt/@1a4a4a4"');
354
+ expect(html).toContain('href="#/attempt/@1b5b5b5"');
355
+ expect(html.match(/algebra\/quadratic/g)).toHaveLength(1);
356
+ expect(html).toContain("nre-experiment-eval-header");
357
+ expect(html.match(/nre-experiment-attempt-row/g)).toHaveLength(3);
358
+ expect(html).toContain("├─");
359
+ expect(html).toContain("└─");
388
360
  });
389
361
 
390
362
  it("零 JS 靠原生 <details>,无 <script>", () => {
@@ -422,15 +394,4 @@ describe("跨组件契约", () => {
422
394
  ].join("");
423
395
  expect(all).not.toContain("<script");
424
396
  });
425
-
426
- it("组件源码零 hooks(本实验的「不 hydrate 也完整」用最笨的方式保证)", () => {
427
- const dir = dirname(fileURLToPath(import.meta.url));
428
- const sources = readdirSync(dir)
429
- .filter((f) => (f.endsWith(".tsx") || f.endsWith(".ts")) && !f.includes(".test."))
430
- .map((f) => readFileSync(join(dir, f), "utf8"));
431
- expect(sources.length).toBeGreaterThanOrEqual(12);
432
- for (const src of sources) {
433
- expect(src).not.toMatch(/\buse[A-Z][A-Za-z]*\s*\(/);
434
- }
435
- });
436
397
  });
@@ -42,7 +42,7 @@
42
42
  margin: 1rem 0;
43
43
  }
44
44
 
45
- /* ---- 判定符与证据能力徽标:AttemptList / EvalList / ExperimentList 共用的最小单元 ---- */
45
+ /* ---- locator 与判定符:AttemptList / EvalList / ExperimentList 共用的最小单元 ---- */
46
46
  .nre .nre-locator { display:inline-flex; align-items:center; gap:4px; color:var(--nre-c0); font-variant-numeric:tabular-nums; text-decoration:none; white-space:nowrap; }
47
47
  .nre .nre-locator:hover { text-decoration:underline; }
48
48
  .nre .nre-locator-mark { font-weight:700; }
@@ -50,7 +50,6 @@
50
50
  .nre .nre-locator.nre-verdict-failed .nre-locator-mark,
51
51
  .nre .nre-locator.nre-verdict-errored .nre-locator-mark { color:var(--bad); }
52
52
  .nre .nre-locator.nre-verdict-skipped .nre-locator-mark { color:var(--soft); }
53
- .nre .nre-locator-caps { color:var(--muted); font-size:.85em; }
54
53
 
55
54
  /* ---- AttemptList:实体列表的叶子层,一条卡片一个 Attempt ---- */
56
55
  .nre-attempt-list .nre-attempts { list-style:none; margin:0; padding:0; }
@@ -62,6 +61,7 @@
62
61
  .nre-attempt-list .nre-attempt-experiment,
63
62
  .nre-attempt-list .nre-attempt-duration,
64
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; }
65
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; }
66
66
  .nre-attempt-list .nre-assertions { list-style:none; margin:.4rem 0 0; padding:0; display:grid; gap:6px; }
67
67
  .nre-attempt-list .nre-assertion { padding-left:.25rem; }
@@ -76,6 +76,77 @@
76
76
  .nre-attempt-list .nre-truncated { color:var(--warn); font-size:.9em; }
77
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); }
78
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
+
79
150
  /* ---- EvalList:实体列表的第二级,一条 <details> 一个 experimentId × evalId ---- */
80
151
  .nre-eval-list .nre-evals { list-style:none; margin:0; padding:0; }
81
152
  .nre-eval-list .nre-eval { border-bottom:1px solid var(--line); }
@@ -89,29 +160,56 @@
89
160
  .nre-eval-list .nre-eval-attempts-count,
90
161
  .nre-eval-list .nre-eval-duration,
91
162
  .nre-eval-list .nre-eval-cost { color:var(--muted); font-size:.9em; font-variant-numeric:tabular-nums; }
92
- .nre-eval-list .nre-eval-reason { color:color-mix(in oklch,var(--muted),var(--text) 6%); }
93
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%); }
94
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); }
95
165
 
96
- /* ---- ExperimentList:实体列表的第一级,一条 <details> 一个 experiment ---- */
97
- .nre-experiment-list .nre-experiments { list-style:none; margin:0; padding:0; border:1px solid var(--line); border-radius:9px; overflow:hidden; background:var(--panel); }
166
+ /* ---- ExperimentList:web 面固定八列比较表,一条 <details> 一个 experiment ---- */
167
+ .nre-experiment-table { border:1px solid var(--line); border-radius:9px; overflow:hidden; background:var(--panel); }
168
+ .nre-experiment-head,
169
+ .nre-experiment-summary { display:grid; grid-template-columns:minmax(220px,1.25fr) minmax(120px,.8fr) minmax(90px,.6fr) minmax(100px,.65fr) minmax(90px,.55fr) minmax(110px,.7fr) minmax(90px,.6fr) minmax(160px,.9fr); column-gap:16px; align-items:center; }
170
+ .nre-experiment-head { padding:12px 20px; color:var(--muted); font-size:12px; font-weight:560; text-transform:uppercase; letter-spacing:.05em; border-bottom:1px solid var(--line); }
171
+ .nre-experiment-head button { appearance:none; border:0; padding:0; background:transparent; color:inherit; font:inherit; text-transform:inherit; letter-spacing:inherit; text-align:left; cursor:pointer; }
172
+ .nre-experiment-head button::after { content:"↕"; margin-left:6px; color:var(--soft); font-size:11px; }
173
+ .nre-experiment-head button.nre-sort-asc::after { content:"↑"; color:var(--text); }
174
+ .nre-experiment-head button.nre-sort-desc::after { content:"↓"; color:var(--text); }
175
+ .nre-experiment-table-wrap > .nre-filter { height:34px; min-width:260px; margin-bottom:16px; border:1px solid var(--line); border-radius:7px; padding:0 11px; background:var(--panel); color:var(--text); font:inherit; }
98
176
  .nre-experiment-list .nre-experiment-entry { border-bottom:1px solid var(--line); }
99
177
  .nre-experiment-list .nre-experiment-entry:last-child { border-bottom:0; }
100
- .nre-experiment-list .nre-experiment-summary { display:flex; flex-wrap:wrap; gap:.9em; align-items:baseline; padding:13px 20px; cursor:pointer; list-style:none; }
178
+ .nre-experiment-list .nre-experiment-summary { padding:13px 20px; cursor:pointer; list-style:none; transition:background .12s; }
101
179
  .nre-experiment-list .nre-experiment-summary::-webkit-details-marker { display:none; }
102
180
  .nre-experiment-list .nre-experiment-summary:hover { background:color-mix(in oklch,var(--panel-2),var(--panel) 25%); }
103
- .nre-experiment-list .nre-experiment-id { font-weight:650; overflow-wrap:anywhere; }
104
- .nre-experiment-list .nre-experiment-agent,
105
- .nre-experiment-list .nre-experiment-model { color:var(--muted); font-size:.9em; }
106
- .nre-experiment-list .nre-experiment-cell { font-variant-numeric:tabular-nums; }
107
- .nre-experiment-list .nre-experiment-meta-sub { flex-basis:100%; color:var(--soft); font-size:12px; }
108
- .nre-experiment-list .nre-experiment-evals { list-style:none; margin:0; padding:8px 20px 16px; display:grid; gap:6px; background:color-mix(in oklch,var(--panel-2),var(--panel) 24%); }
109
- .nre-experiment-list .nre-experiment-eval-row { display:flex; flex-wrap:wrap; gap:.6em; align-items:baseline; font-size:13px; }
181
+ .nre-experiment-list .nre-experiment-summary > span { min-width:0; }
182
+ .nre-experiment-list .nre-experiment-name { position:relative; padding-left:30px; }
183
+ .nre-experiment-list .nre-experiment-name::before { content:""; position:absolute; left:5px; top:6px; width:7px; height:7px; border-right:1.5px solid var(--muted); border-bottom:1.5px solid var(--muted); transform:rotate(-45deg); transition:transform .15s; }
184
+ .nre-experiment-list .nre-experiment-entry[open] .nre-experiment-name::before { transform:rotate(45deg); }
185
+ .nre-experiment-list .nre-experiment-id { display:block; overflow-wrap:anywhere; font-weight:650; }
186
+ .nre-experiment-list .nre-experiment-name small { display:block; margin-top:3px; color:var(--muted); font-size:12px; font-weight:400; }
187
+ .nre-experiment-list .nre-num { font-variant-numeric:tabular-nums; font-feature-settings:"tnum"; white-space:nowrap; }
188
+ .nre-experiment-list .nre-good { color:var(--good); }
189
+ .nre-experiment-list .nre-warn { color:var(--warn); }
190
+ .nre-experiment-list .nre-bad { color:var(--bad); }
191
+ .nre-experiment-list .nre-experiment-pill { display:inline-flex; min-height:22px; align-items:center; border:1px solid var(--line); border-radius:999px; padding:0 8px; color:var(--muted); font-size:12px; white-space:nowrap; }
192
+ .nre-experiment-list .nre-experiment-detail { padding:18px 30px 22px; background:color-mix(in oklch,var(--panel-2),var(--panel) 24%); border-top:1px solid var(--line); }
193
+ .nre-experiment-list .nre-experiment-flags { display:flex; flex-wrap:wrap; gap:8px; margin:0 0 14px; color:var(--soft); font-size:12px; }
194
+ .nre-experiment-list .nre-experiment-flags b { border:1px solid color-mix(in oklch,var(--line),transparent 16%); border-radius:999px; padding:3px 8px; color:var(--text); font-weight:560; }
195
+ .nre-experiment-list .nre-experiment-evals { list-style:none; margin:0; padding:0; display:grid; gap:6px; }
196
+ .nre-experiment-list .nre-experiment-eval,
197
+ .nre-experiment-list .nre-experiment-attempts { list-style:none; margin:0; padding:0; }
198
+ .nre-experiment-list .nre-experiment-eval { padding:4px 0 9px; border-bottom:1px solid color-mix(in oklch,var(--line),transparent 28%); }
199
+ .nre-experiment-list .nre-experiment-eval:last-child { border-bottom:0; }
200
+ .nre-experiment-list .nre-experiment-eval-header,
201
+ .nre-experiment-list .nre-experiment-attempt-row { display:grid; grid-template-columns:20px minmax(220px,1fr) minmax(150px,.7fr) minmax(260px,1fr); column-gap:12px; align-items:baseline; font-size:13px; }
202
+ .nre-experiment-list .nre-experiment-eval-header { padding:7px 0 5px; }
203
+ .nre-experiment-list .nre-experiment-attempt-row { padding:5px 0 5px 20px; color:var(--muted); }
110
204
  .nre-experiment-list .nre-eval-id { font-weight:560; overflow-wrap:anywhere; }
205
+ .nre-experiment-list .nre-eval-attempt-count { color:var(--soft); font-size:12px; }
206
+ .nre-experiment-list .nre-eval-rollup { color:var(--muted); font-size:.92em; overflow-wrap:anywhere; }
207
+ .nre-experiment-list .nre-attempt-branch { color:var(--line-strong); font-variant-numeric:tabular-nums; }
111
208
  .nre-experiment-list .nre-eval-attempt-badges { display:inline-flex; flex-wrap:wrap; gap:.6em; }
112
- .nre-experiment-list .nre-eval-avg,
209
+ .nre-experiment-list .nre-eval-attempt-metrics,
113
210
  .nre-experiment-list .nre-eval-reason { color:var(--muted); font-size:.92em; }
114
211
  .nre-experiment-list .nre-experiment-list-empty { border:1px dashed var(--line-strong); border-radius:8px; padding:18px; color:var(--muted); text-align:center; background:var(--panel); }
212
+ @media(max-width:1000px){.nre-experiment-table{overflow-x:auto}.nre-experiment-head,.nre-experiment-summary{min-width:1040px}.nre-experiment-detail{min-width:1040px}}
115
213
 
116
214
  /* ---- verdict 判定色(AttemptList/EvalList/ExperimentList 的 verdict 文字与判定符共用) ---- */
117
215
  .nre .nre-verdict-passed { color:var(--good); }