niceeval 0.5.4 → 0.6.1

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 (337) hide show
  1. package/INDEX.md +55 -0
  2. package/dist/agents/types.d.ts +321 -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/otlp/parse.d.ts +3 -0
  9. package/dist/o11y/otlp/receiver.d.ts +21 -0
  10. package/dist/o11y/otlp/turn-otel.d.ts +43 -0
  11. package/dist/o11y/types.d.ts +190 -0
  12. package/dist/report/aggregate.d.ts +57 -0
  13. package/dist/report/aggregate.js +212 -0
  14. package/dist/report/built-ins/experiment-comparison.d.ts +1 -0
  15. package/dist/report/built-ins/experiment-comparison.js +13 -0
  16. package/dist/report/built-ins/index.d.ts +1 -0
  17. package/dist/report/built-ins/index.js +2 -0
  18. package/dist/report/components.d.ts +180 -0
  19. package/dist/report/components.js +132 -0
  20. package/dist/report/compute.d.ts +106 -0
  21. package/dist/report/compute.js +600 -0
  22. package/dist/report/flag.d.ts +6 -0
  23. package/dist/report/flag.js +8 -0
  24. package/dist/report/format.d.ts +26 -0
  25. package/dist/report/format.js +109 -0
  26. package/dist/report/index.d.ts +19 -0
  27. package/dist/report/index.js +30 -0
  28. package/dist/report/load.d.ts +12 -0
  29. package/dist/report/load.js +56 -0
  30. package/dist/report/locale.d.ts +94 -0
  31. package/dist/report/locale.js +183 -0
  32. package/dist/report/metrics.d.ts +21 -0
  33. package/dist/report/metrics.js +115 -0
  34. package/dist/report/primitives.d.ts +64 -0
  35. package/dist/report/primitives.js +113 -0
  36. package/dist/report/react/AttemptList.d.ts +23 -0
  37. package/dist/report/react/AttemptList.js +19 -0
  38. package/dist/report/react/DeltaTable.d.ts +8 -0
  39. package/dist/report/react/DeltaTable.js +24 -0
  40. package/dist/report/react/EvalList.d.ts +10 -0
  41. package/dist/report/react/EvalList.js +0 -0
  42. package/dist/report/react/ExperimentList.d.ts +11 -0
  43. package/dist/report/react/ExperimentList.js +67 -0
  44. package/dist/report/react/GroupSummary.d.ts +8 -0
  45. package/dist/report/react/GroupSummary.js +8 -0
  46. package/dist/report/react/MetricBars.d.ts +10 -0
  47. package/dist/report/react/MetricBars.js +48 -0
  48. package/dist/report/react/MetricLine.d.ts +9 -0
  49. package/dist/report/react/MetricLine.js +68 -0
  50. package/dist/report/react/MetricMatrix.d.ts +10 -0
  51. package/dist/report/react/MetricMatrix.js +23 -0
  52. package/dist/report/react/MetricScatter.d.ts +9 -0
  53. package/dist/report/react/MetricScatter.js +96 -0
  54. package/dist/report/react/MetricTable.d.ts +11 -0
  55. package/dist/report/react/MetricTable.js +26 -0
  56. package/dist/report/react/RunOverview.d.ts +8 -0
  57. package/dist/report/react/RunOverview.js +12 -0
  58. package/dist/report/react/Scoreboard.d.ts +8 -0
  59. package/dist/report/react/Scoreboard.js +26 -0
  60. package/dist/report/react/cell.d.ts +9 -0
  61. package/dist/report/react/cell.js +9 -0
  62. package/dist/report/react/chart-math.d.ts +14 -0
  63. package/dist/report/react/chart-math.js +68 -0
  64. package/dist/report/react/colors.d.ts +14 -0
  65. package/dist/report/react/colors.js +45 -0
  66. package/dist/report/react/fixtures.d.ts +19 -0
  67. package/dist/report/react/fixtures.js +398 -0
  68. package/dist/report/react/format.d.ts +3 -0
  69. package/dist/report/react/format.js +7 -0
  70. package/dist/report/react/index.d.ts +6 -0
  71. package/dist/report/react/index.js +14 -0
  72. package/dist/report/report.d.ts +24 -0
  73. package/dist/report/report.js +49 -0
  74. package/dist/report/text/faces.d.ts +29 -0
  75. package/dist/report/text/faces.js +541 -0
  76. package/dist/report/text/layout.d.ts +75 -0
  77. package/dist/report/text/layout.js +173 -0
  78. package/dist/report/text/plot.d.ts +37 -0
  79. package/dist/report/text/plot.js +81 -0
  80. package/dist/report/text/table.d.ts +8 -0
  81. package/dist/report/text/table.js +86 -0
  82. package/dist/report/tree.d.ts +99 -0
  83. package/dist/report/tree.js +228 -0
  84. package/dist/report/types.d.ts +408 -0
  85. package/dist/report/types.js +4 -0
  86. package/dist/report/web.d.ts +15 -0
  87. package/dist/report/web.js +37 -0
  88. package/dist/results/locator.d.ts +75 -0
  89. package/dist/results/locator.js +0 -0
  90. package/dist/results/select.d.ts +45 -0
  91. package/dist/results/select.js +230 -0
  92. package/dist/results/source-hash.d.ts +9 -0
  93. package/dist/results/types.d.ts +204 -0
  94. package/dist/runner/eval-source.d.ts +22 -0
  95. package/dist/runner/fingerprint.d.ts +23 -0
  96. package/dist/runner/types.d.ts +332 -0
  97. package/dist/sandbox/docker-stream.d.ts +24 -0
  98. package/dist/sandbox/docker.d.ts +84 -0
  99. package/dist/sandbox/e2b.d.ts +31 -0
  100. package/dist/sandbox/errors.d.ts +4 -0
  101. package/dist/sandbox/local-files.d.ts +6 -0
  102. package/dist/sandbox/paths.d.ts +4 -0
  103. package/dist/sandbox/registry.d.ts +13 -0
  104. package/dist/sandbox/resolve.d.ts +40 -0
  105. package/dist/sandbox/retry.d.ts +10 -0
  106. package/dist/sandbox/shell.d.ts +11 -0
  107. package/dist/sandbox/source-files.d.ts +13 -0
  108. package/dist/sandbox/types.d.ts +203 -0
  109. package/dist/sandbox/vercel.d.ts +37 -0
  110. package/dist/scoring/types.d.ts +70 -0
  111. package/dist/shared/aggregate.d.ts +29 -0
  112. package/dist/shared/aggregate.js +55 -0
  113. package/dist/shared/types.d.ts +27 -0
  114. package/dist/shared/verdict.d.ts +27 -0
  115. package/dist/shared/verdict.js +34 -0
  116. package/dist/tty-line.d.ts +6 -0
  117. package/dist/types.d.ts +7 -0
  118. package/dist/util.d.ts +22 -0
  119. package/docs-site/zh/concepts/adapter.mdx +7 -7
  120. package/docs-site/zh/concepts/assert.mdx +19 -18
  121. package/docs-site/zh/concepts/evals.mdx +12 -11
  122. package/docs-site/zh/concepts/experiment.mdx +2 -2
  123. package/docs-site/zh/concepts/judge.mdx +9 -9
  124. package/docs-site/zh/concepts/overview.mdx +3 -3
  125. package/docs-site/zh/concepts/tier.mdx +6 -6
  126. package/docs-site/zh/example/claude-code-codex-plugin.mdx +9 -5
  127. package/docs-site/zh/example/claude-code-codex-skill.mdx +12 -7
  128. package/docs-site/zh/guides/agent-feedback-loop.mdx +237 -78
  129. package/docs-site/zh/guides/connect-otel.mdx +3 -3
  130. package/docs-site/zh/guides/connect-your-agent.mdx +2 -2
  131. package/docs-site/zh/guides/custom-reports.mdx +196 -36
  132. package/docs-site/zh/guides/experiments.mdx +7 -1
  133. package/docs-site/zh/guides/fixtures.mdx +4 -2
  134. package/docs-site/zh/guides/official-adapters.mdx +31 -9
  135. package/docs-site/zh/guides/publish-report.mdx +4 -4
  136. package/docs-site/zh/guides/report-components.mdx +194 -55
  137. package/docs-site/zh/guides/reporters.mdx +2 -2
  138. package/docs-site/zh/guides/results-data.mdx +61 -34
  139. package/docs-site/zh/guides/runner.mdx +2 -2
  140. package/docs-site/zh/guides/sandbox-agent.mdx +3 -2
  141. package/docs-site/zh/guides/{sandbox-backends.mdx → sandbox-providers.mdx} +12 -12
  142. package/docs-site/zh/guides/viewing-results.mdx +200 -79
  143. package/docs-site/zh/guides/write-experiment.mdx +9 -1
  144. package/docs-site/zh/guides/write-send.mdx +4 -3
  145. package/docs-site/zh/index.mdx +4 -3
  146. package/docs-site/zh/introduction.mdx +2 -1
  147. package/docs-site/zh/quickstart.mdx +5 -3
  148. package/docs-site/zh/reference/builtin-agents.mdx +38 -13
  149. package/docs-site/zh/reference/capabilities.mdx +1 -1
  150. package/docs-site/zh/reference/cli.mdx +36 -13
  151. package/docs-site/zh/reference/define-agent.mdx +15 -13
  152. package/docs-site/zh/reference/define-config.mdx +1 -1
  153. package/docs-site/zh/reference/define-eval.mdx +3 -3
  154. package/docs-site/zh/reference/events.mdx +9 -3
  155. package/docs-site/zh/reference/expect.mdx +13 -8
  156. package/package.json +10 -6
  157. package/src/agents/bub.ts +129 -42
  158. package/src/agents/claude-code.test.ts +152 -0
  159. package/src/agents/claude-code.ts +127 -13
  160. package/src/agents/codex.test.ts +192 -0
  161. package/src/agents/codex.ts +151 -12
  162. package/src/agents/index.ts +9 -3
  163. package/src/agents/manifest.ts +41 -0
  164. package/src/agents/plugin-config.test.ts +94 -0
  165. package/src/agents/shared.ts +33 -2
  166. package/src/agents/skills.test.ts +214 -0
  167. package/src/agents/skills.ts +304 -0
  168. package/src/agents/streaming.ts +5 -5
  169. package/src/agents/types.ts +59 -3
  170. package/src/cli.ts +74 -47
  171. package/src/context/context.test.ts +20 -2
  172. package/src/context/control-flow.ts +1 -1
  173. package/src/context/session.test.ts +92 -1
  174. package/src/context/session.ts +23 -1
  175. package/src/context/types.ts +4 -4
  176. package/src/define.test.ts +8 -8
  177. package/src/define.ts +11 -11
  178. package/src/expect/index.ts +5 -5
  179. package/src/i18n/en.ts +33 -15
  180. package/src/i18n/zh-CN.ts +37 -21
  181. package/src/index.ts +1 -1
  182. package/src/o11y/execution-tree.test.ts +452 -0
  183. package/src/o11y/execution-tree.ts +367 -0
  184. package/src/o11y/otlp/receiver.ts +1 -1
  185. package/src/o11y/otlp/select.ts +7 -3
  186. package/src/o11y/parsers/claude-code.test.ts +175 -0
  187. package/src/o11y/parsers/claude-code.ts +32 -0
  188. package/src/o11y/types.ts +6 -1
  189. package/src/report/aggregate.ts +26 -20
  190. package/src/report/built-in-user-parity.test.tsx +640 -0
  191. package/src/report/built-ins/experiment-comparison.tsx +19 -0
  192. package/src/report/built-ins/index.ts +2 -0
  193. package/src/report/components.tsx +225 -50
  194. package/src/report/compute.ts +335 -54
  195. package/src/report/dual-render.test.tsx +1129 -0
  196. package/src/report/flag.ts +1 -1
  197. package/src/report/format.ts +35 -2
  198. package/src/report/index.ts +47 -14
  199. package/src/report/load.ts +1 -1
  200. package/src/report/locale.ts +229 -0
  201. package/src/report/metrics.ts +39 -15
  202. package/src/report/primitives.tsx +100 -2
  203. package/src/report/react/AttemptList.tsx +113 -0
  204. package/src/report/react/DeltaTable.tsx +9 -6
  205. package/src/report/react/EvalList.tsx +0 -0
  206. package/src/report/react/ExperimentList.tsx +220 -0
  207. package/src/report/react/GroupSummary.tsx +66 -0
  208. package/src/report/react/MetricBars.tsx +24 -13
  209. package/src/report/react/MetricLine.tsx +21 -21
  210. package/src/report/react/MetricMatrix.tsx +8 -4
  211. package/src/report/react/MetricScatter.tsx +126 -96
  212. package/src/report/react/MetricTable.tsx +115 -15
  213. package/src/report/react/RunOverview.tsx +48 -26
  214. package/src/report/react/Scoreboard.tsx +15 -7
  215. package/src/report/react/cell.tsx +18 -9
  216. package/src/{view/app/lib/chart.ts → report/react/chart-math.ts} +2 -6
  217. package/src/report/react/colors.ts +24 -15
  218. package/src/report/react/enhance.js +168 -0
  219. package/src/report/react/fixtures.ts +169 -34
  220. package/src/report/react/format.ts +1 -1
  221. package/src/report/react/index.tsx +21 -6
  222. package/src/report/react/render.test.tsx +180 -31
  223. package/src/report/react/styles.css +432 -213
  224. package/src/report/report.test.ts +398 -94
  225. package/src/report/report.ts +29 -11
  226. package/src/report/text/faces.ts +344 -96
  227. package/src/report/text/layout.ts +76 -11
  228. package/src/report/text/table.ts +98 -0
  229. package/src/report/tree.ts +103 -20
  230. package/src/report/types.ts +184 -25
  231. package/src/report/web.ts +46 -19
  232. package/src/results/annotated-source.test.ts +146 -0
  233. package/src/results/annotated-source.ts +128 -0
  234. package/src/results/attempt-evidence.test.ts +158 -0
  235. package/src/results/attempt-evidence.ts +158 -0
  236. package/src/results/attempt-source.ts +48 -0
  237. package/src/results/copy.ts +154 -139
  238. package/src/results/format.ts +54 -47
  239. package/src/results/host-equivalence.test.ts +527 -0
  240. package/src/results/index.ts +56 -14
  241. package/src/results/locator.test.ts +198 -0
  242. package/src/results/locator.ts +0 -0
  243. package/src/results/open.ts +392 -175
  244. package/src/results/results.test.ts +943 -494
  245. package/src/results/select.ts +129 -23
  246. package/src/results/skipped-notice.ts +0 -0
  247. package/src/results/source-hash.ts +28 -0
  248. package/src/results/types.ts +98 -62
  249. package/src/results/writer.ts +304 -176
  250. package/src/runner/attempt.test.ts +170 -0
  251. package/src/runner/attempt.ts +48 -13
  252. package/src/runner/discover.test.ts +65 -0
  253. package/src/runner/discover.ts +6 -2
  254. package/src/runner/eval-source.test.ts +121 -0
  255. package/src/runner/eval-source.ts +45 -0
  256. package/src/runner/fingerprint.ts +54 -1
  257. package/src/runner/remote-sandbox.ts +1 -1
  258. package/src/runner/report.test.ts +6 -8
  259. package/src/runner/report.ts +5 -4
  260. package/src/runner/reporters/artifacts.ts +27 -41
  261. package/src/runner/reporters/braintrust.test.ts +3 -3
  262. package/src/runner/reporters/braintrust.ts +1 -1
  263. package/src/runner/reporters/console.ts +6 -6
  264. package/src/runner/reporters/json.ts +4 -4
  265. package/src/runner/reporters/live.test.ts +56 -0
  266. package/src/runner/reporters/live.ts +36 -10
  267. package/src/runner/reporters/quiet.test.ts +66 -0
  268. package/src/runner/reporters/quiet.ts +49 -0
  269. package/src/runner/reporters/shared.ts +5 -5
  270. package/src/runner/reporters/table.ts +17 -17
  271. package/src/runner/run.ts +61 -102
  272. package/src/runner/types.ts +43 -32
  273. package/src/sandbox/checkpoint.ts +1 -1
  274. package/src/sandbox/docker.ts +15 -2
  275. package/src/sandbox/e2b.ts +11 -5
  276. package/src/sandbox/errors.ts +13 -0
  277. package/src/sandbox/index.ts +3 -3
  278. package/src/sandbox/local-files.ts +1 -1
  279. package/src/sandbox/registry.ts +6 -5
  280. package/src/sandbox/resolve.ts +38 -23
  281. package/src/sandbox/retry.test.ts +68 -0
  282. package/src/sandbox/retry.ts +44 -0
  283. package/src/sandbox/types.ts +24 -24
  284. package/src/sandbox/vercel.ts +17 -3
  285. package/src/scoring/collector.ts +3 -3
  286. package/src/scoring/judge.ts +2 -2
  287. package/src/scoring/scoped.ts +13 -1
  288. package/src/scoring/types.ts +7 -7
  289. package/src/scoring/verdict.ts +4 -4
  290. package/src/shared/aggregate.ts +23 -3
  291. package/src/shared/types.ts +2 -2
  292. package/src/shared/{outcome.ts → verdict.ts} +15 -15
  293. package/src/show/compose.ts +12 -132
  294. package/src/show/index.ts +119 -95
  295. package/src/show/render.ts +462 -159
  296. package/src/show/show.test.ts +471 -253
  297. package/src/source-loc.ts +1 -1
  298. package/src/tty-line.ts +32 -0
  299. package/src/types.ts +2 -2
  300. package/src/util.test.ts +21 -1
  301. package/src/util.ts +5 -1
  302. package/src/view/app/App.tsx +33 -198
  303. package/src/view/app/components/AttemptModal.tsx +6 -6
  304. package/src/view/app/components/CodeView.tsx +2 -2
  305. package/src/view/app/components/CopyControls.tsx +32 -36
  306. package/src/view/app/components/SkippedRunsBanner.tsx +140 -0
  307. package/src/view/app/components/ui/badge.tsx +1 -1
  308. package/src/view/app/components/ui/dialog.tsx +2 -2
  309. package/src/view/app/i18n.ts +40 -31
  310. package/src/view/app/lib/artifact-url.ts +1 -1
  311. package/src/view/app/lib/attempt-route.test.ts +37 -47
  312. package/src/view/app/lib/attempt-route.ts +26 -26
  313. package/src/view/app/lib/rows.ts +9 -112
  314. package/src/view/app/lib/verdict.ts +25 -0
  315. package/src/view/app/main.tsx +9 -12
  316. package/src/view/app/pages/RunsPage.tsx +4 -4
  317. package/src/view/app/pages/TracesPage.tsx +3 -3
  318. package/src/view/app/shared.ts +0 -1
  319. package/src/view/app/types.ts +6 -51
  320. package/src/view/artifact-serving.test.ts +136 -0
  321. package/src/view/client-dist/app.css +1 -1
  322. package/src/view/client-dist/app.js +21 -21
  323. package/src/view/data.test.ts +214 -143
  324. package/src/view/data.ts +166 -120
  325. package/src/view/index.ts +23 -11
  326. package/src/view/server.ts +52 -17
  327. package/src/view/shared/types.ts +22 -31
  328. package/src/view/styles.css +117 -327
  329. package/src/view/view-report.test.ts +146 -98
  330. package/src/report/default-report.tsx +0 -222
  331. package/src/report/dual-face.test.tsx +0 -527
  332. package/src/report/react/CaseList.tsx +0 -70
  333. package/src/view/app/components/CostScoreChart.tsx +0 -147
  334. package/src/view/app/components/ExperimentTable.tsx +0 -284
  335. package/src/view/app/components/GroupSelector.tsx +0 -61
  336. package/src/view/app/components/primitives.tsx +0 -43
  337. package/src/view/app/lib/outcome.ts +0 -82
@@ -1,19 +1,24 @@
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
+ AttemptListItem,
15
+ AttemptLocator,
15
16
  DeltaData,
16
17
  DimensionInput,
18
+ EvalListItem,
19
+ ExperimentListEvalRow,
20
+ ExperimentListItem,
21
+ GroupSummaryData,
17
22
  LineData,
18
23
  MatrixData,
19
24
  Metric,
@@ -23,7 +28,11 @@ import type {
23
28
  ScatterData,
24
29
  ScoreboardData,
25
30
  TableData,
31
+ TableRowMeta,
26
32
  } from "./types.ts";
33
+ import type { AssertionResult, EvalResult } from "../types.ts";
34
+ import type { AttemptHandle } from "../results/types.ts";
35
+ import { evalLevelStats, foldEvalVerdict } from "../shared/verdict.ts";
27
36
  import {
28
37
  applyAggregator,
29
38
  assertUniqueMetricNames,
@@ -40,13 +49,14 @@ import {
40
49
  filterItems,
41
50
  groupItems,
42
51
  flagAxisValue,
52
+ locatorOf,
43
53
  resolveInput,
44
54
  snapshotKeyOf,
45
55
  toColumn,
46
56
  type Item,
47
57
  type SnapshotsInput,
48
58
  } from "./aggregate.ts";
49
- import { attemptCostUSD, examScore } from "./metrics.ts";
59
+ import { attemptCostUSD, costUSD, durationMs, examScore, passRate, tokens } from "./metrics.ts";
50
60
  import { formatMetricValue, formatPlainNumber } from "./format.ts";
51
61
 
52
62
  // ───────────────────────── MetricTable.data ─────────────────────────
@@ -62,6 +72,132 @@ export interface TableDataOptions<M extends readonly Metric[]> {
62
72
  evals?: string | string[];
63
73
  }
64
74
 
75
+ // 一组 Item 的 eval 全身份键:experimentId + eval id。单 experiment 场景(如 experimentRowMeta,
76
+ // 一组本就只有一个 experimentId)下退化为只按 eval id 折叠,与旧行为一致;多 experiment 场景
77
+ // (GroupSummary 的组可能跨多个 experiment)下避免两个 experiment 里同名 eval 被误合并成一道题。
78
+ // 分隔符是 NUL(同 aggregate.ts 的 KEY_SEP 手法):不会出现在 eval id / experimentId 里,拼接键不串味。
79
+ const GROUP_KEY_SEP = "\u0000";
80
+ function fullEvalKey(item: Item): string {
81
+ return `${experimentIdOf(item)}${GROUP_KEY_SEP}${evalIdOf(item)}`;
82
+ }
83
+
84
+ /**
85
+ * 一批 Item 的组级统计:eval 级折叠计票(evalLevelStats,与 view 榜单 / `TableRowMeta.verdicts`
86
+ * 同一套 foldEvalVerdict 口径,按完整身份键折叠)、experiment/eval/attempt 数量、总成本
87
+ * (null-safe 求和)、最后运行时间(组内快照 startedAt 的最大值)。`experimentRowMeta` 与
88
+ * `groupSummaryData` 共用这一份实现,不各自拼装 evalLevelStats。
89
+ *
90
+ * 内部纯函数,不导出、不进 index.ts:对外只经 `experimentRowMeta`(挑 verdicts)与
91
+ * `groupSummaryData`(挑全部字段,包成 `GroupSummaryData`)暴露,调用方拿不到 `Item[]`
92
+ * 本身,所以这里也不用担心被越权复用。
93
+ */
94
+ function summarizeItems(items: Item[]): {
95
+ experiments: number;
96
+ evals: number;
97
+ attempts: number;
98
+ verdicts: { passed: number; failed: number; errored: number; skipped: number };
99
+ /** 折叠后代表每个「已跑」(非 skipped)eval 的一条 attempt 引用,与 ran 同序同数。 */
100
+ refs: AttemptLocator[];
101
+ /** 计入通过率分母的 eval 数(passed + failed + errored,不含 skipped)。 */
102
+ ran: number;
103
+ totalCostUSD: number | null;
104
+ lastRunAt: string | undefined;
105
+ } {
106
+ const experimentIds = new Set<string>();
107
+ for (const item of items) experimentIds.add(experimentIdOf(item));
108
+
109
+ const byEval = new Map<string, Item[]>();
110
+ for (const item of items) {
111
+ const key = fullEvalKey(item);
112
+ const list = byEval.get(key);
113
+ if (list) list.push(item);
114
+ else byEval.set(key, [item]);
115
+ }
116
+ const stats = evalLevelStats(
117
+ items.map((item) => ({ verdict: item.attempt.result.verdict, key: fullEvalKey(item) })),
118
+ (r) => r.key,
119
+ );
120
+ // 折叠代表 attempt:每个已跑的 eval 挑一条与折叠判定一致的 attempt 做证据引用,
121
+ // skipped 的 eval 不进分母、不出证据。
122
+ const refs: AttemptLocator[] = [];
123
+ for (const group of byEval.values()) {
124
+ const verdict = foldEvalVerdict(group.map((item) => item.attempt.result));
125
+ if (verdict === "skipped") continue;
126
+ const rep = group.find((item) => item.attempt.result.verdict === verdict) ?? group[0]!;
127
+ refs.push(locatorOf(rep));
128
+ }
129
+
130
+ let totalCostUSD: number | null = null;
131
+ for (const item of items) {
132
+ const cost = attemptCostUSD(item.attempt.result);
133
+ if (cost !== null) totalCostUSD = (totalCostUSD ?? 0) + cost;
134
+ }
135
+
136
+ let lastRunAt: string | undefined;
137
+ for (const item of items) {
138
+ const startedAt = item.snapshot.startedAt;
139
+ if (lastRunAt === undefined || startedAt > lastRunAt) lastRunAt = startedAt;
140
+ }
141
+
142
+ return {
143
+ experiments: experimentIds.size,
144
+ evals: stats.evals,
145
+ attempts: items.length,
146
+ verdicts: { passed: stats.passed, failed: stats.failed, errored: stats.errored, skipped: stats.skipped },
147
+ refs,
148
+ ran: stats.passed + stats.failed + stats.errored,
149
+ totalCostUSD,
150
+ lastRunAt,
151
+ };
152
+ }
153
+
154
+ /**
155
+ * experiment 行的元信息:agent/model 身份(组内去重后拼接)+ eval 级折叠计票 + eval/attempt
156
+ * 数量与最后运行时间(summarizeItems,即 view 榜单 / ExperimentList 的同一套 foldEvalVerdict
157
+ * 口径)。其它行维度(agent/eval/自定义…)没有唯一身份,不携带。
158
+ */
159
+ function experimentRowMeta(group: Item[]): TableRowMeta {
160
+ const agents = new Set<string>();
161
+ const models = new Set<string>();
162
+ for (const item of group) {
163
+ agents.add(item.attempt.result.agent);
164
+ const model = item.attempt.result.model ?? item.snapshot.model;
165
+ if (model !== undefined) models.add(model);
166
+ }
167
+ const stats = summarizeItems(group);
168
+ return {
169
+ ...(agents.size > 0 ? { agent: [...agents].join(", ") } : {}),
170
+ ...(models.size > 0 ? { model: [...models].join(", ") } : {}),
171
+ verdicts: stats.verdicts,
172
+ evals: stats.evals,
173
+ attempts: stats.attempts,
174
+ ...(stats.lastRunAt !== undefined ? { lastRunAt: stats.lastRunAt } : {}),
175
+ };
176
+ }
177
+
178
+ /**
179
+ * 一次 attempt 未通过的 gate 断言,原始声明顺序不变;soft 断言不参与判定,不算「失败原因」,
180
+ * 只影响得分,永不出现在这份列表里。`EvalList` / `ExperimentList` 的失败诊断与 `AttemptList`
181
+ * 的断言列表共用这同一份材料,保证同一个 attempt 在各处给出同一个原因。
182
+ */
183
+ export function failingGateAssertions(result: EvalResult): AssertionResult[] {
184
+ return result.assertions.filter((a) => !a.passed && a.severity === "gate");
185
+ }
186
+
187
+ /**
188
+ * 一次 attempt 的失败原因文案,按优先级取第一个在场的:
189
+ * `error` → `skipReason` → 未通过的 gate 断言(原始声明顺序,`name`,detail 在场则
190
+ * `"name: detail"`,多条用「, 」连接)→ 都缺席则无原因(如某道题恰好没有失败信号)。
191
+ * soft 断言永不进入这份原因文案,soft 得分是独立概念,不与 reason 混用同一个字段。
192
+ */
193
+ export function reasonFor(result: EvalResult): string | undefined {
194
+ if (result.error !== undefined) return result.error;
195
+ if (result.skipReason !== undefined) return result.skipReason;
196
+ const gates = failingGateAssertions(result);
197
+ if (gates.length === 0) return undefined;
198
+ return gates.map((a) => (a.detail ? `${a.name}: ${a.detail}` : a.name)).join(", ");
199
+ }
200
+
65
201
  export async function tableData<const M extends readonly Metric[]>(
66
202
  input: SnapshotsInput,
67
203
  opts: TableDataOptions<M>,
@@ -70,7 +206,7 @@ export async function tableData<const M extends readonly Metric[]>(
70
206
  const { snapshots } = resolveInput(input);
71
207
  const items = filterItems(collectItems(snapshots), opts.evals);
72
208
  const groups = groupItems(items, opts.rows);
73
- const rows: { key: string; cells: Record<string, MetricCell> }[] = [];
209
+ const rows: TableData["rows"] = [];
74
210
  const sortCells = new Map<string, MetricCell>();
75
211
  for (const [key, group] of groups) {
76
212
  const cells: Record<string, MetricCell> = {};
@@ -79,7 +215,12 @@ export async function tableData<const M extends readonly Metric[]>(
79
215
  // sort 指标不在 columns 里时单独算一遍,只用于排序、不进输出
80
216
  sortCells.set(key, cells[opts.sort.name] ?? (await computeCell(opts.sort, group)));
81
217
  }
82
- rows.push({ key, cells });
218
+ const meta: TableRowMeta = opts.rows === "experiment" ? experimentRowMeta(group) : {};
219
+ rows.push({
220
+ key,
221
+ cells,
222
+ ...(Object.keys(meta).length > 0 ? { meta } : {}),
223
+ });
83
224
  }
84
225
  if (opts.sort) {
85
226
  const better = opts.sort.better ?? "higher";
@@ -99,6 +240,147 @@ export async function tableData<const M extends readonly Metric[]>(
99
240
  } as TableData<M[number]["name"]>;
100
241
  }
101
242
 
243
+ // ───────────────────────── ExperimentList.data / EvalList.data / AttemptList.data ─────────────────────────
244
+ //
245
+ // 三个实体列表逐级下钻(experiment → experimentId × eval → attempt),固定展示实体事实,
246
+ // 没有列配置;过滤是报告作者对返回数组调用 .filter()/.slice() 的事,不进这里
247
+ // (docs/feature/reports/library.md「实体列表」)。AttemptListItem 是三者共用的叶子形状——
248
+ // ExperimentList / EvalList 的下钻数组直接复用它,不各自精简一份。
249
+
250
+ /** 自由文本(error / 断言 detail / evidence)的发布消毒钩子;身份字段(name/severity/loc)不经它。 */
251
+ function redactAssertions(assertions: AssertionResult[], redact: (text: string) => string): AssertionResult[] {
252
+ if (assertions.length === 0) return assertions;
253
+ return assertions.map((a) => ({
254
+ ...a,
255
+ ...(a.detail !== undefined ? { detail: redact(a.detail) } : {}),
256
+ ...(a.evidence !== undefined ? { evidence: redact(a.evidence) } : {}),
257
+ }));
258
+ }
259
+
260
+ /** AttemptList / ExperimentList / EvalList 共用的叶子构造:一个 Item → 一个 AttemptListItem。 */
261
+ function attemptListItemOf(item: Item, redact: (text: string) => string): AttemptListItem {
262
+ const result = item.attempt.result;
263
+ const cost = attemptCostUSD(result);
264
+ return {
265
+ evalId: evalIdOf(item),
266
+ experimentId: experimentIdOf(item),
267
+ attempt: result.attempt,
268
+ agent: result.agent,
269
+ verdict: result.verdict,
270
+ ...(result.error !== undefined ? { error: redact(result.error) } : {}),
271
+ assertions: redactAssertions(result.assertions, redact),
272
+ durationMs: result.durationMs,
273
+ ...(cost !== null ? { costUSD: cost } : {}),
274
+ locator: locatorOf(item),
275
+ };
276
+ }
277
+
278
+ const identityRedact = (text: string): string => text;
279
+
280
+ export interface AttemptListDataOptions {
281
+ /** 发布消毒:error / 断言 detail / evidence 经这个钩子;身份字段(experimentId/evalId/locator…)不经它。 */
282
+ redact?: (text: string) => string;
283
+ }
284
+
285
+ /** `AttemptList.data(selection)`:每个 Attempt 一项,顺序取自 Selection 展平顺序(不重排)。 */
286
+ export async function attemptListData(
287
+ input: SnapshotsInput,
288
+ opts?: AttemptListDataOptions,
289
+ ): Promise<AttemptListItem[]> {
290
+ const { snapshots } = resolveInput(input);
291
+ const redact = opts?.redact ?? identityRedact;
292
+ const items = collectItems(snapshots);
293
+ return items.map((item) => attemptListItemOf(item, redact));
294
+ }
295
+
296
+ /** `EvalList.data(selection)`:每个 `experimentId + evalId` 一项,按 evalId 再按 experimentId 升序。 */
297
+ export async function evalListData(input: SnapshotsInput): Promise<EvalListItem[]> {
298
+ const { snapshots } = resolveInput(input);
299
+ const items = collectItems(snapshots);
300
+ const groups = new Map<string, Item[]>();
301
+ for (const item of items) {
302
+ const key = fullEvalKey(item);
303
+ const list = groups.get(key);
304
+ if (list) list.push(item);
305
+ else groups.set(key, [item]);
306
+ }
307
+ const out: EvalListItem[] = [];
308
+ for (const group of groups.values()) {
309
+ const sorted = [...group].sort((a, b) => a.attempt.result.attempt - b.attempt.result.attempt);
310
+ const verdict = foldEvalVerdict(sorted.map((item) => item.attempt.result));
311
+ const representative = sorted.find((item) => item.attempt.result.verdict === verdict) ?? sorted[0]!;
312
+ const attempts = sorted.map((item) => attemptListItemOf(item, identityRedact));
313
+ out.push({
314
+ evalId: evalIdOf(sorted[0]!),
315
+ experimentId: experimentIdOf(sorted[0]!),
316
+ verdict,
317
+ reason: reasonFor(representative.attempt.result),
318
+ score: await computeCell(examScore, sorted),
319
+ duration: await computeCell(durationMs, sorted),
320
+ cost: await computeCell(costUSD, sorted),
321
+ attempts,
322
+ });
323
+ }
324
+ out.sort((a, b) => a.evalId.localeCompare(b.evalId) || a.experimentId.localeCompare(b.experimentId));
325
+ return out;
326
+ }
327
+
328
+ /** `ExperimentList.data(selection)`:每个 experiment 一项,按 experimentId 升序;展开到每道 Eval。 */
329
+ export async function experimentListData(input: SnapshotsInput): Promise<ExperimentListItem[]> {
330
+ const { snapshots } = resolveInput(input);
331
+ const items = collectItems(snapshots);
332
+ const groups = groupItems(items, "experiment");
333
+ const out: ExperimentListItem[] = [];
334
+ for (const [experimentId, group] of groups) {
335
+ const stats = summarizeItems(group);
336
+ const newest = [...group].sort((a, b) => b.snapshot.startedAt.localeCompare(a.snapshot.startedAt))[0]!;
337
+ const evalGroups = groupItems(group, "eval");
338
+ const evalRows: ExperimentListEvalRow[] = [];
339
+ for (const [evalId, evalItems] of evalGroups) {
340
+ const sorted = [...evalItems].sort((a, b) => a.attempt.result.attempt - b.attempt.result.attempt);
341
+ const verdict = foldEvalVerdict(sorted.map((item) => item.attempt.result));
342
+ const representative = sorted.find((item) => item.attempt.result.verdict === verdict) ?? sorted[0]!;
343
+ const attempts = sorted.map((item) => attemptListItemOf(item, identityRedact));
344
+ evalRows.push({
345
+ evalId,
346
+ verdict,
347
+ reason: reasonFor(representative.attempt.result),
348
+ duration: await computeCell(durationMs, sorted),
349
+ cost: await computeCell(costUSD, sorted),
350
+ attempts,
351
+ });
352
+ }
353
+ evalRows.sort((a, b) => a.evalId.localeCompare(b.evalId));
354
+ const experiment = newest.snapshot.experiment ?? newest.attempt.result.experiment;
355
+ out.push({
356
+ experimentId,
357
+ agent: newest.snapshot.agent,
358
+ ...((newest.attempt.result.model ?? newest.snapshot.model) !== undefined
359
+ ? { model: newest.attempt.result.model ?? newest.snapshot.model }
360
+ : {}),
361
+ ...(experiment?.flags ? { flags: experiment.flags } : {}),
362
+ verdicts: stats.verdicts,
363
+ passRate: await computeCell(passRate, group),
364
+ cost: await computeCell(costUSD, group),
365
+ duration: await computeCell(durationMs, group),
366
+ tokens: await computeCell(tokens, group),
367
+ evals: stats.evals,
368
+ attempts: stats.attempts,
369
+ lastRunAt: stats.lastRunAt!,
370
+ evalRows,
371
+ });
372
+ }
373
+ // ExperimentList 是默认实验比较表:初始态按成功率从高到低,缺数据沉底;
374
+ // 同分时按 experiment id 稳定排序。web 增强可临时重排,text 面沿用同一基准顺序。
375
+ out.sort((a, b) => {
376
+ if (a.passRate.value === null && b.passRate.value === null) return a.experimentId.localeCompare(b.experimentId);
377
+ if (a.passRate.value === null) return 1;
378
+ if (b.passRate.value === null) return -1;
379
+ return b.passRate.value - a.passRate.value || a.experimentId.localeCompare(b.experimentId);
380
+ });
381
+ return out;
382
+ }
383
+
102
384
  // ───────────────────────── MetricMatrix.data(= MetricBars.data)─────────────────────────
103
385
 
104
386
  export interface MatrixDataOptions {
@@ -326,7 +608,7 @@ export async function lineData(input: SnapshotsInput, opts: LineDataOptions): Pr
326
608
 
327
609
  // ───────────────────────── RunOverview.data ─────────────────────────
328
610
 
329
- /** 选集的 warnings 随行进 OverviewData,RunOverview 直接渲染 —— 诚实不靠使用者记得接线。 */
611
+ /** Selection warnings 随行进 OverviewData,RunOverview 直接渲染 —— 诚实不靠使用者记得接线。 */
330
612
  export async function overviewData(input: SnapshotsInput): Promise<OverviewData> {
331
613
  const { snapshots, warnings } = resolveInput(input);
332
614
  const items = collectItems(snapshots);
@@ -340,7 +622,7 @@ export async function overviewData(input: SnapshotsInput): Promise<OverviewData>
340
622
  for (const item of items) {
341
623
  const result = item.attempt.result;
342
624
  evalIds.add(evalIdOf(item));
343
- switch (result.outcome) {
625
+ switch (result.verdict) {
344
626
  case "passed":
345
627
  passed += 1;
346
628
  break;
@@ -358,6 +640,9 @@ export async function overviewData(input: SnapshotsInput): Promise<OverviewData>
358
640
  const cost = attemptCostUSD(result);
359
641
  if (cost !== null) costUSD = (costUSD ?? 0) + cost;
360
642
  }
643
+ // 通过率的唯一官方口径:两级聚合(computeCell),不是从上面四个 verdict 计票现场重算——
644
+ // 一道题内 attempt 部分通过要算部分 credit,不是二元投票。
645
+ const passRateCell = await computeCell(passRate, items);
361
646
  return {
362
647
  snapshots: snapshots.map((s) => ({
363
648
  experimentId: s.experimentId,
@@ -365,11 +650,52 @@ export async function overviewData(input: SnapshotsInput): Promise<OverviewData>
365
650
  model: s.model,
366
651
  startedAt: s.startedAt,
367
652
  })),
368
- totals: { evals: evalIds.size, attempts: items.length, passed, failed, errored, skipped, costUSD, durationMs },
653
+ totals: {
654
+ evals: evalIds.size,
655
+ attempts: items.length,
656
+ passed,
657
+ failed,
658
+ errored,
659
+ skipped,
660
+ passRate: passRateCell,
661
+ costUSD,
662
+ durationMs,
663
+ },
369
664
  warnings: [...warnings],
370
665
  };
371
666
  }
372
667
 
668
+ // ───────────────────────── GroupSummary.data ─────────────────────────
669
+
670
+ /**
671
+ * 一组 experiment 的摘要:experiment/eval/attempt 数量、eval 级折叠计票、通过率(旧
672
+ * `GroupSelector` 卡片口径,见 summarizeItems)、总成本(null-safe 求和)、最后运行时间
673
+ * (组内快照 startedAt 最大值)。`input` 就是调用方已经收窄好的组 Selection(如自定义报告
674
+ * 按 experiment 组前缀 filter 出来的那份)——本函数不再自己分组。
675
+ */
676
+ export async function groupSummaryData(input: SnapshotsInput): Promise<GroupSummaryData> {
677
+ const { snapshots } = resolveInput(input);
678
+ const items = collectItems(snapshots);
679
+ const summary = summarizeItems(items);
680
+ const ratio = summary.ran > 0 ? summary.verdicts.passed / summary.ran : null; // 分母为 0 → 缺数据,不编 0%
681
+ const passRateCell: MetricCell = {
682
+ value: ratio,
683
+ display: ratio === null ? "—" : formatMetricValue(ratio, "%"),
684
+ samples: summary.ran,
685
+ total: summary.evals,
686
+ refs: summary.refs,
687
+ };
688
+ return {
689
+ experiments: summary.experiments,
690
+ evals: summary.evals,
691
+ attempts: summary.attempts,
692
+ verdicts: summary.verdicts,
693
+ passRate: passRateCell,
694
+ totalCostUSD: summary.totalCostUSD,
695
+ ...(summary.lastRunAt !== undefined ? { lastRunAt: summary.lastRunAt } : {}),
696
+ };
697
+ }
698
+
373
699
  // ───────────────────────── DeltaTable.data ─────────────────────────
374
700
 
375
701
  export interface DeltaPair {
@@ -423,48 +749,3 @@ function deltaDisplay(metric: Metric, delta: number | null): string {
423
749
  const text = displayValue(metric, delta); // 负号由格式化自带
424
750
  return delta > 0 ? `+${text}` : text;
425
751
  }
426
-
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
- }