niceeval 0.6.0 → 0.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (314) hide show
  1. package/dist/agents/types.d.ts +72 -6
  2. package/dist/context/types.d.ts +32 -12
  3. package/dist/i18n/en.d.ts +54 -0
  4. package/dist/i18n/zh-CN.d.ts +55 -1
  5. package/dist/o11y/types.d.ts +16 -2
  6. package/dist/report/aggregate.d.ts +5 -3
  7. package/dist/report/aggregate.js +32 -5
  8. package/dist/report/built-ins/experiment-comparison.d.ts +39 -0
  9. package/dist/report/built-ins/experiment-comparison.js +119 -0
  10. package/dist/report/built-ins/index.d.ts +2 -1
  11. package/dist/report/built-ins/index.js +2 -2
  12. package/dist/report/components.d.ts +10 -2
  13. package/dist/report/components.js +3 -3
  14. package/dist/report/compute.d.ts +11 -18
  15. package/dist/report/compute.js +68 -66
  16. package/dist/report/flag.d.ts +16 -1
  17. package/dist/report/flag.js +19 -1
  18. package/dist/report/format.d.ts +16 -14
  19. package/dist/report/format.js +28 -30
  20. package/dist/report/index.d.ts +5 -4
  21. package/dist/report/index.js +6 -5
  22. package/dist/report/locale.d.ts +23 -3
  23. package/dist/report/locale.js +47 -6
  24. package/dist/report/metrics.d.ts +13 -1
  25. package/dist/report/metrics.js +66 -15
  26. package/dist/report/primitives.d.ts +6 -0
  27. package/dist/report/react/AttemptList.d.ts +4 -4
  28. package/dist/report/react/AttemptList.js +8 -10
  29. package/dist/report/react/EvalList.d.ts +1 -1
  30. package/dist/report/react/EvalList.js +0 -0
  31. package/dist/report/react/ExperimentComparison.d.ts +8 -0
  32. package/dist/report/react/ExperimentComparison.js +11 -0
  33. package/dist/report/react/ExperimentList.d.ts +4 -2
  34. package/dist/report/react/ExperimentList.js +57 -7
  35. package/dist/report/react/MetricScatter.js +6 -16
  36. package/dist/report/react/chart-math.d.ts +23 -6
  37. package/dist/report/react/chart-math.js +71 -19
  38. package/dist/report/react/fixtures.d.ts +3 -3
  39. package/dist/report/react/fixtures.js +30 -18
  40. package/dist/report/react/format.d.ts +1 -1
  41. package/dist/report/react/format.js +1 -1
  42. package/dist/report/react/index.d.ts +1 -1
  43. package/dist/report/report.d.ts +5 -1
  44. package/dist/report/report.js +6 -2
  45. package/dist/report/text/faces.d.ts +1 -1
  46. package/dist/report/text/faces.js +100 -61
  47. package/dist/report/text/table.js +36 -5
  48. package/dist/report/types.d.ts +40 -34
  49. package/dist/results/types.d.ts +11 -0
  50. package/dist/runner/feedback/sink.d.ts +110 -0
  51. package/dist/runner/types.d.ts +513 -22
  52. package/dist/sandbox/docker.d.ts +23 -2
  53. package/dist/sandbox/e2b.d.ts +15 -1
  54. package/dist/sandbox/errors.d.ts +30 -3
  55. package/dist/sandbox/io-retry.d.ts +17 -0
  56. package/dist/sandbox/registry.d.ts +2 -0
  57. package/dist/sandbox/resolve.d.ts +18 -5
  58. package/dist/sandbox/retry.d.ts +11 -1
  59. package/dist/sandbox/types.d.ts +39 -5
  60. package/dist/sandbox/vercel.d.ts +7 -1
  61. package/dist/scoring/coverage.d.ts +30 -0
  62. package/dist/scoring/display.d.ts +21 -0
  63. package/dist/scoring/display.js +120 -0
  64. package/dist/scoring/types.d.ts +103 -20
  65. package/dist/shared/aggregate.d.ts +1 -0
  66. package/dist/shared/aggregate.js +3 -3
  67. package/dist/shared/types.d.ts +28 -0
  68. package/dist/tty-line.d.ts +0 -4
  69. package/dist/util.d.ts +23 -0
  70. package/docs-site/zh/concepts/adapter.mdx +24 -6
  71. package/docs-site/zh/concepts/assert.mdx +11 -10
  72. package/docs-site/zh/concepts/evals.mdx +7 -6
  73. package/docs-site/zh/concepts/experiment.mdx +1 -1
  74. package/docs-site/zh/concepts/overview.mdx +7 -7
  75. package/docs-site/zh/guides/agent-feedback-loop.mdx +35 -31
  76. package/docs-site/zh/guides/authoring.mdx +33 -0
  77. package/docs-site/zh/guides/ci-integration.mdx +23 -12
  78. package/docs-site/zh/guides/connect-your-agent.mdx +29 -3
  79. package/docs-site/zh/guides/custom-reports.mdx +29 -34
  80. package/docs-site/zh/guides/dataset-fanout.mdx +25 -3
  81. package/docs-site/zh/guides/debug-sandbox.mdx +57 -0
  82. package/docs-site/zh/guides/debugging.mdx +210 -0
  83. package/docs-site/zh/guides/experiments.mdx +10 -3
  84. package/docs-site/zh/guides/fixtures.mdx +3 -1
  85. package/docs-site/zh/guides/official-adapters.mdx +27 -3
  86. package/docs-site/zh/guides/publish-report.mdx +30 -16
  87. package/docs-site/zh/guides/report-components.mdx +49 -37
  88. package/docs-site/zh/guides/reporters.mdx +2 -2
  89. package/docs-site/zh/guides/results-data.mdx +42 -8
  90. package/docs-site/zh/guides/runner.mdx +17 -7
  91. package/docs-site/zh/guides/sandbox-agent.mdx +57 -7
  92. package/docs-site/zh/guides/sandbox-providers.mdx +258 -10
  93. package/docs-site/zh/guides/scoring-guide.mdx +4 -4
  94. package/docs-site/zh/guides/viewing-results.mdx +85 -41
  95. package/docs-site/zh/guides/write-experiment.mdx +5 -3
  96. package/docs-site/zh/guides/write-send.mdx +19 -2
  97. package/docs-site/zh/index.mdx +1 -1
  98. package/docs-site/zh/reference/builtin-agents.mdx +27 -0
  99. package/docs-site/zh/reference/capabilities.mdx +2 -2
  100. package/docs-site/zh/reference/cli.mdx +35 -9
  101. package/docs-site/zh/reference/define-agent.mdx +60 -5
  102. package/docs-site/zh/reference/define-config.mdx +1 -1
  103. package/docs-site/zh/reference/define-eval.mdx +42 -9
  104. package/docs-site/zh/reference/events.mdx +2 -2
  105. package/docs-site/zh/reference/expect.mdx +36 -6
  106. package/package.json +5 -1
  107. package/src/agents/ai-sdk-otel.test.ts +1 -0
  108. package/src/agents/ai-sdk.test.ts +3 -0
  109. package/src/agents/ai-sdk.ts +3 -0
  110. package/src/agents/bub-install-spec.test.ts +34 -0
  111. package/src/agents/bub-install-spec.ts +32 -0
  112. package/src/agents/bub.ts +31 -32
  113. package/src/agents/claude-code.test.ts +130 -9
  114. package/src/agents/claude-code.ts +76 -4
  115. package/src/agents/codex.test.ts +189 -40
  116. package/src/agents/codex.ts +155 -14
  117. package/src/agents/coding-cli-versions.test.ts +15 -0
  118. package/src/agents/coding-cli-versions.ts +3 -0
  119. package/src/agents/index.ts +11 -0
  120. package/src/agents/langgraph.test.ts +204 -0
  121. package/src/agents/langgraph.ts +495 -0
  122. package/src/agents/marketplace.ts +85 -0
  123. package/src/agents/native-config.test.ts +179 -0
  124. package/src/agents/native-config.ts +267 -0
  125. package/src/agents/openai-compat.test.ts +1 -0
  126. package/src/agents/openclaw.test.ts +31 -0
  127. package/src/agents/openclaw.ts +171 -0
  128. package/src/agents/plugin-config.test.ts +1 -0
  129. package/src/agents/sdk-streams.test.ts +79 -0
  130. package/src/agents/sdk-streams.ts +55 -10
  131. package/src/agents/skills.test.ts +1 -0
  132. package/src/agents/streaming.test.ts +3 -9
  133. package/src/agents/types.ts +73 -6
  134. package/src/agents/ui-message-stream.test.ts +3 -0
  135. package/src/cli.ts +411 -108
  136. package/src/context/context.test.ts +51 -12
  137. package/src/context/context.ts +161 -29
  138. package/src/context/session.test.ts +1 -0
  139. package/src/context/session.ts +114 -6
  140. package/src/context/types.ts +30 -12
  141. package/src/define.test.ts +13 -8
  142. package/src/define.ts +25 -4
  143. package/src/expect/index.ts +53 -23
  144. package/src/i18n/en.ts +65 -4
  145. package/src/i18n/zh-CN.ts +66 -4
  146. package/src/o11y/cost.test.ts +1 -0
  147. package/src/o11y/execution-tree.test.ts +1 -20
  148. package/src/o11y/otlp/mappers/claude-code.test.ts +1 -0
  149. package/src/o11y/otlp/parse.test.ts +1 -0
  150. package/src/o11y/otlp/turn-otel.test.ts +1 -0
  151. package/src/o11y/parsers/bub.test.ts +1 -0
  152. package/src/o11y/parsers/claude-code.test.ts +1 -34
  153. package/src/o11y/parsers/openclaw.test.ts +154 -0
  154. package/src/o11y/parsers/openclaw.ts +310 -0
  155. package/src/o11y/prices.json +746 -311
  156. package/src/o11y/tool-names.test.ts +1 -0
  157. package/src/o11y/types.ts +16 -2
  158. package/src/report/aggregate.ts +34 -5
  159. package/src/report/built-in-user-parity.test.tsx +127 -173
  160. package/src/report/built-ins/experiment-comparison.tsx +179 -0
  161. package/src/report/built-ins/index.ts +7 -2
  162. package/src/report/components.tsx +11 -3
  163. package/src/report/compute.ts +80 -74
  164. package/src/report/dual-render.test.tsx +222 -91
  165. package/src/report/flag.ts +30 -2
  166. package/src/report/format.ts +36 -27
  167. package/src/report/index.ts +23 -6
  168. package/src/report/locale.ts +49 -6
  169. package/src/report/metrics.ts +68 -15
  170. package/src/report/primitives.tsx +6 -0
  171. package/src/report/react/AttemptList.tsx +9 -36
  172. package/src/report/react/EvalList.tsx +0 -0
  173. package/src/report/react/ExperimentComparison.tsx +68 -0
  174. package/src/report/react/ExperimentList.tsx +173 -55
  175. package/src/report/react/MetricScatter.tsx +13 -25
  176. package/src/report/react/chart-math.test.ts +85 -0
  177. package/src/report/react/chart-math.ts +101 -22
  178. package/src/report/react/enhance.js +72 -1
  179. package/src/report/react/fixtures.ts +34 -21
  180. package/src/report/react/format.ts +1 -1
  181. package/src/report/react/index.tsx +0 -1
  182. package/src/report/react/render.test.tsx +30 -69
  183. package/src/report/react/styles.css +112 -14
  184. package/src/report/report.test.ts +308 -105
  185. package/src/report/report.ts +6 -2
  186. package/src/report/text/faces.ts +111 -67
  187. package/src/report/text/table.ts +42 -5
  188. package/src/report/types.ts +42 -34
  189. package/src/results/annotated-source.test.ts +62 -9
  190. package/src/results/annotated-source.ts +64 -6
  191. package/src/results/attempt-evidence.test.ts +9 -7
  192. package/src/results/attempt-evidence.ts +15 -8
  193. package/src/results/attempt-source.ts +6 -3
  194. package/src/results/copy.ts +145 -55
  195. package/src/results/host-equivalence.test.ts +11 -9
  196. package/src/results/index.ts +2 -0
  197. package/src/results/locator.test.ts +1 -22
  198. package/src/results/open.ts +7 -1
  199. package/src/results/publish.ts +149 -0
  200. package/src/results/results.test.ts +85 -51
  201. package/src/results/truncate.ts +90 -0
  202. package/src/results/types.ts +7 -0
  203. package/src/results/writer.ts +31 -13
  204. package/src/runner/attempt.test.ts +138 -7
  205. package/src/runner/attempt.ts +603 -104
  206. package/src/runner/discover.test.ts +47 -0
  207. package/src/runner/discover.ts +36 -2
  208. package/src/runner/eval-source.test.ts +1 -27
  209. package/src/runner/feedback/agent.test.ts +504 -0
  210. package/src/runner/feedback/agent.ts +409 -0
  211. package/src/runner/feedback/ci.test.ts +562 -0
  212. package/src/runner/feedback/ci.ts +401 -0
  213. package/src/runner/feedback/coordinator.test.ts +317 -0
  214. package/src/runner/feedback/coordinator.ts +397 -0
  215. package/src/runner/feedback/failure.ts +40 -0
  216. package/src/runner/feedback/human.test.ts +616 -0
  217. package/src/runner/feedback/human.ts +535 -0
  218. package/src/runner/feedback/index.ts +66 -0
  219. package/src/runner/feedback/io.ts +78 -0
  220. package/src/runner/feedback/profile.test.ts +50 -0
  221. package/src/runner/feedback/profile.ts +58 -0
  222. package/src/runner/feedback/reducer.test.ts +395 -0
  223. package/src/runner/feedback/reducer.ts +260 -0
  224. package/src/runner/feedback/renderer.ts +82 -0
  225. package/src/runner/feedback/sink.ts +203 -0
  226. package/src/runner/feedback/testing.ts +106 -0
  227. package/src/runner/ledger.test.ts +230 -0
  228. package/src/runner/ledger.ts +329 -0
  229. package/src/runner/report.test.ts +128 -3
  230. package/src/runner/report.ts +33 -9
  231. package/src/runner/reporters/artifacts.ts +8 -2
  232. package/src/runner/reporters/braintrust.test.ts +8 -7
  233. package/src/runner/reporters/braintrust.ts +9 -2
  234. package/src/runner/reporters/index.ts +2 -2
  235. package/src/runner/reporters/json.test.ts +162 -0
  236. package/src/runner/reporters/json.ts +35 -8
  237. package/src/runner/reporters/shared.ts +1 -5
  238. package/src/runner/run.test.ts +760 -3
  239. package/src/runner/run.ts +242 -36
  240. package/src/runner/sandbox-prep.ts +3 -42
  241. package/src/runner/timing.ts +158 -0
  242. package/src/runner/types.ts +518 -22
  243. package/src/sandbox/checkpoint.test.ts +55 -0
  244. package/src/sandbox/checkpoint.ts +29 -8
  245. package/src/sandbox/cli-commands.ts +407 -0
  246. package/src/sandbox/docker.ts +115 -16
  247. package/src/sandbox/e2b-agent-template.test.ts +56 -0
  248. package/src/sandbox/e2b-agent-template.ts +94 -0
  249. package/src/sandbox/e2b.ts +74 -9
  250. package/src/sandbox/errors.ts +111 -4
  251. package/src/sandbox/index.ts +2 -0
  252. package/src/sandbox/io-retry.test.ts +58 -0
  253. package/src/sandbox/io-retry.ts +45 -0
  254. package/src/sandbox/keep-registry.test.ts +86 -0
  255. package/src/sandbox/keep-registry.ts +142 -0
  256. package/src/sandbox/keep.ts +178 -0
  257. package/src/sandbox/paths.test.ts +1 -0
  258. package/src/sandbox/paths.ts +19 -8
  259. package/src/sandbox/registry.ts +20 -3
  260. package/src/sandbox/resolve.ts +76 -11
  261. package/src/sandbox/retry.test.ts +70 -0
  262. package/src/sandbox/retry.ts +46 -4
  263. package/src/sandbox/types.ts +44 -6
  264. package/src/sandbox/vercel.ts +43 -20
  265. package/src/scoring/collector.ts +60 -17
  266. package/src/scoring/coverage.ts +95 -0
  267. package/src/scoring/diff.ts +81 -0
  268. package/src/scoring/display.test.ts +121 -0
  269. package/src/scoring/display.ts +133 -0
  270. package/src/scoring/evidence.test.ts +189 -0
  271. package/src/scoring/judge.test.ts +142 -0
  272. package/src/scoring/judge.ts +15 -18
  273. package/src/scoring/scoped.ts +217 -50
  274. package/src/scoring/types.ts +117 -20
  275. package/src/scoring/verdict.ts +16 -4
  276. package/src/shared/aggregate.ts +3 -2
  277. package/src/shared/types.ts +31 -0
  278. package/src/show/compose.ts +2 -2
  279. package/src/show/index.ts +29 -16
  280. package/src/show/render.ts +626 -308
  281. package/src/show/show.test.ts +251 -36
  282. package/src/tty-line.ts +8 -26
  283. package/src/util.test.ts +1 -0
  284. package/src/util.ts +41 -0
  285. package/src/view/app/components/AttemptModal.tsx +153 -2
  286. package/src/view/app/components/CodeView.tsx +32 -11
  287. package/src/view/app/components/CopyControls.tsx +2 -2
  288. package/src/view/app/i18n.ts +6 -0
  289. package/src/view/app/lib/attempt-route.test.ts +1 -0
  290. package/src/view/app/lib/verdict.ts +7 -9
  291. package/src/view/artifact-serving.test.ts +2 -1
  292. package/src/view/client-dist/app.css +1 -1
  293. package/src/view/client-dist/app.js +17 -17
  294. package/src/view/data.test.ts +2 -1
  295. package/src/view/data.ts +17 -7
  296. package/src/view/index.ts +12 -1
  297. package/src/view/server.ts +2 -0
  298. package/src/view/shared/types.ts +1 -1
  299. package/src/view/styles.css +3 -0
  300. package/src/view/view-report.test.ts +11 -10
  301. package/dist/o11y/execution-tree.d.ts +0 -103
  302. package/dist/o11y/otlp/select.d.ts +0 -22
  303. package/dist/report/built-ins/cost-pass-rate-comparison.d.ts +0 -1
  304. package/dist/report/built-ins/cost-pass-rate-comparison.js +0 -17
  305. package/dist/results/annotated-source.d.ts +0 -61
  306. package/dist/results/attempt-evidence.d.ts +0 -69
  307. package/dist/results/attempt-source.d.ts +0 -15
  308. package/src/report/built-ins/cost-pass-rate-comparison.tsx +0 -23
  309. package/src/runner/reporters/console.ts +0 -70
  310. package/src/runner/reporters/live.test.ts +0 -56
  311. package/src/runner/reporters/live.ts +0 -247
  312. package/src/runner/reporters/quiet.test.ts +0 -66
  313. package/src/runner/reporters/quiet.ts +0 -49
  314. package/src/runner/reporters/table.ts +0 -277
@@ -1,3 +1,4 @@
1
+ // cases: docs/engineering/unit-tests/reports/cases.md
1
2
  // 双面验收:每个官方组件对同一份数据,web 面(renderToStaticMarkup)与 text 面
2
3
  // 给出一致判读 —— 排序方向随 better、samples < total 角标、缺数据 — 不补 0、
3
4
  // 截断如实报剩余;text 面形态以内联快照锁定(照 report-components.mdx 的示例形态)。
@@ -35,13 +36,14 @@ 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";
44
- import { CostPassRateComparison } from "./built-ins/index.ts";
46
+ import { ExperimentComparison } from "./built-ins/index.ts";
45
47
  import { renderReportToStaticHtml } from "./web.ts";
46
48
  import { createTextContext, renderNodeToText, validateReportTree } from "./tree.ts";
47
49
  import {
@@ -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,17 +456,22 @@ 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"');
473
- // 证据能力标记两面一致:failedAttempt 有 eval/execution/timing,erroredAttempt 只有 execution
474
- expect(term).toContain("[E,X,⏱]");
475
- expect(html).toContain("[E,X,⏱]");
473
+ expect(term).not.toMatch(/\[[EXD⏱,]+\]/);
474
+ expect(html).not.toMatch(/\[[EXD⏱,]+\]/);
476
475
  });
477
476
 
478
477
  it("total > items.length 时两面都如实报剩余数量", () => {
@@ -482,7 +481,7 @@ describe("AttemptList 双面", () => {
482
481
  expect(termTrunc).toContain("2 more not shown");
483
482
  });
484
483
 
485
- it("text 面截断超长 assertion evidence,并保留 locator 作为完整证据入口", () => {
484
+ it("比较列表不内联 assertion evidence,并保留 locator 作为完整证据入口", () => {
486
485
  const longEvidence = "x".repeat(2_000);
487
486
  const item = {
488
487
  ...attemptListItems[0],
@@ -490,17 +489,17 @@ describe("AttemptList 双面", () => {
490
489
  };
491
490
  const out = text(<AttemptList items={[item]} />);
492
491
  expect(out).toContain(item.locator);
493
- expect(out.replace(/\s+/g, " ")).toContain(`open ${item.locator} for full evidence`);
494
492
  expect(out).not.toContain(longEvidence);
495
493
  expect(out.length).toBeLessThan(1_000);
496
494
  });
497
495
 
498
- it("text 面是逐条卡片,不是 renderTableText 的产物:空行分隔每个 attempt、断言明细逐级缩进——共享表格渲染器不产生这种嵌套形状", () => {
496
+ it("text 面每个 attempt 只显示一条有界结果摘要,不展开完整断言列表", () => {
499
497
  const blocks = term.split("\n\n");
500
- expect(blocks).toHaveLength(attemptListItems.length); // 每个 attempt 独立一块,块间空行分隔;表格行之间不留空行
498
+ expect(blocks).toHaveLength(attemptListItems.length);
501
499
  const lines = term.split("\n");
502
- expect(lines.some((l) => /^ \S/.test(l))).toBe(true); // 断言行缩进两格
503
- 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");
504
503
  });
505
504
  });
506
505
 
@@ -525,6 +524,16 @@ describe("EvalList 双面", () => {
525
524
  expect(blocks).toHaveLength(evalListItems.length);
526
525
  expect(term.split("\n").some((l) => /^ \S/.test(l))).toBe(true); // 展开到 attempt 的行缩进两格
527
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
+ });
528
537
  });
529
538
 
530
539
  describe("ExperimentList 双面", () => {
@@ -536,15 +545,59 @@ describe("ExperimentList 双面", () => {
536
545
  expect(html).toContain(piece);
537
546
  expect(term).toContain(piece);
538
547
  }
539
- // 官方两级聚合 passRate.display 两面同一个数字,不各自重算
548
+ // 官方两级聚合 endToEndPassRate.display 两面同一个数字,不各自重算
540
549
  expect(html).toContain("50%");
541
550
  expect(term).toContain("50%");
542
551
  });
543
552
 
544
- it("text 面是逐条卡片,不是 renderTableText 的产物:空行分隔每个 experiment、展开到 eval 的行缩进——表格没有这层嵌套", () => {
553
+ it("text 面先给比较表,再按 experiment Eval / Attempt 父子表", () => {
545
554
  const blocks = term.split("\n\n");
546
- expect(blocks).toHaveLength(experimentListItems.length);
547
- expect(term.split("\n").some((l) => /^ \S/.test(l))).toBe(true); // 展开到 eval 的行缩进两格
555
+ expect(blocks).toHaveLength(experimentListItems.length + 1);
556
+ expect(blocks[0]).toContain("Model");
557
+ expect(blocks[0]).toContain("Avg");
558
+ expect(blocks[1]!.match(/algebra\/quadratic/g)).toHaveLength(1);
559
+ expect(blocks[1]).toMatch(/✗ failed\s+algebra\/quadratic[\s\S]*├─ @1a4a4a4[\s\S]*└─ @1b5b5b5/);
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");
548
601
  });
549
602
  });
550
603
 
@@ -892,8 +945,8 @@ function fakeContext(): { selection: Selection; results: Results } {
892
945
  }
893
946
 
894
947
  /**
895
- * 三份快照、三个 experiment("compare/bub"、"other/codex"、"solo"),全部通过、无成本——
896
- * 一个多实验 Selection 夹具:ExperimentList 出三项,MetricScatter 三个点都无成本
948
+ * 五份快照、两个双配置目录组 + 一个根目录单例组,全部通过、无成本——
949
+ * 一个多实验 Selection 夹具:ExperimentList 出五项,MetricScatter 五个点都无成本
897
950
  * (0 可画点,如实走空态)。`filter` 落实真实语义(不像 fakeContext 那样恒等返回自己)。
898
951
  */
899
952
  function fakeMultiGroupContext(): { selection: Selection; results: Results } {
@@ -936,7 +989,9 @@ function fakeMultiGroupContext(): { selection: Selection; results: Results } {
936
989
 
937
990
  const snapshots = [
938
991
  mkSnapshot("compare/bub", "bub", "compare_bub/snap-1"),
992
+ mkSnapshot("compare/codex", "codex", "compare_codex/snap-1"),
939
993
  mkSnapshot("other/codex", "codex", "other_codex/snap-1"),
994
+ mkSnapshot("other/bub", "bub", "other_bub/snap-1"),
940
995
  mkSnapshot("solo", "bub", "solo/snap-1"),
941
996
  ];
942
997
 
@@ -987,11 +1042,11 @@ describe("defineReport + 渲染入口", () => {
987
1042
  const out = await renderReportToText(report, fakeContext(), { width: 100 });
988
1043
  // RunOverview(data 形态,预计算)
989
1044
  expect(out).toContain("1 experiment · 2 evals · 2 attempts");
990
- // ExperimentList:主行 + eval 级折叠计票 + 失败诊断(locator 徽标带证据能力标记)
991
- expect(out).toContain("compare/bub · bub");
992
- expect(out).toContain("1 passed / 1 failed");
1045
+ // ExperimentList:主行 + eval 级折叠计票 + 逐 Attempt locator 与失败诊断
1046
+ expect(out).toMatch(/compare\/bub\s+default\s+bub/);
1047
+ expect(out).toMatch(/1 passed[\s\S]*?\/ 1\s+failed/);
993
1048
  expect(out).toContain("50%");
994
- expect(out).toMatch(/✗ algebra\/y\s+@[0-9a-z]+✗/);
1049
+ expect(out).toMatch(/✗ failed\s+algebra\/y[\s\S]*└─ @[0-9a-z]+/);
995
1050
  // 自己的口径:成绩单
996
1051
  expect(out).toContain("考试成绩单");
997
1052
  expect(out).toContain("bub");
@@ -1041,7 +1096,7 @@ describe("defineReport + 渲染入口", () => {
1041
1096
  // selection-form 的官方组件(ExperimentList/EvalList/AttemptList 没有 selection-form,
1042
1097
  // 这条契约不适用于它们)。
1043
1098
  expect(() =>
1044
- renderToStaticMarkup(<MetricScatter selection={selection} points="experiment" x={costUSD} y={passRate} />),
1099
+ renderToStaticMarkup(<MetricScatter selection={selection} points="experiment" x={costUSD} y={taskPassRate} />),
1045
1100
  ).toThrow(/received unresolved \(selection-form\) props/);
1046
1101
  });
1047
1102
  });
@@ -1051,12 +1106,12 @@ describe("defineReport + 渲染入口", () => {
1051
1106
  // ts-expect-error 注释对应一处编译错误。直接标注 Props 类型(而非 JSX)——JSX 的 union
1052
1107
  // 属性检查对「缺必填字段」较宽松,直接的类型赋值才严格钉住互斥不变量:同时传 data 与
1053
1108
  // selection、或两者都不传、或 selection 形态缺必填计算选项,全部编译失败。正向 JSX 用法由
1054
- // CostPassRateComparison / defineReport 报告在同一文件里编译验证。
1109
+ // ExperimentComparison / defineReport 报告在同一文件里编译验证。
1055
1110
  function metricScatterPropsTypeChecks(selection: Selection, data: ScatterData): void {
1056
1111
  const ok1: MetricScatterProps = { data }; // 合法:data 形态
1057
- 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 形态
1058
1113
  // @ts-expect-error 同时传 data 与 selection:非法
1059
- const bad1: MetricScatterProps = { data, selection, points: "experiment", x: costUSD, y: passRate };
1114
+ const bad1: MetricScatterProps = { data, selection, points: "experiment", x: costUSD, y: taskPassRate };
1060
1115
  // @ts-expect-error data 与 selection 都不传:非法
1061
1116
  const bad2: MetricScatterProps = { pointHref: () => "/x" };
1062
1117
  // @ts-expect-error selection 形态缺必填的 x / y:非法
@@ -1070,56 +1125,132 @@ function metricScatterPropsTypeChecks(selection: Selection, data: ScatterData):
1070
1125
 
1071
1126
  void metricScatterPropsTypeChecks;
1072
1127
 
1073
- // ───────────────────────── CostPassRateComparison(内置默认报告)─────────────────────────
1128
+ // ───────────────────────── ExperimentComparison(内置默认报告)─────────────────────────
1074
1129
 
1075
- describe("CostPassRateComparison", () => {
1076
- it("是普通 ReportDefinition;text 面 = 成本×通过率散点 + 实验列表,别无它物", async () => {
1077
- expect(isReportDefinition(CostPassRateComparison)).toBe(true);
1078
- const out = await renderReportToText(CostPassRateComparison, fakeContext(), { width: 100 });
1130
+ describe("ExperimentComparison", () => {
1131
+ it("是普通 ReportDefinition;单组 text 面 = 组摘要 + 成本×端到端成功率散点 + 实验列表", async () => {
1132
+ expect(isReportDefinition(ExperimentComparison)).toBe(true);
1133
+ const out = await renderReportToText(ExperimentComparison, fakeContext(), { width: 100 });
1079
1134
  // 散点:fakeContext 无成本数据 → 0 可画点,显式说明缺哪两个指标(而不是画一张空图)
1080
1135
  expect(out).toContain("No data to plot");
1081
1136
  expect(out).not.toContain("better → upper right");
1082
1137
  // 实验列表主行 + eval 级折叠计票 + 失败诊断(ExperimentList.data 在 build() 里直接 await)
1083
- expect(out).toContain("compare/bub · bub");
1084
- 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/);
1085
1143
  expect(out).toContain("50%");
1086
- expect(out).toMatch(/✗ algebra\/y\s+@[0-9a-z]+✗/);
1087
- // 只有两个直接业务组件:没有 RunOverview / GroupSummary / Section 分组
1144
+ expect(out).toMatch(/✗ failed\s+algebra\/y[\s\S]*└─ @[0-9a-z]+/);
1145
+ // 单组直接进入详情,不出现多组查看命令
1088
1146
  expect(out).not.toContain("Current verdicts");
1147
+ expect(out).not.toContain("niceeval show --experiment");
1089
1148
  });
1090
1149
 
1091
- it("web 面:散点空态 + 实验列表 <details> 展开区,无 <script>,无 Section 分组", async () => {
1092
- const html = await renderReportToStaticHtml(CostPassRateComparison, fakeContext());
1150
+ it("web 面:完整组索引 + 首组 panel + 实验列表 <details> 展开区,无 <script>", async () => {
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=""/);
1093
1154
  expect(html).toContain("nre-metric-scatter");
1094
1155
  expect(html).toContain("nre-scatter-empty"); // 0 可画点的空态
1095
- expect(html).toContain('<li class="nre-experiment-entry">');
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"');
1160
+ expect(html).toContain("nre-experiment-head");
1161
+ expect(html).toContain('data-nre-experiment-filter=""');
1096
1162
  expect(html).toContain("nre-experiment-evals");
1097
- expect(html).not.toContain("nre-section");
1098
1163
  expect(html).not.toContain("<script");
1099
1164
  });
1100
1165
 
1101
1166
  it("locale 变体:en / zh-CN 都渲染(chrome 分语言),散点空态两面同一事实", async () => {
1102
- const zhHtml = await renderReportToStaticHtml(CostPassRateComparison, fakeContext(), { locale: "zh-CN" });
1103
- expect(zhHtml).toContain("成功率"); // passRate 的 zh-CN label(ExperimentList 主行)
1104
- const enHtml = await renderReportToStaticHtml(CostPassRateComparison, fakeContext(), { locale: "en" });
1105
- expect(enHtml).toContain("Pass rate");
1106
- const zhText = await renderReportToText(CostPassRateComparison, fakeContext(), { locale: "zh-CN" });
1167
+ const zhHtml = await renderReportToStaticHtml(ExperimentComparison, fakeContext(), { locale: "zh-CN" });
1168
+ expect(zhHtml).toContain("端到端成功率");
1169
+ const enHtml = await renderReportToStaticHtml(ExperimentComparison, fakeContext(), { locale: "en" });
1170
+ expect(enHtml).toContain("End-to-end pass rate");
1171
+ const zhText = await renderReportToText(ExperimentComparison, fakeContext(), { locale: "zh-CN" });
1107
1172
  expect(zhText).toContain("没有可绘制的数据"); // 散点空态 zh
1108
- const enText = await renderReportToText(CostPassRateComparison, fakeContext(), { locale: "en" });
1173
+ const enText = await renderReportToText(ExperimentComparison, fakeContext(), { locale: "en" });
1109
1174
  expect(enText).toContain("No data to plot");
1110
1175
  });
1111
1176
 
1112
- it("多实验 fixture:每个 experiment 一项,散点如实处理全部无成本", async () => {
1113
- const out = await renderReportToText(CostPassRateComparison, fakeMultiGroupContext(), { width: 100 });
1114
- // 三个 experiment 的身份行都出现(experimentId · agent,不再截短成最后一段)
1115
- expect(out).toContain("compare/bub · bub");
1116
- expect(out).toContain("other/codex · codex");
1117
- expect(out).toContain("solo · bub");
1118
- // 散点空态(三点都无成本)
1119
- expect(out).toContain("No data to plot");
1120
- // 没有组分 Section 标题(不再按目录前缀分组)
1121
- expect(out).not.toMatch(/^compare$/m);
1122
- const html = await renderReportToStaticHtml(CostPassRateComparison, fakeMultiGroupContext());
1123
- expect(html).not.toContain("nre-section");
1177
+ it("多组 fixture:text 只给组索引与命令;web 保留全部独立 panel", async () => {
1178
+ const out = await renderReportToText(ExperimentComparison, fakeMultiGroupContext(), { width: 100 });
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");
1185
+ const html = await renderReportToStaticHtml(ExperimentComparison, fakeMultiGroupContext());
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);
1124
1255
  });
1125
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
+ }