niceeval 0.6.1 → 0.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (296) hide show
  1. package/dist/agents/types.d.ts +67 -5
  2. package/dist/context/types.d.ts +32 -12
  3. package/dist/i18n/en.d.ts +54 -0
  4. package/dist/i18n/zh-CN.d.ts +55 -1
  5. package/dist/o11y/types.d.ts +16 -2
  6. package/dist/report/aggregate.d.ts +5 -3
  7. package/dist/report/aggregate.js +32 -5
  8. package/dist/report/built-ins/experiment-comparison.d.ts +39 -1
  9. package/dist/report/built-ins/experiment-comparison.js +116 -10
  10. package/dist/report/built-ins/index.d.ts +1 -0
  11. package/dist/report/built-ins/index.js +1 -1
  12. package/dist/report/components.d.ts +8 -2
  13. package/dist/report/components.js +3 -3
  14. package/dist/report/compute.d.ts +11 -18
  15. package/dist/report/compute.js +54 -34
  16. package/dist/report/flag.d.ts +16 -1
  17. package/dist/report/flag.js +19 -1
  18. package/dist/report/format.d.ts +16 -8
  19. package/dist/report/format.js +27 -12
  20. package/dist/report/index.d.ts +4 -3
  21. package/dist/report/index.js +5 -4
  22. package/dist/report/locale.d.ts +11 -2
  23. package/dist/report/locale.js +23 -5
  24. package/dist/report/metrics.d.ts +13 -1
  25. package/dist/report/metrics.js +65 -14
  26. package/dist/report/primitives.d.ts +6 -0
  27. package/dist/report/react/AttemptList.d.ts +2 -2
  28. package/dist/report/react/AttemptList.js +5 -6
  29. package/dist/report/react/EvalList.d.ts +1 -1
  30. package/dist/report/react/EvalList.js +0 -0
  31. package/dist/report/react/ExperimentComparison.d.ts +8 -0
  32. package/dist/report/react/ExperimentComparison.js +11 -0
  33. package/dist/report/react/ExperimentList.d.ts +2 -1
  34. package/dist/report/react/ExperimentList.js +8 -10
  35. package/dist/report/react/MetricScatter.js +5 -11
  36. package/dist/report/react/chart-math.d.ts +23 -6
  37. package/dist/report/react/chart-math.js +71 -19
  38. package/dist/report/react/fixtures.d.ts +3 -3
  39. package/dist/report/react/fixtures.js +21 -14
  40. package/dist/report/report.d.ts +5 -1
  41. package/dist/report/report.js +6 -2
  42. package/dist/report/text/faces.d.ts +1 -1
  43. package/dist/report/text/faces.js +42 -41
  44. package/dist/report/text/table.js +36 -5
  45. package/dist/report/types.d.ts +39 -21
  46. package/dist/results/types.d.ts +11 -0
  47. package/dist/runner/feedback/sink.d.ts +110 -0
  48. package/dist/runner/types.d.ts +513 -22
  49. package/dist/sandbox/docker.d.ts +23 -2
  50. package/dist/sandbox/e2b.d.ts +15 -1
  51. package/dist/sandbox/errors.d.ts +30 -3
  52. package/dist/sandbox/io-retry.d.ts +17 -0
  53. package/dist/sandbox/registry.d.ts +2 -0
  54. package/dist/sandbox/resolve.d.ts +18 -5
  55. package/dist/sandbox/retry.d.ts +11 -1
  56. package/dist/sandbox/types.d.ts +39 -5
  57. package/dist/sandbox/vercel.d.ts +7 -1
  58. package/dist/scoring/coverage.d.ts +30 -0
  59. package/dist/scoring/display.d.ts +21 -0
  60. package/dist/scoring/display.js +120 -0
  61. package/dist/scoring/types.d.ts +103 -20
  62. package/dist/shared/aggregate.d.ts +1 -0
  63. package/dist/shared/aggregate.js +3 -3
  64. package/dist/shared/types.d.ts +28 -0
  65. package/dist/tty-line.d.ts +0 -4
  66. package/dist/util.d.ts +23 -0
  67. package/docs-site/zh/concepts/adapter.mdx +22 -4
  68. package/docs-site/zh/concepts/experiment.mdx +1 -1
  69. package/docs-site/zh/concepts/overview.mdx +6 -6
  70. package/docs-site/zh/guides/agent-feedback-loop.mdx +28 -26
  71. package/docs-site/zh/guides/authoring.mdx +33 -0
  72. package/docs-site/zh/guides/ci-integration.mdx +23 -12
  73. package/docs-site/zh/guides/connect-your-agent.mdx +29 -3
  74. package/docs-site/zh/guides/custom-reports.mdx +29 -34
  75. package/docs-site/zh/guides/dataset-fanout.mdx +25 -3
  76. package/docs-site/zh/guides/debug-sandbox.mdx +57 -0
  77. package/docs-site/zh/guides/debugging.mdx +210 -0
  78. package/docs-site/zh/guides/experiments.mdx +10 -3
  79. package/docs-site/zh/guides/official-adapters.mdx +26 -2
  80. package/docs-site/zh/guides/publish-report.mdx +30 -16
  81. package/docs-site/zh/guides/report-components.mdx +42 -30
  82. package/docs-site/zh/guides/reporters.mdx +2 -2
  83. package/docs-site/zh/guides/results-data.mdx +17 -9
  84. package/docs-site/zh/guides/runner.mdx +17 -7
  85. package/docs-site/zh/guides/sandbox-agent.mdx +56 -7
  86. package/docs-site/zh/guides/sandbox-providers.mdx +257 -9
  87. package/docs-site/zh/guides/scoring-guide.mdx +4 -4
  88. package/docs-site/zh/guides/viewing-results.mdx +79 -36
  89. package/docs-site/zh/guides/write-experiment.mdx +5 -3
  90. package/docs-site/zh/guides/write-send.mdx +17 -1
  91. package/docs-site/zh/index.mdx +1 -1
  92. package/docs-site/zh/reference/builtin-agents.mdx +27 -0
  93. package/docs-site/zh/reference/capabilities.mdx +2 -2
  94. package/docs-site/zh/reference/cli.mdx +33 -7
  95. package/docs-site/zh/reference/define-agent.mdx +57 -4
  96. package/docs-site/zh/reference/define-config.mdx +1 -1
  97. package/docs-site/zh/reference/define-eval.mdx +42 -9
  98. package/docs-site/zh/reference/expect.mdx +26 -1
  99. package/package.json +5 -1
  100. package/src/agents/ai-sdk-otel.test.ts +1 -0
  101. package/src/agents/ai-sdk.test.ts +3 -0
  102. package/src/agents/ai-sdk.ts +3 -0
  103. package/src/agents/bub-install-spec.test.ts +34 -0
  104. package/src/agents/bub-install-spec.ts +32 -0
  105. package/src/agents/bub.ts +31 -32
  106. package/src/agents/claude-code.test.ts +130 -9
  107. package/src/agents/claude-code.ts +76 -4
  108. package/src/agents/codex.test.ts +189 -40
  109. package/src/agents/codex.ts +155 -14
  110. package/src/agents/coding-cli-versions.test.ts +15 -0
  111. package/src/agents/coding-cli-versions.ts +3 -0
  112. package/src/agents/index.ts +11 -0
  113. package/src/agents/langgraph.test.ts +204 -0
  114. package/src/agents/langgraph.ts +495 -0
  115. package/src/agents/marketplace.ts +85 -0
  116. package/src/agents/native-config.test.ts +179 -0
  117. package/src/agents/native-config.ts +267 -0
  118. package/src/agents/openai-compat.test.ts +1 -0
  119. package/src/agents/openclaw.test.ts +31 -0
  120. package/src/agents/openclaw.ts +171 -0
  121. package/src/agents/plugin-config.test.ts +1 -0
  122. package/src/agents/sdk-streams.test.ts +79 -0
  123. package/src/agents/sdk-streams.ts +55 -10
  124. package/src/agents/skills.test.ts +1 -0
  125. package/src/agents/streaming.test.ts +3 -9
  126. package/src/agents/types.ts +68 -5
  127. package/src/agents/ui-message-stream.test.ts +3 -0
  128. package/src/cli.ts +411 -108
  129. package/src/context/context.test.ts +51 -12
  130. package/src/context/context.ts +161 -29
  131. package/src/context/session.test.ts +1 -0
  132. package/src/context/session.ts +114 -6
  133. package/src/context/types.ts +30 -12
  134. package/src/define.test.ts +13 -8
  135. package/src/define.ts +25 -4
  136. package/src/expect/index.ts +53 -23
  137. package/src/i18n/en.ts +64 -2
  138. package/src/i18n/zh-CN.ts +65 -3
  139. package/src/o11y/cost.test.ts +1 -0
  140. package/src/o11y/execution-tree.test.ts +1 -20
  141. package/src/o11y/otlp/mappers/claude-code.test.ts +1 -0
  142. package/src/o11y/otlp/parse.test.ts +1 -0
  143. package/src/o11y/otlp/turn-otel.test.ts +1 -0
  144. package/src/o11y/parsers/bub.test.ts +1 -0
  145. package/src/o11y/parsers/claude-code.test.ts +1 -34
  146. package/src/o11y/parsers/openclaw.test.ts +154 -0
  147. package/src/o11y/parsers/openclaw.ts +310 -0
  148. package/src/o11y/prices.json +746 -311
  149. package/src/o11y/tool-names.test.ts +1 -0
  150. package/src/o11y/types.ts +16 -2
  151. package/src/report/aggregate.ts +34 -5
  152. package/src/report/built-in-user-parity.test.tsx +110 -153
  153. package/src/report/built-ins/experiment-comparison.tsx +173 -13
  154. package/src/report/built-ins/index.ts +6 -1
  155. package/src/report/components.tsx +9 -3
  156. package/src/report/compute.ts +70 -40
  157. package/src/report/dual-render.test.tsx +194 -67
  158. package/src/report/flag.ts +30 -2
  159. package/src/report/format.ts +35 -11
  160. package/src/report/index.ts +22 -4
  161. package/src/report/locale.ts +25 -5
  162. package/src/report/metrics.ts +67 -14
  163. package/src/report/primitives.tsx +6 -0
  164. package/src/report/react/AttemptList.tsx +6 -31
  165. package/src/report/react/EvalList.tsx +0 -0
  166. package/src/report/react/ExperimentComparison.tsx +68 -0
  167. package/src/report/react/ExperimentList.tsx +15 -9
  168. package/src/report/react/MetricScatter.tsx +12 -14
  169. package/src/report/react/chart-math.test.ts +85 -0
  170. package/src/report/react/chart-math.ts +101 -22
  171. package/src/report/react/enhance.js +33 -1
  172. package/src/report/react/fixtures.ts +24 -17
  173. package/src/report/react/render.test.tsx +9 -64
  174. package/src/report/react/styles.css +73 -2
  175. package/src/report/report.test.ts +306 -98
  176. package/src/report/report.ts +6 -2
  177. package/src/report/text/faces.ts +47 -43
  178. package/src/report/text/table.ts +42 -5
  179. package/src/report/types.ts +41 -21
  180. package/src/results/annotated-source.test.ts +62 -9
  181. package/src/results/annotated-source.ts +64 -6
  182. package/src/results/attempt-evidence.test.ts +9 -7
  183. package/src/results/attempt-evidence.ts +15 -8
  184. package/src/results/attempt-source.ts +6 -3
  185. package/src/results/copy.ts +145 -55
  186. package/src/results/host-equivalence.test.ts +8 -6
  187. package/src/results/index.ts +2 -0
  188. package/src/results/locator.test.ts +1 -22
  189. package/src/results/open.ts +7 -1
  190. package/src/results/publish.ts +149 -0
  191. package/src/results/results.test.ts +85 -51
  192. package/src/results/truncate.ts +90 -0
  193. package/src/results/types.ts +7 -0
  194. package/src/results/writer.ts +31 -13
  195. package/src/runner/attempt.test.ts +138 -7
  196. package/src/runner/attempt.ts +603 -104
  197. package/src/runner/discover.test.ts +47 -0
  198. package/src/runner/discover.ts +36 -2
  199. package/src/runner/eval-source.test.ts +1 -27
  200. package/src/runner/feedback/agent.test.ts +504 -0
  201. package/src/runner/feedback/agent.ts +409 -0
  202. package/src/runner/feedback/ci.test.ts +562 -0
  203. package/src/runner/feedback/ci.ts +401 -0
  204. package/src/runner/feedback/coordinator.test.ts +317 -0
  205. package/src/runner/feedback/coordinator.ts +397 -0
  206. package/src/runner/feedback/failure.ts +40 -0
  207. package/src/runner/feedback/human.test.ts +616 -0
  208. package/src/runner/feedback/human.ts +535 -0
  209. package/src/runner/feedback/index.ts +66 -0
  210. package/src/runner/feedback/io.ts +78 -0
  211. package/src/runner/feedback/profile.test.ts +50 -0
  212. package/src/runner/feedback/profile.ts +58 -0
  213. package/src/runner/feedback/reducer.test.ts +395 -0
  214. package/src/runner/feedback/reducer.ts +260 -0
  215. package/src/runner/feedback/renderer.ts +82 -0
  216. package/src/runner/feedback/sink.ts +203 -0
  217. package/src/runner/feedback/testing.ts +106 -0
  218. package/src/runner/ledger.test.ts +230 -0
  219. package/src/runner/ledger.ts +329 -0
  220. package/src/runner/report.test.ts +128 -3
  221. package/src/runner/report.ts +33 -9
  222. package/src/runner/reporters/artifacts.ts +8 -2
  223. package/src/runner/reporters/braintrust.test.ts +8 -7
  224. package/src/runner/reporters/braintrust.ts +9 -2
  225. package/src/runner/reporters/index.ts +2 -2
  226. package/src/runner/reporters/json.test.ts +162 -0
  227. package/src/runner/reporters/json.ts +35 -8
  228. package/src/runner/reporters/shared.ts +1 -5
  229. package/src/runner/run.test.ts +760 -3
  230. package/src/runner/run.ts +242 -36
  231. package/src/runner/sandbox-prep.ts +3 -42
  232. package/src/runner/timing.ts +158 -0
  233. package/src/runner/types.ts +518 -22
  234. package/src/sandbox/checkpoint.test.ts +55 -0
  235. package/src/sandbox/checkpoint.ts +29 -8
  236. package/src/sandbox/cli-commands.ts +407 -0
  237. package/src/sandbox/docker.ts +115 -16
  238. package/src/sandbox/e2b-agent-template.test.ts +56 -0
  239. package/src/sandbox/e2b-agent-template.ts +94 -0
  240. package/src/sandbox/e2b.ts +74 -9
  241. package/src/sandbox/errors.ts +111 -4
  242. package/src/sandbox/index.ts +2 -0
  243. package/src/sandbox/io-retry.test.ts +58 -0
  244. package/src/sandbox/io-retry.ts +45 -0
  245. package/src/sandbox/keep-registry.test.ts +86 -0
  246. package/src/sandbox/keep-registry.ts +142 -0
  247. package/src/sandbox/keep.ts +178 -0
  248. package/src/sandbox/paths.test.ts +1 -0
  249. package/src/sandbox/paths.ts +19 -8
  250. package/src/sandbox/registry.ts +20 -3
  251. package/src/sandbox/resolve.ts +76 -11
  252. package/src/sandbox/retry.test.ts +70 -0
  253. package/src/sandbox/retry.ts +46 -4
  254. package/src/sandbox/types.ts +44 -6
  255. package/src/sandbox/vercel.ts +43 -20
  256. package/src/scoring/collector.ts +60 -17
  257. package/src/scoring/coverage.ts +95 -0
  258. package/src/scoring/diff.ts +81 -0
  259. package/src/scoring/display.test.ts +121 -0
  260. package/src/scoring/display.ts +133 -0
  261. package/src/scoring/evidence.test.ts +189 -0
  262. package/src/scoring/judge.test.ts +142 -0
  263. package/src/scoring/judge.ts +15 -18
  264. package/src/scoring/scoped.ts +217 -50
  265. package/src/scoring/types.ts +117 -20
  266. package/src/scoring/verdict.ts +16 -4
  267. package/src/shared/aggregate.ts +3 -2
  268. package/src/shared/types.ts +31 -0
  269. package/src/show/compose.ts +2 -2
  270. package/src/show/index.ts +21 -1
  271. package/src/show/render.ts +619 -104
  272. package/src/show/show.test.ts +235 -19
  273. package/src/tty-line.ts +8 -26
  274. package/src/util.test.ts +1 -0
  275. package/src/util.ts +41 -0
  276. package/src/view/app/components/AttemptModal.tsx +153 -2
  277. package/src/view/app/components/CodeView.tsx +32 -11
  278. package/src/view/app/components/CopyControls.tsx +2 -2
  279. package/src/view/app/i18n.ts +6 -0
  280. package/src/view/app/lib/attempt-route.test.ts +1 -0
  281. package/src/view/app/lib/verdict.ts +7 -9
  282. package/src/view/artifact-serving.test.ts +2 -1
  283. package/src/view/client-dist/app.css +1 -1
  284. package/src/view/client-dist/app.js +17 -17
  285. package/src/view/data.test.ts +1 -0
  286. package/src/view/data.ts +11 -1
  287. package/src/view/index.ts +11 -0
  288. package/src/view/server.ts +2 -0
  289. package/src/view/styles.css +3 -0
  290. package/src/view/view-report.test.ts +6 -5
  291. package/src/runner/reporters/console.ts +0 -70
  292. package/src/runner/reporters/live.test.ts +0 -56
  293. package/src/runner/reporters/live.ts +0 -247
  294. package/src/runner/reporters/quiet.test.ts +0 -66
  295. package/src/runner/reporters/quiet.ts +0 -49
  296. package/src/runner/reporters/table.ts +0 -277
@@ -1,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。 */
@@ -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
 
@@ -186,7 +210,8 @@ describe("ExperimentList.data", () => {
186
210
 
187
211
  const evalB = item.evalRows.find((e) => e.evalId === "memory/b")!;
188
212
  expect(evalB.verdict).toBe("errored");
189
- expect(evalB.reason).toBe("timeout");
213
+ expect(evalB).not.toHaveProperty("reason");
214
+ expect(evalB.attempts[0]!.error?.message).toBe("timeout");
190
215
  });
191
216
 
192
217
  it("展开数组按 evalId 升序,顶层数组按 experimentId 升序", async () => {
@@ -196,10 +221,29 @@ describe("ExperimentList.data", () => {
196
221
  expect(items.map((i) => i.experimentId)).toEqual(["exp/a", "exp/b"]);
197
222
  expect(items.find((i) => i.experimentId === "exp/b")!.evalRows.map((e) => e.evalId)).toEqual(["a", "z"]);
198
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
+ });
199
243
  });
200
244
 
201
245
  describe("EvalList.data", () => {
202
- it("每项一个 experimentId + evalId,判定/分数/成本耗时均值/失败原因与展开到 Attempt 都在", async () => {
246
+ it("每项一个 experimentId + evalId,父项保留判定与题级聚合并展开到含失败事实的 Attempt", async () => {
203
247
  const s = snap({
204
248
  experimentId: "compare/codex",
205
249
  results: [
@@ -207,13 +251,13 @@ describe("EvalList.data", () => {
207
251
  attempt: 0,
208
252
  durationMs: 40_000,
209
253
  usage: { inputTokens: 1, outputTokens: 1, costUSD: 0.02 },
210
- 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" }],
211
255
  }),
212
256
  res("weather/brooklyn", "failed", {
213
257
  attempt: 1,
214
258
  durationMs: 42_000,
215
259
  usage: { inputTokens: 1, outputTokens: 1, costUSD: 0.06 },
216
- 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" }],
217
261
  }),
218
262
  ],
219
263
  });
@@ -223,11 +267,15 @@ describe("EvalList.data", () => {
223
267
  expect(item.evalId).toBe("weather/brooklyn");
224
268
  expect(item.experimentId).toBe("compare/codex");
225
269
  expect(item.verdict).toBe("failed");
226
- expect(item.reason).toBe('calledTool("get_weather"): tool was never called');
270
+ expect(item).not.toHaveProperty("reason");
227
271
  expect(item.score.value).toBe(0); // examScore: failed → 0
228
272
  expect(item.duration.value).toBeCloseTo(41_000); // 平均耗时
229
273
  expect(item.cost.value).toBeCloseTo(0.04); // 平均成本
230
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");
231
279
  expect(item.attempts[0]!.locator).not.toBe(item.attempts[1]!.locator);
232
280
  });
233
281
 
@@ -248,7 +296,7 @@ describe("AttemptList.data", () => {
248
296
  res("A", "failed", {
249
297
  durationMs: 5_000,
250
298
  usage: { inputTokens: 1, outputTokens: 1, costUSD: 0.05 },
251
- 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" }],
252
300
  hasEvents: true,
253
301
  hasSources: true,
254
302
  }),
@@ -270,18 +318,35 @@ describe("AttemptList.data", () => {
270
318
  expect(item.locator).toMatch(/^@1[0-9a-z]{7}$/);
271
319
  });
272
320
 
273
- it("redact 作用于 error 与断言 detail/evidence,不作用于身份字段", async () => {
321
+ it("redact 作用于 error message/cause/stack、diagnostic 的 message/data、断言 detail/evidence,不作用于身份与分类字段", async () => {
274
322
  const s = snap({
275
323
  experimentId: "exp/x",
276
324
  results: [
277
- 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
+ }),
278
343
  res("B", "failed", {
279
344
  assertions: [
280
345
  {
281
346
  name: "includes",
282
347
  severity: "gate",
283
348
  score: 0,
284
- passed: false,
349
+ outcome: "failed" as const,
285
350
  detail: "missing text under /Users/me/repo/src",
286
351
  evidence: "checked /Users/me/repo/src/app.ts",
287
352
  },
@@ -291,10 +356,25 @@ describe("AttemptList.data", () => {
291
356
  });
292
357
  const items = await AttemptList.data([s], { redact: (text) => text.replaceAll("/Users/me/repo", "<repo>") });
293
358
  const a = items.find((i) => i.evalId === "A")!;
294
- 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");
295
374
  const b = items.find((i) => i.evalId === "B")!;
296
- expect(b.assertions[0]!.detail).toBe("missing text under <repo>/src");
297
- 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");
298
378
  // 身份字段不经 redact
299
379
  expect(items.every((i) => i.experimentId === "exp/x")).toBe(true);
300
380
  });
@@ -302,7 +382,7 @@ describe("AttemptList.data", () => {
302
382
  it("不预设只看失败:passed 的 attempt 同样列出,原样携带 assertions", async () => {
303
383
  const s = snap({
304
384
  experimentId: "exp/x",
305
- 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 }] })],
306
386
  });
307
387
  const items = await AttemptList.data([s]);
308
388
  expect(items).toHaveLength(1);
@@ -319,10 +399,10 @@ describe("两级聚合引擎", () => {
319
399
  experimentId: "exp/x",
320
400
  results: [res("A", "passed"), res("B", "failed"), res("B", "failed"), res("B", "failed")],
321
401
  });
322
- const data = await MetricTable.data([s], { rows: "agent", columns: [passRate] });
402
+ const data = await MetricTable.data([s], { rows: "agent", columns: [taskPassRate] });
323
403
  expect(data.dimension).toBe("agent");
324
404
  expect(data.rows).toHaveLength(1);
325
- const cell = data.rows[0].cells["pass-rate"];
405
+ const cell = data.rows[0].cells["task-pass-rate"];
326
406
  expect(cell.value).toBe(0.5);
327
407
  expect(cell.display).toBe("50%");
328
408
  expect(cell.samples).toBe(4);
@@ -342,17 +422,17 @@ describe("两级聚合引擎", () => {
342
422
  experimentId: "exp/x",
343
423
  results: [res("A", "failed"), res("A", "failed"), res("B", "failed"), res("B", "passed")],
344
424
  });
345
- const data = await MetricTable.data([s], { rows: "agent", columns: [passAtK, passRate] });
425
+ const data = await MetricTable.data([s], { rows: "agent", columns: [passAtK, taskPassRate] });
346
426
  // A: max(0,0)=0;B: max(0,1)=1 → (0+1)/2
347
427
  expect(data.rows[0].cells["pass@k"].value).toBe(0.5);
348
- // 对照:默认 mean/mean 的 passRate = (0 + 0.5)/2
349
- 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);
350
430
  });
351
431
 
352
432
  it("skipped 是 null:不稀释均值,但计入 total(覆盖率如实)", async () => {
353
433
  const s = snap({ experimentId: "exp/x", results: [res("A", "passed"), res("B", "skipped")] });
354
- const data = await MetricTable.data([s], { rows: "agent", columns: [passRate] });
355
- 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"];
356
436
  expect(cell.value).toBe(1); // B 整桶为 null,不参与 across,不是 0.5
357
437
  expect(cell.samples).toBe(1);
358
438
  expect(cell.total).toBe(2);
@@ -361,8 +441,8 @@ describe("两级聚合引擎", () => {
361
441
 
362
442
  it("全组 null → value null、display 兜底,不编 0;refs 必填(空数组)", async () => {
363
443
  const s = snap({ experimentId: "exp/x", results: [res("A", "skipped")] });
364
- const data = await MetricTable.data([s], { rows: "agent", columns: [passRate] });
365
- 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"];
366
446
  expect(cell.value).toBeNull();
367
447
  expect(cell.display).toBe("—");
368
448
  expect(cell.samples).toBe(0);
@@ -397,18 +477,18 @@ describe("两级聚合引擎", () => {
397
477
  res("A", "passed", { attempt: 2 }),
398
478
  ],
399
479
  });
400
- const data = await MetricTable.data([s], { rows: byParity, columns: [passRate] });
480
+ const data = await MetricTable.data([s], { rows: byParity, columns: [taskPassRate] });
401
481
  expect(data.dimension).toBe("parity");
402
- 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]));
403
483
  // 同一道题的 attempt 分进两组:even 组内 [1,1] 折成 1,odd 组内 [0] 折成 0
404
484
  expect(byKey).toEqual({ even: 1, odd: 0 });
405
485
  });
406
486
 
407
487
  it("同一次计算里指标重名是错误", async () => {
408
- const dup = defineMetric({ name: "pass-rate", value: () => 1 });
488
+ const dup = defineMetric({ name: "task-pass-rate", value: () => 1 });
409
489
  const s = snap({ experimentId: "exp/x", results: [res("A", "passed")] });
410
- await expect(MetricTable.data([s], { rows: "agent", columns: [passRate, dup] })).rejects.toThrow(
411
- /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"/,
412
492
  );
413
493
  });
414
494
 
@@ -418,16 +498,16 @@ describe("两级聚合引擎", () => {
418
498
  const none = snap({ experimentId: "exp/none", agent: "none", results: [res("A", "skipped", { agent: "none" })] });
419
499
  const data = await MetricTable.data([none, bad, good], {
420
500
  rows: "agent",
421
- columns: [passRate],
422
- sort: passRate,
501
+ columns: [taskPassRate],
502
+ sort: taskPassRate,
423
503
  });
424
504
  expect(data.rows.map((r) => r.key)).toEqual(["good", "bad", "none"]);
425
505
  });
426
506
 
427
507
  it("列键是字面量联合:拼错列名编译不过", async () => {
428
508
  const s = snap({ experimentId: "exp/x", results: [res("A", "passed")] });
429
- const data = await MetricTable.data([s], { rows: "agent", columns: [passRate, costUSD] });
430
- 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]; // 键锚在指标对象上
431
511
  expect(cell.value).toBe(1);
432
512
  // @ts-expect-error 列里没有这个键 —— 编译期挡住,不是运行时 undefined
433
513
  data.rows[0].cells["pass-rat"];
@@ -440,7 +520,7 @@ describe("examScore", () => {
440
520
  it("errored(断言空数组)得 0 —— 不因「gate 全过」空真得满分", async () => {
441
521
  const s = snap({
442
522
  experimentId: "exp/x",
443
- results: [res("A", "errored", { assertions: [], error: "adapter crashed" })],
523
+ results: [res("A", "errored", { assertions: [], error: erroredWith("adapter crashed") })],
444
524
  });
445
525
  const data = await MetricTable.data([s], { rows: "agent", columns: [examScore] });
446
526
  const cell = data.rows[0].cells["exam-score"];
@@ -465,7 +545,7 @@ describe("examScore", () => {
465
545
  assertions: [
466
546
  softAssertion("judge-1", 0.5),
467
547
  softAssertion("judge-2", 1),
468
- { name: "includes", severity: "gate", score: 1, passed: true },
548
+ { name: "includes", severity: "gate", score: 1, outcome: "passed" as const },
469
549
  ],
470
550
  }),
471
551
  ],
@@ -474,7 +554,7 @@ describe("examScore", () => {
474
554
  experimentId: "exp/b",
475
555
  results: [
476
556
  res("B", "passed", {
477
- assertions: [{ name: "includes", severity: "gate", score: 1, passed: true }],
557
+ assertions: [{ name: "includes", severity: "gate", score: 1, outcome: "passed" as const }],
478
558
  }),
479
559
  ],
480
560
  });
@@ -491,6 +571,62 @@ describe("examScore", () => {
491
571
  });
492
572
  });
493
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
+
494
630
  // ───────────────────────── 内置指标口径 ─────────────────────────
495
631
 
496
632
  describe("内置指标", () => {
@@ -651,7 +787,7 @@ describe("MetricScatter.data", () => {
651
787
  points: "experiment",
652
788
  series: "agent",
653
789
  x: costUSD,
654
- y: passRate,
790
+ y: taskPassRate,
655
791
  });
656
792
  expect(data.points).toBe("experiment");
657
793
  expect(data.series).toBe("agent");
@@ -676,7 +812,7 @@ describe("flag()", () => {
676
812
  const withFlags = (id: string, flags: Record<string, unknown> | undefined, verdict: Verdict) =>
677
813
  snap({
678
814
  experimentId: id,
679
- 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 })],
680
816
  });
681
817
 
682
818
  it("MetricLine.data:x 收 flag、按 experiment 聚合;未声明的作轴 x=null 报数", async () => {
@@ -686,7 +822,7 @@ describe("flag()", () => {
686
822
  const data = await MetricLine.data([s1, s2, legacy], {
687
823
  x: flag("latencyMs", { label: "Simulated latency", unit: "ms" }),
688
824
  series: flag("agents", { label: (v) => `${v} agents` }),
689
- y: passRate,
825
+ y: taskPassRate,
690
826
  });
691
827
  expect(data.x).toEqual({ key: "latencyMs", label: "Simulated latency", unit: "ms" });
692
828
  expect(data.series).toBe("agents");
@@ -710,13 +846,84 @@ describe("flag()", () => {
710
846
  const s2 = withFlags("exp/b", { agents: 16 }, "failed");
711
847
  const data = await MetricTable.data([s1, s2], {
712
848
  rows: flag("agents", { label: (v) => `${v} agents` }),
713
- columns: [passRate],
849
+ columns: [taskPassRate],
714
850
  });
715
851
  expect(data.dimension).toBe("agents");
716
852
  expect(data.rows.map((r) => r.key)).toEqual(["1 agents", "16 agents"]);
717
853
  });
718
854
  });
719
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
+
720
927
  // ───────────────────────── MetricTable.data:没有实体下钻 ─────────────────────────
721
928
 
722
929
  describe("MetricTable.data", () => {
@@ -725,7 +932,7 @@ describe("MetricTable.data", () => {
725
932
  experimentId: "exp/x",
726
933
  results: [res("A", "passed"), res("B", "failed")],
727
934
  });
728
- const data = await MetricTable.data([s], { rows: "experiment", columns: [passRate] });
935
+ const data = await MetricTable.data([s], { rows: "experiment", columns: [taskPassRate] });
729
936
  expect(data.rows).toHaveLength(1);
730
937
  expect(data.rows[0]!.meta).not.toHaveProperty("subRows");
731
938
  // 展开到 Eval 是 ExperimentList 的职责,不在这里验证 —— 见 ExperimentList.data 的测试组。
@@ -740,10 +947,10 @@ describe("MetricTable.data", () => {
740
947
  describe("reasonFor", () => {
741
948
  it("同一 result 同时含 error、失败 gate、失败 soft → 只显示 error", () => {
742
949
  const result = res("A", "errored", {
743
- error: "adapter crashed",
950
+ error: erroredWith("adapter crashed"),
744
951
  assertions: [
745
- { name: "includes", severity: "gate", score: 0, passed: false, detail: "missing text" },
746
- 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 }),
747
954
  ],
748
955
  });
749
956
  expect(reasonFor(result)).toBe("adapter crashed");
@@ -752,21 +959,21 @@ describe("reasonFor", () => {
752
959
  it("skipReason 优先于 gate 断言,但让位给 error", () => {
753
960
  const result = res("A", "skipped", {
754
961
  skipReason: "missing fixture",
755
- assertions: [{ name: "includes", severity: "gate", score: 0, passed: false }],
962
+ assertions: [{ name: "includes", severity: "gate", score: 0, outcome: "failed" as const }],
756
963
  });
757
964
  expect(reasonFor(result)).toBe("missing fixture");
758
965
  });
759
966
 
760
- it("多个失败 gate 按原始声明顺序全部保留,拼接成一行;失败 soft 不出现在 reason ", () => {
967
+ it("只展示第一条失败 gate,其余 gate 计数;失败 soft 不进入 gate 计数", () => {
761
968
  const result = res("A", "failed", {
762
969
  assertions: [
763
- softAssertion("judge-first", 0.1, { passed: false }), // 声明顺序第一,但 soft 不进 reason
764
- { name: "includes", severity: "gate", score: 0, passed: false, detail: "missing text" },
765
- { name: "matches", severity: "gate", score: 0, passed: false }, // 无 detail,只有 name
766
- 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
767
974
  ],
768
975
  });
769
- 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");
770
977
  });
771
978
 
772
979
  it("都缺席 → 无原因", () => {
@@ -813,6 +1020,22 @@ describe("RunOverview.data", () => {
813
1020
  const data = await RunOverview.data(selection([s], [warning]));
814
1021
  expect(data.warnings).toEqual([warning]);
815
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
+ });
816
1039
  });
817
1040
 
818
1041
  // ───────────────────────── GroupSummary.data ─────────────────────────
@@ -857,16 +1080,6 @@ describe("GroupSummary.data", () => {
857
1080
  expect(data.lastRunAt).toBe(expB.startedAt);
858
1081
  });
859
1082
 
860
- it('跨 experiment 同名 eval 不合并:两个 experiment 各自的 "x" 独立计一票,不是被折成一票', async () => {
861
- const a = snap({ experimentId: "exp/a", results: [res("x", "passed")] });
862
- const b = snap({ experimentId: "exp/b", results: [res("x", "failed")] });
863
- const data = await GroupSummary.data([a, b]);
864
- expect(data.experiments).toBe(2);
865
- expect(data.evals).toBe(2); // 只按 eval id 折叠(误把两个 experiment 的 "x" 当一道题)会变成 1
866
- expect(data.verdicts).toEqual({ passed: 1, failed: 1, errored: 0, skipped: 0 });
867
- expect(data.passRate.value).toBeCloseTo(0.5, 10); // 误合并会因为「任一轮过即过」变成 100%
868
- });
869
-
870
1083
  it("全组没有任何 attempt 报成本 → totalCostUSD null,不编 0", async () => {
871
1084
  const s = snap({ experimentId: "exp/x", results: [res("A", "passed"), res("B", "failed")] });
872
1085
  const data = await GroupSummary.data([s]);
@@ -883,30 +1096,12 @@ describe("GroupSummary.data", () => {
883
1096
  expect(data.passRate.total).toBe(2);
884
1097
  });
885
1098
 
886
- it("lastRunAt 只从传入的组 Selection 取值,不读全局最新 —— 组外快照更晚也不影响", async () => {
887
- const inGroup = snap({
888
- experimentId: "exp/a",
889
- runStartedAt: "2026-07-01T08:00:00Z",
890
- results: [res("A", "passed")],
891
- });
892
- const outOfGroup = snap({
893
- experimentId: "exp/b",
894
- runStartedAt: "2026-07-09T08:00:00Z",
895
- results: [res("B", "passed")],
896
- });
897
- const sel = selection([inGroup, outOfGroup], []);
898
- const scoped = sel.filter((s) => s.experimentId === "exp/a"); // 只留组内快照,模拟按实验组收窄的 Selection
899
- const data = await GroupSummary.data(scoped);
900
- expect(data.lastRunAt).toBe(inGroup.startedAt);
901
- expect(data.lastRunAt).not.toBe(outOfGroup.startedAt);
902
- });
903
-
904
1099
  it('MetricTable rows: "experiment" 的 verdicts meta 复用同一份 eval 级统计,行为不变(回归)', async () => {
905
1100
  const s = snap({
906
1101
  experimentId: "exp/x",
907
1102
  results: [res("A", "passed", { attempt: 0 }), res("A", "failed", { attempt: 1 }), res("B", "failed")],
908
1103
  });
909
- const data = await MetricTable.data([s], { rows: "experiment", columns: [passRate] });
1104
+ const data = await MetricTable.data([s], { rows: "experiment", columns: [taskPassRate] });
910
1105
  // A 两轮折成 passed(任一轮过即过),B 失败:1 passed / 1 failed
911
1106
  expect(data.rows[0].meta?.verdicts).toEqual({ passed: 1, failed: 1, errored: 0, skipped: 0 });
912
1107
  });
@@ -926,7 +1121,7 @@ describe("DeltaTable.data", () => {
926
1121
  });
927
1122
  const data = await DeltaTable.data([base, plus], {
928
1123
  pairs: [{ a: "exp/base", b: "exp/plus", label: "memory" }],
929
- metrics: [passRate, costUSD],
1124
+ metrics: [taskPassRate, costUSD],
930
1125
  });
931
1126
  expect(data.rows).toHaveLength(1);
932
1127
  const row = data.rows[0];
@@ -934,7 +1129,7 @@ describe("DeltaTable.data", () => {
934
1129
  expect(row.a).toEqual({ experimentId: "exp/base" });
935
1130
  expect(row.b).toEqual({ experimentId: "exp/plus" });
936
1131
 
937
- const pass = row.cells["pass-rate"];
1132
+ const pass = row.cells["task-pass-rate"];
938
1133
  expect(pass.a.value).toBe(0.5);
939
1134
  expect(pass.b.value).toBe(1);
940
1135
  expect(pass.delta).toBeCloseTo(0.5);
@@ -948,9 +1143,9 @@ describe("DeltaTable.data", () => {
948
1143
 
949
1144
  const flat = await DeltaTable.data([base, base], {
950
1145
  pairs: [{ a: "exp/base", b: "exp/base", label: "same" }],
951
- metrics: [passRate],
1146
+ metrics: [taskPassRate],
952
1147
  });
953
- expect(flat.rows[0].cells["pass-rate"].display).toBe("±0");
1148
+ expect(flat.rows[0].cells["task-pass-rate"].display).toBe("±0");
954
1149
  });
955
1150
 
956
1151
  it("时间轴对比:pairs 的 a/b 收快照键 <experimentId> @ <startedAt>", async () => {
@@ -972,9 +1167,9 @@ describe("DeltaTable.data", () => {
972
1167
  label: "this week vs last",
973
1168
  },
974
1169
  ],
975
- metrics: [passRate],
1170
+ metrics: [taskPassRate],
976
1171
  });
977
- const cell = data.rows[0].cells["pass-rate"];
1172
+ const cell = data.rows[0].cells["task-pass-rate"];
978
1173
  expect(cell.a.value).toBe(0); // 旧快照那份
979
1174
  expect(cell.b.value).toBe(1); // 新快照那份
980
1175
  expect(cell.display).toBe("+100%");
@@ -1006,8 +1201,8 @@ describe("身份键去重", () => {
1006
1201
  [older, newer],
1007
1202
  [newer, older],
1008
1203
  ]) {
1009
- const data = await MetricTable.data(order, { rows: "agent", columns: [passRate] });
1010
- 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"];
1011
1206
  expect(cell.total).toBe(1); // 两份只算一份
1012
1207
  expect(cell.value).toBe(1); // 留的是最新 run 里的 passed,与快照传入顺序无关
1013
1208
 
@@ -1073,10 +1268,10 @@ describe("MetricMatrix.data", () => {
1073
1268
  agent: "b1",
1074
1269
  results: [res("A", "failed", { agent: "b1" })], // b1 没跑 B
1075
1270
  });
1076
- 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 });
1077
1272
  expect(data.rows).toBe("eval");
1078
1273
  expect(data.columns).toBe("agent");
1079
- expect(data.metric.key).toBe("pass-rate");
1274
+ expect(data.metric.key).toBe("task-pass-rate");
1080
1275
  expect(data.cells).toHaveLength(3); // A×a1、B×a1、A×b1;B×b1 不出现
1081
1276
  const find = (row: string, column: string) => data.cells.find((c) => c.row === row && c.column === column);
1082
1277
  expect(find("A", "a1")?.cell.value).toBe(1);
@@ -1085,7 +1280,20 @@ describe("MetricMatrix.data", () => {
1085
1280
  expect(find("B", "b1")).toBeUndefined();
1086
1281
  });
1087
1282
 
1088
- it("MetricBars.data 就是 MetricMatrix.data 的别名(同一个函数)", () => {
1089
- 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);
1090
1298
  });
1091
1299
  });