niceeval 0.5.3 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (344) hide show
  1. package/INDEX.md +55 -0
  2. package/dist/agents/types.d.ts +317 -0
  3. package/dist/context/types.d.ts +335 -0
  4. package/dist/i18n/core.d.ts +11 -0
  5. package/dist/i18n/en.d.ts +181 -0
  6. package/dist/i18n/index.d.ts +5 -0
  7. package/dist/i18n/zh-CN.d.ts +183 -0
  8. package/dist/o11y/execution-tree.d.ts +103 -0
  9. package/dist/o11y/otlp/parse.d.ts +3 -0
  10. package/dist/o11y/otlp/receiver.d.ts +21 -0
  11. package/dist/o11y/otlp/select.d.ts +22 -0
  12. package/dist/o11y/otlp/turn-otel.d.ts +43 -0
  13. package/dist/o11y/types.d.ts +190 -0
  14. package/dist/report/aggregate.d.ts +57 -0
  15. package/dist/report/aggregate.js +212 -0
  16. package/dist/report/built-ins/cost-pass-rate-comparison.d.ts +1 -0
  17. package/dist/report/built-ins/cost-pass-rate-comparison.js +17 -0
  18. package/dist/report/built-ins/index.d.ts +1 -0
  19. package/dist/report/built-ins/index.js +2 -0
  20. package/dist/report/components.d.ts +178 -0
  21. package/dist/report/components.js +132 -0
  22. package/dist/report/compute.d.ts +106 -0
  23. package/dist/report/compute.js +618 -0
  24. package/dist/report/flag.d.ts +6 -0
  25. package/dist/report/flag.js +8 -0
  26. package/dist/report/format.d.ts +32 -0
  27. package/dist/report/format.js +126 -0
  28. package/dist/report/index.d.ts +19 -0
  29. package/dist/report/index.js +30 -0
  30. package/dist/report/load.d.ts +12 -0
  31. package/dist/report/load.js +56 -0
  32. package/dist/report/locale.d.ts +83 -0
  33. package/dist/report/locale.js +160 -0
  34. package/dist/report/metrics.d.ts +21 -0
  35. package/dist/report/metrics.js +115 -0
  36. package/dist/report/primitives.d.ts +64 -0
  37. package/dist/report/primitives.js +113 -0
  38. package/dist/report/react/AttemptList.d.ts +23 -0
  39. package/dist/report/react/AttemptList.js +20 -0
  40. package/dist/report/react/DeltaTable.d.ts +8 -0
  41. package/dist/report/react/DeltaTable.js +24 -0
  42. package/dist/report/react/EvalList.d.ts +10 -0
  43. package/dist/report/react/EvalList.js +0 -0
  44. package/dist/report/react/ExperimentList.d.ts +10 -0
  45. package/dist/report/react/ExperimentList.js +15 -0
  46. package/dist/report/react/GroupSummary.d.ts +8 -0
  47. package/dist/report/react/GroupSummary.js +8 -0
  48. package/dist/report/react/MetricBars.d.ts +10 -0
  49. package/dist/report/react/MetricBars.js +48 -0
  50. package/dist/report/react/MetricLine.d.ts +9 -0
  51. package/dist/report/react/MetricLine.js +68 -0
  52. package/dist/report/react/MetricMatrix.d.ts +10 -0
  53. package/dist/report/react/MetricMatrix.js +23 -0
  54. package/dist/report/react/MetricScatter.d.ts +9 -0
  55. package/dist/report/react/MetricScatter.js +100 -0
  56. package/dist/report/react/MetricTable.d.ts +11 -0
  57. package/dist/report/react/MetricTable.js +26 -0
  58. package/dist/report/react/RunOverview.d.ts +8 -0
  59. package/dist/report/react/RunOverview.js +12 -0
  60. package/dist/report/react/Scoreboard.d.ts +8 -0
  61. package/dist/report/react/Scoreboard.js +26 -0
  62. package/dist/report/react/cell.d.ts +9 -0
  63. package/dist/report/react/cell.js +9 -0
  64. package/dist/report/react/chart-math.d.ts +14 -0
  65. package/dist/report/react/chart-math.js +68 -0
  66. package/dist/report/react/colors.d.ts +14 -0
  67. package/dist/report/react/colors.js +45 -0
  68. package/dist/report/react/fixtures.d.ts +19 -0
  69. package/dist/report/react/fixtures.js +393 -0
  70. package/dist/report/react/format.d.ts +3 -0
  71. package/dist/report/react/format.js +7 -0
  72. package/dist/report/react/index.d.ts +6 -0
  73. package/dist/report/react/index.js +14 -0
  74. package/dist/report/report.d.ts +24 -0
  75. package/dist/report/report.js +49 -0
  76. package/dist/report/text/faces.d.ts +29 -0
  77. package/dist/report/text/faces.js +503 -0
  78. package/dist/report/text/layout.d.ts +75 -0
  79. package/dist/report/text/layout.js +173 -0
  80. package/dist/report/text/plot.d.ts +37 -0
  81. package/dist/report/text/plot.js +81 -0
  82. package/dist/report/text/table.d.ts +8 -0
  83. package/dist/report/text/table.js +86 -0
  84. package/dist/report/tree.d.ts +99 -0
  85. package/dist/report/tree.js +228 -0
  86. package/dist/report/types.d.ts +420 -0
  87. package/dist/report/types.js +4 -0
  88. package/dist/report/web.d.ts +15 -0
  89. package/dist/report/web.js +37 -0
  90. package/dist/results/annotated-source.d.ts +61 -0
  91. package/dist/results/attempt-evidence.d.ts +69 -0
  92. package/dist/results/attempt-source.d.ts +15 -0
  93. package/dist/results/locator.d.ts +75 -0
  94. package/dist/results/locator.js +0 -0
  95. package/dist/results/select.d.ts +45 -0
  96. package/dist/results/select.js +230 -0
  97. package/dist/results/source-hash.d.ts +9 -0
  98. package/dist/results/types.d.ts +204 -0
  99. package/dist/runner/eval-source.d.ts +22 -0
  100. package/dist/runner/fingerprint.d.ts +23 -0
  101. package/dist/runner/types.d.ts +332 -0
  102. package/dist/sandbox/docker-stream.d.ts +24 -0
  103. package/dist/sandbox/docker.d.ts +84 -0
  104. package/dist/sandbox/e2b.d.ts +31 -0
  105. package/dist/sandbox/errors.d.ts +4 -0
  106. package/dist/sandbox/local-files.d.ts +6 -0
  107. package/dist/sandbox/paths.d.ts +4 -0
  108. package/dist/sandbox/registry.d.ts +13 -0
  109. package/dist/sandbox/resolve.d.ts +40 -0
  110. package/dist/sandbox/retry.d.ts +10 -0
  111. package/dist/sandbox/shell.d.ts +11 -0
  112. package/dist/sandbox/source-files.d.ts +13 -0
  113. package/dist/sandbox/types.d.ts +203 -0
  114. package/dist/sandbox/vercel.d.ts +37 -0
  115. package/dist/scoring/types.d.ts +70 -0
  116. package/dist/shared/aggregate.d.ts +29 -0
  117. package/dist/shared/aggregate.js +55 -0
  118. package/dist/shared/types.d.ts +27 -0
  119. package/dist/shared/verdict.d.ts +27 -0
  120. package/dist/shared/verdict.js +34 -0
  121. package/dist/tty-line.d.ts +6 -0
  122. package/dist/types.d.ts +7 -0
  123. package/dist/util.d.ts +22 -0
  124. package/docs-site/zh/concepts/adapter.mdx +9 -4
  125. package/docs-site/zh/concepts/assert.mdx +8 -8
  126. package/docs-site/zh/concepts/evals.mdx +5 -5
  127. package/docs-site/zh/concepts/experiment.mdx +3 -1
  128. package/docs-site/zh/concepts/judge.mdx +9 -9
  129. package/docs-site/zh/concepts/overview.mdx +2 -2
  130. package/docs-site/zh/concepts/tier.mdx +6 -6
  131. package/docs-site/zh/example/claude-code-codex-plugin.mdx +9 -5
  132. package/docs-site/zh/example/claude-code-codex-skill.mdx +12 -7
  133. package/docs-site/zh/guides/agent-feedback-loop.mdx +235 -78
  134. package/docs-site/zh/guides/connect-otel.mdx +3 -3
  135. package/docs-site/zh/guides/connect-your-agent.mdx +2 -2
  136. package/docs-site/zh/guides/custom-reports.mdx +196 -36
  137. package/docs-site/zh/guides/experiments.mdx +7 -0
  138. package/docs-site/zh/guides/fixtures.mdx +1 -1
  139. package/docs-site/zh/guides/official-adapters.mdx +31 -9
  140. package/docs-site/zh/guides/publish-report.mdx +4 -4
  141. package/docs-site/zh/guides/report-components.mdx +194 -55
  142. package/docs-site/zh/guides/reporters.mdx +2 -2
  143. package/docs-site/zh/guides/results-data.mdx +35 -34
  144. package/docs-site/zh/guides/runner.mdx +2 -2
  145. package/docs-site/zh/guides/sandbox-agent.mdx +10 -5
  146. package/docs-site/zh/guides/sandbox-providers.mdx +102 -0
  147. package/docs-site/zh/guides/viewing-results.mdx +199 -79
  148. package/docs-site/zh/guides/write-experiment.mdx +23 -1
  149. package/docs-site/zh/guides/write-send.mdx +2 -2
  150. package/docs-site/zh/index.mdx +4 -3
  151. package/docs-site/zh/introduction.mdx +2 -1
  152. package/docs-site/zh/quickstart.mdx +5 -3
  153. package/docs-site/zh/reference/builtin-agents.mdx +38 -13
  154. package/docs-site/zh/reference/capabilities.mdx +1 -1
  155. package/docs-site/zh/reference/cli.mdx +36 -13
  156. package/docs-site/zh/reference/define-agent.mdx +24 -12
  157. package/docs-site/zh/reference/define-config.mdx +1 -1
  158. package/docs-site/zh/reference/define-eval.mdx +3 -3
  159. package/docs-site/zh/reference/events.mdx +7 -1
  160. package/docs-site/zh/reference/expect.mdx +3 -3
  161. package/package.json +10 -6
  162. package/src/agents/bub.ts +134 -43
  163. package/src/agents/claude-code.test.ts +152 -0
  164. package/src/agents/claude-code.ts +127 -13
  165. package/src/agents/codex.test.ts +192 -0
  166. package/src/agents/codex.ts +151 -12
  167. package/src/agents/index.ts +11 -3
  168. package/src/agents/manifest.ts +41 -0
  169. package/src/agents/plugin-config.test.ts +94 -0
  170. package/src/agents/shared.ts +33 -2
  171. package/src/agents/skills.test.ts +214 -0
  172. package/src/agents/skills.ts +304 -0
  173. package/src/agents/streaming.ts +5 -5
  174. package/src/agents/types.ts +62 -2
  175. package/src/cli.ts +74 -47
  176. package/src/context/context.test.ts +20 -2
  177. package/src/context/context.ts +3 -0
  178. package/src/context/control-flow.ts +1 -1
  179. package/src/context/session.test.ts +92 -1
  180. package/src/context/session.ts +26 -1
  181. package/src/context/types.ts +4 -4
  182. package/src/define.test.ts +82 -0
  183. package/src/define.ts +72 -11
  184. package/src/expect/index.ts +5 -5
  185. package/src/i18n/en.ts +36 -15
  186. package/src/i18n/zh-CN.ts +39 -21
  187. package/src/index.ts +1 -1
  188. package/src/o11y/execution-tree.test.ts +452 -0
  189. package/src/o11y/execution-tree.ts +367 -0
  190. package/src/o11y/otlp/receiver.ts +1 -1
  191. package/src/o11y/otlp/select.ts +7 -3
  192. package/src/o11y/parsers/claude-code.test.ts +175 -0
  193. package/src/o11y/parsers/claude-code.ts +32 -0
  194. package/src/o11y/types.ts +6 -1
  195. package/src/report/aggregate.ts +26 -20
  196. package/src/report/built-in-user-parity.test.tsx +643 -0
  197. package/src/report/built-ins/cost-pass-rate-comparison.tsx +23 -0
  198. package/src/report/built-ins/index.ts +2 -0
  199. package/src/report/components.tsx +223 -50
  200. package/src/report/compute.ts +358 -53
  201. package/src/report/dual-render.test.tsx +1125 -0
  202. package/src/report/flag.ts +1 -1
  203. package/src/report/format.ts +50 -2
  204. package/src/report/index.ts +48 -14
  205. package/src/report/load.ts +1 -1
  206. package/src/report/locale.ts +206 -0
  207. package/src/report/metrics.ts +39 -15
  208. package/src/report/primitives.tsx +100 -2
  209. package/src/report/react/AttemptList.tsx +115 -0
  210. package/src/report/react/DeltaTable.tsx +9 -6
  211. package/src/report/react/EvalList.tsx +0 -0
  212. package/src/report/react/ExperimentList.tsx +108 -0
  213. package/src/report/react/GroupSummary.tsx +66 -0
  214. package/src/report/react/MetricBars.tsx +24 -13
  215. package/src/report/react/MetricLine.tsx +21 -21
  216. package/src/report/react/MetricMatrix.tsx +8 -4
  217. package/src/report/react/MetricScatter.tsx +136 -96
  218. package/src/report/react/MetricTable.tsx +115 -15
  219. package/src/report/react/RunOverview.tsx +48 -26
  220. package/src/report/react/Scoreboard.tsx +15 -7
  221. package/src/report/react/cell.tsx +18 -9
  222. package/src/{view/app/lib/chart.ts → report/react/chart-math.ts} +2 -6
  223. package/src/report/react/colors.ts +24 -15
  224. package/src/report/react/enhance.js +129 -0
  225. package/src/report/react/fixtures.ts +163 -34
  226. package/src/report/react/format.ts +1 -1
  227. package/src/report/react/index.tsx +22 -6
  228. package/src/report/react/render.test.tsx +164 -31
  229. package/src/report/react/styles.css +405 -213
  230. package/src/report/report.test.ts +403 -94
  231. package/src/report/report.ts +29 -11
  232. package/src/report/text/faces.ts +303 -95
  233. package/src/report/text/layout.ts +76 -11
  234. package/src/report/text/table.ts +98 -0
  235. package/src/report/tree.ts +103 -20
  236. package/src/report/types.ts +196 -25
  237. package/src/report/web.ts +46 -19
  238. package/src/results/annotated-source.test.ts +146 -0
  239. package/src/results/annotated-source.ts +128 -0
  240. package/src/results/attempt-evidence.test.ts +158 -0
  241. package/src/results/attempt-evidence.ts +158 -0
  242. package/src/results/attempt-source.ts +48 -0
  243. package/src/results/copy.ts +154 -139
  244. package/src/results/format.ts +54 -47
  245. package/src/results/host-equivalence.test.ts +527 -0
  246. package/src/results/index.ts +56 -14
  247. package/src/results/locator.test.ts +198 -0
  248. package/src/results/locator.ts +0 -0
  249. package/src/results/open.ts +392 -175
  250. package/src/results/results.test.ts +943 -494
  251. package/src/results/select.ts +129 -23
  252. package/src/results/skipped-notice.ts +0 -0
  253. package/src/results/source-hash.ts +28 -0
  254. package/src/results/types.ts +98 -62
  255. package/src/results/writer.ts +304 -176
  256. package/src/runner/attempt.test.ts +170 -0
  257. package/src/runner/attempt.ts +97 -15
  258. package/src/runner/discover.test.ts +65 -0
  259. package/src/runner/discover.ts +6 -2
  260. package/src/runner/eval-source.test.ts +121 -0
  261. package/src/runner/eval-source.ts +45 -0
  262. package/src/runner/fingerprint.ts +54 -1
  263. package/src/runner/remote-sandbox.ts +1 -1
  264. package/src/runner/report.test.ts +6 -8
  265. package/src/runner/report.ts +5 -4
  266. package/src/runner/reporters/artifacts.ts +27 -41
  267. package/src/runner/reporters/braintrust.test.ts +3 -3
  268. package/src/runner/reporters/braintrust.ts +1 -1
  269. package/src/runner/reporters/console.ts +6 -6
  270. package/src/runner/reporters/json.ts +4 -4
  271. package/src/runner/reporters/live.test.ts +56 -0
  272. package/src/runner/reporters/live.ts +36 -10
  273. package/src/runner/reporters/quiet.test.ts +66 -0
  274. package/src/runner/reporters/quiet.ts +49 -0
  275. package/src/runner/reporters/shared.ts +5 -5
  276. package/src/runner/reporters/table.ts +17 -17
  277. package/src/runner/run.ts +61 -102
  278. package/src/runner/types.ts +43 -32
  279. package/src/sandbox/checkpoint.ts +1 -1
  280. package/src/sandbox/docker.ts +15 -2
  281. package/src/sandbox/e2b.ts +11 -5
  282. package/src/sandbox/errors.ts +13 -0
  283. package/src/sandbox/index.ts +3 -3
  284. package/src/sandbox/local-files.ts +1 -1
  285. package/src/sandbox/registry.ts +6 -5
  286. package/src/sandbox/resolve.ts +38 -23
  287. package/src/sandbox/retry.test.ts +68 -0
  288. package/src/sandbox/retry.ts +44 -0
  289. package/src/sandbox/types.ts +69 -28
  290. package/src/sandbox/vercel.ts +17 -3
  291. package/src/scoring/collector.ts +3 -3
  292. package/src/scoring/judge.ts +2 -2
  293. package/src/scoring/scoped.ts +13 -1
  294. package/src/scoring/types.ts +7 -7
  295. package/src/scoring/verdict.ts +4 -4
  296. package/src/shared/aggregate.ts +23 -3
  297. package/src/shared/types.ts +2 -2
  298. package/src/shared/{outcome.ts → verdict.ts} +15 -15
  299. package/src/show/compose.ts +12 -132
  300. package/src/show/index.ts +125 -94
  301. package/src/show/render.ts +660 -160
  302. package/src/show/show.test.ts +472 -253
  303. package/src/source-loc.ts +1 -1
  304. package/src/tty-line.ts +32 -0
  305. package/src/types.ts +2 -2
  306. package/src/util.test.ts +21 -1
  307. package/src/util.ts +5 -1
  308. package/src/view/app/App.tsx +33 -198
  309. package/src/view/app/components/AttemptModal.tsx +6 -6
  310. package/src/view/app/components/CodeView.tsx +2 -2
  311. package/src/view/app/components/CopyControls.tsx +32 -36
  312. package/src/view/app/components/SkippedRunsBanner.tsx +140 -0
  313. package/src/view/app/components/ui/badge.tsx +1 -1
  314. package/src/view/app/components/ui/dialog.tsx +2 -2
  315. package/src/view/app/i18n.ts +40 -31
  316. package/src/view/app/lib/artifact-url.ts +1 -1
  317. package/src/view/app/lib/attempt-route.test.ts +37 -47
  318. package/src/view/app/lib/attempt-route.ts +26 -26
  319. package/src/view/app/lib/rows.ts +9 -112
  320. package/src/view/app/lib/verdict.ts +25 -0
  321. package/src/view/app/main.tsx +9 -12
  322. package/src/view/app/pages/RunsPage.tsx +4 -4
  323. package/src/view/app/pages/TracesPage.tsx +3 -3
  324. package/src/view/app/shared.ts +0 -1
  325. package/src/view/app/types.ts +6 -51
  326. package/src/view/artifact-serving.test.ts +136 -0
  327. package/src/view/client-dist/app.css +1 -1
  328. package/src/view/client-dist/app.js +21 -21
  329. package/src/view/data.test.ts +214 -143
  330. package/src/view/data.ts +166 -120
  331. package/src/view/index.ts +23 -11
  332. package/src/view/server.ts +52 -17
  333. package/src/view/shared/types.ts +22 -31
  334. package/src/view/styles.css +117 -327
  335. package/src/view/view-report.test.ts +146 -98
  336. package/docs-site/zh/guides/sandbox-backends.mdx +0 -75
  337. package/src/report/default-report.tsx +0 -222
  338. package/src/report/dual-face.test.tsx +0 -527
  339. package/src/report/react/CaseList.tsx +0 -70
  340. package/src/view/app/components/CostScoreChart.tsx +0 -147
  341. package/src/view/app/components/ExperimentTable.tsx +0 -284
  342. package/src/view/app/components/GroupSelector.tsx +0 -61
  343. package/src/view/app/components/primitives.tsx +0 -43
  344. package/src/view/app/lib/outcome.ts +0 -82
@@ -1,19 +1,25 @@
1
- // 计算函数:选集 → 一份组件数据。跑在 Node 侧,产物是算好的、可序列化的普通 JSON
1
+ // 计算函数:Selection → 一份组件数据。跑在 Node 侧,产物是算好的、可序列化的普通 JSON
2
2
  // (终值 + 渲染提示,不含公式);渲染面(web/text)只做展示。
3
3
  //
4
4
  // 这些函数不做顶层导出,而是挂在对应组件上(MetricTable.data / Scoreboard.data …,
5
5
  // 见 components.tsx):配对打点即发现,泛化名不占顶层导出。
6
6
  //
7
- // 共同约定(docs/reports.md「边界与不变量」):
7
+ // 共同约定(docs/feature/reports/architecture.md「指标聚合不变量」):
8
8
  // - 第一参收 Selection | Snapshot[];收 Selection 时 warnings 随行进 OverviewData;
9
9
  // - 聚合前按身份键去重(dedupeAttempts;missing-startedAt 不去重、如实保留、不透出警告);
10
10
  // - null ≠ 0:缺数据不编数,覆盖率经 samples/total 如实暴露;
11
11
  // - core 中立:只认 Metric / Dimension 接口,不出现具体 agent 名的分支。
12
12
 
13
13
  import type {
14
- CaseListData,
14
+ AttemptEvidenceCapabilities,
15
+ AttemptListItem,
16
+ AttemptLocator,
15
17
  DeltaData,
16
18
  DimensionInput,
19
+ EvalListItem,
20
+ ExperimentListEvalRow,
21
+ ExperimentListItem,
22
+ GroupSummaryData,
17
23
  LineData,
18
24
  MatrixData,
19
25
  Metric,
@@ -23,7 +29,11 @@ import type {
23
29
  ScatterData,
24
30
  ScoreboardData,
25
31
  TableData,
32
+ TableRowMeta,
26
33
  } from "./types.ts";
34
+ import type { AssertionResult, EvalResult } from "../types.ts";
35
+ import type { AttemptHandle } from "../results/types.ts";
36
+ import { evalLevelStats, foldEvalVerdict } from "../shared/verdict.ts";
27
37
  import {
28
38
  applyAggregator,
29
39
  assertUniqueMetricNames,
@@ -40,13 +50,14 @@ import {
40
50
  filterItems,
41
51
  groupItems,
42
52
  flagAxisValue,
53
+ locatorOf,
43
54
  resolveInput,
44
55
  snapshotKeyOf,
45
56
  toColumn,
46
57
  type Item,
47
58
  type SnapshotsInput,
48
59
  } from "./aggregate.ts";
49
- import { attemptCostUSD, examScore } from "./metrics.ts";
60
+ import { attemptCostUSD, costUSD, durationMs, examScore, passRate, tokens } from "./metrics.ts";
50
61
  import { formatMetricValue, formatPlainNumber } from "./format.ts";
51
62
 
52
63
  // ───────────────────────── MetricTable.data ─────────────────────────
@@ -62,6 +73,132 @@ export interface TableDataOptions<M extends readonly Metric[]> {
62
73
  evals?: string | string[];
63
74
  }
64
75
 
76
+ // 一组 Item 的 eval 全身份键:experimentId + eval id。单 experiment 场景(如 experimentRowMeta,
77
+ // 一组本就只有一个 experimentId)下退化为只按 eval id 折叠,与旧行为一致;多 experiment 场景
78
+ // (GroupSummary 的组可能跨多个 experiment)下避免两个 experiment 里同名 eval 被误合并成一道题。
79
+ // 分隔符是 NUL(同 aggregate.ts 的 KEY_SEP 手法):不会出现在 eval id / experimentId 里,拼接键不串味。
80
+ const GROUP_KEY_SEP = "\u0000";
81
+ function fullEvalKey(item: Item): string {
82
+ return `${experimentIdOf(item)}${GROUP_KEY_SEP}${evalIdOf(item)}`;
83
+ }
84
+
85
+ /**
86
+ * 一批 Item 的组级统计:eval 级折叠计票(evalLevelStats,与 view 榜单 / `TableRowMeta.verdicts`
87
+ * 同一套 foldEvalVerdict 口径,按完整身份键折叠)、experiment/eval/attempt 数量、总成本
88
+ * (null-safe 求和)、最后运行时间(组内快照 startedAt 的最大值)。`experimentRowMeta` 与
89
+ * `groupSummaryData` 共用这一份实现,不各自拼装 evalLevelStats。
90
+ *
91
+ * 内部纯函数,不导出、不进 index.ts:对外只经 `experimentRowMeta`(挑 verdicts)与
92
+ * `groupSummaryData`(挑全部字段,包成 `GroupSummaryData`)暴露,调用方拿不到 `Item[]`
93
+ * 本身,所以这里也不用担心被越权复用。
94
+ */
95
+ function summarizeItems(items: Item[]): {
96
+ experiments: number;
97
+ evals: number;
98
+ attempts: number;
99
+ verdicts: { passed: number; failed: number; errored: number; skipped: number };
100
+ /** 折叠后代表每个「已跑」(非 skipped)eval 的一条 attempt 引用,与 ran 同序同数。 */
101
+ refs: AttemptLocator[];
102
+ /** 计入通过率分母的 eval 数(passed + failed + errored,不含 skipped)。 */
103
+ ran: number;
104
+ totalCostUSD: number | null;
105
+ lastRunAt: string | undefined;
106
+ } {
107
+ const experimentIds = new Set<string>();
108
+ for (const item of items) experimentIds.add(experimentIdOf(item));
109
+
110
+ const byEval = new Map<string, Item[]>();
111
+ for (const item of items) {
112
+ const key = fullEvalKey(item);
113
+ const list = byEval.get(key);
114
+ if (list) list.push(item);
115
+ else byEval.set(key, [item]);
116
+ }
117
+ const stats = evalLevelStats(
118
+ items.map((item) => ({ verdict: item.attempt.result.verdict, key: fullEvalKey(item) })),
119
+ (r) => r.key,
120
+ );
121
+ // 折叠代表 attempt:每个已跑的 eval 挑一条与折叠判定一致的 attempt 做证据引用,
122
+ // skipped 的 eval 不进分母、不出证据。
123
+ const refs: AttemptLocator[] = [];
124
+ for (const group of byEval.values()) {
125
+ const verdict = foldEvalVerdict(group.map((item) => item.attempt.result));
126
+ if (verdict === "skipped") continue;
127
+ const rep = group.find((item) => item.attempt.result.verdict === verdict) ?? group[0]!;
128
+ refs.push(locatorOf(rep));
129
+ }
130
+
131
+ let totalCostUSD: number | null = null;
132
+ for (const item of items) {
133
+ const cost = attemptCostUSD(item.attempt.result);
134
+ if (cost !== null) totalCostUSD = (totalCostUSD ?? 0) + cost;
135
+ }
136
+
137
+ let lastRunAt: string | undefined;
138
+ for (const item of items) {
139
+ const startedAt = item.snapshot.startedAt;
140
+ if (lastRunAt === undefined || startedAt > lastRunAt) lastRunAt = startedAt;
141
+ }
142
+
143
+ return {
144
+ experiments: experimentIds.size,
145
+ evals: stats.evals,
146
+ attempts: items.length,
147
+ verdicts: { passed: stats.passed, failed: stats.failed, errored: stats.errored, skipped: stats.skipped },
148
+ refs,
149
+ ran: stats.passed + stats.failed + stats.errored,
150
+ totalCostUSD,
151
+ lastRunAt,
152
+ };
153
+ }
154
+
155
+ /**
156
+ * experiment 行的元信息:agent/model 身份(组内去重后拼接)+ eval 级折叠计票 + eval/attempt
157
+ * 数量与最后运行时间(summarizeItems,即 view 榜单 / ExperimentList 的同一套 foldEvalVerdict
158
+ * 口径)。其它行维度(agent/eval/自定义…)没有唯一身份,不携带。
159
+ */
160
+ function experimentRowMeta(group: Item[]): TableRowMeta {
161
+ const agents = new Set<string>();
162
+ const models = new Set<string>();
163
+ for (const item of group) {
164
+ agents.add(item.attempt.result.agent);
165
+ const model = item.attempt.result.model ?? item.snapshot.model;
166
+ if (model !== undefined) models.add(model);
167
+ }
168
+ const stats = summarizeItems(group);
169
+ return {
170
+ ...(agents.size > 0 ? { agent: [...agents].join(", ") } : {}),
171
+ ...(models.size > 0 ? { model: [...models].join(", ") } : {}),
172
+ verdicts: stats.verdicts,
173
+ evals: stats.evals,
174
+ attempts: stats.attempts,
175
+ ...(stats.lastRunAt !== undefined ? { lastRunAt: stats.lastRunAt } : {}),
176
+ };
177
+ }
178
+
179
+ /**
180
+ * 一次 attempt 未通过的 gate 断言,原始声明顺序不变;soft 断言不参与判定,不算「失败原因」,
181
+ * 只影响得分,永不出现在这份列表里。`EvalList` / `ExperimentList` 的失败诊断与 `AttemptList`
182
+ * 的断言列表共用这同一份材料,保证同一个 attempt 在各处给出同一个原因。
183
+ */
184
+ export function failingGateAssertions(result: EvalResult): AssertionResult[] {
185
+ return result.assertions.filter((a) => !a.passed && a.severity === "gate");
186
+ }
187
+
188
+ /**
189
+ * 一次 attempt 的失败原因文案,按优先级取第一个在场的:
190
+ * `error` → `skipReason` → 未通过的 gate 断言(原始声明顺序,`name`,detail 在场则
191
+ * `"name: detail"`,多条用「, 」连接)→ 都缺席则无原因(如某道题恰好没有失败信号)。
192
+ * soft 断言永不进入这份原因文案,soft 得分是独立概念,不与 reason 混用同一个字段。
193
+ */
194
+ export function reasonFor(result: EvalResult): string | undefined {
195
+ if (result.error !== undefined) return result.error;
196
+ if (result.skipReason !== undefined) return result.skipReason;
197
+ const gates = failingGateAssertions(result);
198
+ if (gates.length === 0) return undefined;
199
+ return gates.map((a) => (a.detail ? `${a.name}: ${a.detail}` : a.name)).join(", ");
200
+ }
201
+
65
202
  export async function tableData<const M extends readonly Metric[]>(
66
203
  input: SnapshotsInput,
67
204
  opts: TableDataOptions<M>,
@@ -70,7 +207,7 @@ export async function tableData<const M extends readonly Metric[]>(
70
207
  const { snapshots } = resolveInput(input);
71
208
  const items = filterItems(collectItems(snapshots), opts.evals);
72
209
  const groups = groupItems(items, opts.rows);
73
- const rows: { key: string; cells: Record<string, MetricCell> }[] = [];
210
+ const rows: TableData["rows"] = [];
74
211
  const sortCells = new Map<string, MetricCell>();
75
212
  for (const [key, group] of groups) {
76
213
  const cells: Record<string, MetricCell> = {};
@@ -79,7 +216,12 @@ export async function tableData<const M extends readonly Metric[]>(
79
216
  // sort 指标不在 columns 里时单独算一遍,只用于排序、不进输出
80
217
  sortCells.set(key, cells[opts.sort.name] ?? (await computeCell(opts.sort, group)));
81
218
  }
82
- rows.push({ key, cells });
219
+ const meta: TableRowMeta = opts.rows === "experiment" ? experimentRowMeta(group) : {};
220
+ rows.push({
221
+ key,
222
+ cells,
223
+ ...(Object.keys(meta).length > 0 ? { meta } : {}),
224
+ });
83
225
  }
84
226
  if (opts.sort) {
85
227
  const better = opts.sort.better ?? "higher";
@@ -99,6 +241,169 @@ export async function tableData<const M extends readonly Metric[]>(
99
241
  } as TableData<M[number]["name"]>;
100
242
  }
101
243
 
244
+ // ───────────────────────── ExperimentList.data / EvalList.data / AttemptList.data ─────────────────────────
245
+ //
246
+ // 三个实体列表逐级下钻(experiment → experimentId × eval → attempt),固定展示实体事实,
247
+ // 没有列配置;过滤是报告作者对返回数组调用 .filter()/.slice() 的事,不进这里
248
+ // (docs/feature/reports/library.md「实体列表」)。AttemptListItem 是三者共用的叶子形状——
249
+ // ExperimentList / EvalList 的下钻数组直接复用它,不各自精简一份。
250
+
251
+ /**
252
+ * 一个 attempt 的证据能力标记,只读已有的瘦身摘要位(`hasSources` / `hasEvents` / `hasTrace`)
253
+ * 加一次 `diff()` 懒加载——不调用完整的 `loadAttemptEvidence`(那还会额外装配 Eval 源码标注
254
+ * 与 ExecutionTree,这里只要四个布尔位)。三位定义与 `AttemptEvidence.capabilities` 完全一致:
255
+ *
256
+ * - `eval`:`hasSources` 为真。真正的门槛是 `AnnotatedEvalSource` 非空且有内容(源码行或断言
257
+ * 非空),但 sources 存在时 `buildAnnotatedEvalSource` 恒产出至少一行源码,断言又永远来自
258
+ * `result.assertions`(不需要 IO)——`hasSources` 是这个条件的精确瘦身版,不是近似。
259
+ * - `execution`:`hasEvents` 为真,与 `loadAttemptEvidence` 的 `events !== null && events.length > 0`
260
+ * 同一个判定,写入面按同一条规则算的 `hasEvents` 就是它的瘦身镜像。
261
+ * - `timing`:`hasEvents && hasTrace` 为真——`execution` 成立且这次运行接入过 OTel
262
+ * (`ExecutionTree.timingAvailable` = `spans.length > 0`,`hasTrace` 正是这个判定的瘦身镜像)。
263
+ * - `diff`:唯一需要 IO 的一位,`attempt.diff()` 非空且至少改动/删除了一个文件——落盘没有
264
+ * 与 `hasEvents` 对应的瘦身摘要位,只能懒加载判定;成本是每个 item 一次 `diff()` 读取,
265
+ * 不是完整证据装配的四路 Promise.all。
266
+ */
267
+ async function attemptCapabilities(attempt: AttemptHandle): Promise<AttemptEvidenceCapabilities> {
268
+ const result = attempt.result;
269
+ const diff = await attempt.diff();
270
+ const diffCapable = diff !== null && (Object.keys(diff.generatedFiles).length > 0 || diff.deletedFiles.length > 0);
271
+ return {
272
+ eval: result.hasSources === true,
273
+ execution: result.hasEvents === true,
274
+ timing: result.hasEvents === true && result.hasTrace === true,
275
+ diff: diffCapable,
276
+ };
277
+ }
278
+
279
+ /** 自由文本(error / 断言 detail / evidence)的发布消毒钩子;身份字段(name/severity/loc)不经它。 */
280
+ function redactAssertions(assertions: AssertionResult[], redact: (text: string) => string): AssertionResult[] {
281
+ if (assertions.length === 0) return assertions;
282
+ return assertions.map((a) => ({
283
+ ...a,
284
+ ...(a.detail !== undefined ? { detail: redact(a.detail) } : {}),
285
+ ...(a.evidence !== undefined ? { evidence: redact(a.evidence) } : {}),
286
+ }));
287
+ }
288
+
289
+ /** AttemptList / ExperimentList / EvalList 共用的叶子构造:一个 Item → 一个 AttemptListItem。 */
290
+ async function attemptListItemOf(item: Item, redact: (text: string) => string): Promise<AttemptListItem> {
291
+ const result = item.attempt.result;
292
+ const cost = attemptCostUSD(result);
293
+ return {
294
+ evalId: evalIdOf(item),
295
+ experimentId: experimentIdOf(item),
296
+ attempt: result.attempt,
297
+ agent: result.agent,
298
+ verdict: result.verdict,
299
+ ...(result.error !== undefined ? { error: redact(result.error) } : {}),
300
+ assertions: redactAssertions(result.assertions, redact),
301
+ durationMs: result.durationMs,
302
+ ...(cost !== null ? { costUSD: cost } : {}),
303
+ locator: locatorOf(item),
304
+ capabilities: await attemptCapabilities(item.attempt),
305
+ };
306
+ }
307
+
308
+ const identityRedact = (text: string): string => text;
309
+
310
+ export interface AttemptListDataOptions {
311
+ /** 发布消毒:error / 断言 detail / evidence 经这个钩子;身份字段(experimentId/evalId/locator…)不经它。 */
312
+ redact?: (text: string) => string;
313
+ }
314
+
315
+ /** `AttemptList.data(selection)`:每个 Attempt 一项,顺序取自 Selection 展平顺序(不重排)。 */
316
+ export async function attemptListData(
317
+ input: SnapshotsInput,
318
+ opts?: AttemptListDataOptions,
319
+ ): Promise<AttemptListItem[]> {
320
+ const { snapshots } = resolveInput(input);
321
+ const redact = opts?.redact ?? identityRedact;
322
+ const items = collectItems(snapshots);
323
+ return Promise.all(items.map((item) => attemptListItemOf(item, redact)));
324
+ }
325
+
326
+ /** `EvalList.data(selection)`:每个 `experimentId + evalId` 一项,按 evalId 再按 experimentId 升序。 */
327
+ export async function evalListData(input: SnapshotsInput): Promise<EvalListItem[]> {
328
+ const { snapshots } = resolveInput(input);
329
+ const items = collectItems(snapshots);
330
+ const groups = new Map<string, Item[]>();
331
+ for (const item of items) {
332
+ const key = fullEvalKey(item);
333
+ const list = groups.get(key);
334
+ if (list) list.push(item);
335
+ else groups.set(key, [item]);
336
+ }
337
+ const out: EvalListItem[] = [];
338
+ for (const group of groups.values()) {
339
+ const sorted = [...group].sort((a, b) => a.attempt.result.attempt - b.attempt.result.attempt);
340
+ const verdict = foldEvalVerdict(sorted.map((item) => item.attempt.result));
341
+ const representative = sorted.find((item) => item.attempt.result.verdict === verdict) ?? sorted[0]!;
342
+ const attempts = await Promise.all(sorted.map((item) => attemptListItemOf(item, identityRedact)));
343
+ out.push({
344
+ evalId: evalIdOf(sorted[0]!),
345
+ experimentId: experimentIdOf(sorted[0]!),
346
+ verdict,
347
+ reason: reasonFor(representative.attempt.result),
348
+ score: await computeCell(examScore, sorted),
349
+ duration: await computeCell(durationMs, sorted),
350
+ cost: await computeCell(costUSD, sorted),
351
+ attempts,
352
+ });
353
+ }
354
+ out.sort((a, b) => a.evalId.localeCompare(b.evalId) || a.experimentId.localeCompare(b.experimentId));
355
+ return out;
356
+ }
357
+
358
+ /** `ExperimentList.data(selection)`:每个 experiment 一项,按 experimentId 升序;展开到每道 Eval。 */
359
+ export async function experimentListData(input: SnapshotsInput): Promise<ExperimentListItem[]> {
360
+ const { snapshots } = resolveInput(input);
361
+ const items = collectItems(snapshots);
362
+ const groups = groupItems(items, "experiment");
363
+ const out: ExperimentListItem[] = [];
364
+ for (const [experimentId, group] of groups) {
365
+ const stats = summarizeItems(group);
366
+ const newest = [...group].sort((a, b) => b.snapshot.startedAt.localeCompare(a.snapshot.startedAt))[0]!;
367
+ const evalGroups = groupItems(group, "eval");
368
+ const evalRows: ExperimentListEvalRow[] = [];
369
+ for (const [evalId, evalItems] of evalGroups) {
370
+ const sorted = [...evalItems].sort((a, b) => a.attempt.result.attempt - b.attempt.result.attempt);
371
+ const verdict = foldEvalVerdict(sorted.map((item) => item.attempt.result));
372
+ const representative = sorted.find((item) => item.attempt.result.verdict === verdict) ?? sorted[0]!;
373
+ const attempts = await Promise.all(sorted.map((item) => attemptListItemOf(item, identityRedact)));
374
+ evalRows.push({
375
+ evalId,
376
+ verdict,
377
+ reason: reasonFor(representative.attempt.result),
378
+ duration: await computeCell(durationMs, sorted),
379
+ cost: await computeCell(costUSD, sorted),
380
+ attempts,
381
+ });
382
+ }
383
+ evalRows.sort((a, b) => a.evalId.localeCompare(b.evalId));
384
+ const experiment = newest.snapshot.experiment ?? newest.attempt.result.experiment;
385
+ out.push({
386
+ experimentId,
387
+ agent: newest.snapshot.agent,
388
+ ...((newest.attempt.result.model ?? newest.snapshot.model) !== undefined
389
+ ? { model: newest.attempt.result.model ?? newest.snapshot.model }
390
+ : {}),
391
+ ...(experiment?.flags ? { flags: experiment.flags } : {}),
392
+ verdicts: stats.verdicts,
393
+ passRate: await computeCell(passRate, group),
394
+ cost: await computeCell(costUSD, group),
395
+ duration: await computeCell(durationMs, group),
396
+ tokens: await computeCell(tokens, group),
397
+ evals: stats.evals,
398
+ attempts: stats.attempts,
399
+ lastRunAt: stats.lastRunAt!,
400
+ evalRows,
401
+ });
402
+ }
403
+ out.sort((a, b) => a.experimentId.localeCompare(b.experimentId));
404
+ return out;
405
+ }
406
+
102
407
  // ───────────────────────── MetricMatrix.data(= MetricBars.data)─────────────────────────
103
408
 
104
409
  export interface MatrixDataOptions {
@@ -326,7 +631,7 @@ export async function lineData(input: SnapshotsInput, opts: LineDataOptions): Pr
326
631
 
327
632
  // ───────────────────────── RunOverview.data ─────────────────────────
328
633
 
329
- /** 选集的 warnings 随行进 OverviewData,RunOverview 直接渲染 —— 诚实不靠使用者记得接线。 */
634
+ /** Selection warnings 随行进 OverviewData,RunOverview 直接渲染 —— 诚实不靠使用者记得接线。 */
330
635
  export async function overviewData(input: SnapshotsInput): Promise<OverviewData> {
331
636
  const { snapshots, warnings } = resolveInput(input);
332
637
  const items = collectItems(snapshots);
@@ -340,7 +645,7 @@ export async function overviewData(input: SnapshotsInput): Promise<OverviewData>
340
645
  for (const item of items) {
341
646
  const result = item.attempt.result;
342
647
  evalIds.add(evalIdOf(item));
343
- switch (result.outcome) {
648
+ switch (result.verdict) {
344
649
  case "passed":
345
650
  passed += 1;
346
651
  break;
@@ -358,6 +663,9 @@ export async function overviewData(input: SnapshotsInput): Promise<OverviewData>
358
663
  const cost = attemptCostUSD(result);
359
664
  if (cost !== null) costUSD = (costUSD ?? 0) + cost;
360
665
  }
666
+ // 通过率的唯一官方口径:两级聚合(computeCell),不是从上面四个 verdict 计票现场重算——
667
+ // 一道题内 attempt 部分通过要算部分 credit,不是二元投票。
668
+ const passRateCell = await computeCell(passRate, items);
361
669
  return {
362
670
  snapshots: snapshots.map((s) => ({
363
671
  experimentId: s.experimentId,
@@ -365,11 +673,52 @@ export async function overviewData(input: SnapshotsInput): Promise<OverviewData>
365
673
  model: s.model,
366
674
  startedAt: s.startedAt,
367
675
  })),
368
- totals: { evals: evalIds.size, attempts: items.length, passed, failed, errored, skipped, costUSD, durationMs },
676
+ totals: {
677
+ evals: evalIds.size,
678
+ attempts: items.length,
679
+ passed,
680
+ failed,
681
+ errored,
682
+ skipped,
683
+ passRate: passRateCell,
684
+ costUSD,
685
+ durationMs,
686
+ },
369
687
  warnings: [...warnings],
370
688
  };
371
689
  }
372
690
 
691
+ // ───────────────────────── GroupSummary.data ─────────────────────────
692
+
693
+ /**
694
+ * 一组 experiment 的摘要:experiment/eval/attempt 数量、eval 级折叠计票、通过率(旧
695
+ * `GroupSelector` 卡片口径,见 summarizeItems)、总成本(null-safe 求和)、最后运行时间
696
+ * (组内快照 startedAt 最大值)。`input` 就是调用方已经收窄好的组 Selection(如自定义报告
697
+ * 按 experiment 组前缀 filter 出来的那份)——本函数不再自己分组。
698
+ */
699
+ export async function groupSummaryData(input: SnapshotsInput): Promise<GroupSummaryData> {
700
+ const { snapshots } = resolveInput(input);
701
+ const items = collectItems(snapshots);
702
+ const summary = summarizeItems(items);
703
+ const ratio = summary.ran > 0 ? summary.verdicts.passed / summary.ran : null; // 分母为 0 → 缺数据,不编 0%
704
+ const passRateCell: MetricCell = {
705
+ value: ratio,
706
+ display: ratio === null ? "—" : formatMetricValue(ratio, "%"),
707
+ samples: summary.ran,
708
+ total: summary.evals,
709
+ refs: summary.refs,
710
+ };
711
+ return {
712
+ experiments: summary.experiments,
713
+ evals: summary.evals,
714
+ attempts: summary.attempts,
715
+ verdicts: summary.verdicts,
716
+ passRate: passRateCell,
717
+ totalCostUSD: summary.totalCostUSD,
718
+ ...(summary.lastRunAt !== undefined ? { lastRunAt: summary.lastRunAt } : {}),
719
+ };
720
+ }
721
+
373
722
  // ───────────────────────── DeltaTable.data ─────────────────────────
374
723
 
375
724
  export interface DeltaPair {
@@ -424,47 +773,3 @@ function deltaDisplay(metric: Metric, delta: number | null): string {
424
773
  return delta > 0 ? `+${text}` : text;
425
774
  }
426
775
 
427
- // ───────────────────────── CaseList.data ─────────────────────────
428
-
429
- export interface CaseListDataOptions {
430
- /** 要列出的判决;默认 failed + errored。 */
431
- outcomes?: ("failed" | "errored")[];
432
- /** 超出如实报 truncated,不静默截断。 */
433
- limit?: number;
434
- /** 自由文本(error / 断言 detail / judge evidence)的发布消毒钩子;身份字段不经它。 */
435
- redact?: (text: string) => string;
436
- }
437
-
438
- export async function caseListData(input: SnapshotsInput, opts?: CaseListDataOptions): Promise<CaseListData> {
439
- const { snapshots } = resolveInput(input);
440
- const wanted = new Set<"failed" | "errored">(opts?.outcomes ?? ["failed", "errored"]);
441
- const redact = opts?.redact ?? ((text: string) => text);
442
- const selected = collectItems(snapshots).filter((item) => {
443
- const outcome = item.attempt.result.outcome;
444
- return (outcome === "failed" || outcome === "errored") && wanted.has(outcome);
445
- });
446
- const shown = opts?.limit === undefined ? selected : selected.slice(0, opts.limit);
447
- const rows: CaseListData["rows"] = shown.map((item) => {
448
- const result = item.attempt.result;
449
- const cost = attemptCostUSD(result);
450
- return {
451
- eval: evalIdOf(item),
452
- experimentId: experimentIdOf(item),
453
- agent: result.agent,
454
- outcome: result.outcome as "failed" | "errored",
455
- error: result.error === undefined ? undefined : redact(result.error),
456
- failedAssertions: result.assertions
457
- .filter((assertion) => !assertion.passed)
458
- .map((assertion) => ({
459
- name: assertion.name,
460
- score: assertion.score,
461
- detail: assertion.detail === undefined ? undefined : redact(assertion.detail),
462
- evidence: assertion.evidence === undefined ? undefined : redact(assertion.evidence),
463
- })),
464
- durationMs: result.durationMs,
465
- costUSD: cost ?? undefined,
466
- ref: item.attempt.ref,
467
- };
468
- });
469
- return { rows, truncated: selected.length - shown.length };
470
- }