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
  // 双面验收:每个官方组件对同一份数据,web 面(renderToStaticMarkup)与 text 面
2
3
  // 给出一致判读 —— 排序方向随 better、samples < total 角标、缺数据 — 不补 0、
3
4
  // 截断如实报剩余;text 面形态以内联快照锁定(照 report-components.mdx 的示例形态)。
@@ -35,9 +36,10 @@ import {
35
36
  costUSD,
36
37
  defineComponent,
37
38
  defineReport,
39
+ endToEndPassRate,
38
40
  isReportDefinition,
39
41
  padEnd,
40
- passRate,
42
+ taskPassRate,
41
43
  renderReportToText,
42
44
  stringWidth,
43
45
  } from "./index.ts";
@@ -69,7 +71,7 @@ describe("RunOverview 双面", () => {
69
71
 
70
72
  it("text 面形态:头行(含通过率)+ 判定行 + 警告行", () => {
71
73
  expect(term).toMatchInlineSnapshot(`
72
- "2 experiments · 12 evals · 48 attempts · Pass rate 70% 46/48 · composed from 2 runs · latest 2026-07-01T11:30:00Z
74
+ "2 experiments · 12 evals · 48 attempts · End-to-end pass rate 70% 46/48 · composed from 2 runs · latest 2026-07-01T11:30:00Z
73
75
  passed 36 · failed 8 · errored 2 · skipped 2 · no data · 4m 21s
74
76
  ! snapshot covers 9 of 12 evals seen in history; re-run \`niceeval exp compare/bub\` for a full snapshot"
75
77
  `);
@@ -89,13 +91,14 @@ describe("RunOverview 双面", () => {
89
91
 
90
92
  // fixtures.overviewData 是手工摆好的终值,只验证渲染面「原样显示 MetricCell,不重算」;
91
93
  // 下面这组用真实 Selection 走一遍 RunOverview.data(= compute.ts 的 overviewData()),
92
- // 专门验证 totals.passRate 本身的计算口径 —— 三种通过率公式在这个 fixture 上各不相同:
93
- // 两级聚合(唯一官方口径,docs/feature/reports/architecture.md「指标聚合不变量」):eval a 题内 2/3 通过、eval b 题内 1,
94
- // 跨题均值 (2/3 + 1) / 2 = 5/683.3%
95
- // attempt 原始占比(旧 bug 公式,曾经的 RunOverview 现场重算):3 passed / (3 passed + 1 failed) = 75%
96
- // eval 折叠投票(evalLevelStats,GroupSummary/MetricTable meta 的口径):a、b 都折成 passed 2/2 = 100%
97
- // 三个数互不相同,任何一处偷懒复用另一个公式都会在这里露馅。
98
- describe("RunOverview.data · passRate 两级聚合口径", () => {
94
+ // 专门验证 totals.passRate 本身的计算口径 —— 四种通过率公式在这个 fixture 上各不相同:
95
+ // 默认端到端两级聚合:eval a 题内 2/3eval b 题内 1、eval c errored=0,
96
+ // 跨题均值 (2/3 + 1 + 0) / 3 = 5/955.6%
97
+ // 条件 taskPassRate 排除 errored:(2/3 + 1) / 2 = 5/6 ≈ 83.3%
98
+ // 端到端 attempt 平铺:3 passed / (3 passed + 1 failed + 1 errored) = 60%
99
+ // eval 折叠投票:a、b passed,c errored → 2/3 ≈ 66.7%
100
+ // 四个数互不相同,任何一处偷懒复用另一个公式都会在这里露馅。
101
+ describe("RunOverview.data · endToEndPassRate 两级聚合口径", () => {
99
102
  function fakeVaryingAttemptsContext(): { selection: Selection; attempts: { evalId: string; result: { verdict: EvalResult["verdict"] } }[] } {
100
103
  const dir = "/results/compare_bub/snap-1";
101
104
  const base = {
@@ -106,7 +109,7 @@ describe("RunOverview.data · passRate 两级聚合口径", () => {
106
109
  schemaVersion: 1,
107
110
  dir,
108
111
  };
109
- const mk = (evalId: string, verdict: "passed" | "failed" | "skipped", attemptIndex: number, minute: number) => ({
112
+ const mk = (evalId: string, verdict: "passed" | "failed" | "errored" | "skipped", attemptIndex: number, minute: number) => ({
110
113
  evalId,
111
114
  experimentId: "compare/bub",
112
115
  result: {
@@ -133,8 +136,10 @@ describe("RunOverview.data · passRate 两级聚合口径", () => {
133
136
  mk("algebra/a", "passed", 2, 2),
134
137
  // eval b:1 attempt 通过 → 题内 1
135
138
  mk("algebra/b", "passed", 0, 3),
136
- // eval c:1 attempt 跳过 —— 两级聚合与 eval 折叠计票都要把它排除在分母外
137
- mk("algebra/c", "skipped", 0, 4),
139
+ // eval c:1 attempt errored —— 默认端到端记 0,条件 taskPassRate 才排除
140
+ mk("algebra/c", "errored", 0, 4),
141
+ // eval d:1 attempt 跳过 —— 两级聚合与 eval 折叠计票都要把它排除在分母外
142
+ mk("algebra/d", "skipped", 0, 5),
138
143
  ];
139
144
  const evalIds = [...new Set(attempts.map((a) => a.evalId))];
140
145
  const snapshot: Snapshot = {
@@ -146,18 +151,18 @@ describe("RunOverview.data · passRate 两级聚合口径", () => {
146
151
  return { selection, attempts };
147
152
  }
148
153
 
149
- it("totals.passRate = 两级聚合 83.3%,既不等于 attempt 原始占比 75%,也不等于 eval 折叠投票 100%", async () => {
154
+ it("totals.passRate = 端到端两级聚合 55.6%,不排除 errored,也不复用平铺或 eval 折叠投票", async () => {
150
155
  const { selection, attempts } = fakeVaryingAttemptsContext();
151
156
  const data = await RunOverview.data(selection);
152
157
 
153
- expect(data.totals.passRate.value).toBeCloseTo(5 / 6, 10);
154
- expect(data.totals.passRate.display).toBe("83.3%");
155
- expect(data.totals.passRate.samples).toBe(4); // 5 attempts - 1 skipped(不进桶)
156
- expect(data.totals.passRate.total).toBe(5);
158
+ expect(data.totals.passRate.value).toBeCloseTo(5 / 9, 10);
159
+ expect(data.totals.passRate.display).toBe("55.6%");
160
+ expect(data.totals.passRate.samples).toBe(5); // 6 attempts - 1 skipped(errored 进桶且记 0)
161
+ expect(data.totals.passRate.total).toBe(6);
157
162
 
158
163
  // attempt 原始占比(旧 bug 公式):必须与两级聚合不同,证明没有从 passed/failed/errored 现算
159
164
  const attemptFraction = data.totals.passed / (data.totals.passed + data.totals.failed + data.totals.errored);
160
- expect(attemptFraction).toBeCloseTo(0.75, 10);
165
+ expect(attemptFraction).toBeCloseTo(0.6, 10);
161
166
  expect(attemptFraction).not.toBeCloseTo(data.totals.passRate.value as number, 3);
162
167
 
163
168
  // eval 折叠投票(evalLevelStats,GroupSummary 的口径):也必须与两级聚合不同
@@ -165,20 +170,9 @@ describe("RunOverview.data · passRate 两级聚合口径", () => {
165
170
  attempts.map((a) => ({ verdict: a.result.verdict, key: a.evalId })),
166
171
  (r) => r.key,
167
172
  );
168
- expect(stats.passRate).toBeCloseTo(1, 10);
173
+ expect(stats.passRate).toBeCloseTo(2 / 3, 10);
169
174
  expect(stats.passRate).not.toBeCloseTo(data.totals.passRate.value as number, 3);
170
175
  });
171
-
172
- it("web 面与 text 面显示同一个 passRate.display,覆盖率角标(4/5)两面一致", async () => {
173
- const { selection } = fakeVaryingAttemptsContext();
174
- const data = await RunOverview.data(selection);
175
- const html = renderToStaticMarkup(<RunOverview data={data} />);
176
- const term = text(<RunOverview data={data} />);
177
- for (const face of [html, term]) {
178
- expect(face).toContain(data.totals.passRate.display);
179
- expect(face).toContain("4/5");
180
- }
181
- });
182
176
  });
183
177
 
184
178
  // ───────────────────────── GroupSummary ─────────────────────────
@@ -189,7 +183,7 @@ describe("GroupSummary 双面", () => {
189
183
 
190
184
  it("text 面形态:一行头(通过率 + experiment/eval 数 + failed/errored + 总成本)+ 最后运行时间", () => {
191
185
  expect(term).toMatchInlineSnapshot(`
192
- "Pass rate 60% 5/6 · 2 experiments · 6 evals · failed 1 · errored 1 · $1.50
186
+ "End-to-end pass rate 60% 5/6 · 2 experiments · 6 evals · failed 1 · errored 1 · $1.50
193
187
  latest 2026-07-01T11:30:00Z"
194
188
  `);
195
189
  });
@@ -237,9 +231,9 @@ describe("GroupSummary 双面", () => {
237
231
  expect(term).toContain("errored 1");
238
232
  });
239
233
 
240
- it("zh-CN locale:web 面走中文字典(通过率/失败/错误/总成本/实验数),text 面同理;display 数字不本地化", () => {
234
+ it("zh-CN locale:web 面走中文字典(端到端成功率/失败/错误/总成本/实验数),text 面同理;display 数字不本地化", () => {
241
235
  const zhHtml = renderToStaticMarkup(<GroupSummary data={groupSummaryData} locale="zh-CN" />);
242
- expect(zhHtml).toContain("通过率");
236
+ expect(zhHtml).toContain("端到端成功率");
243
237
  expect(zhHtml).toContain("失败");
244
238
  expect(zhHtml).toContain("错误");
245
239
  expect(zhHtml).toContain("总成本");
@@ -248,7 +242,7 @@ describe("GroupSummary 双面", () => {
248
242
 
249
243
  const zhCtx = createTextContext({ width: 80, locale: "zh-CN" });
250
244
  const zhTerm = renderNodeToText(<GroupSummary data={groupSummaryData} />, zhCtx);
251
- expect(zhTerm).toContain("通过率");
245
+ expect(zhTerm).toContain("端到端成功率");
252
246
  expect(zhTerm).toContain("失败 1");
253
247
  expect(zhTerm).toContain("错误 1");
254
248
  expect(zhTerm).toContain("60%");
@@ -462,11 +456,17 @@ describe("AttemptList 双面", () => {
462
456
  const html = renderToStaticMarkup(<AttemptList items={attemptListItems} />);
463
457
  const term = text(<AttemptList items={attemptListItems} />);
464
458
 
465
- it("两面同口径:判定符 + locator + 断言/error 明细 + 下钻命令一致", () => {
466
- for (const piece of ["roots-correct", "expected x=2, got x=3", "TypeError", "@1a4a4a4", "@1c1c1c1"]) {
459
+ it("两面同口径:判定符 + locator + 主失败/error 摘要一致", () => {
460
+ for (const piece of ["roots-correct", "expected x=2", "received x=3", "TypeError", "@1a4a4a4", "@1c1c1c1"]) {
467
461
  expect(html).toContain(piece);
468
462
  expect(term).toContain(piece);
469
463
  }
464
+ // 结构化 error 只显示一层摘要:stack / cause / diagnostics 属于 locator 下钻详情,
465
+ // 随数据携带但不塞进比较列表(docs/feature/reports/library.md「AttemptList」)。
466
+ for (const face of [html, term]) {
467
+ expect(face).not.toContain("adapter.ts:42"); // error.stack
468
+ expect(face).not.toContain("sandbox teardown timed out"); // diagnostics[].message
469
+ }
470
470
  // web 面走证据室路由(#/attempt/@<locator>);text 面只列 locator 本身,不重复整条命令
471
471
  // (docs/feature/reports/architecture.md「text 输出只在整份报告末尾给一次命令模板」)。
472
472
  expect(html).toContain('href="#/attempt/@1a4a4a4"');
@@ -481,7 +481,7 @@ describe("AttemptList 双面", () => {
481
481
  expect(termTrunc).toContain("2 more not shown");
482
482
  });
483
483
 
484
- it("text 面截断超长 assertion evidence,并保留 locator 作为完整证据入口", () => {
484
+ it("比较列表不内联 assertion evidence,并保留 locator 作为完整证据入口", () => {
485
485
  const longEvidence = "x".repeat(2_000);
486
486
  const item = {
487
487
  ...attemptListItems[0],
@@ -489,17 +489,17 @@ describe("AttemptList 双面", () => {
489
489
  };
490
490
  const out = text(<AttemptList items={[item]} />);
491
491
  expect(out).toContain(item.locator);
492
- expect(out.replace(/\s+/g, " ")).toContain(`open ${item.locator} for full evidence`);
493
492
  expect(out).not.toContain(longEvidence);
494
493
  expect(out.length).toBeLessThan(1_000);
495
494
  });
496
495
 
497
- it("text 面是逐条卡片,不是 renderTableText 的产物:空行分隔每个 attempt、断言明细逐级缩进——共享表格渲染器不产生这种嵌套形状", () => {
496
+ it("text 面每个 attempt 只显示一条有界结果摘要,不展开完整断言列表", () => {
498
497
  const blocks = term.split("\n\n");
499
- expect(blocks).toHaveLength(attemptListItems.length); // 每个 attempt 独立一块,块间空行分隔;表格行之间不留空行
498
+ expect(blocks).toHaveLength(attemptListItems.length);
500
499
  const lines = term.split("\n");
501
- expect(lines.some((l) => /^ \S/.test(l))).toBe(true); // 断言行缩进两格
502
- expect(lines.some((l) => /^ \S/.test(l))).toBe(true); // 断言 detail 再缩进两格,是卡片的层级,不是列
500
+ expect(lines.some((l) => /^ \S/.test(l))).toBe(true);
501
+ expect(lines.some((l) => /^ \S/.test(l))).toBe(false);
502
+ expect(term).not.toContain("strong-evidence");
503
503
  });
504
504
  });
505
505
 
@@ -524,6 +524,16 @@ describe("EvalList 双面", () => {
524
524
  expect(blocks).toHaveLength(evalListItems.length);
525
525
  expect(term.split("\n").some((l) => /^ \S/.test(l))).toBe(true); // 展开到 attempt 的行缩进两格
526
526
  });
527
+
528
+ // bug: memory/eval-parent-repeats-attempt-failure.md
529
+ it("Eval 父行不复述 Attempt 失败摘要,失败题仍显示题级平均值", () => {
530
+ const reason = "roots-correct · expected x=2 · received x=3";
531
+ for (const face of [html, term]) {
532
+ expect(face.split(reason)).toHaveLength(2);
533
+ expect(face).toContain("32.0s avg");
534
+ expect(face).toContain("$0.12 avg");
535
+ }
536
+ });
527
537
  });
528
538
 
529
539
  describe("ExperimentList 双面", () => {
@@ -535,7 +545,7 @@ describe("ExperimentList 双面", () => {
535
545
  expect(html).toContain(piece);
536
546
  expect(term).toContain(piece);
537
547
  }
538
- // 官方两级聚合 passRate.display 两面同一个数字,不各自重算
548
+ // 官方两级聚合 endToEndPassRate.display 两面同一个数字,不各自重算
539
549
  expect(html).toContain("50%");
540
550
  expect(term).toContain("50%");
541
551
  });
@@ -548,6 +558,47 @@ describe("ExperimentList 双面", () => {
548
558
  expect(blocks[1]!.match(/algebra\/quadratic/g)).toHaveLength(1);
549
559
  expect(blocks[1]).toMatch(/✗ failed\s+algebra\/quadratic[\s\S]*├─ @1a4a4a4[\s\S]*└─ @1b5b5b5/);
550
560
  });
561
+
562
+ // bug: memory/eval-parent-repeats-attempt-failure.md
563
+ it("web 的 Eval 父行使用固定题级汇总,失败摘要只在 Attempt 子行出现", () => {
564
+ const reason = "roots-correct · expected x=2 · received x=3";
565
+ expect(html.split(reason)).toHaveLength(3); // 两个 failed Attempt,各显示一次
566
+ expect(html).toContain('<span class="nre-eval-rollup">32.0s avg · $0.12 avg</span>');
567
+ expect(term).toMatch(/✗ failed\s+algebra\/quadratic\s+32\.0s avg\s+\$0\.12 avg/);
568
+ });
569
+
570
+ it("Result 是两行收口的预览:received 携带整份多行源码时既不逐行铺表也不无限折行", () => {
571
+ const fatAttempt = {
572
+ ...experimentListItems[0]!.evalRows[0]!.attempts[0]!,
573
+ assertions: [{
574
+ name: "includes(/['\"]use cache['\"];?/)",
575
+ groupPath: ["Catalog reads use use-cache directive and products cache tag"],
576
+ severity: "gate" as const,
577
+ score: 0,
578
+ outcome: "failed" as const,
579
+ expected: "matches /['\"]use cache['\"];?/",
580
+ received: `\n// next.config.ts\n${"import type { NextConfig } from 'next';\n\n".repeat(50)}`,
581
+ }],
582
+ };
583
+ const fatItems = [{
584
+ ...experimentListItems[0]!,
585
+ evalRows: [{
586
+ ...experimentListItems[0]!.evalRows[0]!,
587
+ attempts: [fatAttempt],
588
+ }],
589
+ }];
590
+ const detail = renderNodeToText(<ExperimentList items={fatItems} />, createTextContext({ width: 100 }))
591
+ .split("\n\n")
592
+ .at(-1)!;
593
+ const lines = detail.split("\n");
594
+ // 表头 1 + eval 父行 1 + attempt 子行 ≤2(Result 列 maxLines: 2),外加 experimentId 头行
595
+ expect(lines.length).toBeLessThanOrEqual(6);
596
+ expect(detail).toContain("…");
597
+ expect(lines.every((l) => l.trim().length > 0)).toBe(true); // 值里的空行不进表
598
+ // 优先级让位:语义标题先截(全称不再出现),事实字段(expected/received)最后截
599
+ expect(detail).not.toContain("Catalog reads use use-cache directive and products cache tag");
600
+ expect(detail).toContain("expected");
601
+ });
551
602
  });
552
603
 
553
604
  // ───────────────────────── 排版原语 ─────────────────────────
@@ -894,8 +945,8 @@ function fakeContext(): { selection: Selection; results: Results } {
894
945
  }
895
946
 
896
947
  /**
897
- * 三份快照、三个 experiment("compare/bub"、"other/codex"、"solo"),全部通过、无成本——
898
- * 一个多实验 Selection 夹具:ExperimentList 出三项,MetricScatter 三个点都无成本
948
+ * 五份快照、两个双配置目录组 + 一个根目录单例组,全部通过、无成本——
949
+ * 一个多实验 Selection 夹具:ExperimentList 出五项,MetricScatter 五个点都无成本
899
950
  * (0 可画点,如实走空态)。`filter` 落实真实语义(不像 fakeContext 那样恒等返回自己)。
900
951
  */
901
952
  function fakeMultiGroupContext(): { selection: Selection; results: Results } {
@@ -938,7 +989,9 @@ function fakeMultiGroupContext(): { selection: Selection; results: Results } {
938
989
 
939
990
  const snapshots = [
940
991
  mkSnapshot("compare/bub", "bub", "compare_bub/snap-1"),
992
+ mkSnapshot("compare/codex", "codex", "compare_codex/snap-1"),
941
993
  mkSnapshot("other/codex", "codex", "other_codex/snap-1"),
994
+ mkSnapshot("other/bub", "bub", "other_bub/snap-1"),
942
995
  mkSnapshot("solo", "bub", "solo/snap-1"),
943
996
  ];
944
997
 
@@ -991,7 +1044,7 @@ describe("defineReport + 渲染入口", () => {
991
1044
  expect(out).toContain("1 experiment · 2 evals · 2 attempts");
992
1045
  // ExperimentList:主行 + eval 级折叠计票 + 逐 Attempt locator 与失败诊断
993
1046
  expect(out).toMatch(/compare\/bub\s+default\s+bub/);
994
- expect(out).toContain("1 passed / 1 failed");
1047
+ expect(out).toMatch(/1 passed[\s\S]*?\/ 1\s+failed/);
995
1048
  expect(out).toContain("50%");
996
1049
  expect(out).toMatch(/✗ failed\s+algebra\/y[\s\S]*└─ @[0-9a-z]+/);
997
1050
  // 自己的口径:成绩单
@@ -1043,7 +1096,7 @@ describe("defineReport + 渲染入口", () => {
1043
1096
  // selection-form 的官方组件(ExperimentList/EvalList/AttemptList 没有 selection-form,
1044
1097
  // 这条契约不适用于它们)。
1045
1098
  expect(() =>
1046
- renderToStaticMarkup(<MetricScatter selection={selection} points="experiment" x={costUSD} y={passRate} />),
1099
+ renderToStaticMarkup(<MetricScatter selection={selection} points="experiment" x={costUSD} y={taskPassRate} />),
1047
1100
  ).toThrow(/received unresolved \(selection-form\) props/);
1048
1101
  });
1049
1102
  });
@@ -1056,9 +1109,9 @@ describe("defineReport + 渲染入口", () => {
1056
1109
  // ExperimentComparison / defineReport 报告在同一文件里编译验证。
1057
1110
  function metricScatterPropsTypeChecks(selection: Selection, data: ScatterData): void {
1058
1111
  const ok1: MetricScatterProps = { data }; // 合法:data 形态
1059
- const ok2: MetricScatterProps = { selection, points: "experiment", series: "agent", x: costUSD, y: passRate }; // 合法:selection 形态
1112
+ const ok2: MetricScatterProps = { selection, points: "experiment", series: "agent", x: costUSD, y: taskPassRate }; // 合法:selection 形态
1060
1113
  // @ts-expect-error 同时传 data 与 selection:非法
1061
- const bad1: MetricScatterProps = { data, selection, points: "experiment", x: costUSD, y: passRate };
1114
+ const bad1: MetricScatterProps = { data, selection, points: "experiment", x: costUSD, y: taskPassRate };
1062
1115
  // @ts-expect-error data 与 selection 都不传:非法
1063
1116
  const bad2: MetricScatterProps = { pointHref: () => "/x" };
1064
1117
  // @ts-expect-error selection 形态缺必填的 x / y:非法
@@ -1075,55 +1128,129 @@ void metricScatterPropsTypeChecks;
1075
1128
  // ───────────────────────── ExperimentComparison(内置默认报告)─────────────────────────
1076
1129
 
1077
1130
  describe("ExperimentComparison", () => {
1078
- it("是普通 ReportDefinition;text 面 = 成本×通过率散点 + 实验列表,别无它物", async () => {
1131
+ it("是普通 ReportDefinition;单组 text 面 = 组摘要 + 成本×端到端成功率散点 + 实验列表", async () => {
1079
1132
  expect(isReportDefinition(ExperimentComparison)).toBe(true);
1080
1133
  const out = await renderReportToText(ExperimentComparison, fakeContext(), { width: 100 });
1081
1134
  // 散点:fakeContext 无成本数据 → 0 可画点,显式说明缺哪两个指标(而不是画一张空图)
1082
1135
  expect(out).toContain("No data to plot");
1083
1136
  expect(out).not.toContain("better → upper right");
1084
1137
  // 实验列表主行 + eval 级折叠计票 + 失败诊断(ExperimentList.data 在 build() 里直接 await)
1085
- expect(out).toMatch(/compare\/bub\s+default\s+bub/);
1086
- expect(out).toContain("1 passed / 1 failed");
1138
+ // 组已由 section 标题 "compare" 标注,列表行传 relativeTo=组键 去掉前缀,只显示 id 末段
1139
+ expect(out).toMatch(/^compare$/m);
1140
+ expect(out).toMatch(/^\s*bub\s+default\s+bub/m);
1141
+ expect(out).not.toContain("compare/bub");
1142
+ expect(out).toMatch(/1 passed[\s\S]*?\/ 1\s+failed/);
1087
1143
  expect(out).toContain("50%");
1088
1144
  expect(out).toMatch(/✗ failed\s+algebra\/y[\s\S]*└─ @[0-9a-z]+/);
1089
- // 只有两个直接业务组件:没有 RunOverview / GroupSummary / Section 分组
1145
+ // 单组直接进入详情,不出现多组查看命令
1090
1146
  expect(out).not.toContain("Current verdicts");
1147
+ expect(out).not.toContain("niceeval show --experiment");
1091
1148
  });
1092
1149
 
1093
- it("web 面:散点空态 + 实验列表 <details> 展开区,无 <script>,无 Section 分组", async () => {
1150
+ it("web 面:完整组索引 + 首组 panel + 实验列表 <details> 展开区,无 <script>", async () => {
1094
1151
  const html = await renderReportToStaticHtml(ExperimentComparison, fakeContext());
1152
+ expect(html).toContain("nre-experiment-group-tabs");
1153
+ expect(html).toMatch(/<details[^>]*data-nre-experiment-group-panel="0"[^>]*\sopen=""/);
1095
1154
  expect(html).toContain("nre-metric-scatter");
1096
1155
  expect(html).toContain("nre-scatter-empty"); // 0 可画点的空态
1097
1156
  expect(html).toContain('<details class="nre-experiment-entry">');
1157
+ // 行标签去掉组前缀只显示 id 末段;完整 id 仍留在 data-sort-value 作排序/身份键
1158
+ expect(html).toMatch(/nre-experiment-id[^>]*>\s*bub\s*<\/b>/);
1159
+ expect(html).toContain('data-sort-value="compare/bub"');
1098
1160
  expect(html).toContain("nre-experiment-head");
1099
1161
  expect(html).toContain('data-nre-experiment-filter=""');
1100
1162
  expect(html).toContain("nre-experiment-evals");
1101
- expect(html).not.toContain("nre-section");
1102
1163
  expect(html).not.toContain("<script");
1103
1164
  });
1104
1165
 
1105
1166
  it("locale 变体:en / zh-CN 都渲染(chrome 分语言),散点空态两面同一事实", async () => {
1106
1167
  const zhHtml = await renderReportToStaticHtml(ExperimentComparison, fakeContext(), { locale: "zh-CN" });
1107
- expect(zhHtml).toContain("成功率"); // passRate 的 zh-CN label(ExperimentList 主行)
1168
+ expect(zhHtml).toContain("端到端成功率");
1108
1169
  const enHtml = await renderReportToStaticHtml(ExperimentComparison, fakeContext(), { locale: "en" });
1109
- expect(enHtml).toContain("Pass rate");
1170
+ expect(enHtml).toContain("End-to-end pass rate");
1110
1171
  const zhText = await renderReportToText(ExperimentComparison, fakeContext(), { locale: "zh-CN" });
1111
1172
  expect(zhText).toContain("没有可绘制的数据"); // 散点空态 zh
1112
1173
  const enText = await renderReportToText(ExperimentComparison, fakeContext(), { locale: "en" });
1113
1174
  expect(enText).toContain("No data to plot");
1114
1175
  });
1115
1176
 
1116
- it("多实验 fixture:每个 experiment 一项,散点如实处理全部无成本", async () => {
1177
+ it("多组 fixture:text 只给组索引与命令;web 保留全部独立 panel", async () => {
1117
1178
  const out = await renderReportToText(ExperimentComparison, fakeMultiGroupContext(), { width: 100 });
1118
- // 三个 experiment 的身份行都出现(experimentId · agent,不再截短成最后一段)
1119
- expect(out).toMatch(/compare\/bub\s+default\s+bub/);
1120
- expect(out).toMatch(/other\/codex\s+default\s+codex/);
1121
- expect(out).toMatch(/solo\s+default\s+bub/);
1122
- // 散点空态(三点都无成本)
1123
- expect(out).toContain("No data to plot");
1124
- // 没有组分 Section 标题(不再按目录前缀分组)
1125
- expect(out).not.toMatch(/^compare$/m);
1179
+ expect(out).toContain("Experiment groups");
1180
+ expect(out).toContain("niceeval show --experiment compare");
1181
+ expect(out).toContain("niceeval show --experiment other");
1182
+ expect(out).toContain("niceeval show --experiment solo");
1183
+ expect(out).not.toMatch(/compare\/bub\s+default\s+bub/);
1184
+ expect(out).not.toContain("No data to plot");
1126
1185
  const html = await renderReportToStaticHtml(ExperimentComparison, fakeMultiGroupContext());
1127
- expect(html).not.toContain("nre-section");
1186
+ expect(html.match(/data-nre-experiment-group-panel=/g)).toHaveLength(3);
1187
+ expect(html.match(/<details[^>]*nre-experiment-group-panel[^>]*\sopen=""/g)).toHaveLength(1);
1188
+ expect(html).toContain("compare/bub");
1189
+ expect(html).toContain("other/codex");
1190
+ expect(html).toContain("solo");
1191
+ });
1192
+
1193
+ it(".data():组内三个子块 = 对该组单独调用 GroupSummary / MetricScatter / ExperimentList", async () => {
1194
+ const { selection } = fakeContext();
1195
+ const data = await ExperimentComparison.data(selection);
1196
+ expect(data.groups).toHaveLength(1);
1197
+ expect(data.groups[0]!.key).toBe("compare");
1198
+ expect(data.groups[0]!.summary).toEqual(await GroupSummary.data(selection));
1199
+ expect(data.groups[0]!.scatter).toEqual(
1200
+ await MetricScatter.data(selection, { points: "experiment", series: "agent", x: costUSD, y: endToEndPassRate }),
1201
+ );
1202
+ expect(data.groups[0]!.experiments).toEqual(await ExperimentList.data(selection));
1203
+ });
1204
+
1205
+ it(".data():计算前按完整父路径分区,两个目录组和根目录单例互不串数据或 refs", async () => {
1206
+ const { selection } = fakeMultiGroupContext();
1207
+ const data = await ExperimentComparison.data(selection);
1208
+ expect(data.groups.map((group) => group.key)).toEqual(["compare", "other", "solo"]);
1209
+
1210
+ for (const group of data.groups) {
1211
+ const groupSelection = selection.filter((snapshot) => {
1212
+ const slash = snapshot.experimentId.lastIndexOf("/");
1213
+ const key = slash === -1 ? snapshot.experimentId : snapshot.experimentId.slice(0, slash);
1214
+ return key === group.key;
1215
+ });
1216
+ expect(group.summary).toEqual(await GroupSummary.data(groupSelection));
1217
+ expect(group.scatter).toEqual(
1218
+ await MetricScatter.data(groupSelection, {
1219
+ points: "experiment",
1220
+ series: "agent",
1221
+ x: costUSD,
1222
+ y: endToEndPassRate,
1223
+ }),
1224
+ );
1225
+ expect(group.experiments).toEqual(await ExperimentList.data(groupSelection));
1226
+ expect(group.scatter.rows.every((row) => group.experiments.some((item) => item.experimentId === row.key))).toBe(
1227
+ true,
1228
+ );
1229
+ }
1230
+
1231
+ expect(data.groups.find((group) => group.key === "compare")!.experiments.map((item) => item.experimentId)).toEqual([
1232
+ "compare/bub",
1233
+ "compare/codex",
1234
+ ]);
1235
+ expect(data.groups.find((group) => group.key === "other")!.experiments.map((item) => item.experimentId)).toEqual([
1236
+ "other/bub",
1237
+ "other/codex",
1238
+ ]);
1239
+ expect(data.groups.find((group) => group.key === "solo")!.experiments.map((item) => item.experimentId)).toEqual([
1240
+ "solo",
1241
+ ]);
1242
+ });
1243
+
1244
+ it("组合件形态:<ExperimentComparison data={await .data(selection)}/> 与裸跑(build 面)渲染同一事实", async () => {
1245
+ const data = await ExperimentComparison.data(fakeContext().selection);
1246
+ const asComponent = defineReport(() => <ExperimentComparison data={data} />);
1247
+ // text 面逐字相等:同一份数据、同一套子组件口径
1248
+ const componentText = await renderReportToText(asComponent, fakeContext(), { width: 100 });
1249
+ const bareText = await renderReportToText(ExperimentComparison, fakeContext(), { width: 100 });
1250
+ expect(componentText).toBe(bareText);
1251
+ // web 面同一事实
1252
+ const componentHtml = await renderReportToStaticHtml(asComponent, fakeContext());
1253
+ const bareHtml = await renderReportToStaticHtml(ExperimentComparison, fakeContext());
1254
+ expect(componentHtml).toBe(bareHtml);
1128
1255
  });
1129
1256
  });
@@ -1,8 +1,15 @@
1
- // flag():把 experiment 声明的 flags 当维度或轴(docs/feature/reports/library.md「维度与 flags」)。
1
+ // flag() / config():把 experiment 声明的变量当维度或轴(docs/feature/reports/library.md「维度与 flags」)。
2
2
  // 变量来自配置,不来自命名 —— 报告不解析 experiment id 字符串抠变量。
3
+ // flag() 只读 `ExperimentDef.flags` 里显式声明的 KV;model / reasoningEffort / budget / runs
4
+ // 这类顶层运行配置不在 flags 里,用 config() 读快照的 ExperimentRunInfo 投影。
3
5
 
4
- import type { FlagRef } from "./types.ts";
6
+ import type { ConfigRef, FlagRef } from "./types.ts";
5
7
 
8
+ /**
9
+ * 把 experiment 声明的一个 flag 当分组维度(series / rows / columns / points 槽)或数值轴
10
+ * (MetricLine 的 x 槽)。只读 `ExperimentDef.flags` 里显式声明的 KV;未声明的 experiment
11
+ * 分组归 `(unset)`,作轴不画点并报告缺失。
12
+ */
6
13
  export function flag(
7
14
  name: string,
8
15
  opts?: {
@@ -16,3 +23,24 @@ export function flag(
16
23
  }
17
24
  return { kind: "flag", name, label: opts?.label, unit: opts?.unit };
18
25
  }
26
+
27
+ /**
28
+ * 把一项顶层运行配置当分组维度或数值轴,与 {@link flag} 同一套用法。读快照的
29
+ * `ExperimentRunInfo` 投影(可用键是那张接口的字段全集),外加桥接到快照顶层权威字段的
30
+ * `model` / `agent` 两个键。未投影的值分组归 `(unset)`,作轴不画点并报告缺失。
31
+ */
32
+ export function config(
33
+ name: string,
34
+ opts?: {
35
+ /** 组标签 / 轴标签;函数形态把投影值折成组名(如 `(v) => \`effort ${v}\``)。 */
36
+ label?: string | ((value: string | number | boolean) => string);
37
+ unit?: string;
38
+ },
39
+ ): ConfigRef {
40
+ if (typeof name !== "string" || name.length === 0) {
41
+ throw new Error(
42
+ "config: name must be a non-empty string (an ExperimentRunInfo field, or the bridged \"model\" / \"agent\" keys).",
43
+ );
44
+ }
45
+ return { kind: "config", name, label: opts?.label, unit: opts?.unit };
46
+ }
@@ -3,6 +3,21 @@
3
3
  // metric.display 可整体覆盖;这里只负责默认。
4
4
 
5
5
  import type { AssertionResult, Verdict } from "../types.ts";
6
+ import { compactAssertionSummary, fitCompactAssertionSummary, primaryAssertionSummary } from "../scoring/display.ts";
7
+
8
+ /**
9
+ * experiment 行的显示名:给了父路径 `relativeTo` 且它确是前缀,就去掉 `relativeTo + "/"`,
10
+ * 只留 id 末段——用在已经以组为标题的上下文(如默认 `ExperimentComparison` 的每组面板)里,
11
+ * 避免每行重复文件夹名。组键就是 experiment id 的父目录,因此这里的末段与 `MetricScatter`
12
+ * 点标签取的末段同源。不给 `relativeTo`、或它不是前缀(如根目录单例组)时原样返回完整 id。
13
+ * 完整 id 仍是排序 / 着色 / 折叠的键,调用方不要拿这个显示名当身份用。
14
+ */
15
+ export function experimentDisplayName(experimentId: string, relativeTo?: string): string {
16
+ if (relativeTo && experimentId.startsWith(`${relativeTo}/`)) {
17
+ return experimentId.slice(relativeTo.length + 1);
18
+ }
19
+ return experimentId;
20
+ }
6
21
 
7
22
  /** 一位小数、去掉无意义的 ".0" 尾巴。 */
8
23
  function trimmed(n: number): string {
@@ -94,16 +109,25 @@ export function verdictMark(verdict: Verdict): string {
94
109
  }
95
110
 
96
111
  /**
97
- * 一个 AttemptListItem 的失败原因摘要,按优先级取第一个在场的:`error` 未通过的 gate 断言
98
- * (原始声明顺序,`name`,detail 在场则 `"name: detail"`,多条用「, 」连接)→ 缺席。与
99
- * compute.ts `reasonFor(EvalResult)` 同一口径,只是输入换成瘦身后的 AttemptListItem——
100
- * 没有 `skipReason` 字段(AttemptListItem 不携带),skipped 的 attempt 因此没有原因摘要,
101
- * 这与它们本来就不该出现在「为什么失败」列表里的事实一致。EvalList / ExperimentList 的
102
- * 逐 attempt 徽标行用它给每个 attempt 自己的原因(而不是复用整道题的代表原因)。
112
+ * Attempt 比较项的一层结果摘要;完整 assertions 只在 locator 详情里展开。
113
+ * maxChars(可选)是渲染面的宽度收口预算(如两行单元格 = 2 × 列宽):断言摘要按
114
+ * fitCompactAssertionSummary 的优先级让位,error 摘要折单行后尾截。
103
115
  */
104
- export function attemptItemReason(item: { error?: string; assertions: AssertionResult[] }): string | undefined {
105
- if (item.error !== undefined) return item.error;
106
- const gates = item.assertions.filter((a) => !a.passed && a.severity === "gate");
107
- if (gates.length === 0) return undefined;
108
- return gates.map((a) => (a.detail ? `${a.name}: ${a.detail}` : a.name)).join(", ");
116
+ export function attemptItemReason(
117
+ item: {
118
+ verdict: Verdict;
119
+ error?: { message: string };
120
+ assertions: AssertionResult[];
121
+ },
122
+ maxChars?: number,
123
+ ): string | undefined {
124
+ if (item.error !== undefined) {
125
+ const message = item.error.message.replace(/\s+/g, " ").trim();
126
+ return maxChars !== undefined && message.length > maxChars
127
+ ? `${message.slice(0, Math.max(0, maxChars - 1))}…`
128
+ : message;
129
+ }
130
+ const summary = primaryAssertionSummary(item.assertions, item.verdict);
131
+ if (summary === undefined) return undefined;
132
+ return maxChars === undefined ? compactAssertionSummary(summary) : fitCompactAssertionSummary(summary, maxChars);
109
133
  }
@@ -8,9 +8,19 @@
8
8
  // ./web.ts,只有那一侧 import react-dom。写报告文件的项目要装 react(.tsx 编译产物
9
9
  // import react/jsx-runtime)。
10
10
 
11
- // 指标与 flag
12
- export { defineMetric, passRate, examScore, durationMs, tokens, costUSD, turns } from "./metrics.ts";
13
- export { flag } from "./flag.ts";
11
+ // 指标与维度读取器(flag / config)
12
+ export {
13
+ defineMetric,
14
+ taskPassRate,
15
+ executionReliability,
16
+ endToEndPassRate,
17
+ examScore,
18
+ durationMs,
19
+ tokens,
20
+ costUSD,
21
+ turns,
22
+ } from "./metrics.ts";
23
+ export { flag, config } from "./flag.ts";
14
24
 
15
25
  // 报告基座与双面组件基座
16
26
  export { defineReport, isReportDefinition, renderReportToText } from "./report.ts";
@@ -44,8 +54,14 @@ export {
44
54
  } from "./text/layout.ts";
45
55
  export type { ColumnAlign } from "./text/layout.ts";
46
56
 
47
- // 内置报告(show / view 裸跑时报告槽的出厂填充,一份普通 ReportDefinition,无 renderer 特权)
57
+ // 内置报告兼组合件(show / view 裸跑时报告槽的出厂填充;也可作组件整体引用,
58
+ // `<ExperimentComparison data={await ExperimentComparison.data(selection)} />`),无 renderer 特权
48
59
  export { ExperimentComparison } from "./built-ins/index.ts";
60
+ export type {
61
+ ExperimentComparisonData,
62
+ ExperimentComparisonGroupData,
63
+ ExperimentComparisonProps,
64
+ } from "./built-ins/index.ts";
49
65
 
50
66
  // locale:官方组件 chrome 文案的语言(en / zh-CN);指标 label 可按 locale 给字典
51
67
  export { DEFAULT_REPORT_LOCALE, resolveMetricLabel } from "./locale.ts";
@@ -97,6 +113,8 @@ export type {
97
113
  Aggregator,
98
114
  AttemptListItem,
99
115
  AttemptLocator,
116
+ AxisInput,
117
+ ConfigRef,
100
118
  DeltaData,
101
119
  Dimension,
102
120
  DimensionInput,