niceeval 0.5.4 → 0.6.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 (337) hide show
  1. package/INDEX.md +55 -0
  2. package/dist/agents/types.d.ts +321 -0
  3. package/dist/context/types.d.ts +335 -0
  4. package/dist/i18n/core.d.ts +11 -0
  5. package/dist/i18n/en.d.ts +181 -0
  6. package/dist/i18n/index.d.ts +5 -0
  7. package/dist/i18n/zh-CN.d.ts +183 -0
  8. package/dist/o11y/otlp/parse.d.ts +3 -0
  9. package/dist/o11y/otlp/receiver.d.ts +21 -0
  10. package/dist/o11y/otlp/turn-otel.d.ts +43 -0
  11. package/dist/o11y/types.d.ts +190 -0
  12. package/dist/report/aggregate.d.ts +57 -0
  13. package/dist/report/aggregate.js +212 -0
  14. package/dist/report/built-ins/experiment-comparison.d.ts +1 -0
  15. package/dist/report/built-ins/experiment-comparison.js +13 -0
  16. package/dist/report/built-ins/index.d.ts +1 -0
  17. package/dist/report/built-ins/index.js +2 -0
  18. package/dist/report/components.d.ts +180 -0
  19. package/dist/report/components.js +132 -0
  20. package/dist/report/compute.d.ts +106 -0
  21. package/dist/report/compute.js +600 -0
  22. package/dist/report/flag.d.ts +6 -0
  23. package/dist/report/flag.js +8 -0
  24. package/dist/report/format.d.ts +26 -0
  25. package/dist/report/format.js +109 -0
  26. package/dist/report/index.d.ts +19 -0
  27. package/dist/report/index.js +30 -0
  28. package/dist/report/load.d.ts +12 -0
  29. package/dist/report/load.js +56 -0
  30. package/dist/report/locale.d.ts +94 -0
  31. package/dist/report/locale.js +183 -0
  32. package/dist/report/metrics.d.ts +21 -0
  33. package/dist/report/metrics.js +115 -0
  34. package/dist/report/primitives.d.ts +64 -0
  35. package/dist/report/primitives.js +113 -0
  36. package/dist/report/react/AttemptList.d.ts +23 -0
  37. package/dist/report/react/AttemptList.js +19 -0
  38. package/dist/report/react/DeltaTable.d.ts +8 -0
  39. package/dist/report/react/DeltaTable.js +24 -0
  40. package/dist/report/react/EvalList.d.ts +10 -0
  41. package/dist/report/react/EvalList.js +0 -0
  42. package/dist/report/react/ExperimentList.d.ts +11 -0
  43. package/dist/report/react/ExperimentList.js +67 -0
  44. package/dist/report/react/GroupSummary.d.ts +8 -0
  45. package/dist/report/react/GroupSummary.js +8 -0
  46. package/dist/report/react/MetricBars.d.ts +10 -0
  47. package/dist/report/react/MetricBars.js +48 -0
  48. package/dist/report/react/MetricLine.d.ts +9 -0
  49. package/dist/report/react/MetricLine.js +68 -0
  50. package/dist/report/react/MetricMatrix.d.ts +10 -0
  51. package/dist/report/react/MetricMatrix.js +23 -0
  52. package/dist/report/react/MetricScatter.d.ts +9 -0
  53. package/dist/report/react/MetricScatter.js +96 -0
  54. package/dist/report/react/MetricTable.d.ts +11 -0
  55. package/dist/report/react/MetricTable.js +26 -0
  56. package/dist/report/react/RunOverview.d.ts +8 -0
  57. package/dist/report/react/RunOverview.js +12 -0
  58. package/dist/report/react/Scoreboard.d.ts +8 -0
  59. package/dist/report/react/Scoreboard.js +26 -0
  60. package/dist/report/react/cell.d.ts +9 -0
  61. package/dist/report/react/cell.js +9 -0
  62. package/dist/report/react/chart-math.d.ts +14 -0
  63. package/dist/report/react/chart-math.js +68 -0
  64. package/dist/report/react/colors.d.ts +14 -0
  65. package/dist/report/react/colors.js +45 -0
  66. package/dist/report/react/fixtures.d.ts +19 -0
  67. package/dist/report/react/fixtures.js +398 -0
  68. package/dist/report/react/format.d.ts +3 -0
  69. package/dist/report/react/format.js +7 -0
  70. package/dist/report/react/index.d.ts +6 -0
  71. package/dist/report/react/index.js +14 -0
  72. package/dist/report/report.d.ts +24 -0
  73. package/dist/report/report.js +49 -0
  74. package/dist/report/text/faces.d.ts +29 -0
  75. package/dist/report/text/faces.js +541 -0
  76. package/dist/report/text/layout.d.ts +75 -0
  77. package/dist/report/text/layout.js +173 -0
  78. package/dist/report/text/plot.d.ts +37 -0
  79. package/dist/report/text/plot.js +81 -0
  80. package/dist/report/text/table.d.ts +8 -0
  81. package/dist/report/text/table.js +86 -0
  82. package/dist/report/tree.d.ts +99 -0
  83. package/dist/report/tree.js +228 -0
  84. package/dist/report/types.d.ts +408 -0
  85. package/dist/report/types.js +4 -0
  86. package/dist/report/web.d.ts +15 -0
  87. package/dist/report/web.js +37 -0
  88. package/dist/results/locator.d.ts +75 -0
  89. package/dist/results/locator.js +0 -0
  90. package/dist/results/select.d.ts +45 -0
  91. package/dist/results/select.js +230 -0
  92. package/dist/results/source-hash.d.ts +9 -0
  93. package/dist/results/types.d.ts +204 -0
  94. package/dist/runner/eval-source.d.ts +22 -0
  95. package/dist/runner/fingerprint.d.ts +23 -0
  96. package/dist/runner/types.d.ts +332 -0
  97. package/dist/sandbox/docker-stream.d.ts +24 -0
  98. package/dist/sandbox/docker.d.ts +84 -0
  99. package/dist/sandbox/e2b.d.ts +31 -0
  100. package/dist/sandbox/errors.d.ts +4 -0
  101. package/dist/sandbox/local-files.d.ts +6 -0
  102. package/dist/sandbox/paths.d.ts +4 -0
  103. package/dist/sandbox/registry.d.ts +13 -0
  104. package/dist/sandbox/resolve.d.ts +40 -0
  105. package/dist/sandbox/retry.d.ts +10 -0
  106. package/dist/sandbox/shell.d.ts +11 -0
  107. package/dist/sandbox/source-files.d.ts +13 -0
  108. package/dist/sandbox/types.d.ts +203 -0
  109. package/dist/sandbox/vercel.d.ts +37 -0
  110. package/dist/scoring/types.d.ts +70 -0
  111. package/dist/shared/aggregate.d.ts +29 -0
  112. package/dist/shared/aggregate.js +55 -0
  113. package/dist/shared/types.d.ts +27 -0
  114. package/dist/shared/verdict.d.ts +27 -0
  115. package/dist/shared/verdict.js +34 -0
  116. package/dist/tty-line.d.ts +6 -0
  117. package/dist/types.d.ts +7 -0
  118. package/dist/util.d.ts +22 -0
  119. package/docs-site/zh/concepts/adapter.mdx +7 -7
  120. package/docs-site/zh/concepts/assert.mdx +19 -18
  121. package/docs-site/zh/concepts/evals.mdx +12 -11
  122. package/docs-site/zh/concepts/experiment.mdx +2 -2
  123. package/docs-site/zh/concepts/judge.mdx +9 -9
  124. package/docs-site/zh/concepts/overview.mdx +3 -3
  125. package/docs-site/zh/concepts/tier.mdx +6 -6
  126. package/docs-site/zh/example/claude-code-codex-plugin.mdx +9 -5
  127. package/docs-site/zh/example/claude-code-codex-skill.mdx +12 -7
  128. package/docs-site/zh/guides/agent-feedback-loop.mdx +237 -78
  129. package/docs-site/zh/guides/connect-otel.mdx +3 -3
  130. package/docs-site/zh/guides/connect-your-agent.mdx +2 -2
  131. package/docs-site/zh/guides/custom-reports.mdx +196 -36
  132. package/docs-site/zh/guides/experiments.mdx +7 -1
  133. package/docs-site/zh/guides/fixtures.mdx +4 -2
  134. package/docs-site/zh/guides/official-adapters.mdx +31 -9
  135. package/docs-site/zh/guides/publish-report.mdx +4 -4
  136. package/docs-site/zh/guides/report-components.mdx +194 -55
  137. package/docs-site/zh/guides/reporters.mdx +2 -2
  138. package/docs-site/zh/guides/results-data.mdx +61 -34
  139. package/docs-site/zh/guides/runner.mdx +2 -2
  140. package/docs-site/zh/guides/sandbox-agent.mdx +3 -2
  141. package/docs-site/zh/guides/{sandbox-backends.mdx → sandbox-providers.mdx} +12 -12
  142. package/docs-site/zh/guides/viewing-results.mdx +200 -79
  143. package/docs-site/zh/guides/write-experiment.mdx +9 -1
  144. package/docs-site/zh/guides/write-send.mdx +4 -3
  145. package/docs-site/zh/index.mdx +4 -3
  146. package/docs-site/zh/introduction.mdx +2 -1
  147. package/docs-site/zh/quickstart.mdx +5 -3
  148. package/docs-site/zh/reference/builtin-agents.mdx +38 -13
  149. package/docs-site/zh/reference/capabilities.mdx +1 -1
  150. package/docs-site/zh/reference/cli.mdx +36 -13
  151. package/docs-site/zh/reference/define-agent.mdx +15 -13
  152. package/docs-site/zh/reference/define-config.mdx +1 -1
  153. package/docs-site/zh/reference/define-eval.mdx +3 -3
  154. package/docs-site/zh/reference/events.mdx +9 -3
  155. package/docs-site/zh/reference/expect.mdx +13 -8
  156. package/package.json +10 -6
  157. package/src/agents/bub.ts +129 -42
  158. package/src/agents/claude-code.test.ts +152 -0
  159. package/src/agents/claude-code.ts +127 -13
  160. package/src/agents/codex.test.ts +192 -0
  161. package/src/agents/codex.ts +151 -12
  162. package/src/agents/index.ts +9 -3
  163. package/src/agents/manifest.ts +41 -0
  164. package/src/agents/plugin-config.test.ts +94 -0
  165. package/src/agents/shared.ts +33 -2
  166. package/src/agents/skills.test.ts +214 -0
  167. package/src/agents/skills.ts +304 -0
  168. package/src/agents/streaming.ts +5 -5
  169. package/src/agents/types.ts +59 -3
  170. package/src/cli.ts +74 -47
  171. package/src/context/context.test.ts +20 -2
  172. package/src/context/control-flow.ts +1 -1
  173. package/src/context/session.test.ts +92 -1
  174. package/src/context/session.ts +23 -1
  175. package/src/context/types.ts +4 -4
  176. package/src/define.test.ts +8 -8
  177. package/src/define.ts +11 -11
  178. package/src/expect/index.ts +5 -5
  179. package/src/i18n/en.ts +33 -15
  180. package/src/i18n/zh-CN.ts +37 -21
  181. package/src/index.ts +1 -1
  182. package/src/o11y/execution-tree.test.ts +452 -0
  183. package/src/o11y/execution-tree.ts +367 -0
  184. package/src/o11y/otlp/receiver.ts +1 -1
  185. package/src/o11y/otlp/select.ts +7 -3
  186. package/src/o11y/parsers/claude-code.test.ts +175 -0
  187. package/src/o11y/parsers/claude-code.ts +32 -0
  188. package/src/o11y/types.ts +6 -1
  189. package/src/report/aggregate.ts +26 -20
  190. package/src/report/built-in-user-parity.test.tsx +640 -0
  191. package/src/report/built-ins/experiment-comparison.tsx +19 -0
  192. package/src/report/built-ins/index.ts +2 -0
  193. package/src/report/components.tsx +225 -50
  194. package/src/report/compute.ts +335 -54
  195. package/src/report/dual-render.test.tsx +1129 -0
  196. package/src/report/flag.ts +1 -1
  197. package/src/report/format.ts +35 -2
  198. package/src/report/index.ts +47 -14
  199. package/src/report/load.ts +1 -1
  200. package/src/report/locale.ts +229 -0
  201. package/src/report/metrics.ts +39 -15
  202. package/src/report/primitives.tsx +100 -2
  203. package/src/report/react/AttemptList.tsx +113 -0
  204. package/src/report/react/DeltaTable.tsx +9 -6
  205. package/src/report/react/EvalList.tsx +0 -0
  206. package/src/report/react/ExperimentList.tsx +220 -0
  207. package/src/report/react/GroupSummary.tsx +66 -0
  208. package/src/report/react/MetricBars.tsx +24 -13
  209. package/src/report/react/MetricLine.tsx +21 -21
  210. package/src/report/react/MetricMatrix.tsx +8 -4
  211. package/src/report/react/MetricScatter.tsx +126 -96
  212. package/src/report/react/MetricTable.tsx +115 -15
  213. package/src/report/react/RunOverview.tsx +48 -26
  214. package/src/report/react/Scoreboard.tsx +15 -7
  215. package/src/report/react/cell.tsx +18 -9
  216. package/src/{view/app/lib/chart.ts → report/react/chart-math.ts} +2 -6
  217. package/src/report/react/colors.ts +24 -15
  218. package/src/report/react/enhance.js +168 -0
  219. package/src/report/react/fixtures.ts +169 -34
  220. package/src/report/react/format.ts +1 -1
  221. package/src/report/react/index.tsx +21 -6
  222. package/src/report/react/render.test.tsx +180 -31
  223. package/src/report/react/styles.css +432 -213
  224. package/src/report/report.test.ts +398 -94
  225. package/src/report/report.ts +29 -11
  226. package/src/report/text/faces.ts +344 -96
  227. package/src/report/text/layout.ts +76 -11
  228. package/src/report/text/table.ts +98 -0
  229. package/src/report/tree.ts +103 -20
  230. package/src/report/types.ts +184 -25
  231. package/src/report/web.ts +46 -19
  232. package/src/results/annotated-source.test.ts +146 -0
  233. package/src/results/annotated-source.ts +128 -0
  234. package/src/results/attempt-evidence.test.ts +158 -0
  235. package/src/results/attempt-evidence.ts +158 -0
  236. package/src/results/attempt-source.ts +48 -0
  237. package/src/results/copy.ts +154 -139
  238. package/src/results/format.ts +54 -47
  239. package/src/results/host-equivalence.test.ts +527 -0
  240. package/src/results/index.ts +56 -14
  241. package/src/results/locator.test.ts +198 -0
  242. package/src/results/locator.ts +0 -0
  243. package/src/results/open.ts +392 -175
  244. package/src/results/results.test.ts +943 -494
  245. package/src/results/select.ts +129 -23
  246. package/src/results/skipped-notice.ts +0 -0
  247. package/src/results/source-hash.ts +28 -0
  248. package/src/results/types.ts +98 -62
  249. package/src/results/writer.ts +304 -176
  250. package/src/runner/attempt.test.ts +170 -0
  251. package/src/runner/attempt.ts +48 -13
  252. package/src/runner/discover.test.ts +65 -0
  253. package/src/runner/discover.ts +6 -2
  254. package/src/runner/eval-source.test.ts +121 -0
  255. package/src/runner/eval-source.ts +45 -0
  256. package/src/runner/fingerprint.ts +54 -1
  257. package/src/runner/remote-sandbox.ts +1 -1
  258. package/src/runner/report.test.ts +6 -8
  259. package/src/runner/report.ts +5 -4
  260. package/src/runner/reporters/artifacts.ts +27 -41
  261. package/src/runner/reporters/braintrust.test.ts +3 -3
  262. package/src/runner/reporters/braintrust.ts +1 -1
  263. package/src/runner/reporters/console.ts +6 -6
  264. package/src/runner/reporters/json.ts +4 -4
  265. package/src/runner/reporters/live.test.ts +56 -0
  266. package/src/runner/reporters/live.ts +36 -10
  267. package/src/runner/reporters/quiet.test.ts +66 -0
  268. package/src/runner/reporters/quiet.ts +49 -0
  269. package/src/runner/reporters/shared.ts +5 -5
  270. package/src/runner/reporters/table.ts +17 -17
  271. package/src/runner/run.ts +61 -102
  272. package/src/runner/types.ts +43 -32
  273. package/src/sandbox/checkpoint.ts +1 -1
  274. package/src/sandbox/docker.ts +15 -2
  275. package/src/sandbox/e2b.ts +11 -5
  276. package/src/sandbox/errors.ts +13 -0
  277. package/src/sandbox/index.ts +3 -3
  278. package/src/sandbox/local-files.ts +1 -1
  279. package/src/sandbox/registry.ts +6 -5
  280. package/src/sandbox/resolve.ts +38 -23
  281. package/src/sandbox/retry.test.ts +68 -0
  282. package/src/sandbox/retry.ts +44 -0
  283. package/src/sandbox/types.ts +24 -24
  284. package/src/sandbox/vercel.ts +17 -3
  285. package/src/scoring/collector.ts +3 -3
  286. package/src/scoring/judge.ts +2 -2
  287. package/src/scoring/scoped.ts +13 -1
  288. package/src/scoring/types.ts +7 -7
  289. package/src/scoring/verdict.ts +4 -4
  290. package/src/shared/aggregate.ts +23 -3
  291. package/src/shared/types.ts +2 -2
  292. package/src/shared/{outcome.ts → verdict.ts} +15 -15
  293. package/src/show/compose.ts +12 -132
  294. package/src/show/index.ts +119 -95
  295. package/src/show/render.ts +462 -159
  296. package/src/show/show.test.ts +471 -253
  297. package/src/source-loc.ts +1 -1
  298. package/src/tty-line.ts +32 -0
  299. package/src/types.ts +2 -2
  300. package/src/util.test.ts +21 -1
  301. package/src/util.ts +5 -1
  302. package/src/view/app/App.tsx +33 -198
  303. package/src/view/app/components/AttemptModal.tsx +6 -6
  304. package/src/view/app/components/CodeView.tsx +2 -2
  305. package/src/view/app/components/CopyControls.tsx +32 -36
  306. package/src/view/app/components/SkippedRunsBanner.tsx +140 -0
  307. package/src/view/app/components/ui/badge.tsx +1 -1
  308. package/src/view/app/components/ui/dialog.tsx +2 -2
  309. package/src/view/app/i18n.ts +40 -31
  310. package/src/view/app/lib/artifact-url.ts +1 -1
  311. package/src/view/app/lib/attempt-route.test.ts +37 -47
  312. package/src/view/app/lib/attempt-route.ts +26 -26
  313. package/src/view/app/lib/rows.ts +9 -112
  314. package/src/view/app/lib/verdict.ts +25 -0
  315. package/src/view/app/main.tsx +9 -12
  316. package/src/view/app/pages/RunsPage.tsx +4 -4
  317. package/src/view/app/pages/TracesPage.tsx +3 -3
  318. package/src/view/app/shared.ts +0 -1
  319. package/src/view/app/types.ts +6 -51
  320. package/src/view/artifact-serving.test.ts +136 -0
  321. package/src/view/client-dist/app.css +1 -1
  322. package/src/view/client-dist/app.js +21 -21
  323. package/src/view/data.test.ts +214 -143
  324. package/src/view/data.ts +166 -120
  325. package/src/view/index.ts +23 -11
  326. package/src/view/server.ts +52 -17
  327. package/src/view/shared/types.ts +22 -31
  328. package/src/view/styles.css +117 -327
  329. package/src/view/view-report.test.ts +146 -98
  330. package/src/report/default-report.tsx +0 -222
  331. package/src/report/dual-face.test.tsx +0 -527
  332. package/src/report/react/CaseList.tsx +0 -70
  333. package/src/view/app/components/CostScoreChart.tsx +0 -147
  334. package/src/view/app/components/ExperimentTable.tsx +0 -284
  335. package/src/view/app/components/GroupSelector.tsx +0 -61
  336. package/src/view/app/components/primitives.tsx +0 -43
  337. package/src/view/app/lib/outcome.ts +0 -82
@@ -1,26 +1,27 @@
1
1
  // niceeval show 终端宿主的测试(行为规范:docs-site/zh/guides/viewing-results.mdx;
2
- // 组合语义:docs/reports.md「宿主输入的组合语义」)。覆盖:
3
- // - 榜单合成口径:每 experiment × eval 取最新判决,局部重跑从更早 run 补齐,头部标注合成自几个 run;
4
- // - 前缀过滤收窄选集,覆盖警告分母 = 已知并集 ∩ 范围;
2
+ // 组合语义:docs/feature/reports/architecture.md「Selection 是计算入口」)。覆盖:
3
+ // - 榜单合成口径:每 experiment × eval 取最新判定,局部重跑从更早快照补齐,头部标注合成自几个快照;
4
+ // - 前缀过滤收窄 Selection,覆盖警告分母 = 已知并集 ∩ 范围;
5
5
  // - --history 时间轴只列真实执行,resume 携带的复印件不占行;
6
- // - --report 装载(合法 / 非法默认导出 / 文件缺失)、位置前缀收窄注入选集、attemptCommand 下钻;
6
+ // - --report 装载(合法 / 非法默认导出 / 文件缺失)、位置前缀收窄注入 Selection、attemptCommand 下钻;
7
7
  // - 互斥:--history 与 --report;
8
- // - 单 eval 详情与 --transcript 证据切面的输出形态。
8
+ // - 单 eval 详情、`@<locator>` 精确定位与 --eval / --execution / --diff 证据切面的输出形态。
9
+ //
10
+ // fixture 直接写新布局(<expDir>/<snapDir>/snapshot.json + <evalId>/a<n>/result.json),
11
+ // 依据是 docs/feature/results/architecture.md 的稳定磁盘契约,不经 writer 运行时 API(避免与并行重写的
12
+ // niceeval/results 写入面签名耦合)。
9
13
 
10
- import { afterEach, describe, expect, it } from "vitest";
14
+ import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest";
11
15
  import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
12
16
  import { tmpdir } from "node:os";
13
17
  import { join } from "node:path";
14
- import {
15
- RESULTS_FORMAT,
16
- RESULTS_SCHEMA_VERSION,
17
- createRunWriter,
18
- openResults,
19
- type EvalResult,
20
- type RunSummary,
21
- } from "../results/index.ts";
22
- import { composeShowSelection, evalHistory } from "./compose.ts";
18
+ import { createHash } from "node:crypto";
19
+ import { openResults } from "../results/index.ts";
20
+ import { RESULTS_FORMAT, RESULTS_SCHEMA_VERSION, type EvalResult, type Verdict } from "../types.ts";
21
+ import { selectCurrentResults } from "../results/select.ts";
22
+ import { evalHistory } from "./compose.ts";
23
23
  import { runShow, type ShowFlags } from "./index.ts";
24
+ import { stringWidth } from "../report/text/layout.ts";
24
25
 
25
26
  // ───────────────────────── fixture 工具 ─────────────────────────
26
27
 
@@ -34,40 +35,72 @@ afterEach(async () => {
34
35
  await Promise.all(roots.splice(0).map((r) => rm(r, { recursive: true, force: true })));
35
36
  });
36
37
 
37
- function res(over: Partial<EvalResult> & Pick<EvalResult, "id">): EvalResult {
38
- return {
39
- agent: "bub",
40
- outcome: "passed",
41
- attempt: 0,
42
- durationMs: 1000,
43
- assertions: [],
44
- ...over,
45
- };
38
+ // show 的报告 chrome 跟随 CLI 界面语言(detectLocale);本文件的断言按英文写,
39
+ // 固定 en 让用例不随宿主机 LANG 漂移;zh-CN 传递单独一条用例覆盖。
40
+ let langBackup: string | undefined;
41
+ beforeAll(() => {
42
+ langBackup = process.env.NICEEVAL_LANG;
43
+ process.env.NICEEVAL_LANG = "en";
44
+ });
45
+ afterAll(() => {
46
+ if (langBackup === undefined) delete process.env.NICEEVAL_LANG;
47
+ else process.env.NICEEVAL_LANG = langBackup;
48
+ });
49
+
50
+ /** 一条 attempt 的最小 fixture;字段照 docs/feature/results/architecture.md 的 AttemptRecord。 */
51
+ type AttemptFixture = Pick<EvalResult, "id" | "verdict"> &
52
+ Partial<
53
+ Pick<
54
+ EvalResult,
55
+ "attempt" | "durationMs" | "assertions" | "estimatedCostUSD" | "usage" | "error" | "startedAt" | "artifactBase" | "hasEvents" | "hasTrace"
56
+ >
57
+ >;
58
+
59
+ function res(id: string, verdict: Verdict, extra: Partial<AttemptFixture> = {}): AttemptFixture {
60
+ return { id, verdict, attempt: 0, durationMs: 1000, assertions: [], ...extra };
61
+ }
62
+
63
+ /** 实验目录名的清洗:与 docs/feature/results/architecture.md 一致(/ 与非 [\w.@-] 换成 _)。 */
64
+ function cleanDirName(id: string): string {
65
+ return id.replace(/[^\w.@-]/g, "_");
66
+ }
67
+
68
+ interface SnapshotOpts {
69
+ experimentId: string;
70
+ agent?: string;
71
+ model?: string;
72
+ startedAt: string;
73
+ completedAt?: string;
74
+ knownEvalIds?: string[];
46
75
  }
47
76
 
48
- function summaryOf(results: EvalResult[], over: Partial<RunSummary> = {}): RunSummary {
49
- const count = (o: EvalResult["outcome"]) => results.filter((r) => r.outcome === o).length;
50
- return {
77
+ /** 写一份新布局快照:snapshot.json + attempt result.json。返回快照目录绝对路径。 */
78
+ async function writeSnapshot(
79
+ root: string,
80
+ snapDirName: string,
81
+ opts: SnapshotOpts,
82
+ results: AttemptFixture[],
83
+ ): Promise<string> {
84
+ const dir = join(root, cleanDirName(opts.experimentId), snapDirName);
85
+ await mkdir(dir, { recursive: true });
86
+ const meta = {
51
87
  format: RESULTS_FORMAT,
52
88
  schemaVersion: RESULTS_SCHEMA_VERSION,
53
89
  producer: { name: "niceeval", version: "0.4.6" },
54
- agent: results[0]?.agent ?? "bub",
55
- startedAt: "2026-07-08T10:00:00.000Z",
56
- completedAt: "2026-07-08T10:10:00.000Z",
57
- passed: count("passed"),
58
- failed: count("failed"),
59
- skipped: count("skipped"),
60
- errored: count("errored"),
61
- durationMs: 60_000,
62
- results,
63
- ...over,
90
+ experimentId: opts.experimentId,
91
+ agent: opts.agent ?? "bub",
92
+ ...(opts.model !== undefined ? { model: opts.model } : {}),
93
+ startedAt: opts.startedAt,
94
+ completedAt: opts.completedAt ?? opts.startedAt,
95
+ ...(opts.knownEvalIds ? { knownEvalIds: opts.knownEvalIds } : {}),
64
96
  };
65
- }
66
-
67
- async function writeRun(root: string, dirName: string, summary: RunSummary): Promise<void> {
68
- const dir = join(root, dirName);
69
- await mkdir(dir, { recursive: true });
70
- await writeFile(join(dir, "summary.json"), JSON.stringify(summary, null, 2), "utf-8");
97
+ await writeFile(join(dir, "snapshot.json"), JSON.stringify(meta, null, 2), "utf-8");
98
+ for (const r of results) {
99
+ const attemptDir = join(dir, r.id, `a${r.attempt ?? 0}`);
100
+ await mkdir(attemptDir, { recursive: true });
101
+ await writeFile(join(attemptDir, "result.json"), JSON.stringify(r, null, 2), "utf-8");
102
+ }
103
+ return dir;
71
104
  }
72
105
 
73
106
  interface Captured {
@@ -76,89 +109,113 @@ interface Captured {
76
109
  code: number;
77
110
  }
78
111
 
79
- async function show(root: string, patterns: string[], flags: ShowFlags = {}): Promise<Captured> {
112
+ async function show(root: string, patterns: string[], flags: ShowFlags = {}, width = 100): Promise<Captured> {
80
113
  let out = "";
81
114
  let err = "";
82
115
  const code = await runShow(root, patterns, { run: root, ...flags }, {
83
116
  out: (s) => (out += s),
84
117
  err: (s) => (err += s),
85
- width: 100,
118
+ width,
86
119
  now: Date.parse("2026-07-09T10:01:00.000Z"),
87
120
  });
88
121
  return { out, err, code };
89
122
  }
90
123
 
91
- /** 两个 run:老 run 全量(a ✓ b ✓),新 run 只重跑 b(✗)—— 榜单该跨 run 合成。 */
124
+ /** 两个快照:老快照全量(a ✓ b ✓),新快照只重跑 b(✗)—— 榜单该跨快照合成。 */
92
125
  async function seedComposedRoot(): Promise<string> {
93
126
  const root = await makeRoot();
94
- await writeRun(
95
- root,
96
- "2026-07-08T10-00-00-000Z",
97
- summaryOf(
98
- [
99
- res({ id: "weather/brooklyn", experimentId: "compare/bub", startedAt: "2026-07-08T10:00:01.000Z" }),
100
- res({ id: "fixtures/button", experimentId: "compare/bub", startedAt: "2026-07-08T10:00:02.000Z" }),
101
- ],
102
- { startedAt: "2026-07-08T10:00:00.000Z" },
103
- ),
104
- );
105
- await writeRun(
106
- root,
107
- "2026-07-09T10-00-00-000Z",
108
- summaryOf(
109
- [
110
- res({
111
- id: "fixtures/button",
112
- experimentId: "compare/bub",
113
- outcome: "failed",
114
- startedAt: "2026-07-09T10:00:01.000Z",
115
- assertions: [
116
- {
117
- name: 'fileChanged("src/components/Button.tsx")',
118
- severity: "gate",
119
- score: 0,
120
- passed: false,
121
- detail: "file was not modified",
122
- },
123
- ],
124
- }),
127
+ await writeSnapshot(root, "2026-07-08T10-00-00-000Z", { experimentId: "compare/bub", startedAt: "2026-07-08T10:00:00.000Z" }, [
128
+ res("weather/brooklyn", "passed"),
129
+ res("fixtures/button", "passed"),
130
+ ]);
131
+ await writeSnapshot(root, "2026-07-09T10-00-00-000Z", { experimentId: "compare/bub", startedAt: "2026-07-09T10:00:00.000Z" }, [
132
+ res("fixtures/button", "failed", {
133
+ assertions: [
134
+ {
135
+ name: 'fileChanged("src/components/Button.tsx")',
136
+ severity: "gate",
137
+ score: 0,
138
+ passed: false,
139
+ detail: "file was not modified",
140
+ },
125
141
  ],
126
- { startedAt: "2026-07-09T10:00:00.000Z" },
127
- ),
128
- );
142
+ }),
143
+ ]);
129
144
  return root;
130
145
  }
131
146
 
132
147
  // ───────────────────────── 榜单合成口径 ─────────────────────────
133
148
 
134
- describe("榜单:跨 run 合成的现刻水位", () => {
135
- it("局部重跑不撕榜单:另一题从更早 run 补齐,头部如实标注合成自 2 个 run", async () => {
149
+ describe("默认报告:跨快照合成的现刻水位(ExperimentComparison text 面)", () => {
150
+ it("局部重跑不撕报告:另一题从更早快照补齐,缺成本时散点明确空态", async () => {
136
151
  const root = await seedComposedRoot();
137
152
  const { out, code } = await show(root, []);
138
153
  expect(code).toBe(0);
139
- expect(out).toContain("Current verdicts · 1 experiment · composed from 2 runs");
140
- expect(out).toContain("latest 2026-07-09T10-00-00-000Z");
141
- // eval 级折叠计票:2 题里 1 题通过;两题都在(没有 partial-coverage 撕榜)
142
- expect(out).toContain("1/2");
143
- expect(out).toContain("50%");
144
- expect(out).not.toContain("verdicts cover");
145
- // Failing 清单:新判决的 fixtures/button,带失败断言与下钻命令
146
- expect(out).toContain("Failing:");
147
- expect(out).toContain("✗ fixtures/button");
148
- expect(out).toContain('gate fileChanged("src/components/Button.tsx")');
149
- expect(out).toContain("→ niceeval show fixtures/button");
150
- expect(out).not.toContain(" weather/brooklyn");
151
- });
152
-
153
- it("合成选集:每 experiment × eval 取最新判决;compose 不产生残缺警告", async () => {
154
+ expect(out).toContain("No data to plot Cost × Pass rate");
155
+ expect(out).toContain("1 point missing data");
156
+ expect(out).not.toContain("needed to compare");
157
+ expect(out).not.toContain("COMPARISON");
158
+ expect(out).toMatch(/compare\/bub\s+default\s+bub\s+1s\s+50%/);
159
+ expect(out).toContain("1 passed / 1 failed");
160
+ expect(out).toMatch(/✗ failed\s+fixtures\/button[\s\S]*└─ @1[0-9a-z]{7}[\s\S]*fileChanged/);
161
+ expect(out).toMatch(/✓ passed\s+weather\/brooklyn[\s\S]*└─ @1[0-9a-z]{7}/);
162
+ expect(out).not.toMatch(/\[[EXD⏱,]+\]/);
163
+ });
164
+
165
+ it(" show 的默认报告 chrome 跟随 locale", async () => {
166
+ const root = await seedComposedRoot();
167
+ process.env.NICEEVAL_LANG = "zh-CN";
168
+ try {
169
+ const { out, code } = await show(root, []);
170
+ expect(code).toBe(0);
171
+ expect(out).toContain("预估成本 × 成功率 没有可绘制的数据");
172
+ expect(out).toContain("1 通过 / 1 失败");
173
+ expect(out).toContain("compare/bub");
174
+ } finally {
175
+ process.env.NICEEVAL_LANG = "en";
176
+ }
177
+ });
178
+
179
+ it("窄终端截断长 eval 与原因,每一行都不超过终端显示宽度", async () => {
180
+ const root = await seedComposedRoot();
181
+ const { out, code } = await show(root, [], {}, 60);
182
+ expect(code).toBe(0);
183
+ expect(out).toContain("fixtures/button");
184
+ for (const line of out.trimEnd().split("\n")) {
185
+ expect(stringWidth(line), line).toBeLessThanOrEqual(60);
186
+ }
187
+ });
188
+
189
+ it("多个 experiment 显示散点图,再逐 experiment → Eval → Attempt", async () => {
190
+ const root = await makeRoot();
191
+ await writeSnapshot(root, "2026-07-08T10-00-00-000Z", { experimentId: "compare/a", agent: "codex", model: "mini", startedAt: "2026-07-08T10:00:00.000Z" }, [
192
+ res("q1", "passed", { estimatedCostUSD: 0.1 }),
193
+ res("q2", "failed", { estimatedCostUSD: 0.2 }),
194
+ ]);
195
+ await writeSnapshot(root, "2026-07-08T11-00-00-000Z", { experimentId: "compare/b", agent: "claude", model: "large", startedAt: "2026-07-08T11:00:00.000Z" }, [
196
+ res("q1", "passed", { estimatedCostUSD: 0.3 }),
197
+ res("q2", "passed", { estimatedCostUSD: 0.3 }),
198
+ ]);
199
+ const { out, code } = await show(root, []);
200
+ expect(code).toBe(0);
201
+ expect(out).toContain("better → upper right");
202
+ expect(out).toContain("A compare/a B compare/b");
203
+ expect(out).toMatch(/compare\/b\s+large\s+claude\s+1s\s+100%/);
204
+ expect(out).toMatch(/compare\/a\s+mini\s+codex\s+1s\s+50%/);
205
+ expect(out).toMatch(/✓ passed\s+q1[\s\S]*└─ @1[0-9a-z]{7}/);
206
+ expect(out).toMatch(/✗ failed\s+q2[\s\S]*└─ @1[0-9a-z]{7}/);
207
+ expect(out).not.toContain("needed to compare");
208
+ });
209
+
210
+ it("合成 Selection:每 experiment × eval 取最新判定;compose 不产生残缺警告", async () => {
154
211
  const root = await seedComposedRoot();
155
212
  const results = await openResults(root);
156
- const selection = composeShowSelection(results);
213
+ const selection = selectCurrentResults(results);
157
214
  expect(selection.snapshots).toHaveLength(1);
158
215
  const evals = selection.snapshots[0].evals.map((e) => e.id).sort();
159
216
  expect(evals).toEqual(["fixtures/button", "weather/brooklyn"]);
160
217
  const button = selection.snapshots[0].evals.find((e) => e.id === "fixtures/button")!;
161
- expect(button.attempts[0].result.outcome).toBe("failed"); // 新 run 的判决赢
218
+ expect(button.attempts[0].result.verdict).toBe("failed"); // 新快照的判定赢
162
219
  expect(selection.warnings).toEqual([]);
163
220
  // 对照:results.latest() 的最新快照是残缺的(这正是宿主要合成的原因)
164
221
  expect(results.latest().warnings.some((w) => w.kind === "partial-coverage")).toBe(true);
@@ -168,32 +225,28 @@ describe("榜单:跨 run 合成的现刻水位", () => {
168
225
  // ───────────────────────── 前缀过滤 ─────────────────────────
169
226
 
170
227
  describe("位置前缀收窄", () => {
171
- it("前缀收窄选集覆盖的 eval;覆盖警告分母 = 已知并集 ∩ 范围", async () => {
228
+ it("前缀收窄 Selection 覆盖的 eval;覆盖警告分母 = 已知并集 ∩ 范围", async () => {
172
229
  const root = await makeRoot();
173
- await writeRun(
230
+ await writeSnapshot(
174
231
  root,
175
232
  "2026-07-08T10-00-00-000Z",
176
- summaryOf(
177
- [
178
- res({ id: "weather/brooklyn", experimentId: "compare/bub", startedAt: "2026-07-08T10:00:01.000Z" }),
179
- res({ id: "algebra/quadratic", experimentId: "compare/bub", startedAt: "2026-07-08T10:00:02.000Z" }),
180
- ],
181
- {
182
- startedAt: "2026-07-08T10:00:00.000Z",
183
- // 已知并集包含一道从未落盘的题:algebra 范围外,不该刷 weather 范围的屏
184
- snapshots: { "compare/bub": { knownEvalIds: ["weather/brooklyn", "weather/queens", "algebra/quadratic"] } },
185
- },
186
- ),
233
+ {
234
+ experimentId: "compare/bub",
235
+ startedAt: "2026-07-08T10:00:00.000Z",
236
+ // 已知并集包含一道从未落盘的题:algebra 范围外,不该刷 weather 范围的屏
237
+ knownEvalIds: ["weather/brooklyn", "weather/queens", "algebra/quadratic"],
238
+ },
239
+ [res("weather/brooklyn", "passed"), res("algebra/quadratic", "passed")],
187
240
  );
188
241
  const results = await openResults(root);
189
242
 
190
- const weather = composeShowSelection(results, { patterns: ["weather"] });
243
+ const weather = selectCurrentResults(results, { patterns: ["weather"] });
191
244
  expect(weather.snapshots[0].evals.map((e) => e.id)).toEqual(["weather/brooklyn"]);
192
245
  // 分母 = {weather/brooklyn, weather/queens}:缺 queens → 1/2;algebra 的缺口不进来
193
246
  const coverage = weather.warnings.find((w) => w.kind === "partial-coverage");
194
247
  expect(coverage).toMatchObject({ covered: 1, total: 2 });
195
248
 
196
- const algebra = composeShowSelection(results, { patterns: ["algebra"] });
249
+ const algebra = selectCurrentResults(results, { patterns: ["algebra"] });
197
250
  expect(algebra.warnings.filter((w) => w.kind === "partial-coverage")).toEqual([]);
198
251
  });
199
252
 
@@ -211,52 +264,36 @@ describe("位置前缀收窄", () => {
211
264
  describe("单 eval 详情", () => {
212
265
  it("attempt 行 + 断言明细 + artifacts 路径 + 下钻提示", async () => {
213
266
  const root = await makeRoot();
214
- await writeRun(
267
+ await writeSnapshot(
215
268
  root,
216
269
  "2026-07-09T10-00-00-000Z",
217
- summaryOf(
218
- [
219
- res({
220
- id: "weather/brooklyn",
221
- experimentId: "compare/codex",
222
- agent: "codex",
223
- outcome: "failed",
224
- attempt: 0,
225
- startedAt: "2026-07-09T10:00:01.000Z",
226
- durationMs: 40_000,
227
- }),
228
- res({
229
- id: "weather/brooklyn",
230
- experimentId: "compare/codex",
231
- agent: "codex",
232
- outcome: "failed",
233
- attempt: 1,
234
- startedAt: "2026-07-09T10:00:42.000Z",
235
- durationMs: 41_000,
236
- usage: { inputTokens: 12_000, outputTokens: 300 },
237
- estimatedCostUSD: 0.04,
238
- artifactsDir: "artifacts/compare__codex__weather__brooklyn__1",
239
- assertions: [
240
- {
241
- name: 'calledTool("get_weather")',
242
- severity: "gate",
243
- score: 0,
244
- passed: false,
245
- detail: "tool was never called",
246
- },
247
- { name: "succeeded()", severity: "gate", score: 1, passed: true },
248
- {
249
- name: 'judge("回答基于实时数据")',
250
- severity: "soft",
251
- score: 0.2,
252
- passed: false,
253
- detail: "reply invents a temperature without any tool call",
254
- },
255
- ],
256
- }),
257
- ],
258
- { agent: "codex", startedAt: "2026-07-09T10:00:00.000Z" },
259
- ),
270
+ { experimentId: "compare/codex", agent: "codex", startedAt: "2026-07-09T10:00:00.000Z" },
271
+ [
272
+ res("weather/brooklyn", "failed", { attempt: 0, durationMs: 40_000 }),
273
+ res("weather/brooklyn", "failed", {
274
+ attempt: 1,
275
+ durationMs: 41_000,
276
+ usage: { inputTokens: 12_000, outputTokens: 300 },
277
+ estimatedCostUSD: 0.04,
278
+ assertions: [
279
+ {
280
+ name: 'calledTool("get_weather")',
281
+ severity: "gate",
282
+ score: 0,
283
+ passed: false,
284
+ detail: "tool was never called",
285
+ },
286
+ { name: "succeeded()", severity: "gate", score: 1, passed: true },
287
+ {
288
+ name: 'judge("回答基于实时数据")',
289
+ severity: "soft",
290
+ score: 0.2,
291
+ passed: false,
292
+ detail: "reply invents a temperature without any tool call",
293
+ },
294
+ ],
295
+ }),
296
+ ],
260
297
  );
261
298
  const { out, code } = await show(root, ["weather/brooklyn"]);
262
299
  expect(code).toBe(0);
@@ -269,63 +306,44 @@ describe("单 eval 详情", () => {
269
306
  expect(out).toContain('✗ gate calledTool("get_weather") — tool was never called');
270
307
  expect(out).toContain("✓ gate succeeded()");
271
308
  expect(out).toContain('✗ soft judge("回答基于实时数据") — 0.2/1: reply invents a temperature');
272
- expect(out).toContain("artifacts/compare__codex__weather__brooklyn__1/");
273
- expect(out).toContain("next: niceeval show weather/brooklyn --transcript | --trace | --diff");
274
- });
275
-
276
- it("--attempt 指定不存在的 attempt:直说可用序号", async () => {
277
- const root = await seedComposedRoot();
278
- const { err, code } = await show(root, ["fixtures/button"], { attempt: 5 });
279
- expect(code).toBe(1);
280
- expect(err).toContain("Attempt 5 not found for fixtures/button");
309
+ // artifacts 路径 = <experiment-dir>/<snapshot-dir>/<evalId>/a<n>(root 相对)
310
+ expect(out).toContain("compare_codex/2026-07-09T10-00-00-000Z/weather/brooklyn/a1");
311
+ // 每 experiment 一行都带紧凑索引(locator + 失败原因),详情块再补一条精确的 attempt locator
312
+ expect(out).toMatch(/✗ failed\s+2 attempts.*@1[0-9a-z]{7}.*gate calledTool/);
313
+ expect(out).toMatch(/attempt locator: @1[0-9a-z]{7}/);
314
+ expect(out).toMatch(/next: niceeval show @1[0-9a-z]{7} \[--eval\|--execution\|--diff\]/);
281
315
  });
282
316
  });
283
317
 
284
318
  // ───────────────────────── --history:复印件不占行 ─────────────────────────
285
319
 
286
320
  describe("--history 时间轴", () => {
287
- /** run1 真实执行;run2 resume 携带同一判决(身份键相同的复印件)+ 新题真实执行。 */
321
+ /** 快照1 真实执行;快照2 resume 携带同一判定(身份键相同的复印件)+ 新题真实执行。 */
288
322
  async function seedHistoryRoot(): Promise<string> {
289
323
  const root = await makeRoot();
290
- const original = res({
291
- id: "weather/brooklyn",
292
- experimentId: "compare/bub",
293
- outcome: "passed",
294
- startedAt: "2026-07-07T09:00:01.000Z",
295
- estimatedCostUSD: 0.03,
296
- artifactsDir: "artifacts/compare__bub__weather__brooklyn__0",
297
- });
298
- await writeRun(
324
+ await writeSnapshot(
299
325
  root,
300
326
  "2026-07-07T09-00-00-000Z",
301
- summaryOf([original], { startedAt: "2026-07-07T09:00:00.000Z" }),
327
+ { experimentId: "compare/bub", startedAt: "2026-07-07T09:00:00.000Z" },
328
+ [res("weather/brooklyn", "passed", { estimatedCostUSD: 0.03 })],
302
329
  );
303
- // 复印件:同 id / attempt / startedAt,artifactBase 指回原 run(resume 合入的形状)
304
- const { artifactsDir: _dropped, ...carriedBase } = original;
305
- const carried: EvalResult = {
306
- ...carriedBase,
307
- artifactBase: "2026-07-07T09-00-00-000Z/artifacts/compare__bub__weather__brooklyn__0",
308
- };
309
- await writeRun(
330
+ // 复印件:同 id / attempt / startedAt(锚定原快照的 startedAt),artifactBase 指回原快照。
331
+ await writeSnapshot(
310
332
  root,
311
333
  "2026-07-09T10-00-00-000Z",
312
- summaryOf(
313
- [
314
- carried,
315
- res({
316
- id: "weather/brooklyn",
317
- experimentId: "compare/bub",
318
- outcome: "failed",
319
- attempt: 1,
320
- startedAt: "2026-07-09T10:00:05.000Z",
321
- estimatedCostUSD: 0.04,
322
- assertions: [
323
- { name: 'calledTool("get_weather")', severity: "gate", score: 0, passed: false },
324
- ],
325
- }),
326
- ],
327
- { startedAt: "2026-07-09T10:00:00.000Z" },
328
- ),
334
+ { experimentId: "compare/bub", startedAt: "2026-07-09T10:00:00.000Z" },
335
+ [
336
+ res("weather/brooklyn", "passed", {
337
+ estimatedCostUSD: 0.03,
338
+ startedAt: "2026-07-07T09:00:00.000Z",
339
+ artifactBase: "compare_bub/2026-07-07T09-00-00-000Z/weather/brooklyn/a0",
340
+ }),
341
+ res("weather/brooklyn", "failed", {
342
+ attempt: 1,
343
+ estimatedCostUSD: 0.04,
344
+ assertions: [{ name: 'calledTool("get_weather")', severity: "gate", score: 0, passed: false }],
345
+ }),
346
+ ],
329
347
  );
330
348
  return root;
331
349
  }
@@ -335,18 +353,18 @@ describe("--history 时间轴", () => {
335
353
  const results = await openResults(root);
336
354
  const exp = results.experiments.find((e) => e.id === "compare/bub")!;
337
355
  const rows = evalHistory(exp, "weather/brooklyn");
338
- // run2 里复印件被识别,真实执行只有:run1 的 passed + run2 的 failed(新 attempt)
356
+ // 快照2 里复印件被识别,真实执行只有:快照1 的 passed + 快照2 的 failed(新 attempt)
339
357
  expect(rows).toHaveLength(2);
340
- expect(rows[0]).toMatchObject({ outcome: "failed", attempts: 1, costUSD: 0.04 });
358
+ expect(rows[0]).toMatchObject({ verdict: "failed", attempts: 1, costUSD: 0.04 });
341
359
  expect(rows[0].failedAssertion).toBe('gate calledTool("get_weather")');
342
- expect(rows[1]).toMatchObject({ outcome: "passed", attempts: 1, costUSD: 0.03 });
360
+ expect(rows[1]).toMatchObject({ verdict: "passed", attempts: 1, costUSD: 0.03 });
343
361
 
344
362
  const { out, code } = await show(root, ["weather/brooklyn"], { history: true });
345
363
  expect(code).toBe(0);
346
364
  expect(out).toContain("compare/bub · 2 runs · passed 1/2");
347
365
  expect(out).toContain("2026-07-09T10-00");
348
366
  expect(out).toContain("2026-07-07T09-00");
349
- // 复印件那份 passed 判决只出现一行(run1),不在 run2 再占一行
367
+ // 复印件那份 passed 判定只出现一行(快照1),不在快照2 再占一行
350
368
  expect(out.match(/✓ passed/g)).toHaveLength(1);
351
369
  });
352
370
 
@@ -355,7 +373,7 @@ describe("--history 时间轴", () => {
355
373
  const { out, code } = await show(root, [], { history: true });
356
374
  expect(code).toBe(0);
357
375
  expect(out).toContain("compare/bub · 2 runs");
358
- // 每个快照一行(run2 折叠含携带的 passed 复印件,任一轮通过 → passed)
376
+ // 每个快照一行(快照2 折叠含携带的 passed 复印件,任一轮通过 → passed)
359
377
  expect(out.match(/1\/1 passed/g)).toHaveLength(2);
360
378
  });
361
379
  });
@@ -373,7 +391,7 @@ describe("--report 装载", () => {
373
391
  "const Custom = () => null;",
374
392
  "Custom[FACES] = {",
375
393
  " web: () => null,",
376
- " text: (props, ctx) => `CUSTOM ${props.evals} · drill ${ctx.attemptCommand(props.ref)}`,",
394
+ " text: (props, ctx) => `CUSTOM ${props.evals} · drill ${ctx.attemptCommand(props.locator)}`,",
377
395
  "};",
378
396
  "export default {",
379
397
  ' [Symbol.for("niceeval.report.definition")]: true,',
@@ -381,7 +399,7 @@ describe("--report 装载", () => {
381
399
  " type: Custom,",
382
400
  " props: {",
383
401
  " evals: ctx.selection.snapshots.flatMap((s) => s.evals.map((e) => e.id)).sort().join(\",\"),",
384
- " ref: ctx.selection.snapshots[0].evals[0].attempts[0].ref,",
402
+ " locator: ctx.selection.snapshots[0].evals[0].attempts[0].locator,",
385
403
  " },",
386
404
  " }),",
387
405
  "};",
@@ -392,7 +410,7 @@ describe("--report 装载", () => {
392
410
  return path;
393
411
  }
394
412
 
395
- it("装载 + 注入选集 + attemptCommand 下钻命令", async () => {
413
+ it("装载 + 注入 Selection + attemptCommand 下钻命令", async () => {
396
414
  const root = await seedComposedRoot();
397
415
  const report = await writeReportFile(root);
398
416
  const { out, code } = await show(root, [], { report });
@@ -401,7 +419,7 @@ describe("--report 装载", () => {
401
419
  expect(out).toContain("drill niceeval show");
402
420
  });
403
421
 
404
- it("位置前缀对 --report 生效:收窄注入选集覆盖的 eval", async () => {
422
+ it("位置前缀对 --report 生效:收窄注入 Selection 覆盖的 eval", async () => {
405
423
  const root = await seedComposedRoot();
406
424
  const report = await writeReportFile(root);
407
425
  const { out, code } = await show(root, ["weather"], { report });
@@ -409,18 +427,16 @@ describe("--report 装载", () => {
409
427
  expect(out).toContain("CUSTOM weather/brooklyn ·");
410
428
  });
411
429
 
412
- it("--experiment 让选集只留该实验", async () => {
430
+ it("--experiment 让 Selection 只留该实验", async () => {
413
431
  const root = await seedComposedRoot();
414
- await writeRun(
432
+ await writeSnapshot(
415
433
  root,
416
434
  "2026-07-09T11-00-00-000Z",
417
- summaryOf(
418
- [res({ id: "weather/brooklyn", experimentId: "compare/codex", agent: "codex", startedAt: "2026-07-09T11:00:01.000Z" })],
419
- { agent: "codex", startedAt: "2026-07-09T11:00:00.000Z" },
420
- ),
435
+ { experimentId: "compare/codex", agent: "codex", startedAt: "2026-07-09T11:00:00.000Z" },
436
+ [res("weather/brooklyn", "passed")],
421
437
  );
422
438
  const results = await openResults(root);
423
- const selection = composeShowSelection(results, { experiment: "compare/codex" });
439
+ const selection = selectCurrentResults(results, { experiment: "compare/codex" });
424
440
  expect(selection.snapshots.map((s) => s.experimentId)).toEqual(["compare/codex"]);
425
441
  });
426
442
 
@@ -454,52 +470,254 @@ describe("--report 装载", () => {
454
470
  const { out, code } = await show(root, ["fixtures/button"], { report, diff: true });
455
471
  expect(code).toBe(0);
456
472
  expect(out).not.toContain("CUSTOM");
457
- expect(out).toContain("no diff recorded"); // fixture 没有 diff 工件:如实说缺
473
+ expect(out).toContain("no diff recorded"); // fixture 没有 diff artifact:如实说缺
458
474
  });
459
475
  });
460
476
 
461
- // ───────────────────────── 证据切面:--transcript ─────────────────────────
477
+ // ───────────────────────── 证据切面:--execution ─────────────────────────
462
478
 
463
- describe("--transcript", () => {
464
- it("逐轮对话 + 截断标注(事件数 · 工具调用数 · 原始工件路径)", async () => {
479
+ describe("--execution", () => {
480
+ it("标准事件流(消息);没有 OTel 时如实标 timing unavailable", async () => {
465
481
  const root = await makeRoot();
466
- const writer = await createRunWriter(root, { producer: { name: "niceeval", version: "0.0.0" } });
467
- const snap = writer.snapshot({
468
- experiment: "compare/codex",
469
- agent: "codex",
470
- startedAt: "2026-07-09T10:00:00.000Z",
471
- });
472
- await snap.writeAttempt(
473
- {
474
- id: "weather/brooklyn",
475
- outcome: "failed",
476
- attempt: 2,
477
- durationMs: 41_000,
478
- assertions: [],
479
- },
480
- {
481
- events: [
482
- { type: "message", role: "user", text: "布鲁克林今天天气怎么样?" },
483
- { type: "message", role: "assistant", text: "布鲁克林今天大约 24°C,晴。" },
484
- ],
485
- },
482
+ const dir = await writeSnapshot(
483
+ root,
484
+ "2026-07-09T10-00-00-000Z",
485
+ { experimentId: "compare/codex", agent: "codex", startedAt: "2026-07-09T10:00:00.000Z" },
486
+ [res("weather/brooklyn", "failed", { attempt: 2, durationMs: 41_000, hasEvents: true })],
486
487
  );
487
- await writer.finish();
488
+ await writeFile(
489
+ join(dir, "weather/brooklyn/a2/events.json"),
490
+ JSON.stringify([
491
+ { type: "message", role: "user", text: "布鲁克林今天天气怎么样?" },
492
+ { type: "message", role: "assistant", text: "布鲁克林今天大约 24°C,晴。" },
493
+ ]),
494
+ "utf-8",
495
+ );
496
+
497
+ const { out, code } = await show(root, ["weather/brooklyn"], { execution: true });
498
+ expect(code).toBe(0);
499
+ expect(out).toMatch(/^@1[0-9a-z]{7} · weather\/brooklyn · compare\/codex · failed/m);
500
+ expect(out).toMatch(/USER\s+布鲁克林今天天气怎么样?/);
501
+ expect(out).toMatch(/ASSISTANT\s+布鲁克林今天大约 24°C,晴。/);
502
+ expect(out).toContain("timing unavailable · OTel trace was not collected");
503
+ expect(out).toContain("full events: ");
504
+ expect(out).toContain("events.json");
505
+ });
488
506
 
489
- const { out, code } = await show(root, ["weather/brooklyn"], { transcript: true });
507
+ it("execution === null 时如实说没有事件记录", async () => {
508
+ const root = await makeRoot();
509
+ await writeSnapshot(
510
+ root,
511
+ "2026-07-08T10-00-00-000Z",
512
+ { experimentId: "compare/bub", startedAt: "2026-07-08T10:00:00.000Z" },
513
+ [res("weather/brooklyn", "passed")],
514
+ );
515
+ const { out, code } = await show(root, ["weather/brooklyn"], { execution: true });
490
516
  expect(code).toBe(0);
491
- expect(out).toContain("attempt 3 · compare/codex · failed");
492
- expect(out).toContain("[user]");
493
- expect(out).toContain("布鲁克林今天天气怎么样?");
494
- expect(out).toContain("[assistant]");
495
- expect(out).toContain("(2 events · no tool calls · full stream: ");
496
- expect(out).toContain("events.json)");
517
+ expect(out).toContain("no events recorded for this attempt");
497
518
  });
498
519
 
499
- it("多个 eval 匹配时证据切面报错:先收窄到单个 eval", async () => {
520
+ it("不把未关联的 telemetry spans 混进 Agent 执行记录", async () => {
521
+ const root = await makeRoot();
522
+ const dir = await writeSnapshot(root, "2026-07-09T10-00-00-000Z", { experimentId: "compare/codex", startedAt: "2026-07-09T10:00:00.000Z" }, [
523
+ res("weather/brooklyn", "failed", { hasEvents: true, hasTrace: true }),
524
+ ]);
525
+ await writeFile(join(dir, "weather/brooklyn/a0/events.json"), JSON.stringify([{ type: "message", role: "user", text: "hello" }]), "utf-8");
526
+ await writeFile(join(dir, "weather/brooklyn/a0/trace.json"), JSON.stringify([{ traceId: "t", spanId: "s", name: "sdk.internal", startMs: 1, endMs: 2 }]), "utf-8");
527
+ const { out } = await show(root, ["weather/brooklyn"], { execution: true });
528
+ expect(out).toContain("USER");
529
+ expect(out).not.toMatch(/telemetry\s+sdk\.internal/);
530
+ expect(out).toContain("unlinked telemetry spans omitted");
531
+ });
532
+
533
+ it("多个 eval 匹配时证据切面报错:给紧凑索引(locator + 失败原因)而不是只报个数", async () => {
500
534
  const root = await seedComposedRoot();
501
- const { err, code } = await show(root, [], { transcript: true });
535
+ const { err, code } = await show(root, [], { execution: true });
536
+ expect(code).toBe(1);
537
+ expect(err).toContain("matched 2 evals");
538
+ expect(err).toMatch(/✗ fixtures\/button\s+@1[0-9a-z]{7}/);
539
+ expect(err).toContain('gate fileChanged("src/components/Button.tsx")');
540
+ });
541
+ });
542
+
543
+ // ───────────────────────── 证据切面:--eval ─────────────────────────
544
+
545
+ describe("--eval", () => {
546
+ it("evalSource === null 时如实说源码未捕获", async () => {
547
+ const root = await makeRoot();
548
+ await writeSnapshot(
549
+ root,
550
+ "2026-07-08T10-00-00-000Z",
551
+ { experimentId: "compare/bub", startedAt: "2026-07-08T10:00:00.000Z" },
552
+ [res("weather/brooklyn", "failed", { assertions: [{ name: "succeeded()", severity: "gate", score: 0, passed: false }] })],
553
+ );
554
+ const { out, code } = await show(root, ["weather/brooklyn"], { eval: true });
555
+ expect(code).toBe(0);
556
+ expect(out).toContain("eval source unavailable for this attempt");
557
+ });
558
+
559
+ it("有捕获的源码时:源码行标回断言,unmapped 断言单独成段,断言计票摘要", async () => {
560
+ const root = await makeRoot();
561
+ const dir = await writeSnapshot(
562
+ root,
563
+ "2026-07-08T10-00-00-000Z",
564
+ { experimentId: "compare/bub", startedAt: "2026-07-08T10:00:00.000Z" },
565
+ [
566
+ res("weather/brooklyn", "failed", {
567
+ assertions: [
568
+ {
569
+ name: 'calledTool("get_weather")',
570
+ severity: "gate",
571
+ score: 0,
572
+ passed: false,
573
+ detail: "tool was never called",
574
+ loc: { file: "evals/weather/brooklyn.eval.ts", line: 2 },
575
+ },
576
+ { name: "unlocated()", severity: "soft", score: 0.5, passed: false, detail: "no loc on this one" },
577
+ ],
578
+ }),
579
+ ],
580
+ );
581
+ const attemptDir = join(dir, "weather/brooklyn/a0");
582
+ const content = 'defineEval({\n turn.calledTool("get_weather");\n});\n';
583
+ const sha = createHash("sha256").update(content).digest("hex");
584
+ await writeFile(join(attemptDir, "sources.json"), JSON.stringify([{ path: "evals/weather/brooklyn.eval.ts", sha256: sha }]), "utf-8");
585
+ await mkdir(join(dir, "sources"), { recursive: true });
586
+ await writeFile(join(dir, "sources", `${sha}.json`), JSON.stringify({ content }), "utf-8");
587
+
588
+ const { out, code } = await show(root, ["weather/brooklyn"], { eval: true });
589
+ expect(code).toBe(0);
590
+ expect(out).toContain(`eval source: evals/weather/brooklyn.eval.ts · sha256:${sha.slice(0, 8)}`);
591
+ // 源码行的缩进是语义的一部分,渲染必须原样保留(不按词重排折叠掉这两个空格)。
592
+ expect(out).toMatch(/2✗ {3}turn\.calledTool\("get_weather"\);/);
593
+ expect(out).toContain('gate · calledTool("get_weather") · tool was never called');
594
+ expect(out).toContain("unmapped assertions (1, no source location):");
595
+ expect(out).toContain("unlocated()");
596
+ expect(out).toContain("assertions: 1 gate failed · 1 soft below target");
597
+ });
598
+
599
+ it("源码行标注保留 group 与值断言的 expected/received", async () => {
600
+ const root = await makeRoot();
601
+ const dir = await writeSnapshot(root, "2026-07-08T10-00-00-000Z", { experimentId: "compare/bub", startedAt: "2026-07-08T10:00:00.000Z" }, [
602
+ res("manager", "failed", { assertions: [{ name: "equals(4)", severity: "gate", score: 0, passed: false, evidence: "1", group: "Issue 15193", loc: { file: "evals/manager.eval.ts", line: 1 } }] }),
603
+ ]);
604
+ const attemptDir = join(dir, "manager/a0");
605
+ const content = "t.check(actual, equals(expected));\n";
606
+ const sha = createHash("sha256").update(content).digest("hex");
607
+ await writeFile(join(attemptDir, "sources.json"), JSON.stringify([{ path: "evals/manager.eval.ts", sha256: sha }]), "utf-8");
608
+ await mkdir(join(dir, "sources"), { recursive: true });
609
+ await writeFile(join(dir, "sources", `${sha}.json`), JSON.stringify({ content }), "utf-8");
610
+ const { out } = await show(root, ["manager"], { eval: true });
611
+ expect(out).toContain("gate · Issue 15193 · equals(4) · expected 4 · received 1");
612
+ });
613
+ });
614
+
615
+ // ───────────────────────── @<locator> ─────────────────────────
616
+
617
+ describe("show @<locator>", () => {
618
+ it("默认页直接解释失败断言的 group、期望值、实际值和源码位置", async () => {
619
+ const root = await makeRoot();
620
+ await writeSnapshot(root, "2026-07-08T10-00-00-000Z", { experimentId: "compare/bub", startedAt: "2026-07-08T10:00:00.000Z" }, [
621
+ res("manager", "failed", { assertions: [{ name: "equals(4)", severity: "gate", score: 0, passed: false, evidence: "1", group: "Issue 15193", loc: { file: "evals/manager.eval.ts", line: 40, column: 11 } }] }),
622
+ ]);
623
+ const results = await openResults(root);
624
+ const locator = results.experiments[0]!.latest.evals[0]!.attempts[0]!.locator!;
625
+ const { out } = await show(root, [locator]);
626
+ expect(out).toContain("gate · Issue 15193");
627
+ expect(out).toContain("assertion: equals(4)");
628
+ expect(out).toContain("expected: 4");
629
+ expect(out).toContain("received: 1");
630
+ expect(out).toContain("source: evals/manager.eval.ts:40:11");
631
+ });
632
+ it("解析到对应 attempt,渲染紧凑全景(不当成 eval id 前缀匹配)", async () => {
633
+ const root = await makeRoot();
634
+ await writeSnapshot(root, "2026-07-08T10-00-00-000Z", { experimentId: "compare/bub", startedAt: "2026-07-08T10:00:00.000Z" }, [
635
+ res("weather/brooklyn", "passed"),
636
+ ]);
637
+ const results = await openResults(root);
638
+ const attempt = results.experiments[0]!.latest.evals[0]!.attempts[0]!;
639
+ const locator = attempt.locator!;
640
+ expect(locator).toMatch(/^@1[0-9a-z]{7}$/);
641
+
642
+ const { out, code } = await show(root, [locator]);
643
+ expect(code).toBe(0);
644
+ expect(out).toContain(`${locator} · weather/brooklyn · compare/bub · passed`);
645
+ expect(out).toContain("attempt 1 · ");
646
+ expect(out).toContain("execution: unavailable (no events recorded for this attempt)");
647
+ expect(out).toContain("changes: diff unavailable");
648
+ expect(out).not.toContain("evidence:");
649
+ expect(out).not.toContain("available:");
650
+ });
651
+
652
+ it("available 只逐行列出实际存在的证据命令,不打印能力字母或合并式 next", async () => {
653
+ const root = await makeRoot();
654
+ const dir = await writeSnapshot(root, "2026-07-08T10-00-00-000Z", { experimentId: "compare/bub", startedAt: "2026-07-08T10:00:00.000Z" }, [
655
+ res("weather/brooklyn", "failed", { hasEvents: true }),
656
+ ]);
657
+ const attemptDir = join(dir, "weather/brooklyn/a0");
658
+ const content = "t.check(false, equals(true));\n";
659
+ const sha = createHash("sha256").update(content).digest("hex");
660
+ await writeFile(join(attemptDir, "sources.json"), JSON.stringify([{ path: "evals/weather.eval.ts", sha256: sha }]), "utf-8");
661
+ await mkdir(join(dir, "sources"), { recursive: true });
662
+ await writeFile(join(dir, "sources", `${sha}.json`), JSON.stringify({ content }), "utf-8");
663
+ await writeFile(join(attemptDir, "events.json"), JSON.stringify([{ type: "message", role: "user", text: "hello" }]), "utf-8");
664
+ const results = await openResults(root);
665
+ const locator = results.experiments[0]!.latest.evals[0]!.attempts[0]!.locator!;
666
+
667
+ const { out, code } = await show(root, [locator]);
668
+ expect(code).toBe(0);
669
+ expect(out).toContain(`available:\n niceeval show ${locator} --eval\n niceeval show ${locator} --execution`);
670
+ expect(out).not.toContain(`niceeval show ${locator} --diff`);
671
+ expect(out).not.toContain("evidence:");
672
+ expect(out).not.toContain("next:");
673
+ });
674
+
675
+ it("配 --execution 走证据切面,不是紧凑全景", async () => {
676
+ const root = await makeRoot();
677
+ await writeSnapshot(root, "2026-07-08T10-00-00-000Z", { experimentId: "compare/bub", startedAt: "2026-07-08T10:00:00.000Z" }, [
678
+ res("weather/brooklyn", "passed"),
679
+ ]);
680
+ const results = await openResults(root);
681
+ const locator = results.experiments[0]!.latest.evals[0]!.attempts[0]!.locator!;
682
+
683
+ const { out, code } = await show(root, [locator], { execution: true });
684
+ expect(code).toBe(0);
685
+ expect(out).toContain(`${locator} · weather/brooklyn · compare/bub · passed`);
686
+ expect(out).toContain("no events recorded for this attempt");
687
+ // 紧凑全景独有的行不应该出现在证据切面输出里
688
+ expect(out).not.toContain("available:");
689
+ });
690
+
691
+ it("语法不对的 locator 报「not a valid attempt locator」,退出码 1,不崩", async () => {
692
+ const root = await makeRoot();
693
+ await writeSnapshot(root, "2026-07-08T10-00-00-000Z", { experimentId: "compare/bub", startedAt: "2026-07-08T10:00:00.000Z" }, [
694
+ res("weather/brooklyn", "passed"),
695
+ ]);
696
+ const { err, code } = await show(root, ["@not-valid"]);
697
+ expect(code).toBe(1);
698
+ expect(err).toContain("not a valid attempt locator");
699
+ });
700
+
701
+ it("语法合法但索引里没有的 locator 报「No attempt found」,退出码 1,不崩", async () => {
702
+ const root = await makeRoot();
703
+ await writeSnapshot(root, "2026-07-08T10-00-00-000Z", { experimentId: "compare/bub", startedAt: "2026-07-08T10:00:00.000Z" }, [
704
+ res("weather/brooklyn", "passed"),
705
+ ]);
706
+ const { err, code } = await show(root, ["@1nosuch1"]);
707
+ expect(code).toBe(1);
708
+ expect(err).toContain("No attempt found");
709
+ });
710
+
711
+ it("locator 与其它位置参数混用时报错,不静默只取第一个", async () => {
712
+ const root = await makeRoot();
713
+ await writeSnapshot(root, "2026-07-08T10-00-00-000Z", { experimentId: "compare/bub", startedAt: "2026-07-08T10:00:00.000Z" }, [
714
+ res("weather/brooklyn", "passed"),
715
+ ]);
716
+ const results = await openResults(root);
717
+ const locator = results.experiments[0]!.latest.evals[0]!.attempts[0]!.locator!;
718
+
719
+ const { err, code } = await show(root, [locator, "weather/brooklyn"]);
502
720
  expect(code).toBe(1);
503
- expect(err).toContain("Narrow to a single eval id");
721
+ expect(err).toContain("must be the only positional argument");
504
722
  });
505
723
  });