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,3 +1,4 @@
1
+ // cases: docs/engineering/unit-tests/reports/cases.md
1
2
  // niceeval/report 计算层的单元测试:全部用内存 fake(Snapshot / AttemptHandle 按
2
3
  // niceeval/results 的读取契约手工构造),专门覆盖 docs/feature/reports/architecture.md 点名的坑 ——
3
4
  // 两级聚合 vs 平铺、pass@k、examScore 空真、skipped 稀释、scoreboard 固定分母与
@@ -6,11 +7,26 @@
6
7
 
7
8
  import { describe, expect, it } from "vitest";
8
9
 
9
- import type { AssertionResult, EvalResult, O11ySummary, Verdict } from "../types.ts";
10
+ import type { AssertionResult, AttemptError, EvalResult, O11ySummary, Verdict } from "../types.ts";
11
+
12
+ /** 结构化 `AttemptError` 的最小构造(测试用):只关心一层 message,operation/code 取中性默认值。 */
13
+ function erroredWith(message: string): AttemptError {
14
+ return { code: "unexpected-error", message, phase: "eval.run" };
15
+ }
10
16
  import type { AttemptHandle, Selection, SelectionWarning, Snapshot } from "../results/index.ts";
11
17
  import type { Dimension, MetricCell } from "./types.ts";
12
- import { costUSD, defineMetric, durationMs, examScore, passRate, tokens, turns } from "./metrics.ts";
13
- import { flag } from "./flag.ts";
18
+ import {
19
+ costUSD,
20
+ defineMetric,
21
+ durationMs,
22
+ endToEndPassRate,
23
+ examScore,
24
+ executionReliability,
25
+ taskPassRate,
26
+ tokens,
27
+ turns,
28
+ } from "./metrics.ts";
29
+ import { config, flag } from "./flag.ts";
14
30
  import { formatMetricValue } from "./format.ts";
15
31
  import {
16
32
  AttemptList,
@@ -48,7 +64,7 @@ function res(id: string, verdict: Verdict, extra: Partial<EvalResult> = {}): Eva
48
64
  }
49
65
 
50
66
  function softAssertion(name: string, score: number, extra: Partial<AssertionResult> = {}): AssertionResult {
51
- return { name, severity: "soft", score, passed: true, ...extra };
67
+ return { name, severity: "soft", score, outcome: "passed" as const, ...extra } as AssertionResult;
52
68
  }
53
69
 
54
70
  /** 最小合规 O11ySummary,只有 totalTurns 会变;供 turns 指标测试内联挂到 EvalResult.o11y。 */
@@ -136,7 +152,7 @@ function selection(snapshots: Snapshot[], warnings: SelectionWarning[]): Selecti
136
152
  }
137
153
 
138
154
  describe("ExperimentList.data", () => {
139
- it("身份、flags、Eval 判定构成、官方两级聚合汇总指标,展开到 Eval 携带 locator 与证据能力", async () => {
155
+ it("身份、flags、Eval 判定构成、官方两级聚合汇总指标,展开到 Eval 携带全部 Attempt locator", async () => {
140
156
  const s = snap({
141
157
  experimentId: "group/codex-e2b--mempal",
142
158
  agent: "codex",
@@ -146,7 +162,7 @@ describe("ExperimentList.data", () => {
146
162
  res("memory/a", "failed", {
147
163
  attempt: 0,
148
164
  durationMs: 1_000,
149
- assertions: [{ name: "gate", severity: "gate", passed: false, score: 0, detail: "wrong" }],
165
+ assertions: [{ name: "gate", severity: "gate", outcome: "failed" as const, score: 0, detail: "wrong" }],
150
166
  usage: { inputTokens: 10, outputTokens: 5, costUSD: 0.1 },
151
167
  hasEvents: true,
152
168
  }),
@@ -155,10 +171,17 @@ describe("ExperimentList.data", () => {
155
171
  durationMs: 2_000,
156
172
  usage: { inputTokens: 20, outputTokens: 10, costUSD: 0.2 },
157
173
  }),
158
- res("memory/b", "errored", { error: "timeout", durationMs: 3_000 }),
174
+ res("memory/b", "errored", { error: erroredWith("timeout"), durationMs: 3_000 }),
159
175
  ],
160
176
  });
161
- s.experiment = { id: s.experimentId, runs: 2, earlyExit: false, sandbox: "e2b:fast", budget: 2, flags: { cache: true } };
177
+ s.experiment = {
178
+ runs: 2,
179
+ earlyExit: false,
180
+ sandbox: { provider: "e2b", params: { template: "fast" } },
181
+ budget: 2,
182
+ flags: { cache: true },
183
+ selectedEvalIds: [],
184
+ };
162
185
 
163
186
  const items = await ExperimentList.data([s]);
164
187
  expect(items).toHaveLength(1);
@@ -173,7 +196,8 @@ describe("ExperimentList.data", () => {
173
196
  attempts: 3,
174
197
  lastRunAt: "2026-07-10T22:44:00.000Z",
175
198
  });
176
- // 官方两级聚合口径:memory/a 题内均值 (0+1)/2=0.5,memory/b 题内 0;跨题均值 0.25
199
+ // 官方端到端两级聚合:memory/a 题内均值 (0+1)/2=0.5;
200
+ // memory/b errored → 0 → 跨题均值 (0.5+0)/2=0.25
177
201
  expect(item.passRate.value).toBeCloseTo(0.25);
178
202
  expect(item.cost.samples).toBe(2); // 只有 memory/a 的两次 attempt 报了成本
179
203
 
@@ -181,14 +205,13 @@ describe("ExperimentList.data", () => {
181
205
  expect(evalA.verdict).toBe("passed"); // 任一轮通过 → 该题通过
182
206
  expect(evalA.attempts.map((a) => a.attempt)).toEqual([0, 1]);
183
207
  expect(evalA.attempts[0]!.locator).toMatch(/^@1[0-9a-z]{7}$/);
184
- expect(evalA.attempts[0]!.capabilities.execution).toBe(true); // attempt0 的 hasEvents:true
185
- expect(evalA.attempts[1]!.capabilities.execution).toBe(false);
186
208
  // 每次 attempt 独立编码,即使同一道题也不撞车
187
209
  expect(evalA.attempts[0]!.locator).not.toBe(evalA.attempts[1]!.locator);
188
210
 
189
211
  const evalB = item.evalRows.find((e) => e.evalId === "memory/b")!;
190
212
  expect(evalB.verdict).toBe("errored");
191
- expect(evalB.reason).toBe("timeout");
213
+ expect(evalB).not.toHaveProperty("reason");
214
+ expect(evalB.attempts[0]!.error?.message).toBe("timeout");
192
215
  });
193
216
 
194
217
  it("展开数组按 evalId 升序,顶层数组按 experimentId 升序", async () => {
@@ -198,10 +221,29 @@ describe("ExperimentList.data", () => {
198
221
  expect(items.map((i) => i.experimentId)).toEqual(["exp/a", "exp/b"]);
199
222
  expect(items.find((i) => i.experimentId === "exp/b")!.evalRows.map((e) => e.evalId)).toEqual(["a", "z"]);
200
223
  });
224
+
225
+ it("默认成功率包含 errored 并据此排序:2 passed / 5 errored = 2/7,不能以条件 100% 排第一", async () => {
226
+ const unstable = snap({
227
+ experimentId: "exp/unstable",
228
+ results: [
229
+ res("pass/a", "passed"),
230
+ res("pass/b", "passed"),
231
+ ...Array.from({ length: 5 }, (_, i) => res(`error/${i}`, "errored", { error: erroredWith("boom") })),
232
+ ],
233
+ });
234
+ const stable = snap({
235
+ experimentId: "exp/stable",
236
+ results: [res("pass", "passed"), res("fail", "failed")],
237
+ });
238
+
239
+ const items = await ExperimentList.data([unstable, stable]);
240
+ expect(items.find((item) => item.experimentId === "exp/unstable")!.passRate.value).toBeCloseTo(2 / 7, 10);
241
+ expect(items.map((item) => item.experimentId)).toEqual(["exp/stable", "exp/unstable"]);
242
+ });
201
243
  });
202
244
 
203
245
  describe("EvalList.data", () => {
204
- it("每项一个 experimentId + evalId,判定/分数/成本耗时均值/失败原因与展开到 Attempt 都在", async () => {
246
+ it("每项一个 experimentId + evalId,父项保留判定与题级聚合并展开到含失败事实的 Attempt", async () => {
205
247
  const s = snap({
206
248
  experimentId: "compare/codex",
207
249
  results: [
@@ -209,13 +251,13 @@ describe("EvalList.data", () => {
209
251
  attempt: 0,
210
252
  durationMs: 40_000,
211
253
  usage: { inputTokens: 1, outputTokens: 1, costUSD: 0.02 },
212
- assertions: [{ name: 'calledTool("get_weather")', severity: "gate", passed: false, score: 0, detail: "tool was never called" }],
254
+ assertions: [{ name: 'calledTool("get_weather")', severity: "gate", outcome: "failed" as const, score: 0, expected: "at least one matching call", received: "no tool calls" }],
213
255
  }),
214
256
  res("weather/brooklyn", "failed", {
215
257
  attempt: 1,
216
258
  durationMs: 42_000,
217
259
  usage: { inputTokens: 1, outputTokens: 1, costUSD: 0.06 },
218
- assertions: [{ name: 'calledTool("get_weather")', severity: "gate", passed: false, score: 0, detail: "tool was never called" }],
260
+ assertions: [{ name: 'calledTool("get_weather")', severity: "gate", outcome: "failed" as const, score: 0, expected: "at least one matching call", received: "no tool calls" }],
219
261
  }),
220
262
  ],
221
263
  });
@@ -225,11 +267,15 @@ describe("EvalList.data", () => {
225
267
  expect(item.evalId).toBe("weather/brooklyn");
226
268
  expect(item.experimentId).toBe("compare/codex");
227
269
  expect(item.verdict).toBe("failed");
228
- expect(item.reason).toBe('calledTool("get_weather"): tool was never called');
270
+ expect(item).not.toHaveProperty("reason");
229
271
  expect(item.score.value).toBe(0); // examScore: failed → 0
230
272
  expect(item.duration.value).toBeCloseTo(41_000); // 平均耗时
231
273
  expect(item.cost.value).toBeCloseTo(0.04); // 平均成本
232
274
  expect(item.attempts.map((a) => a.attempt)).toEqual([0, 1]);
275
+ const assertion = item.attempts[0]!.assertions[0]!;
276
+ expect(assertion.outcome).toBe("failed");
277
+ if (assertion.outcome !== "failed") throw new Error("expected failed assertion fixture");
278
+ expect(assertion.received).toBe("no tool calls");
233
279
  expect(item.attempts[0]!.locator).not.toBe(item.attempts[1]!.locator);
234
280
  });
235
281
 
@@ -243,14 +289,14 @@ describe("EvalList.data", () => {
243
289
  });
244
290
 
245
291
  describe("AttemptList.data", () => {
246
- it("每项一个 Attempt:身份、判定、assertions、耗时、成本、locator 与证据能力标记", async () => {
292
+ it("每项一个 Attempt:身份、判定、assertions、耗时、成本与 locator", async () => {
247
293
  const s = snap({
248
294
  experimentId: "exp/x",
249
295
  results: [
250
296
  res("A", "failed", {
251
297
  durationMs: 5_000,
252
298
  usage: { inputTokens: 1, outputTokens: 1, costUSD: 0.05 },
253
- assertions: [{ name: "gate", severity: "gate", passed: false, score: 0, detail: "nope" }],
299
+ assertions: [{ name: "gate", severity: "gate", outcome: "failed" as const, score: 0, detail: "nope" }],
254
300
  hasEvents: true,
255
301
  hasSources: true,
256
302
  }),
@@ -270,23 +316,37 @@ describe("AttemptList.data", () => {
270
316
  });
271
317
  expect(item.assertions).toHaveLength(1);
272
318
  expect(item.locator).toMatch(/^@1[0-9a-z]{7}$/);
273
- // 证据能力:cheap 计算,不调用完整 loadAttemptEvidence——execution 来自 hasEvents,
274
- // eval 来自 hasSources,diff 懒加载(fixture 的 diff() 恒返回 null → false)。
275
- expect(item.capabilities).toEqual({ eval: true, execution: true, timing: false, diff: false });
276
319
  });
277
320
 
278
- it("redact 作用于 error 与断言 detail/evidence,不作用于身份字段", async () => {
321
+ it("redact 作用于 error message/cause/stack、diagnostic 的 message/data、断言 detail/evidence,不作用于身份与分类字段", async () => {
279
322
  const s = snap({
280
323
  experimentId: "exp/x",
281
324
  results: [
282
- res("A", "errored", { error: "ENOENT /Users/me/repo/tool" }),
325
+ res("A", "errored", {
326
+ error: {
327
+ code: "sandbox-provision-failed",
328
+ message: "ENOENT /Users/me/repo/tool",
329
+ phase: "sandbox.create",
330
+ stack: "Error: ENOENT /Users/me/repo/tool\n at spawn (/Users/me/repo/src/run.ts:3:1)",
331
+ cause: { name: "SystemError", code: "ENOENT", message: "no such file /Users/me/repo/tool" },
332
+ },
333
+ diagnostics: [
334
+ {
335
+ code: "teardown-failed",
336
+ level: "warning",
337
+ message: "cleanup left /Users/me/repo/tmp behind",
338
+ phase: "sandbox.teardown",
339
+ data: { path: "/Users/me/repo/tmp", attempts: 2 },
340
+ },
341
+ ],
342
+ }),
283
343
  res("B", "failed", {
284
344
  assertions: [
285
345
  {
286
346
  name: "includes",
287
347
  severity: "gate",
288
348
  score: 0,
289
- passed: false,
349
+ outcome: "failed" as const,
290
350
  detail: "missing text under /Users/me/repo/src",
291
351
  evidence: "checked /Users/me/repo/src/app.ts",
292
352
  },
@@ -296,10 +356,25 @@ describe("AttemptList.data", () => {
296
356
  });
297
357
  const items = await AttemptList.data([s], { redact: (text) => text.replaceAll("/Users/me/repo", "<repo>") });
298
358
  const a = items.find((i) => i.evalId === "A")!;
299
- expect(a.error).toBe("ENOENT <repo>/tool");
359
+ // error:自由文本(message/stack/cause.message)全部经钩子
360
+ expect(a.error?.message).toBe("ENOENT <repo>/tool");
361
+ expect(a.error?.stack).toBe("Error: ENOENT <repo>/tool\n at spawn (<repo>/src/run.ts:3:1)");
362
+ expect(a.error?.cause?.message).toBe("no such file <repo>/tool");
363
+ // 分类字段(code/operation/cause.name/cause.code)原样保留,不经钩子
364
+ expect(a.error?.code).toBe("sandbox-provision-failed");
365
+ expect(a.error?.phase).toBe("sandbox.create");
366
+ expect(a.error?.cause?.name).toBe("SystemError");
367
+ expect(a.error?.cause?.code).toBe("ENOENT");
368
+ // diagnostics:message 与 data 的字符串值经钩子;code/operation/level 原样
369
+ expect(a.diagnostics).toHaveLength(1);
370
+ expect(a.diagnostics![0]!.message).toBe("cleanup left <repo>/tmp behind");
371
+ expect(a.diagnostics![0]!.data).toEqual({ path: "<repo>/tmp", attempts: 2 });
372
+ expect(a.diagnostics![0]!.code).toBe("teardown-failed");
373
+ expect(a.diagnostics![0]!.phase).toBe("sandbox.teardown");
300
374
  const b = items.find((i) => i.evalId === "B")!;
301
- expect(b.assertions[0]!.detail).toBe("missing text under <repo>/src");
302
- expect(b.assertions[0]!.evidence).toBe("checked <repo>/src/app.ts");
375
+ const bAssert = b.assertions[0]!;
376
+ expect(bAssert.detail).toBe("missing text under <repo>/src");
377
+ expect(bAssert.outcome !== "unavailable" ? bAssert.evidence : undefined).toBe("checked <repo>/src/app.ts");
303
378
  // 身份字段不经 redact
304
379
  expect(items.every((i) => i.experimentId === "exp/x")).toBe(true);
305
380
  });
@@ -307,7 +382,7 @@ describe("AttemptList.data", () => {
307
382
  it("不预设只看失败:passed 的 attempt 同样列出,原样携带 assertions", async () => {
308
383
  const s = snap({
309
384
  experimentId: "exp/x",
310
- results: [res("A", "passed", { assertions: [{ name: "ok", severity: "gate", passed: true, score: 1 }] })],
385
+ results: [res("A", "passed", { assertions: [{ name: "ok", severity: "gate", outcome: "passed" as const, score: 1 }] })],
311
386
  });
312
387
  const items = await AttemptList.data([s]);
313
388
  expect(items).toHaveLength(1);
@@ -324,10 +399,10 @@ describe("两级聚合引擎", () => {
324
399
  experimentId: "exp/x",
325
400
  results: [res("A", "passed"), res("B", "failed"), res("B", "failed"), res("B", "failed")],
326
401
  });
327
- const data = await MetricTable.data([s], { rows: "agent", columns: [passRate] });
402
+ const data = await MetricTable.data([s], { rows: "agent", columns: [taskPassRate] });
328
403
  expect(data.dimension).toBe("agent");
329
404
  expect(data.rows).toHaveLength(1);
330
- const cell = data.rows[0].cells["pass-rate"];
405
+ const cell = data.rows[0].cells["task-pass-rate"];
331
406
  expect(cell.value).toBe(0.5);
332
407
  expect(cell.display).toBe("50%");
333
408
  expect(cell.samples).toBe(4);
@@ -347,17 +422,17 @@ describe("两级聚合引擎", () => {
347
422
  experimentId: "exp/x",
348
423
  results: [res("A", "failed"), res("A", "failed"), res("B", "failed"), res("B", "passed")],
349
424
  });
350
- const data = await MetricTable.data([s], { rows: "agent", columns: [passAtK, passRate] });
425
+ const data = await MetricTable.data([s], { rows: "agent", columns: [passAtK, taskPassRate] });
351
426
  // A: max(0,0)=0;B: max(0,1)=1 → (0+1)/2
352
427
  expect(data.rows[0].cells["pass@k"].value).toBe(0.5);
353
- // 对照:默认 mean/mean 的 passRate = (0 + 0.5)/2
354
- expect(data.rows[0].cells["pass-rate"].value).toBe(0.25);
428
+ // 对照:默认 mean/mean 的 taskPassRate = (0 + 0.5)/2
429
+ expect(data.rows[0].cells["task-pass-rate"].value).toBe(0.25);
355
430
  });
356
431
 
357
432
  it("skipped 是 null:不稀释均值,但计入 total(覆盖率如实)", async () => {
358
433
  const s = snap({ experimentId: "exp/x", results: [res("A", "passed"), res("B", "skipped")] });
359
- const data = await MetricTable.data([s], { rows: "agent", columns: [passRate] });
360
- const cell = data.rows[0].cells["pass-rate"];
434
+ const data = await MetricTable.data([s], { rows: "agent", columns: [taskPassRate] });
435
+ const cell = data.rows[0].cells["task-pass-rate"];
361
436
  expect(cell.value).toBe(1); // B 整桶为 null,不参与 across,不是 0.5
362
437
  expect(cell.samples).toBe(1);
363
438
  expect(cell.total).toBe(2);
@@ -366,8 +441,8 @@ describe("两级聚合引擎", () => {
366
441
 
367
442
  it("全组 null → value null、display 兜底,不编 0;refs 必填(空数组)", async () => {
368
443
  const s = snap({ experimentId: "exp/x", results: [res("A", "skipped")] });
369
- const data = await MetricTable.data([s], { rows: "agent", columns: [passRate] });
370
- const cell = data.rows[0].cells["pass-rate"];
444
+ const data = await MetricTable.data([s], { rows: "agent", columns: [taskPassRate] });
445
+ const cell = data.rows[0].cells["task-pass-rate"];
371
446
  expect(cell.value).toBeNull();
372
447
  expect(cell.display).toBe("—");
373
448
  expect(cell.samples).toBe(0);
@@ -402,18 +477,18 @@ describe("两级聚合引擎", () => {
402
477
  res("A", "passed", { attempt: 2 }),
403
478
  ],
404
479
  });
405
- const data = await MetricTable.data([s], { rows: byParity, columns: [passRate] });
480
+ const data = await MetricTable.data([s], { rows: byParity, columns: [taskPassRate] });
406
481
  expect(data.dimension).toBe("parity");
407
- const byKey = Object.fromEntries(data.rows.map((r) => [r.key, r.cells["pass-rate"].value]));
482
+ const byKey = Object.fromEntries(data.rows.map((r) => [r.key, r.cells["task-pass-rate"].value]));
408
483
  // 同一道题的 attempt 分进两组:even 组内 [1,1] 折成 1,odd 组内 [0] 折成 0
409
484
  expect(byKey).toEqual({ even: 1, odd: 0 });
410
485
  });
411
486
 
412
487
  it("同一次计算里指标重名是错误", async () => {
413
- const dup = defineMetric({ name: "pass-rate", value: () => 1 });
488
+ const dup = defineMetric({ name: "task-pass-rate", value: () => 1 });
414
489
  const s = snap({ experimentId: "exp/x", results: [res("A", "passed")] });
415
- await expect(MetricTable.data([s], { rows: "agent", columns: [passRate, dup] })).rejects.toThrow(
416
- /Duplicate metric name "pass-rate"/,
490
+ await expect(MetricTable.data([s], { rows: "agent", columns: [taskPassRate, dup] })).rejects.toThrow(
491
+ /Duplicate metric name "task-pass-rate"/,
417
492
  );
418
493
  });
419
494
 
@@ -423,16 +498,16 @@ describe("两级聚合引擎", () => {
423
498
  const none = snap({ experimentId: "exp/none", agent: "none", results: [res("A", "skipped", { agent: "none" })] });
424
499
  const data = await MetricTable.data([none, bad, good], {
425
500
  rows: "agent",
426
- columns: [passRate],
427
- sort: passRate,
501
+ columns: [taskPassRate],
502
+ sort: taskPassRate,
428
503
  });
429
504
  expect(data.rows.map((r) => r.key)).toEqual(["good", "bad", "none"]);
430
505
  });
431
506
 
432
507
  it("列键是字面量联合:拼错列名编译不过", async () => {
433
508
  const s = snap({ experimentId: "exp/x", results: [res("A", "passed")] });
434
- const data = await MetricTable.data([s], { rows: "agent", columns: [passRate, costUSD] });
435
- const cell: MetricCell = data.rows[0].cells[passRate.name]; // 键锚在指标对象上
509
+ const data = await MetricTable.data([s], { rows: "agent", columns: [taskPassRate, costUSD] });
510
+ const cell: MetricCell = data.rows[0].cells[taskPassRate.name]; // 键锚在指标对象上
436
511
  expect(cell.value).toBe(1);
437
512
  // @ts-expect-error 列里没有这个键 —— 编译期挡住,不是运行时 undefined
438
513
  data.rows[0].cells["pass-rat"];
@@ -445,7 +520,7 @@ describe("examScore", () => {
445
520
  it("errored(断言空数组)得 0 —— 不因「gate 全过」空真得满分", async () => {
446
521
  const s = snap({
447
522
  experimentId: "exp/x",
448
- results: [res("A", "errored", { assertions: [], error: "adapter crashed" })],
523
+ results: [res("A", "errored", { assertions: [], error: erroredWith("adapter crashed") })],
449
524
  });
450
525
  const data = await MetricTable.data([s], { rows: "agent", columns: [examScore] });
451
526
  const cell = data.rows[0].cells["exam-score"];
@@ -470,7 +545,7 @@ describe("examScore", () => {
470
545
  assertions: [
471
546
  softAssertion("judge-1", 0.5),
472
547
  softAssertion("judge-2", 1),
473
- { name: "includes", severity: "gate", score: 1, passed: true },
548
+ { name: "includes", severity: "gate", score: 1, outcome: "passed" as const },
474
549
  ],
475
550
  }),
476
551
  ],
@@ -479,7 +554,7 @@ describe("examScore", () => {
479
554
  experimentId: "exp/b",
480
555
  results: [
481
556
  res("B", "passed", {
482
- assertions: [{ name: "includes", severity: "gate", score: 1, passed: true }],
557
+ assertions: [{ name: "includes", severity: "gate", score: 1, outcome: "passed" as const }],
483
558
  }),
484
559
  ],
485
560
  });
@@ -496,6 +571,62 @@ describe("examScore", () => {
496
571
  });
497
572
  });
498
573
 
574
+ // ───────────────────────── 三个通过率指标(docs/feature/reports/library.md「内置指标」)─────────────────────────
575
+ //
576
+ // 同一份 passed / failed / errored / skipped 混合 fixture 上,三个口径必须给出三个不同的数,
577
+ // 任何一处偷懒复用另一个公式都会在这里露馅:
578
+ // taskPassRate:errored → null 不进分母 → (1 + 0) / 2 = 50%
579
+ // executionReliability:errored → 0,passed/failed → 1 → (1 + 1 + 0) / 3 ≈ 66.7%
580
+ // endToEndPassRate:failed/errored → 0 → (1 + 0 + 0) / 3 ≈ 33.3%
581
+
582
+ describe("taskPassRate / executionReliability / endToEndPassRate", () => {
583
+ const mixed = () =>
584
+ snap({
585
+ experimentId: "exp/mixed",
586
+ results: [
587
+ res("A", "passed"),
588
+ res("B", "failed"),
589
+ res("C", "errored", { error: erroredWith("adapter crashed") }),
590
+ res("D", "skipped"),
591
+ ],
592
+ });
593
+
594
+ it("taskPassRate:errored 记 null 不进分母 —— 基建故障不伪装成 Agent 答错", async () => {
595
+ const data = await MetricTable.data([mixed()], { rows: "agent", columns: [taskPassRate] });
596
+ const cell = data.rows[0].cells["task-pass-rate"];
597
+ expect(cell.value).toBeCloseTo(0.5, 10); // (1 + 0) / 2:errored 与 skipped 都不在分母
598
+ expect(cell.samples).toBe(2); // errored、skipped 都是 null,不算有效样本
599
+ expect(cell.total).toBe(4);
600
+ });
601
+
602
+ it("executionReliability:跑到可判定(passed/failed)= 1,errored = 0,skipped → null", async () => {
603
+ const data = await MetricTable.data([mixed()], { rows: "agent", columns: [executionReliability] });
604
+ const cell = data.rows[0].cells["execution-reliability"];
605
+ expect(cell.value).toBeCloseTo(2 / 3, 10);
606
+ expect(cell.samples).toBe(3); // errored 测得了(= 0),skipped 测不了
607
+ expect(cell.total).toBe(4);
608
+ });
609
+
610
+ it("endToEndPassRate:failed / errored 都记 0 —— 端到端合成,哪边拖累用前两个拆开看", async () => {
611
+ const data = await MetricTable.data([mixed()], { rows: "agent", columns: [endToEndPassRate] });
612
+ const cell = data.rows[0].cells["end-to-end-pass-rate"];
613
+ expect(cell.value).toBeCloseTo(1 / 3, 10);
614
+ expect(cell.samples).toBe(3);
615
+ expect(cell.total).toBe(4);
616
+ });
617
+
618
+ it("全部 errored:taskPassRate 缺数据(null),executionReliability 如实 0 —— 两个指标不互相冒充", async () => {
619
+ const s = snap({
620
+ experimentId: "exp/broken",
621
+ results: [res("A", "errored", { error: erroredWith("boom") })],
622
+ });
623
+ const task = await MetricTable.data([s], { rows: "agent", columns: [taskPassRate] });
624
+ expect(task.rows[0].cells["task-pass-rate"].value).toBeNull(); // 没有一道题真正被答过
625
+ const reliability = await MetricTable.data([s], { rows: "agent", columns: [executionReliability] });
626
+ expect(reliability.rows[0].cells["execution-reliability"].value).toBe(0);
627
+ });
628
+ });
629
+
499
630
  // ───────────────────────── 内置指标口径 ─────────────────────────
500
631
 
501
632
  describe("内置指标", () => {
@@ -656,7 +787,7 @@ describe("MetricScatter.data", () => {
656
787
  points: "experiment",
657
788
  series: "agent",
658
789
  x: costUSD,
659
- y: passRate,
790
+ y: taskPassRate,
660
791
  });
661
792
  expect(data.points).toBe("experiment");
662
793
  expect(data.series).toBe("agent");
@@ -681,7 +812,7 @@ describe("flag()", () => {
681
812
  const withFlags = (id: string, flags: Record<string, unknown> | undefined, verdict: Verdict) =>
682
813
  snap({
683
814
  experimentId: id,
684
- results: [res("A", verdict, { experimentId: id, experiment: { id, flags } })],
815
+ results: [res("A", verdict, { experimentId: id, experiment: { flags, runs: 1, earlyExit: true, selectedEvalIds: [] } as import("../types.ts").ExperimentRunInfo })],
685
816
  });
686
817
 
687
818
  it("MetricLine.data:x 收 flag、按 experiment 聚合;未声明的作轴 x=null 报数", async () => {
@@ -691,7 +822,7 @@ describe("flag()", () => {
691
822
  const data = await MetricLine.data([s1, s2, legacy], {
692
823
  x: flag("latencyMs", { label: "Simulated latency", unit: "ms" }),
693
824
  series: flag("agents", { label: (v) => `${v} agents` }),
694
- y: passRate,
825
+ y: taskPassRate,
695
826
  });
696
827
  expect(data.x).toEqual({ key: "latencyMs", label: "Simulated latency", unit: "ms" });
697
828
  expect(data.series).toBe("agents");
@@ -715,13 +846,84 @@ describe("flag()", () => {
715
846
  const s2 = withFlags("exp/b", { agents: 16 }, "failed");
716
847
  const data = await MetricTable.data([s1, s2], {
717
848
  rows: flag("agents", { label: (v) => `${v} agents` }),
718
- columns: [passRate],
849
+ columns: [taskPassRate],
719
850
  });
720
851
  expect(data.dimension).toBe("agents");
721
852
  expect(data.rows.map((r) => r.key)).toEqual(["1 agents", "16 agents"]);
722
853
  });
723
854
  });
724
855
 
856
+ // ───────────────────────── config():顶层运行配置当维度与轴 ─────────────────────────
857
+ // flag() 只读 ExperimentDef.flags 的显式声明;model / reasoningEffort / budget / runs 这类
858
+ // 顶层运行配置用 config() 读快照的 ExperimentRunInfo 投影,外加桥接 model / agent 两个键
859
+ // (docs/feature/reports/library.md「维度与 flags」)。
860
+
861
+ describe("config()", () => {
862
+ /** 快照声明 ExperimentRunInfo 投影;字段全集以 docs 的接口为准,这里按键名读值。 */
863
+ const withConfig = (id: string, info: Record<string, unknown> | undefined, verdict: Verdict, model?: string) => {
864
+ const s = snap({ experimentId: id, model, results: [res("A", verdict, { experimentId: id })] });
865
+ if (info) s.experiment = info as unknown as (typeof s)["experiment"];
866
+ return s;
867
+ };
868
+
869
+ it("config 当维度用:按投影值分组;未投影的 experiment 如实归 (unset)", async () => {
870
+ const high = withConfig("effort/high", { reasoningEffort: "high" }, "passed");
871
+ const low = withConfig("effort/low", { reasoningEffort: "low" }, "failed");
872
+ const legacy = withConfig("effort/legacy", undefined, "passed");
873
+ const data = await MetricTable.data([high, low, legacy], {
874
+ rows: config("reasoningEffort", { label: "Reasoning effort" }),
875
+ columns: [taskPassRate],
876
+ });
877
+ expect(data.dimension).toBe("reasoningEffort");
878
+ expect(data.rows.map((r) => r.key)).toEqual(["high", "low", "(unset)"]);
879
+ });
880
+
881
+ it("config 当数值轴用(MetricLine.data 的 x):非数值或未投影 → x=null 不伪造,注脚可数", async () => {
882
+ const b1 = withConfig("budget/one", { budget: 1 }, "passed");
883
+ const b2 = withConfig("budget/two", { budget: 2 }, "failed");
884
+ const none = withConfig("budget/none", { reasoningEffort: "high" }, "passed"); // 没投影 budget
885
+ const data = await MetricLine.data([b1, b2, none], {
886
+ x: config("budget", { label: "Budget", unit: "USD" }),
887
+ y: taskPassRate,
888
+ });
889
+ expect(data.x).toEqual({ key: "budget", label: "Budget", unit: "USD" });
890
+ const one = data.rows.find((r) => r.key === "budget/one")!;
891
+ expect(one.x).toBe(1);
892
+ expect(one.y.value).toBe(1);
893
+ const missing = data.rows.find((r) => r.key === "budget/none")!;
894
+ expect(missing.x).toBeNull();
895
+ expect(missing.xDisplay).toBe("");
896
+ expect(data.rows.filter((r) => r.x === null)).toHaveLength(1);
897
+ });
898
+
899
+ it("model / agent 两个键桥接到快照顶层权威字段,ExperimentRunInfo 不复制这两个事实", async () => {
900
+ const mini = withConfig("m/mini", { budget: 1 }, "passed", "gpt-mini");
901
+ const maxi = withConfig("m/maxi", { budget: 1 }, "failed", "gpt-maxi");
902
+ const data = await MetricTable.data([mini, maxi], {
903
+ rows: config("model"),
904
+ columns: [taskPassRate],
905
+ });
906
+ expect(data.rows.map((r) => r.key)).toEqual(["gpt-mini", "gpt-maxi"]);
907
+
908
+ const byAgent = await MetricTable.data([mini, maxi], { rows: config("agent"), columns: [taskPassRate] });
909
+ expect(byAgent.rows.map((r) => r.key)).toEqual(["agent-x"]); // res() 默认 agent-x
910
+ });
911
+
912
+ it("label 函数折组名,与 flag() 同一套规则", async () => {
913
+ const s1 = withConfig("runs/3", { runs: 3 }, "passed");
914
+ const s2 = withConfig("runs/5", { runs: 5 }, "passed");
915
+ const data = await MetricTable.data([s1, s2], {
916
+ rows: config("runs", { label: (v) => `${v} runs` }),
917
+ columns: [taskPassRate],
918
+ });
919
+ expect(data.rows.map((r) => r.key)).toEqual(["3 runs", "5 runs"]);
920
+ });
921
+
922
+ it("空 name 直接报错,与 flag() 同一门槛", () => {
923
+ expect(() => config("")).toThrow(/non-empty string/);
924
+ });
925
+ });
926
+
725
927
  // ───────────────────────── MetricTable.data:没有实体下钻 ─────────────────────────
726
928
 
727
929
  describe("MetricTable.data", () => {
@@ -730,7 +932,7 @@ describe("MetricTable.data", () => {
730
932
  experimentId: "exp/x",
731
933
  results: [res("A", "passed"), res("B", "failed")],
732
934
  });
733
- const data = await MetricTable.data([s], { rows: "experiment", columns: [passRate] });
935
+ const data = await MetricTable.data([s], { rows: "experiment", columns: [taskPassRate] });
734
936
  expect(data.rows).toHaveLength(1);
735
937
  expect(data.rows[0]!.meta).not.toHaveProperty("subRows");
736
938
  // 展开到 Eval 是 ExperimentList 的职责,不在这里验证 —— 见 ExperimentList.data 的测试组。
@@ -745,10 +947,10 @@ describe("MetricTable.data", () => {
745
947
  describe("reasonFor", () => {
746
948
  it("同一 result 同时含 error、失败 gate、失败 soft → 只显示 error", () => {
747
949
  const result = res("A", "errored", {
748
- error: "adapter crashed",
950
+ error: erroredWith("adapter crashed"),
749
951
  assertions: [
750
- { name: "includes", severity: "gate", score: 0, passed: false, detail: "missing text" },
751
- softAssertion("judge", 0.2, { passed: false }),
952
+ { name: "includes", severity: "gate", score: 0, outcome: "failed" as const, detail: "missing text" },
953
+ softAssertion("judge", 0.2, { outcome: "failed" as const }),
752
954
  ],
753
955
  });
754
956
  expect(reasonFor(result)).toBe("adapter crashed");
@@ -757,21 +959,21 @@ describe("reasonFor", () => {
757
959
  it("skipReason 优先于 gate 断言,但让位给 error", () => {
758
960
  const result = res("A", "skipped", {
759
961
  skipReason: "missing fixture",
760
- assertions: [{ name: "includes", severity: "gate", score: 0, passed: false }],
962
+ assertions: [{ name: "includes", severity: "gate", score: 0, outcome: "failed" as const }],
761
963
  });
762
964
  expect(reasonFor(result)).toBe("missing fixture");
763
965
  });
764
966
 
765
- it("多个失败 gate 按原始声明顺序全部保留,拼接成一行;失败 soft 不出现在 reason ", () => {
967
+ it("只展示第一条失败 gate,其余 gate 计数;失败 soft 不进入 gate 计数", () => {
766
968
  const result = res("A", "failed", {
767
969
  assertions: [
768
- softAssertion("judge-first", 0.1, { passed: false }), // 声明顺序第一,但 soft 不进 reason
769
- { name: "includes", severity: "gate", score: 0, passed: false, detail: "missing text" },
770
- { name: "matches", severity: "gate", score: 0, passed: false }, // 无 detail,只有 name
771
- softAssertion("judge-last", 0.3, { passed: false }), // soft 依旧不进 reason
970
+ softAssertion("judge-first", 0.1, { outcome: "failed" as const }), // 声明顺序第一,但 soft 不进 reason
971
+ { name: "includes", groupPath: ["answer contains required text"], severity: "gate", score: 0, outcome: "failed" as const, expected: "contains Brooklyn", received: "Manhattan" },
972
+ { name: "matches", severity: "gate", score: 0, outcome: "failed" as const }, // 无 detail,只有 name
973
+ softAssertion("judge-last", 0.3, { outcome: "failed" as const }), // soft 依旧不进 reason
772
974
  ],
773
975
  });
774
- expect(reasonFor(result)).toBe("includes: missing text, matches");
976
+ expect(reasonFor(result)).toBe("gate: answer contains required text · includes · expected contains Brooklyn · received Manhattan · +1 more failures");
775
977
  });
776
978
 
777
979
  it("都缺席 → 无原因", () => {
@@ -818,6 +1020,22 @@ describe("RunOverview.data", () => {
818
1020
  const data = await RunOverview.data(selection([s], [warning]));
819
1021
  expect(data.warnings).toEqual([warning]);
820
1022
  });
1023
+
1024
+ it("默认成功率把 errored 记 0:2 passed / 5 errored = 2/7,不是 100% 2/7", async () => {
1025
+ const s = snap({
1026
+ experimentId: "exp/unstable",
1027
+ results: [
1028
+ res("pass/a", "passed"),
1029
+ res("pass/b", "passed"),
1030
+ ...Array.from({ length: 5 }, (_, i) => res(`error/${i}`, "errored", { error: erroredWith("boom") })),
1031
+ ],
1032
+ });
1033
+ const data = await RunOverview.data([s]);
1034
+ expect(data.totals.passRate.value).toBeCloseTo(2 / 7, 10);
1035
+ expect(data.totals.passRate.display).toBe("28.6%");
1036
+ expect(data.totals.passRate.samples).toBe(7);
1037
+ expect(data.totals.passRate.total).toBe(7);
1038
+ });
821
1039
  });
822
1040
 
823
1041
  // ───────────────────────── GroupSummary.data ─────────────────────────
@@ -862,16 +1080,6 @@ describe("GroupSummary.data", () => {
862
1080
  expect(data.lastRunAt).toBe(expB.startedAt);
863
1081
  });
864
1082
 
865
- it('跨 experiment 同名 eval 不合并:两个 experiment 各自的 "x" 独立计一票,不是被折成一票', async () => {
866
- const a = snap({ experimentId: "exp/a", results: [res("x", "passed")] });
867
- const b = snap({ experimentId: "exp/b", results: [res("x", "failed")] });
868
- const data = await GroupSummary.data([a, b]);
869
- expect(data.experiments).toBe(2);
870
- expect(data.evals).toBe(2); // 只按 eval id 折叠(误把两个 experiment 的 "x" 当一道题)会变成 1
871
- expect(data.verdicts).toEqual({ passed: 1, failed: 1, errored: 0, skipped: 0 });
872
- expect(data.passRate.value).toBeCloseTo(0.5, 10); // 误合并会因为「任一轮过即过」变成 100%
873
- });
874
-
875
1083
  it("全组没有任何 attempt 报成本 → totalCostUSD null,不编 0", async () => {
876
1084
  const s = snap({ experimentId: "exp/x", results: [res("A", "passed"), res("B", "failed")] });
877
1085
  const data = await GroupSummary.data([s]);
@@ -888,30 +1096,12 @@ describe("GroupSummary.data", () => {
888
1096
  expect(data.passRate.total).toBe(2);
889
1097
  });
890
1098
 
891
- it("lastRunAt 只从传入的组 Selection 取值,不读全局最新 —— 组外快照更晚也不影响", async () => {
892
- const inGroup = snap({
893
- experimentId: "exp/a",
894
- runStartedAt: "2026-07-01T08:00:00Z",
895
- results: [res("A", "passed")],
896
- });
897
- const outOfGroup = snap({
898
- experimentId: "exp/b",
899
- runStartedAt: "2026-07-09T08:00:00Z",
900
- results: [res("B", "passed")],
901
- });
902
- const sel = selection([inGroup, outOfGroup], []);
903
- const scoped = sel.filter((s) => s.experimentId === "exp/a"); // 只留组内快照,模拟按实验组收窄的 Selection
904
- const data = await GroupSummary.data(scoped);
905
- expect(data.lastRunAt).toBe(inGroup.startedAt);
906
- expect(data.lastRunAt).not.toBe(outOfGroup.startedAt);
907
- });
908
-
909
1099
  it('MetricTable rows: "experiment" 的 verdicts meta 复用同一份 eval 级统计,行为不变(回归)', async () => {
910
1100
  const s = snap({
911
1101
  experimentId: "exp/x",
912
1102
  results: [res("A", "passed", { attempt: 0 }), res("A", "failed", { attempt: 1 }), res("B", "failed")],
913
1103
  });
914
- const data = await MetricTable.data([s], { rows: "experiment", columns: [passRate] });
1104
+ const data = await MetricTable.data([s], { rows: "experiment", columns: [taskPassRate] });
915
1105
  // A 两轮折成 passed(任一轮过即过),B 失败:1 passed / 1 failed
916
1106
  expect(data.rows[0].meta?.verdicts).toEqual({ passed: 1, failed: 1, errored: 0, skipped: 0 });
917
1107
  });
@@ -931,7 +1121,7 @@ describe("DeltaTable.data", () => {
931
1121
  });
932
1122
  const data = await DeltaTable.data([base, plus], {
933
1123
  pairs: [{ a: "exp/base", b: "exp/plus", label: "memory" }],
934
- metrics: [passRate, costUSD],
1124
+ metrics: [taskPassRate, costUSD],
935
1125
  });
936
1126
  expect(data.rows).toHaveLength(1);
937
1127
  const row = data.rows[0];
@@ -939,7 +1129,7 @@ describe("DeltaTable.data", () => {
939
1129
  expect(row.a).toEqual({ experimentId: "exp/base" });
940
1130
  expect(row.b).toEqual({ experimentId: "exp/plus" });
941
1131
 
942
- const pass = row.cells["pass-rate"];
1132
+ const pass = row.cells["task-pass-rate"];
943
1133
  expect(pass.a.value).toBe(0.5);
944
1134
  expect(pass.b.value).toBe(1);
945
1135
  expect(pass.delta).toBeCloseTo(0.5);
@@ -953,9 +1143,9 @@ describe("DeltaTable.data", () => {
953
1143
 
954
1144
  const flat = await DeltaTable.data([base, base], {
955
1145
  pairs: [{ a: "exp/base", b: "exp/base", label: "same" }],
956
- metrics: [passRate],
1146
+ metrics: [taskPassRate],
957
1147
  });
958
- expect(flat.rows[0].cells["pass-rate"].display).toBe("±0");
1148
+ expect(flat.rows[0].cells["task-pass-rate"].display).toBe("±0");
959
1149
  });
960
1150
 
961
1151
  it("时间轴对比:pairs 的 a/b 收快照键 <experimentId> @ <startedAt>", async () => {
@@ -977,9 +1167,9 @@ describe("DeltaTable.data", () => {
977
1167
  label: "this week vs last",
978
1168
  },
979
1169
  ],
980
- metrics: [passRate],
1170
+ metrics: [taskPassRate],
981
1171
  });
982
- const cell = data.rows[0].cells["pass-rate"];
1172
+ const cell = data.rows[0].cells["task-pass-rate"];
983
1173
  expect(cell.a.value).toBe(0); // 旧快照那份
984
1174
  expect(cell.b.value).toBe(1); // 新快照那份
985
1175
  expect(cell.display).toBe("+100%");
@@ -1011,8 +1201,8 @@ describe("身份键去重", () => {
1011
1201
  [older, newer],
1012
1202
  [newer, older],
1013
1203
  ]) {
1014
- const data = await MetricTable.data(order, { rows: "agent", columns: [passRate] });
1015
- const cell = data.rows[0].cells["pass-rate"];
1204
+ const data = await MetricTable.data(order, { rows: "agent", columns: [taskPassRate] });
1205
+ const cell = data.rows[0].cells["task-pass-rate"];
1016
1206
  expect(cell.total).toBe(1); // 两份只算一份
1017
1207
  expect(cell.value).toBe(1); // 留的是最新 run 里的 passed,与快照传入顺序无关
1018
1208
 
@@ -1078,10 +1268,10 @@ describe("MetricMatrix.data", () => {
1078
1268
  agent: "b1",
1079
1269
  results: [res("A", "failed", { agent: "b1" })], // b1 没跑 B
1080
1270
  });
1081
- const data = await MetricMatrix.data([a, b], { rows: "eval", columns: "agent", cell: passRate });
1271
+ const data = await MetricMatrix.data([a, b], { rows: "eval", columns: "agent", cell: taskPassRate });
1082
1272
  expect(data.rows).toBe("eval");
1083
1273
  expect(data.columns).toBe("agent");
1084
- expect(data.metric.key).toBe("pass-rate");
1274
+ expect(data.metric.key).toBe("task-pass-rate");
1085
1275
  expect(data.cells).toHaveLength(3); // A×a1、B×a1、A×b1;B×b1 不出现
1086
1276
  const find = (row: string, column: string) => data.cells.find((c) => c.row === row && c.column === column);
1087
1277
  expect(find("A", "a1")?.cell.value).toBe(1);
@@ -1090,7 +1280,20 @@ describe("MetricMatrix.data", () => {
1090
1280
  expect(find("B", "b1")).toBeUndefined();
1091
1281
  });
1092
1282
 
1093
- it("MetricBars.data 就是 MetricMatrix.data 的别名(同一个函数)", () => {
1094
- expect(MetricBars.data).toBe(MetricMatrix.data);
1283
+ it("MetricBars.data MetricMatrix.data 同一输入给出相同数据(别名契约,不锁函数引用)", async () => {
1284
+ const a = snap({
1285
+ experimentId: "exp/a",
1286
+ agent: "a1",
1287
+ results: [res("A", "passed", { agent: "a1" }), res("B", "failed", { agent: "a1" })],
1288
+ });
1289
+ const b = snap({
1290
+ experimentId: "exp/b",
1291
+ agent: "b1",
1292
+ results: [res("A", "failed", { agent: "b1" })],
1293
+ });
1294
+ const options = { rows: "eval", columns: "agent", cell: taskPassRate } as const;
1295
+ const bars = await MetricBars.data([a, b], options);
1296
+ const matrix = await MetricMatrix.data([a, b], options);
1297
+ expect(bars).toEqual(matrix);
1095
1298
  });
1096
1299
  });