niceeval 0.6.1 → 0.7.0

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 (378) hide show
  1. package/INDEX.md +23 -23
  2. package/README.zh.md +6 -6
  3. package/dist/agents/types.d.ts +69 -7
  4. package/dist/context/types.d.ts +32 -12
  5. package/dist/i18n/en.d.ts +54 -0
  6. package/dist/i18n/zh-CN.d.ts +57 -3
  7. package/dist/o11y/types.d.ts +16 -2
  8. package/dist/report/aggregate.d.ts +32 -24
  9. package/dist/report/aggregate.js +158 -50
  10. package/dist/report/built-in/index.d.ts +2 -0
  11. package/dist/report/built-in/index.js +8 -0
  12. package/dist/report/components.d.ts +93 -160
  13. package/dist/report/components.js +377 -114
  14. package/dist/report/compute.d.ts +87 -81
  15. package/dist/report/compute.js +597 -417
  16. package/dist/report/flag.d.ts +32 -6
  17. package/dist/report/flag.js +92 -4
  18. package/dist/report/format.d.ts +19 -11
  19. package/dist/report/format.js +30 -13
  20. package/dist/report/index.d.ts +16 -16
  21. package/dist/report/index.js +20 -21
  22. package/dist/report/load.js +3 -2
  23. package/dist/report/locale.d.ts +57 -33
  24. package/dist/report/locale.js +122 -56
  25. package/dist/report/metrics.d.ts +23 -4
  26. package/dist/report/metrics.js +110 -25
  27. package/dist/report/primitives.d.ts +48 -15
  28. package/dist/report/primitives.js +135 -26
  29. package/dist/report/react/AttemptList.d.ts +9 -7
  30. package/dist/report/react/AttemptList.js +17 -10
  31. package/dist/report/react/DeltaTable.js +19 -18
  32. package/dist/report/react/EvalList.d.ts +4 -4
  33. package/dist/report/react/EvalList.js +0 -0
  34. package/dist/report/react/ExperimentComparison.d.ts +10 -0
  35. package/dist/report/react/ExperimentComparison.js +12 -0
  36. package/dist/report/react/ExperimentList.d.ts +4 -3
  37. package/dist/report/react/ExperimentList.js +17 -18
  38. package/dist/report/react/MetricBars.js +5 -4
  39. package/dist/report/react/MetricLine.js +12 -5
  40. package/dist/report/react/MetricMatrix.js +1 -1
  41. package/dist/report/react/MetricScatter.js +59 -28
  42. package/dist/report/react/MetricTable.js +2 -12
  43. package/dist/report/react/ScopeSummary.d.ts +10 -0
  44. package/dist/report/react/ScopeSummary.js +17 -0
  45. package/dist/report/react/Scoreboard.js +6 -6
  46. package/dist/report/react/cell.js +2 -2
  47. package/dist/report/react/chart-math.d.ts +23 -6
  48. package/dist/report/react/chart-math.js +71 -19
  49. package/dist/report/react/fixtures.d.ts +5 -9
  50. package/dist/report/react/fixtures.js +110 -147
  51. package/dist/report/react/index.d.ts +15 -5
  52. package/dist/report/react/index.js +18 -7
  53. package/dist/report/report.d.ts +137 -16
  54. package/dist/report/report.js +259 -28
  55. package/dist/report/text/faces.d.ts +17 -19
  56. package/dist/report/text/faces.js +253 -184
  57. package/dist/report/text/plot.js +1 -1
  58. package/dist/report/text/table.js +38 -7
  59. package/dist/report/tree.d.ts +90 -40
  60. package/dist/report/tree.js +252 -94
  61. package/dist/report/types.d.ts +247 -284
  62. package/dist/report/types.js +4 -3
  63. package/dist/report/web.d.ts +21 -5
  64. package/dist/report/web.js +42 -16
  65. package/dist/results/select.d.ts +38 -16
  66. package/dist/results/select.js +73 -25
  67. package/dist/results/types.d.ts +49 -14
  68. package/dist/runner/feedback/sink.d.ts +110 -0
  69. package/dist/runner/types.d.ts +513 -22
  70. package/dist/sandbox/docker.d.ts +23 -2
  71. package/dist/sandbox/e2b.d.ts +15 -1
  72. package/dist/sandbox/errors.d.ts +30 -3
  73. package/dist/sandbox/io-retry.d.ts +17 -0
  74. package/dist/sandbox/registry.d.ts +2 -0
  75. package/dist/sandbox/resolve.d.ts +18 -5
  76. package/dist/sandbox/retry.d.ts +11 -1
  77. package/dist/sandbox/types.d.ts +39 -5
  78. package/dist/sandbox/vercel.d.ts +7 -1
  79. package/dist/scoring/coverage.d.ts +30 -0
  80. package/dist/scoring/display.d.ts +21 -0
  81. package/dist/scoring/display.js +120 -0
  82. package/dist/scoring/types.d.ts +103 -20
  83. package/dist/shared/aggregate.d.ts +4 -2
  84. package/dist/shared/aggregate.js +8 -7
  85. package/dist/shared/types.d.ts +28 -0
  86. package/dist/tty-line.d.ts +0 -4
  87. package/dist/util.d.ts +23 -0
  88. package/docs-site/zh/README.md +44 -0
  89. package/docs-site/zh/examples/ai-agent-application.mdx +63 -0
  90. package/docs-site/zh/examples/coding-agent-extensions.mdx +57 -0
  91. package/docs-site/zh/examples/index.mdx +50 -0
  92. package/docs-site/zh/{concepts → explanation}/adapter.mdx +31 -13
  93. package/docs-site/zh/{concepts → explanation}/assert.mdx +7 -7
  94. package/docs-site/zh/{concepts → explanation}/drive.mdx +8 -8
  95. package/docs-site/zh/{concepts → explanation}/evals.mdx +4 -4
  96. package/docs-site/zh/{concepts → explanation}/experiment.mdx +8 -8
  97. package/docs-site/zh/{concepts → explanation}/hitl.mdx +8 -8
  98. package/docs-site/zh/{concepts → explanation}/judge.mdx +5 -5
  99. package/docs-site/zh/{concepts → explanation}/overview.mdx +11 -11
  100. package/docs-site/zh/{guides → explanation}/runner.mdx +18 -8
  101. package/docs-site/zh/{concepts → explanation}/tier.mdx +6 -6
  102. package/docs-site/zh/{guides → how-to}/agent-feedback-loop.mdx +35 -33
  103. package/docs-site/zh/{guides → how-to}/authoring.mdx +35 -2
  104. package/docs-site/zh/{guides → how-to}/ci-integration.mdx +23 -12
  105. package/docs-site/zh/{guides → how-to}/connect-otel.mdx +6 -6
  106. package/docs-site/zh/{guides → how-to}/connect-your-agent.mdx +47 -21
  107. package/docs-site/zh/{guides → how-to}/custom-reports.mdx +34 -39
  108. package/docs-site/zh/{guides → how-to}/dataset-fanout.mdx +25 -3
  109. package/docs-site/zh/{guides → how-to}/experiments.mdx +12 -5
  110. package/docs-site/zh/how-to/publish-report.mdx +105 -0
  111. package/docs-site/zh/{guides → how-to}/reporters.mdx +2 -2
  112. package/docs-site/zh/{guides → how-to}/sandbox-agent.mdx +56 -7
  113. package/docs-site/zh/how-to/sandbox-providers.mdx +350 -0
  114. package/docs-site/zh/{guides → how-to}/scoring-guide.mdx +4 -4
  115. package/docs-site/zh/{guides → how-to}/viewing-results.mdx +82 -39
  116. package/docs-site/zh/{guides → how-to}/write-experiment.mdx +6 -4
  117. package/docs-site/zh/{guides → how-to}/write-send.mdx +30 -14
  118. package/docs-site/zh/index.mdx +24 -26
  119. package/docs-site/zh/introduction.mdx +8 -8
  120. package/docs-site/zh/reference/builtin-agents.mdx +32 -5
  121. package/docs-site/zh/reference/capabilities.mdx +8 -8
  122. package/docs-site/zh/reference/cli.mdx +40 -12
  123. package/docs-site/zh/reference/define-agent.mdx +58 -5
  124. package/docs-site/zh/reference/define-config.mdx +1 -1
  125. package/docs-site/zh/reference/define-eval.mdx +42 -9
  126. package/docs-site/zh/reference/events.mdx +3 -3
  127. package/docs-site/zh/reference/expect.mdx +26 -1
  128. package/docs-site/zh/{guides → reference}/official-adapters.mdx +32 -8
  129. package/docs-site/zh/{guides → reference}/report-components.mdx +45 -33
  130. package/docs-site/zh/{guides → reference}/results-data.mdx +21 -13
  131. package/docs-site/zh/troubleshooting/debug-sandbox.mdx +57 -0
  132. package/docs-site/zh/troubleshooting/debugging.mdx +212 -0
  133. package/docs-site/zh/{quickstart.mdx → tutorials/quickstart.mdx} +5 -17
  134. package/package.json +10 -2
  135. package/src/agents/ai-sdk-otel.test.ts +1 -0
  136. package/src/agents/ai-sdk.test.ts +3 -0
  137. package/src/agents/ai-sdk.ts +3 -0
  138. package/src/agents/bub-install-spec.test.ts +34 -0
  139. package/src/agents/bub-install-spec.ts +32 -0
  140. package/src/agents/bub.ts +31 -32
  141. package/src/agents/claude-code.test.ts +130 -9
  142. package/src/agents/claude-code.ts +76 -4
  143. package/src/agents/codex.test.ts +189 -40
  144. package/src/agents/codex.ts +155 -14
  145. package/src/agents/coding-cli-versions.test.ts +15 -0
  146. package/src/agents/coding-cli-versions.ts +3 -0
  147. package/src/agents/index.ts +13 -2
  148. package/src/agents/langgraph.test.ts +204 -0
  149. package/src/agents/langgraph.ts +495 -0
  150. package/src/agents/marketplace.ts +85 -0
  151. package/src/agents/native-config.test.ts +179 -0
  152. package/src/agents/native-config.ts +267 -0
  153. package/src/agents/openai-compat.test.ts +1 -0
  154. package/src/agents/openai-compat.ts +1 -1
  155. package/src/agents/openclaw.test.ts +31 -0
  156. package/src/agents/openclaw.ts +171 -0
  157. package/src/agents/plugin-config.test.ts +1 -0
  158. package/src/agents/sdk-streams.test.ts +79 -0
  159. package/src/agents/sdk-streams.ts +55 -10
  160. package/src/agents/skills.test.ts +1 -0
  161. package/src/agents/streaming.test.ts +3 -9
  162. package/src/agents/streaming.ts +2 -2
  163. package/src/agents/types.ts +71 -8
  164. package/src/agents/ui-message-stream.test.ts +3 -0
  165. package/src/cli.ts +446 -124
  166. package/src/context/context.test.ts +51 -12
  167. package/src/context/context.ts +162 -30
  168. package/src/context/session.test.ts +2 -1
  169. package/src/context/session.ts +115 -7
  170. package/src/context/types.ts +30 -12
  171. package/src/define.test.ts +13 -8
  172. package/src/define.ts +25 -4
  173. package/src/expect/index.ts +53 -23
  174. package/src/i18n/en.ts +81 -17
  175. package/src/i18n/zh-CN.ts +80 -17
  176. package/src/o11y/cost.test.ts +1 -0
  177. package/src/o11y/execution-tree.test.ts +1 -20
  178. package/src/o11y/otlp/mappers/claude-code.test.ts +1 -0
  179. package/src/o11y/otlp/parse.test.ts +1 -0
  180. package/src/o11y/otlp/turn-otel.test.ts +1 -0
  181. package/src/o11y/parsers/bub.test.ts +1 -0
  182. package/src/o11y/parsers/claude-code.test.ts +1 -34
  183. package/src/o11y/parsers/openclaw.test.ts +154 -0
  184. package/src/o11y/parsers/openclaw.ts +310 -0
  185. package/src/o11y/prices.json +746 -311
  186. package/src/o11y/tool-names.test.ts +1 -0
  187. package/src/o11y/types.ts +16 -2
  188. package/src/report/aggregate.ts +178 -61
  189. package/src/report/built-in/index.tsx +9 -0
  190. package/src/report/components.tsx +625 -279
  191. package/src/report/compute.ts +723 -491
  192. package/src/report/dual-render.test.tsx +741 -1024
  193. package/src/report/flag.ts +104 -12
  194. package/src/report/format.ts +32 -12
  195. package/src/report/index.ts +119 -46
  196. package/src/report/load.ts +3 -2
  197. package/src/report/locale.ts +136 -65
  198. package/src/report/metrics.ts +108 -25
  199. package/src/report/primitives.tsx +196 -45
  200. package/src/report/react/AttemptList.tsx +30 -43
  201. package/src/report/react/DeltaTable.tsx +63 -45
  202. package/src/report/react/EvalList.tsx +0 -0
  203. package/src/report/react/ExperimentComparison.tsx +73 -0
  204. package/src/report/react/ExperimentList.tsx +50 -32
  205. package/src/report/react/MetricBars.tsx +5 -4
  206. package/src/report/react/MetricLine.tsx +13 -8
  207. package/src/report/react/MetricMatrix.tsx +2 -2
  208. package/src/report/react/MetricScatter.tsx +86 -34
  209. package/src/report/react/MetricTable.tsx +4 -76
  210. package/src/report/react/ScopeSummary.tsx +86 -0
  211. package/src/report/react/Scoreboard.tsx +28 -10
  212. package/src/report/react/cell.tsx +2 -2
  213. package/src/report/react/chart-math.test.ts +85 -0
  214. package/src/report/react/chart-math.ts +101 -22
  215. package/src/report/react/enhance.js +89 -5
  216. package/src/report/react/fixtures.ts +114 -154
  217. package/src/report/react/index.tsx +24 -39
  218. package/src/report/react/render.test.tsx +138 -158
  219. package/src/report/react/styles.css +243 -82
  220. package/src/report/report.test.ts +779 -841
  221. package/src/report/report.ts +423 -41
  222. package/src/report/text/faces.ts +290 -193
  223. package/src/report/text/plot.ts +1 -1
  224. package/src/report/text/table.ts +44 -7
  225. package/src/report/tree.ts +362 -104
  226. package/src/report/types.ts +261 -271
  227. package/src/report/web.ts +63 -20
  228. package/src/results/annotated-source.test.ts +62 -9
  229. package/src/results/annotated-source.ts +64 -6
  230. package/src/results/attempt-evidence.test.ts +13 -11
  231. package/src/results/attempt-evidence.ts +20 -13
  232. package/src/results/attempt-source.ts +6 -3
  233. package/src/results/copy.ts +150 -60
  234. package/src/results/host-equivalence.test.ts +34 -20
  235. package/src/results/index.ts +12 -4
  236. package/src/results/locator.test.ts +1 -22
  237. package/src/results/open.ts +15 -5
  238. package/src/results/publish.ts +149 -0
  239. package/src/results/results.test.ts +89 -54
  240. package/src/results/select.ts +104 -34
  241. package/src/results/truncate.ts +90 -0
  242. package/src/results/types.ts +43 -14
  243. package/src/results/writer.ts +31 -13
  244. package/src/runner/attempt.test.ts +138 -7
  245. package/src/runner/attempt.ts +603 -104
  246. package/src/runner/discover.test.ts +47 -0
  247. package/src/runner/discover.ts +36 -2
  248. package/src/runner/eval-source.test.ts +1 -27
  249. package/src/runner/feedback/agent.test.ts +504 -0
  250. package/src/runner/feedback/agent.ts +409 -0
  251. package/src/runner/feedback/ci.test.ts +562 -0
  252. package/src/runner/feedback/ci.ts +401 -0
  253. package/src/runner/feedback/coordinator.test.ts +317 -0
  254. package/src/runner/feedback/coordinator.ts +397 -0
  255. package/src/runner/feedback/failure.ts +40 -0
  256. package/src/runner/feedback/human.test.ts +616 -0
  257. package/src/runner/feedback/human.ts +535 -0
  258. package/src/runner/feedback/index.ts +66 -0
  259. package/src/runner/feedback/io.ts +78 -0
  260. package/src/runner/feedback/profile.test.ts +50 -0
  261. package/src/runner/feedback/profile.ts +58 -0
  262. package/src/runner/feedback/reducer.test.ts +395 -0
  263. package/src/runner/feedback/reducer.ts +260 -0
  264. package/src/runner/feedback/renderer.ts +82 -0
  265. package/src/runner/feedback/sink.ts +203 -0
  266. package/src/runner/feedback/testing.ts +106 -0
  267. package/src/runner/ledger.test.ts +230 -0
  268. package/src/runner/ledger.ts +329 -0
  269. package/src/runner/report.test.ts +128 -3
  270. package/src/runner/report.ts +33 -9
  271. package/src/runner/reporters/artifacts.ts +8 -2
  272. package/src/runner/reporters/braintrust.test.ts +8 -7
  273. package/src/runner/reporters/braintrust.ts +9 -2
  274. package/src/runner/reporters/index.ts +2 -2
  275. package/src/runner/reporters/json.test.ts +162 -0
  276. package/src/runner/reporters/json.ts +35 -8
  277. package/src/runner/reporters/shared.ts +1 -5
  278. package/src/runner/run.test.ts +760 -3
  279. package/src/runner/run.ts +243 -37
  280. package/src/runner/sandbox-prep.ts +3 -42
  281. package/src/runner/timing.ts +158 -0
  282. package/src/runner/types.ts +518 -22
  283. package/src/sandbox/checkpoint.test.ts +55 -0
  284. package/src/sandbox/checkpoint.ts +29 -8
  285. package/src/sandbox/cli-commands.ts +407 -0
  286. package/src/sandbox/docker.ts +115 -16
  287. package/src/sandbox/e2b-agent-template.test.ts +56 -0
  288. package/src/sandbox/e2b-agent-template.ts +94 -0
  289. package/src/sandbox/e2b.ts +74 -9
  290. package/src/sandbox/errors.ts +111 -4
  291. package/src/sandbox/index.ts +2 -0
  292. package/src/sandbox/io-retry.test.ts +58 -0
  293. package/src/sandbox/io-retry.ts +45 -0
  294. package/src/sandbox/keep-registry.test.ts +86 -0
  295. package/src/sandbox/keep-registry.ts +142 -0
  296. package/src/sandbox/keep.ts +178 -0
  297. package/src/sandbox/paths.test.ts +1 -0
  298. package/src/sandbox/paths.ts +19 -8
  299. package/src/sandbox/registry.ts +20 -3
  300. package/src/sandbox/resolve.ts +76 -11
  301. package/src/sandbox/retry.test.ts +70 -0
  302. package/src/sandbox/retry.ts +46 -4
  303. package/src/sandbox/types.ts +44 -6
  304. package/src/sandbox/vercel.ts +43 -20
  305. package/src/scoring/collector.ts +60 -17
  306. package/src/scoring/coverage.ts +95 -0
  307. package/src/scoring/diff.ts +81 -0
  308. package/src/scoring/display.test.ts +121 -0
  309. package/src/scoring/display.ts +133 -0
  310. package/src/scoring/evidence.test.ts +189 -0
  311. package/src/scoring/judge.test.ts +142 -0
  312. package/src/scoring/judge.ts +15 -18
  313. package/src/scoring/scoped.ts +217 -50
  314. package/src/scoring/types.ts +117 -20
  315. package/src/scoring/verdict.ts +16 -4
  316. package/src/shared/aggregate.ts +8 -6
  317. package/src/shared/types.ts +31 -0
  318. package/src/show/compose.ts +50 -67
  319. package/src/show/index.ts +127 -56
  320. package/src/show/render.ts +662 -131
  321. package/src/show/report-host.test.ts +188 -0
  322. package/src/show/report-host.ts +375 -0
  323. package/src/show/show.test.ts +320 -54
  324. package/src/tty-line.ts +8 -26
  325. package/src/util.test.ts +1 -0
  326. package/src/util.ts +41 -0
  327. package/src/view/app/App.test.tsx +69 -0
  328. package/src/view/app/App.tsx +144 -48
  329. package/src/view/app/components/AttemptModal.tsx +423 -11
  330. package/src/view/app/components/CodeView.tsx +41 -14
  331. package/src/view/app/components/CopyControls.tsx +2 -2
  332. package/src/view/app/i18n.ts +37 -17
  333. package/src/view/app/lib/attempt-route.test.ts +1 -0
  334. package/src/view/app/lib/verdict.ts +7 -9
  335. package/src/view/app/main.tsx +13 -8
  336. package/src/view/app/pages/{RunsPage.tsx → AttemptsPage.tsx} +6 -6
  337. package/src/view/app/types.ts +4 -1
  338. package/src/view/artifact-serving.test.ts +2 -1
  339. package/src/view/client-dist/app.css +1 -1
  340. package/src/view/client-dist/app.js +17 -17
  341. package/src/view/data.test.ts +10 -3
  342. package/src/view/data.ts +155 -49
  343. package/src/view/index.ts +56 -41
  344. package/src/view/server.ts +37 -15
  345. package/src/view/shared/types.ts +34 -5
  346. package/src/view/styles.css +227 -0
  347. package/src/view/view-report.test.ts +167 -62
  348. package/dist/report/built-ins/experiment-comparison.d.ts +0 -1
  349. package/dist/report/built-ins/experiment-comparison.js +0 -13
  350. package/dist/report/built-ins/index.d.ts +0 -1
  351. package/dist/report/built-ins/index.js +0 -2
  352. package/dist/report/react/GroupSummary.d.ts +0 -8
  353. package/dist/report/react/GroupSummary.js +0 -8
  354. package/dist/report/react/RunOverview.d.ts +0 -8
  355. package/dist/report/react/RunOverview.js +0 -12
  356. package/docs-site/zh/example/ai-agent-application.mdx +0 -152
  357. package/docs-site/zh/example/claude-code-codex-plugin.mdx +0 -167
  358. package/docs-site/zh/example/claude-code-codex-skill.mdx +0 -152
  359. package/docs-site/zh/example/showcase.mdx +0 -39
  360. package/docs-site/zh/guides/publish-report.mdx +0 -91
  361. package/docs-site/zh/guides/sandbox-providers.mdx +0 -102
  362. package/src/report/built-in-user-parity.test.tsx +0 -640
  363. package/src/report/built-ins/experiment-comparison.tsx +0 -19
  364. package/src/report/built-ins/index.ts +0 -2
  365. package/src/report/react/GroupSummary.tsx +0 -66
  366. package/src/report/react/RunOverview.tsx +0 -109
  367. package/src/runner/reporters/console.ts +0 -70
  368. package/src/runner/reporters/live.test.ts +0 -56
  369. package/src/runner/reporters/live.ts +0 -247
  370. package/src/runner/reporters/quiet.test.ts +0 -66
  371. package/src/runner/reporters/quiet.ts +0 -49
  372. package/src/runner/reporters/table.ts +0 -277
  373. /package/docs-site/zh/{example/tier1-ai-sdk-v7.mdx → examples/integrations/ai-sdk-v7.mdx} +0 -0
  374. /package/docs-site/zh/{example/tier1-claude-sdk.mdx → examples/integrations/claude-sdk.mdx} +0 -0
  375. /package/docs-site/zh/{example/tier1-codex-sdk.mdx → examples/integrations/codex-sdk.mdx} +0 -0
  376. /package/docs-site/zh/{example/tier1-langgraph.mdx → examples/integrations/langgraph.mdx} +0 -0
  377. /package/docs-site/zh/{example/tier1-pi-sdk.mdx → examples/integrations/pi-sdk.mdx} +0 -0
  378. /package/docs-site/zh/{guides → how-to}/fixtures.mdx +0 -0
@@ -1,193 +1,221 @@
1
1
  // 官方组件的 text 面:同一份算好的数据,渲染成终端字符(niceeval show 的形态)。
2
- // 输出形态照 docs-site/zh/guides/report-components.mdx 的示例块;与 web 面共守
3
- // 诚实契约:排序随 better、samples < total 角标、缺数据 — 不补 0、截断报剩余。
2
+ // web 面共守诚实契约:排序随 better、samples < total 角标、缺数据 — 不补 0、截断报剩余。
4
3
  // 零 react、零 IO、纯同步 —— 这是 text 宿主不需要 react-dom 的那一半。
5
- // chrome 文案(注脚、verdict 词、截断提示)经 ctx.locale 查 locale 字典,
6
- // 默认 en 与历史输出逐字一致;数据(display、键、warnings message)不本地化。
4
+ // chrome 文案(注脚、verdict 词、截断提示)经 ctx.locale 查 locale 字典;
5
+ // 数据 display 是 LocalizedText,按 LocalizedText 回退规则取值。
7
6
 
8
7
  import type {
9
8
  AttemptListItem,
10
9
  DeltaData,
11
10
  EvalListItem,
11
+ ExperimentComparisonData,
12
+ ExperimentComparisonGroupData,
12
13
  ExperimentListItem,
13
- GroupSummaryData,
14
14
  LineData,
15
15
  MatrixData,
16
+ MetricCell,
16
17
  MetricColumn,
17
- OverviewData,
18
18
  ScatterData,
19
+ ScopeSummaryData,
19
20
  ScoreboardData,
20
21
  TableData,
22
+ VerdictTally,
21
23
  } from "../types.ts";
22
24
  import type { TextContext } from "../tree.ts";
23
25
  import type { TableColumn, TableRow } from "../primitives.tsx";
24
26
  import {
25
- attemptItemReason,
27
+ experimentDisplayName,
28
+ fitFailureSummary,
26
29
  formatDurationMs,
27
30
  formatMetricValue,
28
31
  formatPlainNumber,
29
32
  formatUSD,
30
33
  verdictMark,
31
34
  } from "../format.ts";
32
- import { countText, localeText, resolveMetricLabel, type ReportLocale } from "../locale.ts";
33
- import { indentBlock, padDisplay, textBar, wrapDisplay } from "./layout.ts";
35
+ import {
36
+ countText,
37
+ localeText,
38
+ resolveLocalizedText,
39
+ resolveMetricLabel,
40
+ type ReportLocale,
41
+ } from "../locale.ts";
42
+ import { indentBlock, padDisplay, stringWidth, textBar, wrapDisplay } from "./layout.ts";
34
43
  import { renderTableText } from "./table.ts";
35
44
  import { renderCharPlot, renderCoordinateTable, type PlotPoint } from "./plot.ts";
36
45
 
37
46
  const MISSING_MARK = "—";
38
47
 
39
- /** 缺数据文案随 locale(en = "no data",与 MISSING_TEXT 一致)。 */
48
+ /** 缺数据文案随 locale(en = "no data")。 */
40
49
  function missingText(locale: ReportLocale): string {
41
50
  return localeText(locale, "cell.missing");
42
51
  }
43
52
 
44
- /** 格子的文本形态:缺数据 —,覆盖不全带 samples/total 角标。 */
45
- export function cellText(cell: { value: number | null; display: string; samples: number; total: number }): string {
53
+ /** 格子的文本形态:缺数据 —,覆盖不全带 samples/total 角标;display 按 locale 解析。 */
54
+ export function cellText(cell: MetricCell, locale: ReportLocale): string {
46
55
  if (cell.value === null) return MISSING_MARK;
47
- return cell.samples < cell.total ? `${cell.display} ${cell.samples}/${cell.total}` : cell.display;
56
+ const display = resolveLocalizedText(cell.display, locale);
57
+ return cell.samples < cell.total ? `${display} ${cell.samples}/${cell.total}` : display;
48
58
  }
49
59
 
50
- // ───────────────────────── RunOverview ─────────────────────────
60
+ /** verdict 计票的紧凑文案("3 passed / 1 failed"):非零判定逐个列,全部为零如实 —。 */
61
+ export function verdictTallyText(tally: VerdictTally, locale: ReportLocale): string {
62
+ const parts: string[] = [];
63
+ for (const kind of ["passed", "failed", "errored", "skipped"] as const) {
64
+ if (tally[kind] > 0) parts.push(`${tally[kind]} ${localeText(locale, `verdict.${kind}`)}`);
65
+ }
66
+ return parts.length > 0 ? parts.join(" / ") : MISSING_MARK;
67
+ }
51
68
 
52
- export function overviewText(data: OverviewData, ctx: TextContext): string {
53
- const { totals, snapshots } = data;
54
- const locale = ctx.locale;
55
- const runs = new Set(snapshots.map((s) => s.startedAt)).size;
56
- const latest = snapshots.map((s) => s.startedAt).sort().at(-1);
57
- const head = [
58
- countText(locale, "overview.experiments", snapshots.length),
59
- localeText(locale, "overview.evalsCount", { n: totals.evals }),
60
- localeText(locale, "overview.attemptsCount", { n: totals.attempts }),
61
- // 通过率只渲染 computeCell 算好的同一个 MetricCell(cellText 复用缺数据/coverage 角标语义),
62
- // 不从 passed/failed/errored 现场重算——与 web 面 RunOverview 同一份数据同一种读法。
63
- `${localeText(locale, "overview.passRate")} ${cellText(totals.passRate)}`,
64
- countText(locale, "composedFrom", runs),
65
- ...(latest ? [localeText(locale, "latestRun", { run: latest })] : []),
66
- ].join(" · ");
67
- const tallies = [
68
- `${localeText(locale, "verdict.passed")} ${totals.passed}`,
69
- `${localeText(locale, "verdict.failed")} ${totals.failed}`,
70
- `${localeText(locale, "verdict.errored")} ${totals.errored}`,
71
- `${localeText(locale, "verdict.skipped")} ${totals.skipped}`,
72
- totals.costUSD === null ? missingText(locale) : formatUSD(totals.costUSD),
73
- formatDurationMs(totals.durationMs),
74
- ].join(" · ");
75
- const lines = [head, tallies];
76
- for (const warning of data.warnings) lines.push(`! ${warning.message}`);
77
- return lines.join("\n");
69
+ /** ISO 时间 "YYYY-MM-DD HH:mm"(本地时区);不可解析原样返回。 */
70
+ function formatDateTimeMinute(iso: string): string {
71
+ const date = new Date(iso);
72
+ if (Number.isNaN(date.valueOf())) return iso;
73
+ const pad = (n: number) => String(n).padStart(2, "0");
74
+ return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`;
78
75
  }
79
76
 
80
- // ───────────────────────── GroupSummary ─────────────────────────
77
+ // ───────────────────────── ScopeSummary ─────────────────────────
81
78
 
82
79
  /**
83
- * 一至两行:头行是通过率(GroupSummaryData.passRate.display,不重算比例)+ experiment/eval 数 +
84
- * failed(+ errored,非零才列,与旧 GroupSelector 卡片一致)+ 总成本;第二行(有则加)是最后运行时间。
85
- * 不依赖固定网格宽度,窄终端自然换行。
80
+ * 一至两行:头行是端到端成功率(官方 MetricCell,不重算)+ experiment/eval/attempt 数 +
81
+ * `votes` 选中的那级计票 + 总成本;第二行(有则加)是快照时间窗。
86
82
  */
87
- export function groupSummaryText(data: GroupSummaryData, ctx: TextContext): string {
83
+ export function scopeSummaryText(data: ScopeSummaryData, votes: "eval" | "attempt", ctx: TextContext): string {
88
84
  const locale = ctx.locale;
85
+ const tally = votes === "attempt" ? data.attemptVerdicts : data.evalVerdicts;
89
86
  const head = [
90
- `${localeText(locale, "overview.passRate")} ${cellText(data.passRate)}`,
87
+ `${localeText(locale, "scopeSummary.passRate")} ${cellText(data.endToEndPassRate, locale)}`,
91
88
  countText(locale, "overview.experiments", data.experiments),
92
89
  localeText(locale, "overview.evalsCount", { n: data.evals }),
93
- `${localeText(locale, "verdict.failed")} ${data.verdicts.failed}`,
94
- ...(data.verdicts.errored > 0 ? [`${localeText(locale, "verdict.errored")} ${data.verdicts.errored}`] : []),
95
- data.totalCostUSD === null ? missingText(locale) : formatUSD(data.totalCostUSD),
90
+ localeText(locale, "overview.attemptsCount", { n: data.attempts }),
91
+ verdictTallyText(tally, locale),
92
+ `${localeText(locale, "scopeSummary.totalCost")} ${cellText(data.totalCostUSD, locale)}`,
96
93
  ].join(" · ");
97
94
  const lines = [head];
98
- if (data.lastRunAt) lines.push(localeText(locale, "latestRun", { run: data.lastRunAt }));
95
+ if (data.range.latestStartedAt !== null) {
96
+ const from = data.range.earliestStartedAt;
97
+ const to = data.range.latestStartedAt;
98
+ lines.push(
99
+ from !== null && from !== to
100
+ ? localeText(locale, "scopeSummary.range", { from: formatDateTimeMinute(from), to: formatDateTimeMinute(to) })
101
+ : formatDateTimeMinute(to),
102
+ );
103
+ }
99
104
  return lines.join("\n");
100
105
  }
101
106
 
102
- // ───────────────────────── MetricTable ─────────────────────────
107
+ // ───────────────────────── ExperimentComparison ─────────────────────────
108
+
109
+ /** 单组详情:标题行 + 缩进的散点与实验列表(与 web 面同一份预分区数据)。 */
110
+ function comparisonGroupText(group: ExperimentComparisonGroupData, ctx: TextContext): string {
111
+ const inner = Math.max(20, ctx.width - 2);
112
+ const innerCtx: TextContext = {
113
+ width: inner,
114
+ locale: ctx.locale,
115
+ attemptCommand: ctx.attemptCommand,
116
+ experimentCommand: ctx.experimentCommand,
117
+ render: (node, w) => ctx.render(node, w ?? inner),
118
+ };
119
+ const body = [scatterText(group.scatter, innerCtx), experimentListText(group.experiments, innerCtx, group.key)]
120
+ .filter((block) => block.length > 0)
121
+ .join("\n\n");
122
+ return `${localeText(ctx.locale, "experimentComparison.group")} ${group.key}\n\n${indentBlock(body, " ")}`;
123
+ }
103
124
 
104
- /** verdict 计票的紧凑文案("3 passed / 1 failed"):非零判定逐个列,全部为零如实 —。 */
105
- export function verdictTallyText(
106
- verdicts: NonNullable<NonNullable<TableData["rows"][number]["meta"]>["verdicts"]>,
107
- locale: ReportLocale,
125
+ /**
126
+ * text 面:命中多个可比组时只输出组索引与可复制的单组查看命令,不倾倒全部组详情;
127
+ * 命中单组时省略索引,直接输出该组的散点与实验列表。
128
+ */
129
+ export function experimentComparisonText(
130
+ data: ExperimentComparisonData,
131
+ _className: string | undefined,
132
+ ctx: TextContext,
108
133
  ): string {
109
- const parts: string[] = [];
110
- for (const kind of ["passed", "failed", "errored", "skipped"] as const) {
111
- if (verdicts[kind] > 0) parts.push(`${verdicts[kind]} ${localeText(locale, `verdict.${kind}`)}`);
112
- }
113
- return parts.length > 0 ? parts.join(" / ") : MISSING_MARK;
134
+ const locale = ctx.locale;
135
+ if (data.groups.length === 0) return localeText(locale, "experimentComparison.empty");
136
+ if (data.groups.length === 1) return comparisonGroupText(data.groups[0]!, ctx);
137
+
138
+ const columns: TableColumn[] = [
139
+ { key: "group", header: localeText(locale, "experimentComparison.group") },
140
+ { key: "experiments", header: localeText(locale, "scopeSummary.experiments"), align: "right" },
141
+ { key: "evals", header: localeText(locale, "scopeSummary.evals"), align: "right" },
142
+ { key: "passRate", header: localeText(locale, "scopeSummary.passRate"), align: "right" },
143
+ { key: "results", header: localeText(locale, "experimentComparison.results") },
144
+ { key: "cost", header: localeText(locale, "scopeSummary.totalCost"), align: "right" },
145
+ { key: "lastRun", header: localeText(locale, "experimentComparison.lastRun") },
146
+ ] as unknown as TableColumn[];
147
+ const rows: TableRow[] = data.groups.map((group) => ({
148
+ key: group.key,
149
+ cells: {
150
+ group: group.key,
151
+ experiments: String(group.summary.experiments),
152
+ evals: String(group.summary.evals),
153
+ passRate: cellText(group.summary.endToEndPassRate, locale),
154
+ results: verdictTallyText(group.summary.evalVerdicts, locale),
155
+ cost: group.summary.totalCostUSD.value === null ? null : cellText(group.summary.totalCostUSD, locale),
156
+ lastRun: group.summary.range.latestStartedAt === null ? null : formatDateTimeMinute(group.summary.range.latestStartedAt),
157
+ },
158
+ }));
159
+ const table = renderTableText(
160
+ { columns: columns as unknown as [TableColumn, ...TableColumn[]], rows, locale },
161
+ ctx,
162
+ );
163
+ const commands = [
164
+ localeText(locale, "experimentComparison.commandsHead"),
165
+ ...data.groups.map((group) => ` ${ctx.experimentCommand(group.key)}`),
166
+ ].join("\n");
167
+ return `${table}\n\n${commands}`;
114
168
  }
115
169
 
170
+ // ───────────────────────── MetricTable ─────────────────────────
171
+
116
172
  export function tableText(data: TableData, ctx: TextContext): string {
117
173
  const locale = ctx.locale;
118
- // meta 在场时补 Model / Agent / Verdicts 列(rows: "experiment" 的榜单 parity);
119
- // 列序对齐 view 原生榜单:experiment、model、agent、指标列…、verdicts
120
- const hasMeta = data.rows.some((row) => row.meta !== undefined);
121
- const hasModel = data.rows.some((row) => row.meta?.model !== undefined);
122
- const hasVerdicts = data.rows.some((row) => row.meta?.verdicts !== undefined);
123
174
  const columns: TableColumn[] = [
124
- { key: "dimension", header: data.dimension },
125
- ...(hasMeta && hasModel ? [{ key: "model", header: localeText(locale, "table.model") }] : []),
126
- ...(hasMeta ? [{ key: "agent", header: localeText(locale, "table.agent") }] : []),
127
- // 指标列的键用序号,不用 metric name —— 维度名、"model" 这些键都在同一个命名空间里,
128
- // 指标恰好叫 "agent" 时不能把 meta 列顶掉。
175
+ { key: "dimension", header: data.rowDimension },
176
+ // 指标列的键用序号,不用 metric name —— 维度名和指标名在同一个命名空间里,可能撞键。
129
177
  ...data.columns.map((c, i) => ({ key: `metric${i}`, header: resolveMetricLabel(c.label, locale, c.key) })),
130
- ...(hasVerdicts ? [{ key: "verdicts", header: localeText(locale, "table.verdicts") }] : []),
131
178
  ];
132
179
  const rows: TableRow[] = data.rows.map((row) => {
133
180
  const cells: Record<string, string | null> = { dimension: row.key };
134
- if (hasMeta && hasModel) cells.model = row.meta?.model ?? null;
135
- if (hasMeta) cells.agent = row.meta?.agent ?? null;
136
181
  data.columns.forEach((col, i) => {
137
- const cell = (row.cells as Record<string, TableData["rows"][number]["cells"][string]>)[col.key];
138
- cells[`metric${i}`] = cell ? cellText(cell) : null;
182
+ const cell = row.cells[col.key];
183
+ cells[`metric${i}`] = cell ? cellText(cell, locale) : null;
139
184
  });
140
- if (hasVerdicts) cells.verdicts = row.meta?.verdicts ? verdictTallyText(row.meta.verdicts, locale) : null;
141
185
  return { key: row.key, cells };
142
186
  });
143
- const table = renderTableText({ columns, rows, locale }, ctx);
144
-
145
- // rows: "experiment" 专属的行摘要(eval/attempt 数 + 最后运行时间):表格列已经挤满
146
- // dimension/model/agent/指标/verdicts,这几个数字挤进行键下面单独一行,与 web 面
147
- // 「行键下的小 sub-line」同一份信息、同一种「不进列」的取舍。
148
- const metaLines: string[] = [];
149
- for (const row of data.rows) {
150
- if (row.meta?.evals === undefined) continue;
151
- const parts = [localeText(locale, "overview.evalsCount", { n: row.meta.evals })];
152
- if (row.meta.attempts !== undefined && row.meta.attempts > row.meta.evals) {
153
- parts.push(localeText(locale, "overview.attemptsCount", { n: row.meta.attempts }));
154
- }
155
- if (row.meta.lastRunAt) parts.push(localeText(locale, "latestRun", { run: row.meta.lastRunAt }));
156
- metaLines.push(` ${row.key}: ${parts.join(" · ")}`);
157
- }
158
-
159
- const blocks = [table, ...(metaLines.length > 0 ? [metaLines.join("\n")] : [])];
160
- return blocks.join("\n\n");
187
+ return renderTableText({ columns: columns as unknown as [TableColumn, ...TableColumn[]], rows, locale }, ctx);
161
188
  }
162
189
 
163
190
  // ───────────────────────── MetricMatrix ─────────────────────────
164
191
 
165
192
  export function matrixText(data: MatrixData, ctx: TextContext): string {
166
193
  // 表体全是维度键与 display,没有 chrome 文案;"next:" 是命令提示,不本地化。
194
+ const locale = ctx.locale;
167
195
  const rowKeys: string[] = [];
168
196
  const columnKeys: string[] = [];
169
- const byPosition = new Map<string, MatrixData["cells"][number]["cell"]>();
197
+ const byPosition = new Map<string, MetricCell>();
170
198
  for (const entry of data.cells) {
171
199
  if (!rowKeys.includes(entry.row)) rowKeys.push(entry.row);
172
200
  if (!columnKeys.includes(entry.column)) columnKeys.push(entry.column);
173
201
  byPosition.set(JSON.stringify([entry.row, entry.column]), entry.cell);
174
202
  }
175
203
  const columns: TableColumn[] = [
176
- { key: "dimension", header: data.rows },
204
+ { key: "dimension", header: data.rowDimension },
177
205
  ...columnKeys.map((column, i) => ({ key: `column${i}`, header: column })),
178
206
  ];
179
207
  const rows: TableRow[] = rowKeys.map((row) => {
180
208
  const cells: Record<string, string | null> = { dimension: row };
181
209
  columnKeys.forEach((column, i) => {
182
210
  const cell = byPosition.get(JSON.stringify([row, column]));
183
- cells[`column${i}`] = cell ? cellText(cell) : null; // 稀疏格子在文本里以 — 呈现,不编数
211
+ cells[`column${i}`] = cell ? cellText(cell, locale) : null; // 稀疏格子在文本里以 — 呈现,不编数
184
212
  });
185
213
  return { key: row, cells };
186
214
  });
187
- const table = renderTableText({ columns, rows, locale: ctx.locale }, ctx);
215
+ const table = renderTableText({ columns: columns as unknown as [TableColumn, ...TableColumn[]], rows, locale }, ctx);
188
216
 
189
217
  // 下钻命令:行维度是 eval 时,指向最值得看的一行(先挑有缺格的,再挑按 better 最差的)
190
- if (data.rows !== "eval" || rowKeys.length === 0) return table;
218
+ if (data.rowDimension !== "eval" || rowKeys.length === 0) return table;
191
219
  const better = data.metric.better ?? "higher";
192
220
  let next: string | undefined;
193
221
  let worst: { key: string; value: number } | undefined;
@@ -217,10 +245,11 @@ export function matrixText(data: MatrixData, ctx: TextContext): string {
217
245
 
218
246
  const BAR_WIDTH = 20;
219
247
 
220
- export function barsText(data: MatrixData): string {
248
+ export function barsText(data: MatrixData, ctx: TextContext): string {
249
+ const locale = ctx.locale;
221
250
  const groupKeys: string[] = [];
222
251
  const seriesKeys: string[] = [];
223
- const byPosition = new Map<string, MatrixData["cells"][number]["cell"]>();
252
+ const byPosition = new Map<string, MetricCell>();
224
253
  for (const entry of data.cells) {
225
254
  if (!groupKeys.includes(entry.row)) groupKeys.push(entry.row);
226
255
  if (!seriesKeys.includes(entry.column)) seriesKeys.push(entry.column);
@@ -230,10 +259,9 @@ export function barsText(data: MatrixData): string {
230
259
  // 条长刻度:% 的天然域是 [0,1],其余以全图最大值为满条
231
260
  const values = data.cells.map((c) => c.cell.value).filter((v): v is number => v !== null);
232
261
  const maxValue = values.length > 0 ? Math.max(...values) : 0;
233
- const ratioOf = (value: number) =>
234
- data.metric.unit === "%" ? value : maxValue === 0 ? 0 : value / maxValue;
262
+ const ratioOf = (value: number) => (data.metric.unit === "%" ? value : maxValue === 0 ? 0 : value / maxValue);
235
263
 
236
- const seriesWidth = Math.max(...seriesKeys.map((k) => k.length), 0);
264
+ const seriesWidth = Math.max(...seriesKeys.map((k) => stringWidth(k)), 0);
237
265
  const lines: string[] = [];
238
266
  for (const group of groupKeys) {
239
267
  lines.push(group);
@@ -256,7 +284,7 @@ export function barsText(data: MatrixData): string {
256
284
  lines.push(` ${label} ${MISSING_MARK}`);
257
285
  continue;
258
286
  }
259
- lines.push(` ${label} ${textBar(ratioOf(cell.value), BAR_WIDTH)} ${cellText(cell)}`);
287
+ lines.push(` ${label} ${textBar(ratioOf(cell.value), BAR_WIDTH)} ${cellText(cell, locale)}`);
260
288
  }
261
289
  }
262
290
  return lines.join("\n");
@@ -273,14 +301,18 @@ export function scoreboardText(data: ScoreboardData, ctx: TextContext): string {
273
301
  }
274
302
  }
275
303
  const columns: TableColumn[] = [
276
- { key: "dimension", header: data.dimension },
304
+ { key: "dimension", header: data.rowDimension },
277
305
  { key: "total", header: localeText(locale, "scoreboard.totalText") },
278
306
  ...subjectKeys.map((key, i) => ({ key: `subject${i}`, header: key })),
279
307
  ];
280
308
  const rows: TableRow[] = data.rows.map((row) => {
309
+ const totalNotes = [
310
+ ...(row.total.notRun > 0 ? [localeText(locale, "scoreboard.notRunText", { n: row.total.notRun })] : []),
311
+ ...(row.total.unscorable > 0 ? [localeText(locale, "scoreboard.unscorableText", { n: row.total.unscorable })] : []),
312
+ ];
281
313
  const cells: Record<string, string | null> = {
282
314
  dimension: row.key,
283
- total: `${row.total.display}/${data.fullMarks}`,
315
+ total: `${resolveLocalizedText(row.total.display, locale)}/${data.fullMarks}${totalNotes.length > 0 ? ` ${totalNotes.join(" ")}` : ""}`,
284
316
  };
285
317
  subjectKeys.forEach((key, i) => {
286
318
  const subject = row.subjects.find((s) => s.key === key);
@@ -289,18 +321,22 @@ export function scoreboardText(data: ScoreboardData, ctx: TextContext): string {
289
321
  return;
290
322
  }
291
323
  const score = `${formatPlainNumber(subject.earned)}/${formatPlainNumber(subject.possible)}`;
292
- cells[`subject${i}`] =
293
- subject.missing > 0
294
- ? `${score} ${localeText(locale, "scoreboard.missingText", { n: subject.missing })}`
295
- : score;
324
+ const notes = [
325
+ ...(subject.notRun > 0 ? [localeText(locale, "scoreboard.notRunText", { n: subject.notRun })] : []),
326
+ ...(subject.unscorable > 0 ? [localeText(locale, "scoreboard.unscorableText", { n: subject.unscorable })] : []),
327
+ ];
328
+ cells[`subject${i}`] = notes.length > 0 ? `${score} ${notes.join(" ")}` : score;
296
329
  });
297
330
  return { key: row.key, cells };
298
331
  });
299
- const table = renderTableText({ columns, rows, locale }, ctx);
300
- if (data.weights.length === 0) return table;
301
- // 实际生效的权重表 —— 成绩单可审计
302
- const weights = data.weights.map((w) => `${w.prefix} ×${w.weight}`).join(" · ");
303
- return `${table}\n${localeText(locale, "scoreboard.weights")} ${weights} · ${localeText(locale, "scoreboard.othersWeight")}`;
332
+ const table = renderTableText({ columns: columns as unknown as [TableColumn, ...TableColumn[]], rows, locale }, ctx);
333
+ const footnotes: string[] = [];
334
+ if (data.weights.length > 0) {
335
+ const weights = data.weights.map((w) => `${w.prefix} ×${w.weight}`).join(" · ");
336
+ footnotes.push(`${localeText(locale, "scoreboard.weights")} ${weights} · ${localeText(locale, "scoreboard.othersWeight")}`);
337
+ }
338
+ if (data.ignoredEvals > 0) footnotes.push(countText(locale, "scoreboard.ignored", data.ignoredEvals));
339
+ return footnotes.length > 0 ? `${table}\n${footnotes.join("\n")}` : table;
304
340
  }
305
341
 
306
342
  // ───────────────────────── MetricScatter ─────────────────────────
@@ -311,6 +347,26 @@ function axisLabel(col: MetricColumn, locale: ReportLocale): string {
311
347
  return resolveMetricLabel(col.label, locale, col.key);
312
348
  }
313
349
 
350
+ /**
351
+ * 方向提示:轴向已随 better 反正(lower 反向),「更好」恒指向右上,文案恒为
352
+ * 「越靠右上越好」;仅当 x、y 都声明 better 时显示——任一轴未声明,组件不猜
353
+ * 「更好」朝哪边,整图无提示(返回 undefined)。
354
+ */
355
+ function betterCornerText(x: MetricColumn, y: MetricColumn, locale: ReportLocale): string | undefined {
356
+ if (x.better === undefined || y.better === undefined) return undefined;
357
+ return localeText(locale, "scatter.betterUpperRight");
358
+ }
359
+
360
+ /** 轴标题行:"<x label>(越低越好) × <y label>"——better 方向如实标注,不虚构轴向。 */
361
+ function scatterHeading(data: ScatterData, locale: ReportLocale): string {
362
+ const withBetter = (col: MetricColumn) => {
363
+ const label = axisLabel(col, locale);
364
+ if (col.better === undefined) return label;
365
+ return `${label}(${localeText(locale, col.better === "lower" ? "table.lowerBetter" : "table.higherBetter")})`;
366
+ };
367
+ return `${withBetter(data.x)} × ${withBetter(data.y)}`;
368
+ }
369
+
314
370
  export function scatterText(data: ScatterData, ctx: TextContext): string {
315
371
  const locale = ctx.locale;
316
372
  const drawable = data.rows.filter((r) => r.x.value !== null && r.y.value !== null);
@@ -326,10 +382,14 @@ export function scatterText(data: ScatterData, ctx: TextContext): string {
326
382
  // 点太密排不下时降级为坐标表,不硬挤
327
383
  if (drawable.length > POINT_MARKS.length || ctx.width < 44) {
328
384
  const table = renderCoordinateTable(
329
- drawable.map((r) => ({ key: r.key, x: r.x.display, y: r.y.display })),
330
- { key: data.points, x: axisLabel(data.x, locale), y: axisLabel(data.y, locale) },
385
+ drawable.map((r) => ({
386
+ key: r.key,
387
+ x: resolveLocalizedText(r.x.display, locale),
388
+ y: resolveLocalizedText(r.y.display, locale),
389
+ })),
390
+ { key: data.pointDimension, x: axes.x, y: axes.y },
331
391
  );
332
- return [table, ...footnotes].join("\n");
392
+ return [scatterHeading(data, locale), "", table, ...footnotes].join("\n");
333
393
  }
334
394
 
335
395
  const points: PlotPoint[] = drawable.map((r, i) => ({
@@ -347,20 +407,21 @@ export function scatterText(data: ScatterData, ctx: TextContext): string {
347
407
  }
348
408
  for (const list of bySeries.values()) list.sort((a, b) => a.x - b.x);
349
409
 
350
- const invertX = data.x.better === "lower";
351
410
  const plot = renderCharPlot({
352
411
  width: ctx.width,
353
412
  points,
354
413
  lines: [...bySeries.values()].filter((l) => l.length > 1),
355
- xLabel: `${axisLabel(data.x, locale)}${invertX ? ` ${localeText(locale, "scatter.axisReversed")}` : ""}`,
356
- yLabel: axisLabel(data.y, locale),
414
+ xLabel: axes.x,
415
+ yLabel: axes.y,
357
416
  formatX: (v) => formatMetricValue(v, data.x.unit),
358
417
  formatY: (v) => formatMetricValue(v, data.y.unit),
359
- invertX,
418
+ // 轴方向跟随 better(与 web 面同规则):lower 反向,higher 与未声明正向。
419
+ invertX: data.x.better === "lower",
360
420
  invertY: data.y.better === "lower",
361
421
  });
362
422
  const legend = drawable.map((r, i) => `${POINT_MARKS[i]} ${r.key}`).join(" ");
363
- return [plot, "", localeText(locale, "scatter.betterUpperRight"), legend, ...footnotes].join("\n");
423
+ const hint = betterCornerText(data.x, data.y, locale);
424
+ return [scatterHeading(data, locale), plot, "", ...(hint !== undefined ? [hint] : []), legend, ...footnotes].join("\n");
364
425
  }
365
426
 
366
427
  // ───────────────────────── MetricLine ─────────────────────────
@@ -374,6 +435,8 @@ export function lineText(data: LineData, ctx: TextContext): string {
374
435
 
375
436
  if (drawable.length === 0) return [missingText(locale), ...footnotes].join("\n");
376
437
 
438
+ const xLabel = resolveMetricLabel(data.x.label, locale, data.x.key);
439
+
377
440
  // 系列 → 字母;无系列 = 单系列
378
441
  const seriesKeys: string[] = [];
379
442
  for (const r of drawable) {
@@ -383,8 +446,12 @@ export function lineText(data: LineData, ctx: TextContext): string {
383
446
 
384
447
  if (seriesKeys.length > POINT_MARKS.length || ctx.width < 44) {
385
448
  const table = renderCoordinateTable(
386
- drawable.map((r) => ({ key: r.series ? `${r.key} (${r.series})` : r.key, x: r.xDisplay, y: r.y.display })),
387
- { key: "experiment", x: data.x.label, y: axisLabel(data.y, locale) },
449
+ drawable.map((r) => ({
450
+ key: r.series !== undefined ? `${r.series} (${r.key})` : r.key,
451
+ x: resolveLocalizedText(r.xDisplay, locale),
452
+ y: resolveLocalizedText(r.y.display, locale),
453
+ })),
454
+ { key: data.seriesDimension ?? xLabel, x: xLabel, y: axisLabel(data.y, locale) },
388
455
  );
389
456
  return [table, ...footnotes].join("\n");
390
457
  }
@@ -406,11 +473,10 @@ export function lineText(data: LineData, ctx: TextContext): string {
406
473
  width: ctx.width,
407
474
  points,
408
475
  lines: lines.filter((l) => l.length > 1),
409
- xLabel: data.x.label,
476
+ xLabel,
410
477
  yLabel: axisLabel(data.y, locale),
411
478
  formatX: (v) => formatMetricValue(v, data.x.unit),
412
479
  formatY: (v) => formatMetricValue(v, data.y.unit),
413
- invertY: data.y.better === "lower",
414
480
  });
415
481
  const legend = seriesKeys
416
482
  .map((key, i) => `${POINT_MARKS[i]} ${key === "" ? axisLabel(data.y, locale) : key}`)
@@ -421,41 +487,62 @@ export function lineText(data: LineData, ctx: TextContext): string {
421
487
  // ───────────────────────── DeltaTable ─────────────────────────
422
488
 
423
489
  export function deltaText(data: DeltaData, ctx: TextContext): string {
490
+ const locale = ctx.locale;
491
+ // 0 对不是错误:明确空态并报告配对域实验数(派生形态携带;字面形态缺省按 0)。
492
+ if (data.rows.length === 0) {
493
+ return localeText(locale, "delta.empty", { experiments: data.experiments ?? 0 });
494
+ }
424
495
  const columns: TableColumn[] = [
425
- { key: "pair", header: "pair" },
426
- ...data.columns.map((c, i) => ({ key: `metric${i}`, header: resolveMetricLabel(c.label, ctx.locale, c.key) })),
496
+ { key: "pair", header: localeText(locale, "delta.pairHeader") },
497
+ ...data.columns.map((c, i) => ({ key: `metric${i}`, header: resolveMetricLabel(c.label, locale, c.key) })),
427
498
  ];
428
499
  const rows: TableRow[] = data.rows.map((row) => {
429
- const cells: Record<string, string | null> = { pair: row.key };
500
+ const cells: Record<string, string | null> = { pair: resolveLocalizedText(row.label, locale) };
430
501
  data.columns.forEach((col, i) => {
431
- const cell = (row.cells as Record<string, DeltaData["rows"][number]["cells"][string]>)[col.key];
502
+ const cell = row.cells[col.key];
432
503
  if (!cell) {
433
504
  cells[`metric${i}`] = null;
434
505
  return;
435
506
  }
436
- const a = cell.a.value === null ? MISSING_MARK : cell.a.display;
437
- const b = cell.b.value === null ? MISSING_MARK : cell.b.display;
438
- cells[`metric${i}`] = `${a} → ${b} ${cell.display}`;
507
+ const a = cell.a.value === null ? MISSING_MARK : resolveLocalizedText(cell.a.display, locale);
508
+ const b = cell.b.value === null ? MISSING_MARK : resolveLocalizedText(cell.b.display, locale);
509
+ cells[`metric${i}`] = `${a} → ${b} ${resolveLocalizedText(cell.display, locale)}`;
439
510
  });
440
511
  return { key: row.key, cells };
441
512
  });
442
- return renderTableText({ columns, rows, locale: ctx.locale }, ctx);
513
+ return renderTableText({ columns: columns as unknown as [TableColumn, ...TableColumn[]], rows, locale }, ctx);
443
514
  }
444
515
 
445
516
  // ───────────────────────── 实体列表(ExperimentList / EvalList / AttemptList)─────────────────────────
446
517
  //
447
518
  // 三面共用的紧凑标记:`locator✓`(判定符紧跟 locator,中间不留空格)。
448
- // ExperimentList / EvalList 逐 attempt 只列这一个标记 + 各自的原因/耗时摘要,不重复整段
449
- // niceeval show 命令;要看某个 attempt 的完整证据,agent 自己拼 `niceeval show <locator>`——
450
- // 命令模板只在 AttemptList(叶子层)展示完整断言明细时才值得,不在中间层重复。
519
+ // ExperimentList / EvalList 逐 attempt 只列这一个标记 + 各自的摘要,不重复整段
520
+ // niceeval show 命令;要看某个 attempt 的完整证据,agent 自己拼 `niceeval show <locator>`。
451
521
 
452
522
  function locatorBadge(item: { locator: string; verdict: AttemptListItem["verdict"] }): string {
453
523
  return `${item.locator}${verdictMark(item.verdict)}`;
454
524
  }
455
525
 
526
+ /**
527
+ * failureSummary + moreFailures 的展示形态:摘要在计算侧已按 Scoring display 契约折好,
528
+ * 这里只加 "+N more failures" 计数与宽度收口,不重算摘要。
529
+ */
530
+ function attemptReasonText(item: AttemptListItem, locale: ReportLocale, maxChars: number): string | undefined {
531
+ if (item.failureSummary === null) return undefined;
532
+ const withMore =
533
+ item.moreFailures > 0
534
+ ? `${item.failureSummary} · ${countText(locale, "entityList.moreFailures", item.moreFailures)}`
535
+ : item.failureSummary;
536
+ return fitFailureSummary(withMore, Math.max(24, maxChars));
537
+ }
538
+
456
539
  // ── ExperimentList ──
457
540
 
458
- function experimentSummaryTable(items: ExperimentListItem[], ctx: TextContext): string {
541
+ function experimentSummaryTable(
542
+ items: readonly ExperimentListItem[],
543
+ ctx: TextContext,
544
+ relativeTo?: string,
545
+ ): string {
459
546
  const locale = ctx.locale;
460
547
  const compact = ctx.width < 100;
461
548
  const columns: TableColumn[] = [
@@ -466,48 +553,62 @@ function experimentSummaryTable(items: ExperimentListItem[], ctx: TextContext):
466
553
  { key: "passRate", header: compact && locale === "en" ? "Pass" : localeText(locale, "experimentList.passRate"), align: "right" },
467
554
  { key: "result", header: localeText(locale, "experimentList.result") },
468
555
  { key: "tokens", header: localeText(locale, "experimentList.tokens"), align: "right" },
469
- { key: "cost", header: compact && locale === "en" ? "Cost" : localeText(locale, "experimentList.estimatedCost"), align: "right" },
556
+ { key: "cost", header: localeText(locale, "experimentList.cost"), align: "right" },
470
557
  ];
471
558
  const rows: TableRow[] = items.map((item) => ({
472
559
  key: item.experimentId,
473
560
  cells: {
474
- experiment: item.experimentId,
561
+ experiment: experimentDisplayName(item.experimentId, relativeTo),
475
562
  model: item.model ?? localeText(locale, "experimentList.defaultModel"),
476
563
  agent: item.agent,
477
- duration: cellText(item.duration),
478
- passRate: cellText(item.passRate),
479
- result: verdictTallyText(item.verdicts, locale),
480
- tokens: cellText(item.tokens),
481
- cost: cellText(item.cost),
564
+ duration: cellText(item.durationMs, locale),
565
+ passRate: cellText(item.endToEndPassRate, locale),
566
+ result: verdictTallyText(item.evalVerdicts, locale),
567
+ tokens: cellText(item.tokens, locale),
568
+ cost: cellText(item.costUSD, locale),
482
569
  },
483
570
  }));
484
571
  const metadata = items.flatMap((item) =>
485
572
  wrapDisplay(
486
- `${item.experimentId}: ${localeText(locale, "overview.evalsCount", { n: item.evals })} · ${localeText(locale, "overview.attemptsCount", { n: item.attempts })} · ${item.lastRunAt}`,
573
+ `${experimentDisplayName(item.experimentId, relativeTo)}: ${localeText(locale, "overview.evalsCount", { n: item.evals })} · ${localeText(locale, "overview.attemptsCount", { n: item.attempts })} · ${item.lastRunAt}`,
487
574
  Math.max(8, ctx.width - 2),
488
575
  ).map((line) => ` ${line}`),
489
576
  );
490
- return [renderTableText({ columns, rows, locale }, ctx), metadata.join("\n")].join("\n");
577
+ return [renderTableText({ columns: columns as unknown as [TableColumn, ...TableColumn[]], rows, locale }, ctx), metadata.join("\n")].join("\n");
491
578
  }
492
579
 
493
- function experimentDetailTable(item: ExperimentListItem, ctx: TextContext): string {
580
+ function experimentDetailTable(item: ExperimentListItem, ctx: TextContext, relativeTo?: string): string {
494
581
  const locale = ctx.locale;
495
582
  const columns: TableColumn[] = [
496
583
  { key: "status", header: localeText(locale, "experimentList.status") },
497
584
  { key: "entity", header: localeText(locale, "experimentList.evalAttempt") },
498
- { key: "result", header: localeText(locale, "experimentList.result") },
585
+ // Result 是可扫读的失败预览,不是证据面:两行放不下的以 收口,完整值走 locator 下钻。
586
+ { key: "result", header: localeText(locale, "experimentList.result"), maxLines: 2 },
499
587
  { key: "duration", header: localeText(locale, "experimentList.duration"), align: "right" },
500
588
  { key: "cost", header: localeText(locale, "experimentList.cost"), align: "right" },
501
589
  ];
590
+ // Result 的字符预算 ≈ 两行 × 它能分到的列宽(总宽减其它列的自然宽与列距)。这里只做
591
+ // 粗预算;精确的按宽度收口由列的 maxLines 兜底。
592
+ const statusWidth = Math.max(
593
+ stringWidth(localeText(locale, "experimentList.status")),
594
+ ...item.evalRows.map((row) => stringWidth(`${verdictMark(row.verdict)} ${localeText(locale, `verdict.${row.verdict}`)}`)),
595
+ );
596
+ const entityWidth = Math.max(
597
+ stringWidth(localeText(locale, "experimentList.evalAttempt")),
598
+ ...item.evalRows.flatMap((row) => [stringWidth(row.evalId), ...row.attempts.map((a) => stringWidth(a.locator) + 3)]),
599
+ );
600
+ const fixedWidth = statusWidth + entityWidth + 8 /* duration */ + 6 /* cost */ + 3 * 4; /* 4 段列距 */
601
+ const resultBudget = Math.max(24, (ctx.width - fixedWidth) * 2);
502
602
  const rows: TableRow[] = item.evalRows.flatMap((row) => {
603
+ // Eval 父行只承载折叠判定与题级聚合;失败摘要只在 Attempt 子行出现。
503
604
  const parent: TableRow = {
504
605
  key: row.evalId,
505
606
  cells: {
506
607
  status: `${verdictMark(row.verdict)} ${localeText(locale, `verdict.${row.verdict}`)}`,
507
608
  entity: row.evalId,
508
609
  result: "",
509
- duration: "",
510
- cost: "",
610
+ duration: localeText(locale, "entityList.average", { value: cellText(row.durationMs, locale) }),
611
+ cost: localeText(locale, "entityList.average", { value: cellText(row.costUSD, locale) }),
511
612
  },
512
613
  };
513
614
  const attempts: TableRow[] = row.attempts.map((attempt, index) => ({
@@ -515,43 +616,59 @@ function experimentDetailTable(item: ExperimentListItem, ctx: TextContext): stri
515
616
  cells: {
516
617
  status: ` ${verdictMark(attempt.verdict)}`,
517
618
  entity: `${index === row.attempts.length - 1 ? "└─" : "├─"} ${attempt.locator}`,
518
- result: attemptItemReason(attempt) ?? MISSING_MARK,
619
+ result: attemptReasonText(attempt, locale, resultBudget) ?? MISSING_MARK,
519
620
  duration: attempt.verdict === "skipped" && attempt.durationMs === 0 ? null : formatDurationMs(attempt.durationMs),
520
- cost: attempt.costUSD === undefined ? null : formatUSD(attempt.costUSD),
621
+ cost: attempt.costUSD === null ? null : formatUSD(attempt.costUSD),
521
622
  },
522
623
  }));
523
624
  return [parent, ...attempts];
524
625
  });
525
626
  const flags = item.flags && Object.keys(item.flags).length > 0
526
- ? `${localeText(locale, "experimentList.flags")} ${Object.entries(item.flags).map(([key, value]) => `${key}=${String(value)}`).join(" · ")}`
627
+ ? `${localeText(locale, "experimentList.flags")} ${Object.entries(item.flags)
628
+ .map(([key, value]) => `${key}=${typeof value === "string" ? value : JSON.stringify(value)}`)
629
+ .join(" · ")}`
527
630
  : undefined;
528
- return [item.experimentId, flags, renderTableText({ columns, rows, locale }, ctx)].filter(Boolean).join("\n");
631
+ return [
632
+ experimentDisplayName(item.experimentId, relativeTo),
633
+ flags,
634
+ renderTableText({ columns: columns as unknown as [TableColumn, ...TableColumn[]], rows, locale }, ctx),
635
+ ]
636
+ .filter(Boolean)
637
+ .join("\n");
529
638
  }
530
639
 
531
- export function experimentListText(items: ExperimentListItem[], ctx: TextContext): string {
640
+ export function experimentListText(items: readonly ExperimentListItem[], ctx: TextContext, relativeTo?: string): string {
532
641
  if (items.length === 0) return localeText(ctx.locale, "attemptList.empty");
533
- return [experimentSummaryTable(items, ctx), ...items.map((item) => experimentDetailTable(item, ctx))].join("\n\n");
642
+ return [
643
+ experimentSummaryTable(items, ctx, relativeTo),
644
+ ...items.map((item) => experimentDetailTable(item, ctx, relativeTo)),
645
+ ].join("\n\n");
534
646
  }
535
647
 
536
648
  // ── EvalList ──
537
649
 
538
- function evalListAttemptLine(item: AttemptListItem): string {
539
- const reason = attemptItemReason(item);
650
+ function evalListAttemptLine(item: AttemptListItem, ctx: TextContext): string {
651
+ // 行式列表同守「Result 最多两行」:预算 = 两行终端宽,超出按尾截收口。
652
+ const reason = attemptReasonText(item, ctx.locale, ctx.width * 2 - stringWidth(locatorBadge(item)) - 6);
540
653
  return ` ${locatorBadge(item)}${reason ? ` · ${reason}` : ""}`;
541
654
  }
542
655
 
543
- export function evalListText(items: EvalListItem[], ctx: TextContext): string {
656
+ export function evalListText(items: readonly EvalListItem[], ctx: TextContext): string {
544
657
  const locale = ctx.locale;
545
658
  if (items.length === 0) return localeText(locale, "attemptList.empty");
546
659
  const blocks = items.map((item) => {
547
660
  const identity = `${item.evalId} · ${item.experimentId} · ${localeText(locale, `verdict.${item.verdict}`)}`;
548
661
  const summary = [
549
- localeText(locale, "attemptList.score", { score: cellText(item.score) }),
662
+ localeText(locale, "attemptList.score", { score: cellText(item.examScore, locale) }),
550
663
  localeText(locale, "overview.attemptsCount", { n: item.attempts.length }),
551
- `${formatDurationMs(item.duration.value ?? 0)} avg`,
552
- item.cost.value === null ? `${missingText(locale)} avg` : `${formatUSD(item.cost.value)} avg`,
664
+ localeText(locale, "entityList.average", {
665
+ value: item.durationMs.value === null ? missingText(locale) : formatDurationMs(item.durationMs.value),
666
+ }),
667
+ localeText(locale, "entityList.average", {
668
+ value: item.costUSD.value === null ? missingText(locale) : formatUSD(item.costUSD.value),
669
+ }),
553
670
  ].join(" · ");
554
- const attemptLines = item.attempts.map(evalListAttemptLine);
671
+ const attemptLines = item.attempts.map((attempt) => evalListAttemptLine(attempt, ctx));
555
672
  return [identity, ` ${summary}`, ...attemptLines].join("\n");
556
673
  });
557
674
  return blocks.join("\n\n");
@@ -559,45 +676,25 @@ export function evalListText(items: EvalListItem[], ctx: TextContext): string {
559
676
 
560
677
  // ── AttemptList ──
561
678
 
562
- /** 一个 AttemptListItem 的完整 text 卡片:判定符 + locator + 身份 + 耗时/成本,
563
- * 然后逐条断言(gate soft 都列,与 web 面的 AttemptRow 同一份材料)。 */
564
- function attemptListItemText(item: AttemptListItem, ctx: TextContext, locale: ReportLocale): string {
679
+ /** Attempt 比较卡片:只显示一条主失败摘要(至多两行终端宽);完整 assertions locator 下钻。 */
680
+ function attemptListItemText(item: AttemptListItem, ctx: TextContext): string {
565
681
  const head = [
566
682
  `${verdictMark(item.verdict)} ${item.locator}`,
567
683
  item.evalId,
568
684
  item.experimentId,
569
685
  formatDurationMs(item.durationMs),
570
- ...(item.costUSD !== undefined ? [formatUSD(item.costUSD)] : []),
686
+ ...(item.costUSD !== null ? [formatUSD(item.costUSD)] : []),
571
687
  ].join(" · ");
572
688
  const lines = [head];
573
- if (item.error) {
574
- lines.push(indentBlock(wrapDisplay(item.error, ctx.width - 4).join("\n"), " "));
575
- }
576
- for (const assertion of item.assertions) {
577
- const scoreText =
578
- assertion.threshold !== undefined
579
- ? `${formatPlainNumber(assertion.score)}/${formatPlainNumber(assertion.threshold)}`
580
- : formatPlainNumber(assertion.score);
581
- lines.push(
582
- ` ${assertion.severity} ${assertion.name} · ${localeText(locale, `verdict.${assertion.passed ? "passed" : "failed"}`)}${assertion.severity === "soft" ? ` ${scoreText}` : ""}`,
583
- );
584
- if (assertion.detail) lines.push(indentBlock(wrapDisplay(assertion.detail, ctx.width - 4).join("\n"), " "));
585
- if (assertion.evidence) {
586
- const limit = Math.max(240, ctx.width * 5);
587
- const evidence =
588
- assertion.evidence.length <= limit
589
- ? assertion.evidence
590
- : `${assertion.evidence.slice(0, limit)}… (${assertion.evidence.length - limit} more chars; open ${item.locator} for full evidence)`;
591
- lines.push(indentBlock(wrapDisplay(evidence, ctx.width - 6).join("\n"), " "));
592
- }
593
- }
689
+ const reason = attemptReasonText(item, ctx.locale, ctx.width * 2 - 4);
690
+ if (reason) lines.push(` ${reason}`);
594
691
  return lines.join("\n");
595
692
  }
596
693
 
597
- export function attemptListText(items: AttemptListItem[], total: number | undefined, ctx: TextContext): string {
694
+ export function attemptListText(items: readonly AttemptListItem[], total: number | undefined, ctx: TextContext): string {
598
695
  const locale = ctx.locale;
599
696
  if (items.length === 0) return localeText(locale, "attemptList.empty");
600
- const blocks = items.map((item) => attemptListItemText(item, ctx, locale));
697
+ const blocks = items.map((item) => attemptListItemText(item, ctx));
601
698
  const remaining = (total ?? items.length) - items.length;
602
699
  if (remaining > 0) blocks.push(localeText(locale, "attemptList.truncatedText", { n: remaining }));
603
700
  return blocks.join("\n\n");