niceeval 0.4.6 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (174) hide show
  1. package/docs-site/zh/concepts/adapter.mdx +6 -6
  2. package/docs-site/zh/concepts/experiment.mdx +7 -7
  3. package/docs-site/zh/concepts/overview.mdx +1 -1
  4. package/docs-site/zh/concepts/tier.mdx +6 -6
  5. package/docs-site/zh/guides/agent-feedback-loop.mdx +64 -21
  6. package/docs-site/zh/guides/connect-your-agent.mdx +7 -7
  7. package/docs-site/zh/guides/custom-reports.mdx +243 -0
  8. package/docs-site/zh/guides/experiments.mdx +3 -3
  9. package/docs-site/zh/guides/report-components.mdx +252 -0
  10. package/docs-site/zh/guides/reporters.mdx +1 -1
  11. package/docs-site/zh/guides/results-data.mdx +224 -0
  12. package/docs-site/zh/guides/runner.mdx +1 -1
  13. package/docs-site/zh/guides/sandbox-agent.mdx +2 -2
  14. package/docs-site/zh/guides/viewing-results.mdx +147 -77
  15. package/docs-site/zh/guides/write-experiment.mdx +9 -9
  16. package/docs-site/zh/guides/write-send.mdx +6 -6
  17. package/docs-site/zh/index.mdx +1 -1
  18. package/docs-site/zh/introduction.mdx +1 -1
  19. package/docs-site/zh/reference/cli.mdx +11 -3
  20. package/docs-site/zh/reference/define-agent.mdx +5 -5
  21. package/docs-site/zh/reference/define-config.mdx +1 -1
  22. package/docs-site/zh/reference/define-eval.mdx +3 -3
  23. package/package.json +3 -2
  24. package/src/agents/ai-sdk.test.ts +1 -1
  25. package/src/agents/ai-sdk.ts +2 -2
  26. package/src/agents/claude-code.ts +1 -1
  27. package/src/agents/codex.ts +2 -2
  28. package/src/agents/streaming.test.ts +1 -1
  29. package/src/agents/types.ts +4 -4
  30. package/src/agents/ui-message-stream.test.ts +1 -1
  31. package/src/cli.ts +120 -18
  32. package/src/context/context.test.ts +1 -1
  33. package/src/context/context.ts +3 -3
  34. package/src/context/session.ts +2 -2
  35. package/src/context/types.ts +2 -2
  36. package/src/i18n/en.ts +29 -7
  37. package/src/i18n/zh-CN.ts +24 -2
  38. package/src/report/aggregate.ts +79 -50
  39. package/src/report/components.tsx +190 -0
  40. package/src/report/compute.ts +149 -84
  41. package/src/report/default-report.tsx +222 -0
  42. package/src/report/dual-face.test.tsx +527 -0
  43. package/src/report/format.ts +29 -0
  44. package/src/report/index.ts +79 -24
  45. package/src/report/load.ts +67 -0
  46. package/src/report/metrics.ts +2 -1
  47. package/src/report/param.ts +18 -0
  48. package/src/report/primitives.tsx +117 -0
  49. package/src/report/react/CaseList.tsx +2 -2
  50. package/src/report/react/DeltaTable.tsx +2 -2
  51. package/src/report/react/MetricBars.tsx +109 -0
  52. package/src/report/react/MetricLine.tsx +200 -0
  53. package/src/report/react/MetricMatrix.tsx +1 -1
  54. package/src/report/react/MetricScatter.tsx +1 -1
  55. package/src/report/react/MetricTable.tsx +1 -1
  56. package/src/report/react/RunOverview.tsx +4 -4
  57. package/src/report/react/Scoreboard.tsx +3 -3
  58. package/src/report/react/cell.tsx +1 -1
  59. package/src/report/react/fixtures.ts +98 -36
  60. package/src/report/react/format.ts +3 -28
  61. package/src/report/react/index.tsx +41 -24
  62. package/src/report/react/render.test.tsx +8 -8
  63. package/src/report/react/styles.css +77 -0
  64. package/src/report/report.test.ts +248 -128
  65. package/src/report/report.ts +69 -0
  66. package/src/report/text/faces.ts +356 -0
  67. package/src/report/text/layout.ts +125 -0
  68. package/src/report/text/plot.ts +127 -0
  69. package/src/report/tree.ts +220 -0
  70. package/src/report/types.ts +75 -27
  71. package/src/report/web.ts +40 -0
  72. package/src/results/copy.ts +95 -41
  73. package/src/results/format.ts +12 -11
  74. package/src/results/index.ts +34 -17
  75. package/src/results/open.ts +201 -81
  76. package/src/results/results.test.ts +595 -191
  77. package/src/results/select.ts +107 -55
  78. package/src/results/types.ts +137 -45
  79. package/src/results/writer.ts +258 -0
  80. package/src/runner/attempt.ts +3 -3
  81. package/src/runner/fingerprint.ts +1 -1
  82. package/src/runner/reporters/artifacts.ts +38 -78
  83. package/src/runner/reporters/braintrust.test.ts +2 -2
  84. package/src/runner/reporters/braintrust.ts +3 -3
  85. package/src/runner/run.ts +1 -1
  86. package/src/runner/types.ts +18 -12
  87. package/src/show/compose.ts +215 -0
  88. package/src/show/index.ts +263 -0
  89. package/src/show/render.ts +456 -0
  90. package/src/show/show.test.ts +505 -0
  91. package/src/view/app/App.tsx +85 -24
  92. package/src/view/app/components/CostScoreChart.tsx +29 -10
  93. package/src/view/app/components/ExperimentTable.tsx +27 -9
  94. package/src/view/app/components/GroupSelector.tsx +1 -1
  95. package/src/view/app/i18n.ts +18 -3
  96. package/src/view/app/lib/attempt-route.test.ts +9 -9
  97. package/src/view/app/lib/attempt-route.ts +5 -5
  98. package/src/view/app/lib/outcome.ts +1 -3
  99. package/src/view/app/lib/rows.ts +95 -15
  100. package/src/view/app/main.tsx +18 -6
  101. package/src/view/app/pages/RunsPage.tsx +4 -7
  102. package/src/view/app/pages/TracesPage.tsx +3 -8
  103. package/src/view/app/types.ts +50 -6
  104. package/src/view/client-dist/app.css +1 -1
  105. package/src/view/client-dist/app.js +20 -20
  106. package/src/view/data.test.ts +254 -0
  107. package/src/view/data.ts +336 -0
  108. package/src/view/index.ts +64 -10
  109. package/src/view/server.ts +31 -9
  110. package/src/view/shared/types.ts +47 -41
  111. package/src/view/styles.css +5 -0
  112. package/src/view/template.html +1 -0
  113. package/src/view/view-report.test.ts +308 -0
  114. package/docs-site/.mintignore +0 -13
  115. package/docs-site/AGENTS.md +0 -46
  116. package/docs-site/concepts/adapter.mdx +0 -287
  117. package/docs-site/concepts/assert.mdx +0 -243
  118. package/docs-site/concepts/drive.mdx +0 -118
  119. package/docs-site/concepts/evals.mdx +0 -108
  120. package/docs-site/concepts/experiment.mdx +0 -37
  121. package/docs-site/concepts/hitl.mdx +0 -83
  122. package/docs-site/concepts/judge.mdx +0 -129
  123. package/docs-site/concepts/overview.mdx +0 -98
  124. package/docs-site/concepts/tier.mdx +0 -42
  125. package/docs-site/docs-ref/00-index.md +0 -23
  126. package/docs-site/docs-ref/01-page-types.md +0 -43
  127. package/docs-site/docs-ref/03-style-rules.md +0 -45
  128. package/docs-site/docs-ref/04-code-examples.md +0 -37
  129. package/docs-site/docs-ref/05-dx-failure-paths.md +0 -45
  130. package/docs-site/docs-ref/06-checklists.md +0 -53
  131. package/docs-site/docs.json +0 -256
  132. package/docs-site/example/ai-agent-application.mdx +0 -148
  133. package/docs-site/example/claude-code-codex-plugin.mdx +0 -162
  134. package/docs-site/example/claude-code-codex-skill.mdx +0 -147
  135. package/docs-site/example/showcase.mdx +0 -39
  136. package/docs-site/example/tier1-ai-sdk-v7.mdx +0 -136
  137. package/docs-site/example/tier1-claude-sdk.mdx +0 -119
  138. package/docs-site/example/tier1-codex-sdk.mdx +0 -111
  139. package/docs-site/example/tier1-langgraph.mdx +0 -119
  140. package/docs-site/example/tier1-pi-sdk.mdx +0 -119
  141. package/docs-site/favicon.svg +0 -5
  142. package/docs-site/github-diff.css +0 -135
  143. package/docs-site/github-diff.js +0 -11
  144. package/docs-site/guides/authoring.mdx +0 -129
  145. package/docs-site/guides/ci-integration.mdx +0 -97
  146. package/docs-site/guides/connect-otel.mdx +0 -209
  147. package/docs-site/guides/connect-your-agent.mdx +0 -221
  148. package/docs-site/guides/dataset-fanout.mdx +0 -98
  149. package/docs-site/guides/experiments.mdx +0 -71
  150. package/docs-site/guides/fixtures.mdx +0 -147
  151. package/docs-site/guides/reporters.mdx +0 -113
  152. package/docs-site/guides/runner.mdx +0 -79
  153. package/docs-site/guides/sandbox-agent.mdx +0 -138
  154. package/docs-site/guides/sandbox-backends.mdx +0 -64
  155. package/docs-site/guides/scoring-guide.mdx +0 -92
  156. package/docs-site/guides/viewing-results.mdx +0 -195
  157. package/docs-site/guides/write-experiment.mdx +0 -81
  158. package/docs-site/guides/write-send.mdx +0 -347
  159. package/docs-site/index.mdx +0 -181
  160. package/docs-site/introduction.mdx +0 -141
  161. package/docs-site/quickstart.mdx +0 -136
  162. package/docs-site/reference/builtin-agents.mdx +0 -161
  163. package/docs-site/reference/capabilities.mdx +0 -76
  164. package/docs-site/reference/cli.mdx +0 -120
  165. package/docs-site/reference/define-agent.mdx +0 -168
  166. package/docs-site/reference/define-config.mdx +0 -41
  167. package/docs-site/reference/define-eval.mdx +0 -160
  168. package/docs-site/reference/events.mdx +0 -131
  169. package/docs-site/reference/expect.mdx +0 -112
  170. package/docs-site/tracker.js +0 -8
  171. package/src/report/react/data.ts +0 -17
  172. package/src/view/aggregate.ts +0 -103
  173. package/src/view/loader.test.ts +0 -61
  174. package/src/view/loader.ts +0 -257
@@ -1,18 +1,30 @@
1
- // niceeval/report 的单元测试:全部用内存 fake handles(SnapshotHandle docs/results-lib.md
2
- // 的形状手工构造),专门覆盖 docs/reports.md 点名的坑 —— 两级聚合 vs 平铺、pass@k、
3
- // examScore 空真、skipped 稀释、scoreboard 固定分母与最长前缀、scatter/delta 的 null 语义、
4
- // casesredact/truncated、身份键去重。
1
+ // niceeval/report 计算层的单元测试:全部用内存 fake(Snapshot / AttemptHandle
2
+ // niceeval/results 的读取契约手工构造),专门覆盖 docs/reports.md 点名的坑 ——
3
+ // 两级聚合 vs 平铺、pass@k、examScore 空真、skipped 稀释、scoreboard 固定分母与
4
+ // 最长前缀、scatter/deltanull 语义、快照键对比、param 维度与轴、
5
+ // cases 的 redact/truncated、身份键去重、Selection warnings 随行。
5
6
 
6
7
  import { describe, expect, it } from "vitest";
7
8
 
8
9
  import type { AssertionResult, EvalResult, ResultOutcome, RunSummary } from "../types.ts";
9
- import type { AttemptHandle, RunHandle, SnapshotHandle } from "../results/types.ts";
10
- import type { Dimension } from "./types.ts";
10
+ import type { AttemptHandle, RunDir, Selection, SelectionWarning, Snapshot } from "../results/index.ts";
11
+ import type { Dimension, MetricCell } from "./types.ts";
11
12
  import { costUSD, defineMetric, durationMs, examScore, passRate, tokens } from "./metrics.ts";
12
- import { cases, delta, matrix, overview, scatter, scoreboard, table } from "./compute.ts";
13
+ import { param } from "./param.ts";
13
14
  import { formatMetricValue } from "./format.ts";
14
-
15
- // ───────────────────────── fake handles ─────────────────────────
15
+ import {
16
+ CaseList,
17
+ DeltaTable,
18
+ MetricBars,
19
+ MetricLine,
20
+ MetricMatrix,
21
+ MetricScatter,
22
+ MetricTable,
23
+ RunOverview,
24
+ Scoreboard,
25
+ } from "./components.tsx";
26
+
27
+ // ───────────────────────── fake 数据(按 results 读取契约造)─────────────────────────
16
28
 
17
29
  let seq = 0;
18
30
 
@@ -35,31 +47,19 @@ function softAssertion(name: string, score: number, extra: Partial<AssertionResu
35
47
  return { name, severity: "soft", score, passed: true, ...extra };
36
48
  }
37
49
 
38
- function wrap(run: RunHandle, result: EvalResult, index: number): AttemptHandle {
39
- return {
40
- run,
41
- ref: { run: run.dir.split("/").pop()!, result: index },
42
- result,
43
- events: async () => null,
44
- trace: async () => null,
45
- o11y: async () => null,
46
- diff: async () => null,
47
- sources: async () => null,
48
- };
49
- }
50
-
51
50
  interface SnapSpec {
52
51
  experimentId: string;
53
52
  results: EvalResult[];
54
53
  agent?: string;
55
54
  model?: string;
56
55
  runStartedAt?: string;
56
+ knownEvalIds?: string[];
57
57
  }
58
58
 
59
59
  let runSeq = 0;
60
60
 
61
61
  /** 最小构造:一个 run 装一个快照。runStartedAt 决定去重时谁是「最新 run」。 */
62
- function snap(spec: SnapSpec): SnapshotHandle {
62
+ function snap(spec: SnapSpec): Snapshot {
63
63
  runSeq += 1;
64
64
  const startedAt = spec.runStartedAt ?? `2026-06-01T00:00:00.${String(runSeq).padStart(3, "0")}Z`;
65
65
  const summary: RunSummary = {
@@ -73,17 +73,52 @@ function snap(spec: SnapSpec): SnapshotHandle {
73
73
  durationMs: 0,
74
74
  results: spec.results,
75
75
  };
76
- const run: RunHandle = { dir: `/results/run-${runSeq}`, summary, attempts: [] };
77
- run.attempts = spec.results.map((r, i) => wrap(run, r, i));
78
- const attempts = run.attempts;
76
+ const runDir: RunDir = { dir: `/results/run-${runSeq}`, summary, attempts: [] };
77
+ const attempts: AttemptHandle[] = spec.results.map((r, i) => ({
78
+ evalId: r.id,
79
+ experimentId: r.experimentId ?? spec.experimentId,
80
+ result: r,
81
+ ref: { run: `run-${runSeq}`, result: i },
82
+ runDir,
83
+ events: async () => null,
84
+ trace: async () => null,
85
+ o11y: async () => null,
86
+ diff: async () => null,
87
+ sources: async () => null,
88
+ }));
89
+ runDir.attempts = attempts;
90
+ const evals = new Map<string, AttemptHandle[]>();
91
+ for (const attempt of attempts) {
92
+ const list = evals.get(attempt.evalId);
93
+ if (list) list.push(attempt);
94
+ else evals.set(attempt.evalId, [attempt]);
95
+ }
79
96
  return {
80
97
  experimentId: spec.experimentId,
81
- run,
82
98
  startedAt,
83
99
  agent: spec.agent ?? "agent-x",
84
100
  model: spec.model,
101
+ schemaVersion: 1,
102
+ evals: [...evals.entries()].map(([id, list]) => ({ id, attempts: list })),
85
103
  attempts,
86
- evalIds: [...new Set(spec.results.map((r) => r.id))],
104
+ runDir,
105
+ knownEvalIds: spec.knownEvalIds,
106
+ };
107
+ }
108
+
109
+ /** 手工造一个选集(warnings 随行的形状,filter 语义与 results 一致)。 */
110
+ function selection(snapshots: Snapshot[], warnings: SelectionWarning[]): Selection {
111
+ return {
112
+ snapshots,
113
+ warnings,
114
+ filter(predicate) {
115
+ const kept = snapshots.filter(predicate);
116
+ const survivors = new Set(kept.map((s) => s.experimentId));
117
+ return selection(
118
+ kept,
119
+ warnings.filter((w) => typeof w.experimentId !== "string" || survivors.has(w.experimentId)),
120
+ );
121
+ },
87
122
  };
88
123
  }
89
124
 
@@ -93,14 +128,9 @@ describe("两级聚合引擎", () => {
93
128
  it("题内先折再跨题平均:A=[1]、B=[0,0,0] → 0.5,不是平铺的 0.25", async () => {
94
129
  const s = snap({
95
130
  experimentId: "exp/x",
96
- results: [
97
- res("A", "passed"),
98
- res("B", "failed"),
99
- res("B", "failed"),
100
- res("B", "failed"),
101
- ],
131
+ results: [res("A", "passed"), res("B", "failed"), res("B", "failed"), res("B", "failed")],
102
132
  });
103
- const data = await table([s], { rows: "agent", columns: [passRate] });
133
+ const data = await MetricTable.data([s], { rows: "agent", columns: [passRate] });
104
134
  expect(data.dimension).toBe("agent");
105
135
  expect(data.rows).toHaveLength(1);
106
136
  const cell = data.rows[0].cells["pass-rate"];
@@ -116,20 +146,14 @@ describe("两级聚合引擎", () => {
116
146
  name: "pass@k",
117
147
  better: "higher",
118
148
  unit: "%",
119
- value: (a) =>
120
- a.result.outcome === "skipped" ? null : a.result.outcome === "passed" ? 1 : 0,
149
+ value: (a) => (a.result.outcome === "skipped" ? null : a.result.outcome === "passed" ? 1 : 0),
121
150
  aggregate: { perEval: "max", across: "mean" },
122
151
  });
123
152
  const s = snap({
124
153
  experimentId: "exp/x",
125
- results: [
126
- res("A", "failed"),
127
- res("A", "failed"),
128
- res("B", "failed"),
129
- res("B", "passed"),
130
- ],
154
+ results: [res("A", "failed"), res("A", "failed"), res("B", "failed"), res("B", "passed")],
131
155
  });
132
- const data = await table([s], { rows: "agent", columns: [passAtK, passRate] });
156
+ const data = await MetricTable.data([s], { rows: "agent", columns: [passAtK, passRate] });
133
157
  // A: max(0,0)=0;B: max(0,1)=1 → (0+1)/2
134
158
  expect(data.rows[0].cells["pass@k"].value).toBe(0.5);
135
159
  // 对照:默认 mean/mean 的 passRate = (0 + 0.5)/2
@@ -137,11 +161,8 @@ describe("两级聚合引擎", () => {
137
161
  });
138
162
 
139
163
  it("skipped 是 null:不稀释均值,但计入 total(覆盖率如实)", async () => {
140
- const s = snap({
141
- experimentId: "exp/x",
142
- results: [res("A", "passed"), res("B", "skipped")],
143
- });
144
- const data = await table([s], { rows: "agent", columns: [passRate] });
164
+ const s = snap({ experimentId: "exp/x", results: [res("A", "passed"), res("B", "skipped")] });
165
+ const data = await MetricTable.data([s], { rows: "agent", columns: [passRate] });
145
166
  const cell = data.rows[0].cells["pass-rate"];
146
167
  expect(cell.value).toBe(1); // B 整桶为 null,不参与 across,不是 0.5
147
168
  expect(cell.samples).toBe(1);
@@ -149,14 +170,15 @@ describe("两级聚合引擎", () => {
149
170
  expect(cell.refs).toHaveLength(1);
150
171
  });
151
172
 
152
- it("全组 null → value null、display 兜底,不编 0", async () => {
173
+ it("全组 null → value null、display 兜底,不编 0;refs 必填(空数组)", async () => {
153
174
  const s = snap({ experimentId: "exp/x", results: [res("A", "skipped")] });
154
- const data = await table([s], { rows: "agent", columns: [passRate] });
175
+ const data = await MetricTable.data([s], { rows: "agent", columns: [passRate] });
155
176
  const cell = data.rows[0].cells["pass-rate"];
156
177
  expect(cell.value).toBeNull();
157
178
  expect(cell.display).toBe("—");
158
179
  expect(cell.samples).toBe(0);
159
180
  expect(cell.total).toBe(1);
181
+ expect(cell.refs).toEqual([]);
160
182
  });
161
183
 
162
184
  it("where 不满足 → null,不进聚合", async () => {
@@ -166,7 +188,7 @@ describe("两级聚合引擎", () => {
166
188
  value: () => 5,
167
189
  });
168
190
  const s = snap({ experimentId: "exp/x", results: [res("A", "passed"), res("B", "failed")] });
169
- const data = await table([s], { rows: "agent", columns: [onlyPassed] });
191
+ const data = await MetricTable.data([s], { rows: "agent", columns: [onlyPassed] });
170
192
  const cell = data.rows[0].cells["only-passed"];
171
193
  expect(cell.value).toBe(5);
172
194
  expect(cell.samples).toBe(1);
@@ -186,45 +208,41 @@ describe("两级聚合引擎", () => {
186
208
  res("A", "passed", { attempt: 2 }),
187
209
  ],
188
210
  });
189
- const data = await table([s], { rows: byParity, columns: [passRate] });
211
+ const data = await MetricTable.data([s], { rows: byParity, columns: [passRate] });
190
212
  expect(data.dimension).toBe("parity");
191
213
  const byKey = Object.fromEntries(data.rows.map((r) => [r.key, r.cells["pass-rate"].value]));
192
214
  // 同一道题的 attempt 分进两组:even 组内 [1,1] 折成 1,odd 组内 [0] 折成 0
193
- // (若第一级在分组前全局做,两组都会是 2/3)
194
215
  expect(byKey).toEqual({ even: 1, odd: 0 });
195
216
  });
196
217
 
197
218
  it("同一次计算里指标重名是错误", async () => {
198
219
  const dup = defineMetric({ name: "pass-rate", value: () => 1 });
199
220
  const s = snap({ experimentId: "exp/x", results: [res("A", "passed")] });
200
- await expect(table([s], { rows: "agent", columns: [passRate, dup] })).rejects.toThrow(
221
+ await expect(MetricTable.data([s], { rows: "agent", columns: [passRate, dup] })).rejects.toThrow(
201
222
  /Duplicate metric name "pass-rate"/,
202
223
  );
203
224
  });
204
225
 
205
226
  it("sort 方向随 better,缺数据行沉底", async () => {
206
- const good = snap({
207
- experimentId: "exp/good",
208
- agent: "good",
209
- results: [res("A", "passed", { agent: "good" })],
210
- });
211
- const bad = snap({
212
- experimentId: "exp/bad",
213
- agent: "bad",
214
- results: [res("A", "failed", { agent: "bad" })],
215
- });
216
- const none = snap({
217
- experimentId: "exp/none",
218
- agent: "none",
219
- results: [res("A", "skipped", { agent: "none" })],
220
- });
221
- const data = await table([none, bad, good], {
227
+ const good = snap({ experimentId: "exp/good", agent: "good", results: [res("A", "passed", { agent: "good" })] });
228
+ const bad = snap({ experimentId: "exp/bad", agent: "bad", results: [res("A", "failed", { agent: "bad" })] });
229
+ const none = snap({ experimentId: "exp/none", agent: "none", results: [res("A", "skipped", { agent: "none" })] });
230
+ const data = await MetricTable.data([none, bad, good], {
222
231
  rows: "agent",
223
232
  columns: [passRate],
224
233
  sort: passRate,
225
234
  });
226
235
  expect(data.rows.map((r) => r.key)).toEqual(["good", "bad", "none"]);
227
236
  });
237
+
238
+ it("列键是字面量联合:拼错列名编译不过", async () => {
239
+ const s = snap({ experimentId: "exp/x", results: [res("A", "passed")] });
240
+ const data = await MetricTable.data([s], { rows: "agent", columns: [passRate, costUSD] });
241
+ const cell: MetricCell = data.rows[0].cells[passRate.name]; // 键锚在指标对象上
242
+ expect(cell.value).toBe(1);
243
+ // @ts-expect-error 列里没有这个键 —— 编译期挡住,不是运行时 undefined
244
+ data.rows[0].cells["pass-rat"];
245
+ });
228
246
  });
229
247
 
230
248
  // ───────────────────────── examScore ─────────────────────────
@@ -235,7 +253,7 @@ describe("examScore", () => {
235
253
  experimentId: "exp/x",
236
254
  results: [res("A", "errored", { assertions: [], error: "adapter crashed" })],
237
255
  });
238
- const data = await table([s], { rows: "agent", columns: [examScore] });
256
+ const data = await MetricTable.data([s], { rows: "agent", columns: [examScore] });
239
257
  const cell = data.rows[0].cells["exam-score"];
240
258
  expect(cell.value).toBe(0); // 交白卷是 0 分,不是缺数据,更不是满分
241
259
  expect(cell.samples).toBe(1);
@@ -244,11 +262,9 @@ describe("examScore", () => {
244
262
  it("failed 得 0,哪怕 soft 分不低(报告不重新判卷)", async () => {
245
263
  const s = snap({
246
264
  experimentId: "exp/x",
247
- results: [
248
- res("A", "failed", { assertions: [softAssertion("judge", 0.9)] }),
249
- ],
265
+ results: [res("A", "failed", { assertions: [softAssertion("judge", 0.9)] })],
250
266
  });
251
- const data = await table([s], { rows: "agent", columns: [examScore] });
267
+ const data = await MetricTable.data([s], { rows: "agent", columns: [examScore] });
252
268
  expect(data.rows[0].cells["exam-score"].value).toBe(0);
253
269
  });
254
270
 
@@ -273,15 +289,15 @@ describe("examScore", () => {
273
289
  }),
274
290
  ],
275
291
  });
276
- const a = await table([withSoft], { rows: "agent", columns: [examScore] });
292
+ const a = await MetricTable.data([withSoft], { rows: "agent", columns: [examScore] });
277
293
  expect(a.rows[0].cells["exam-score"].value).toBe(0.75);
278
- const b = await table([noSoft], { rows: "agent", columns: [examScore] });
294
+ const b = await MetricTable.data([noSoft], { rows: "agent", columns: [examScore] });
279
295
  expect(b.rows[0].cells["exam-score"].value).toBe(1);
280
296
  });
281
297
 
282
298
  it("skipped → null,不进聚合", async () => {
283
299
  const s = snap({ experimentId: "exp/x", results: [res("A", "skipped")] });
284
- const data = await table([s], { rows: "agent", columns: [examScore] });
300
+ const data = await MetricTable.data([s], { rows: "agent", columns: [examScore] });
285
301
  expect(data.rows[0].cells["exam-score"].value).toBeNull();
286
302
  });
287
303
  });
@@ -299,7 +315,7 @@ describe("内置指标", () => {
299
315
  res("B", "failed"), // 无 usage → null,不稀释
300
316
  ],
301
317
  });
302
- const data = await table([s], { rows: "agent", columns: [tokens] });
318
+ const data = await MetricTable.data([s], { rows: "agent", columns: [tokens] });
303
319
  const cell = data.rows[0].cells["tokens"];
304
320
  expect(cell.value).toBe(1200);
305
321
  expect(cell.display).toBe("1.2k tokens");
@@ -318,16 +334,16 @@ describe("内置指标", () => {
318
334
  res("B", "errored", { durationMs: 3000 }),
319
335
  ],
320
336
  });
321
- const cost = await table([s], { rows: "agent", columns: [costUSD] });
337
+ const cost = await MetricTable.data([s], { rows: "agent", columns: [costUSD] });
322
338
  expect(cost.rows[0].cells["cost"].value).toBe(0.5);
323
- const dur = await table([s], { rows: "agent", columns: [durationMs] });
339
+ const dur = await MetricTable.data([s], { rows: "agent", columns: [durationMs] });
324
340
  expect(dur.rows[0].cells["duration"].value).toBe(2000); // (1000 + 3000)/2,errored 实测照算
325
341
  });
326
342
  });
327
343
 
328
- // ───────────────────────── scoreboard ─────────────────────────
344
+ // ───────────────────────── Scoreboard.data ─────────────────────────
329
345
 
330
- describe("scoreboard", () => {
346
+ describe("Scoreboard.data", () => {
331
347
  it("固定分母 + missing 如实 + 权重最长前缀生效", async () => {
332
348
  const alpha = snap({
333
349
  experimentId: "exp/alpha",
@@ -343,13 +359,13 @@ describe("scoreboard", () => {
343
359
  agent: "beta",
344
360
  results: [res("algebra/x", "passed", { agent: "beta" })],
345
361
  });
346
- const board = await scoreboard([alpha, beta], {
347
- of: "agent",
362
+ const board = await Scoreboard.data([alpha, beta], {
363
+ rows: "agent",
348
364
  subjects: "evalGroup",
349
365
  weights: { "algebra/": 3, "algebra/hard/": 9 },
350
366
  fullMarks: 100,
351
367
  });
352
- expect(board.of).toBe("agent");
368
+ expect(board.dimension).toBe("agent");
353
369
  // 生效权重表可审计:最长前缀在前(匹配顺序)
354
370
  expect(board.weights).toEqual([
355
371
  { prefix: "algebra/hard/", weight: 9 },
@@ -390,32 +406,43 @@ describe("scoreboard", () => {
390
406
  res("algebra/y", "skipped", { agent: "solo" }), // 无有效样本 → missing(按 0 计但如实标注)
391
407
  ],
392
408
  });
393
- const board = await scoreboard([solo], { of: "agent" });
409
+ const board = await Scoreboard.data([solo], { rows: "agent" });
394
410
  const row = board.rows[0];
395
411
  // 两题各 1 分:0.5 + 0(missing)→ 100 × 0.5/2 = 25
396
412
  expect(row.total.value).toBeCloseTo(25);
397
413
  expect(row.subjects[0].missing).toBe(1);
398
414
  expect(row.subjects[0].evals).toBe(2);
399
415
  });
416
+
417
+ it("快照携带的 knownEvalIds 进固定分母(发布目录上的残缺不消失)", async () => {
418
+ const s = snap({
419
+ experimentId: "exp/x",
420
+ agent: "solo",
421
+ results: [res("algebra/x", "passed", { agent: "solo" })],
422
+ knownEvalIds: ["algebra/x", "algebra/y"],
423
+ });
424
+ const board = await Scoreboard.data([s], { rows: "agent" });
425
+ expect(board.rows[0].subjects[0].evals).toBe(2);
426
+ expect(board.rows[0].subjects[0].missing).toBe(1);
427
+ expect(board.rows[0].total.value).toBeCloseTo(50);
428
+ });
400
429
  });
401
430
 
402
- // ───────────────────────── scatter ─────────────────────────
431
+ // ───────────────────────── MetricScatter.data ─────────────────────────
403
432
 
404
- describe("scatter", () => {
433
+ describe("MetricScatter.data", () => {
405
434
  it("任一轴 null 的点仍在 rows 里、可数;series 随组解析", async () => {
406
435
  const withCost = snap({
407
436
  experimentId: "exp/a",
408
437
  agent: "a1",
409
- results: [
410
- res("A", "passed", { agent: "a1", usage: { inputTokens: 10, outputTokens: 5, costUSD: 0.5 } }),
411
- ],
438
+ results: [res("A", "passed", { agent: "a1", usage: { inputTokens: 10, outputTokens: 5, costUSD: 0.5 } })],
412
439
  });
413
440
  const noCost = snap({
414
441
  experimentId: "exp/b",
415
442
  agent: "b1",
416
443
  results: [res("A", "passed", { agent: "b1" })],
417
444
  });
418
- const data = await scatter([withCost, noCost], {
445
+ const data = await MetricScatter.data([withCost, noCost], {
419
446
  points: "experiment",
420
447
  series: "agent",
421
448
  x: costUSD,
@@ -438,12 +465,59 @@ describe("scatter", () => {
438
465
  });
439
466
  });
440
467
 
441
- // ───────────────────────── overview ─────────────────────────
468
+ // ───────────────────────── param():维度与轴 ─────────────────────────
442
469
 
443
- describe("overview", () => {
444
- it("costUSD 全缺为 null 不编 0;有实测/估算则求和;warnings 透传", async () => {
470
+ describe("param()", () => {
471
+ const withParams = (id: string, params: Record<string, unknown> | undefined, outcome: ResultOutcome) =>
472
+ snap({
473
+ experimentId: id,
474
+ results: [res("A", outcome, { experimentId: id, experiment: { id, params } })],
475
+ });
476
+
477
+ it("MetricLine.data:x 收 param、按 experiment 聚合;未声明的作轴 x=null 报数", async () => {
478
+ const s1 = withParams("ultra/lat-100", { latencyMs: 100, agents: 1 }, "passed");
479
+ const s2 = withParams("ultra/lat-300", { latencyMs: 300, agents: 1 }, "failed");
480
+ const legacy = withParams("ultra/legacy", undefined, "passed");
481
+ const data = await MetricLine.data([s1, s2, legacy], {
482
+ x: param("latencyMs", { label: "Simulated latency", unit: "ms" }),
483
+ series: param("agents", { label: (v) => `${v} agents` }),
484
+ y: passRate,
485
+ });
486
+ expect(data.x).toEqual({ key: "latencyMs", label: "Simulated latency", unit: "ms" });
487
+ expect(data.series).toBe("agents");
488
+ expect(data.rows).toHaveLength(3);
489
+
490
+ const p100 = data.rows.find((r) => r.key === "ultra/lat-100")!;
491
+ expect(p100.x).toBe(100);
492
+ expect(p100.xDisplay).toBe("100ms");
493
+ expect(p100.series).toBe("1 agents");
494
+ expect(p100.y.value).toBe(1);
495
+
496
+ // 未声明 param 的 experiment 不猜:作轴 x=null(组件不画、注脚报数),分组归 (unset)
497
+ const legacyRow = data.rows.find((r) => r.key === "ultra/legacy")!;
498
+ expect(legacyRow.x).toBeNull();
499
+ expect(legacyRow.xDisplay).toBe("");
500
+ expect(legacyRow.series).toBe("(unset)");
501
+ });
502
+
503
+ it("param 当维度用:按声明值分组,label 函数折组名", async () => {
504
+ const s1 = withParams("exp/a", { agents: 1 }, "passed");
505
+ const s2 = withParams("exp/b", { agents: 16 }, "failed");
506
+ const data = await MetricTable.data([s1, s2], {
507
+ rows: param("agents", { label: (v) => `${v} agents` }),
508
+ columns: [passRate],
509
+ });
510
+ expect(data.dimension).toBe("agents");
511
+ expect(data.rows.map((r) => r.key)).toEqual(["1 agents", "16 agents"]);
512
+ });
513
+ });
514
+
515
+ // ───────────────────────── RunOverview.data ─────────────────────────
516
+
517
+ describe("RunOverview.data", () => {
518
+ it("costUSD 全缺为 null 不编 0;有实测/估算则求和;Snapshot[] 输入无警告", async () => {
445
519
  const bare = snap({ experimentId: "exp/x", results: [res("A", "passed"), res("B", "failed")] });
446
- const bareOverview = await overview([bare]);
520
+ const bareOverview = await RunOverview.data([bare]);
447
521
  expect(bareOverview.totals.costUSD).toBeNull();
448
522
  expect(bareOverview.totals.attempts).toBe(2);
449
523
  expect(bareOverview.totals.passed).toBe(1);
@@ -457,32 +531,41 @@ describe("overview", () => {
457
531
  res("B", "passed", { usage: { inputTokens: 1, outputTokens: 1, costUSD: 0.2 } }),
458
532
  ],
459
533
  });
460
- const data = await overview([priced], { warnings: ["snapshot covers 1 of 50 evals"] });
534
+ const data = await RunOverview.data([priced]);
461
535
  expect(data.totals.costUSD).toBeCloseTo(0.3);
462
536
  expect(data.totals.evals).toBe(2);
463
- expect(data.warnings).toEqual(["snapshot covers 1 of 50 evals"]);
464
537
  expect(data.snapshots).toEqual([
465
538
  { experimentId: "exp/y", agent: "agent-x", model: undefined, startedAt: priced.startedAt },
466
539
  ]);
467
540
  });
541
+
542
+ it("收 Selection 时 warnings 随行进数据 —— 诚实不靠使用者记得透传", async () => {
543
+ const s = snap({ experimentId: "exp/x", results: [res("A", "passed")] });
544
+ const warning: SelectionWarning = {
545
+ kind: "partial-coverage",
546
+ experimentId: "exp/x",
547
+ covered: 1,
548
+ total: 50,
549
+ message: "snapshot covers 1 of 50 evals seen in history",
550
+ };
551
+ const data = await RunOverview.data(selection([s], [warning]));
552
+ expect(data.warnings).toEqual([warning]);
553
+ });
468
554
  });
469
555
 
470
- // ───────────────────────── delta ─────────────────────────
556
+ // ───────────────────────── DeltaTable.data ─────────────────────────
471
557
 
472
- describe("delta", () => {
473
- it("任一侧 null → delta null 不硬算;双侧有值给带符号 display", async () => {
558
+ describe("DeltaTable.data", () => {
559
+ it("任一侧 null → delta null 不硬算;双侧有值给带符号 display;Δ=0 是 ±0", async () => {
474
560
  const base = snap({
475
561
  experimentId: "exp/base",
476
- results: [
477
- res("A", "failed"),
478
- res("B", "passed", { usage: { inputTokens: 1, outputTokens: 1, costUSD: 0.2 } }),
479
- ],
562
+ results: [res("A", "failed"), res("B", "passed", { usage: { inputTokens: 1, outputTokens: 1, costUSD: 0.2 } })],
480
563
  });
481
564
  const plus = snap({
482
565
  experimentId: "exp/plus",
483
566
  results: [res("A", "passed"), res("B", "passed")], // 无任何成本数据
484
567
  });
485
- const data = await delta([base, plus], {
568
+ const data = await DeltaTable.data([base, plus], {
486
569
  pairs: [{ a: "exp/base", b: "exp/plus", label: "memory" }],
487
570
  metrics: [passRate, costUSD],
488
571
  });
@@ -503,12 +586,45 @@ describe("delta", () => {
503
586
  expect(cost.b.value).toBeNull();
504
587
  expect(cost.delta).toBeNull(); // 单侧缺数据:不硬算
505
588
  expect(cost.display).toBe("—");
589
+
590
+ const flat = await DeltaTable.data([base, base], {
591
+ pairs: [{ a: "exp/base", b: "exp/base", label: "same" }],
592
+ metrics: [passRate],
593
+ });
594
+ expect(flat.rows[0].cells["pass-rate"].display).toBe("±0");
595
+ });
596
+
597
+ it("时间轴对比:pairs 的 a/b 收快照键 <experimentId> @ <startedAt>", async () => {
598
+ const older = snap({
599
+ experimentId: "exp/x",
600
+ runStartedAt: "2026-07-01T08:00:00Z",
601
+ results: [res("A", "failed")],
602
+ });
603
+ const newer = snap({
604
+ experimentId: "exp/x",
605
+ runStartedAt: "2026-07-02T08:00:00Z",
606
+ results: [res("A", "passed")],
607
+ });
608
+ const data = await DeltaTable.data([older, newer], {
609
+ pairs: [
610
+ {
611
+ a: "exp/x @ 2026-07-01T08:00:00Z",
612
+ b: "exp/x @ 2026-07-02T08:00:00Z",
613
+ label: "this week vs last",
614
+ },
615
+ ],
616
+ metrics: [passRate],
617
+ });
618
+ const cell = data.rows[0].cells["pass-rate"];
619
+ expect(cell.a.value).toBe(0); // 旧快照那份
620
+ expect(cell.b.value).toBe(1); // 新快照那份
621
+ expect(cell.display).toBe("+100%");
506
622
  });
507
623
  });
508
624
 
509
- // ───────────────────────── cases ─────────────────────────
625
+ // ───────────────────────── CaseList.data ─────────────────────────
510
626
 
511
- describe("cases", () => {
627
+ describe("CaseList.data", () => {
512
628
  it("默认只列 failed+errored;redact 作用于 error/detail/evidence;truncated 如实", async () => {
513
629
  const s = snap({
514
630
  experimentId: "exp/x",
@@ -532,7 +648,7 @@ describe("cases", () => {
532
648
  res("E", "skipped"),
533
649
  ],
534
650
  });
535
- const data = await cases([s], {
651
+ const data = await CaseList.data([s], {
536
652
  limit: 2,
537
653
  redact: (text) => text.replaceAll("/Users/me/repo", "<repo>"),
538
654
  });
@@ -545,7 +661,7 @@ describe("cases", () => {
545
661
  expect(first.failedAssertions).toHaveLength(1);
546
662
  expect(first.failedAssertions[0].detail).toBe("missing text under <repo>/src");
547
663
  expect(first.failedAssertions[0].evidence).toBe("checked <repo>/src/app.ts");
548
- expect(first.ref).toEqual({ run: s.run.dir.split("/").pop(), result: 0 });
664
+ expect(first.ref.result).toBe(0);
549
665
 
550
666
  expect(second.eval).toBe("B");
551
667
  expect(second.outcome).toBe("errored");
@@ -554,11 +670,8 @@ describe("cases", () => {
554
670
  });
555
671
 
556
672
  it("outcomes 可收窄;不传 limit 不截断", async () => {
557
- const s = snap({
558
- experimentId: "exp/x",
559
- results: [res("A", "failed"), res("B", "errored")],
560
- });
561
- const onlyErrored = await cases([s], { outcomes: ["errored"] });
673
+ const s = snap({ experimentId: "exp/x", results: [res("A", "failed"), res("B", "errored")] });
674
+ const onlyErrored = await CaseList.data([s], { outcomes: ["errored"] });
562
675
  expect(onlyErrored.rows.map((r) => r.eval)).toEqual(["B"]);
563
676
  expect(onlyErrored.truncated).toBe(0);
564
677
  });
@@ -585,25 +698,29 @@ describe("身份键去重", () => {
585
698
  results: [res("A", "passed", identity)],
586
699
  });
587
700
 
588
- for (const order of [[older, newer], [newer, older]]) {
589
- const data = await table(order, { rows: "agent", columns: [passRate] });
701
+ for (const order of [
702
+ [older, newer],
703
+ [newer, older],
704
+ ]) {
705
+ const data = await MetricTable.data(order, { rows: "agent", columns: [passRate] });
590
706
  const cell = data.rows[0].cells["pass-rate"];
591
707
  expect(cell.total).toBe(1); // 两份只算一份
592
708
  expect(cell.value).toBe(1); // 留的是最新 run 里的 passed,与快照传入顺序无关
593
709
 
594
- const ov = await overview(order);
710
+ const ov = await RunOverview.data(order);
595
711
  expect(ov.totals.attempts).toBe(1);
596
712
  expect(ov.totals.passed).toBe(1);
597
713
  expect(ov.totals.failed).toBe(0);
598
714
  }
599
715
  });
600
716
 
601
- it("startedAt 缺失:宁可不去重也不误删", async () => {
717
+ it("startedAt 缺失:不去重、如实保留重复,不透出警告", async () => {
602
718
  const identity = { experimentId: "exp/x", attempt: 0, startedAt: undefined };
603
719
  const one = snap({ experimentId: "exp/x", results: [res("A", "passed", identity)] });
604
720
  const two = snap({ experimentId: "exp/x", results: [res("A", "passed", identity)] });
605
- const ov = await overview([one, two]);
721
+ const ov = await RunOverview.data([one, two]);
606
722
  expect(ov.totals.attempts).toBe(2);
723
+ expect(ov.warnings).toEqual([]); // missing-startedAt 不透出到组件数据(裁决记录 7)
607
724
  });
608
725
  });
609
726
 
@@ -633,14 +750,14 @@ describe("unit 驱动格式化", () => {
633
750
  value: () => 0.5,
634
751
  });
635
752
  const s = snap({ experimentId: "exp/x", results: [res("A", "passed")] });
636
- const data = await table([s], { rows: "agent", columns: [raw] });
753
+ const data = await MetricTable.data([s], { rows: "agent", columns: [raw] });
637
754
  expect(data.rows[0].cells["raw"].display).toBe("0.5 raw");
638
755
  });
639
756
  });
640
757
 
641
- // ───────────────────────── matrix ─────────────────────────
758
+ // ───────────────────────── MetricMatrix.data(= MetricBars.data)─────────────────────────
642
759
 
643
- describe("matrix", () => {
760
+ describe("MetricMatrix.data", () => {
644
761
  it("稀疏:没有 attempt 的 (row, column) 组合不出格", async () => {
645
762
  const a = snap({
646
763
  experimentId: "exp/a",
@@ -652,16 +769,19 @@ describe("matrix", () => {
652
769
  agent: "b1",
653
770
  results: [res("A", "failed", { agent: "b1" })], // b1 没跑 B
654
771
  });
655
- const data = await matrix([a, b], { rows: "eval", columns: "agent", cell: passRate });
772
+ const data = await MetricMatrix.data([a, b], { rows: "eval", columns: "agent", cell: passRate });
656
773
  expect(data.rows).toBe("eval");
657
774
  expect(data.columns).toBe("agent");
658
775
  expect(data.metric.key).toBe("pass-rate");
659
776
  expect(data.cells).toHaveLength(3); // A×a1、B×a1、A×b1;B×b1 不出现
660
- const find = (row: string, column: string) =>
661
- data.cells.find((c) => c.row === row && c.column === column);
777
+ const find = (row: string, column: string) => data.cells.find((c) => c.row === row && c.column === column);
662
778
  expect(find("A", "a1")?.cell.value).toBe(1);
663
779
  expect(find("B", "a1")?.cell.value).toBe(0);
664
780
  expect(find("A", "b1")?.cell.value).toBe(0);
665
781
  expect(find("B", "b1")).toBeUndefined();
666
782
  });
783
+
784
+ it("MetricBars.data 就是 MetricMatrix.data 的别名(同一个函数)", () => {
785
+ expect(MetricBars.data).toBe(MetricMatrix.data);
786
+ });
667
787
  });