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,137 +1,144 @@
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)不本地化。
7
- import { attemptItemReason, formatDurationMs, formatMetricValue, formatPlainNumber, formatUSD, verdictMark, } from "../format.js";
8
- import { countText, localeText, resolveMetricLabel } from "../locale.js";
9
- import { indentBlock, padDisplay, textBar, wrapDisplay } from "./layout.js";
4
+ // chrome 文案(注脚、verdict 词、截断提示)经 ctx.locale 查 locale 字典;
5
+ // 数据 display 是 LocalizedText,按 LocalizedText 回退规则取值。
6
+ import { experimentDisplayName, fitFailureSummary, formatDurationMs, formatMetricValue, formatPlainNumber, formatUSD, verdictMark, } from "../format.js";
7
+ import { countText, localeText, resolveLocalizedText, resolveMetricLabel, } from "../locale.js";
8
+ import { indentBlock, padDisplay, stringWidth, textBar, wrapDisplay } from "./layout.js";
10
9
  import { renderTableText } from "./table.js";
11
10
  import { renderCharPlot, renderCoordinateTable } from "./plot.js";
12
11
  const MISSING_MARK = "—";
13
- /** 缺数据文案随 locale(en = "no data",与 MISSING_TEXT 一致)。 */
12
+ /** 缺数据文案随 locale(en = "no data")。 */
14
13
  function missingText(locale) {
15
14
  return localeText(locale, "cell.missing");
16
15
  }
17
- /** 格子的文本形态:缺数据 —,覆盖不全带 samples/total 角标。 */
18
- export function cellText(cell) {
16
+ /** 格子的文本形态:缺数据 —,覆盖不全带 samples/total 角标;display 按 locale 解析。 */
17
+ export function cellText(cell, locale) {
19
18
  if (cell.value === null)
20
19
  return MISSING_MARK;
21
- return cell.samples < cell.total ? `${cell.display} ${cell.samples}/${cell.total}` : cell.display;
20
+ const display = resolveLocalizedText(cell.display, locale);
21
+ return cell.samples < cell.total ? `${display} ${cell.samples}/${cell.total}` : display;
22
22
  }
23
- // ───────────────────────── RunOverview ─────────────────────────
24
- export function overviewText(data, ctx) {
25
- const { totals, snapshots } = data;
26
- const locale = ctx.locale;
27
- const runs = new Set(snapshots.map((s) => s.startedAt)).size;
28
- const latest = snapshots.map((s) => s.startedAt).sort().at(-1);
29
- const head = [
30
- countText(locale, "overview.experiments", snapshots.length),
31
- localeText(locale, "overview.evalsCount", { n: totals.evals }),
32
- localeText(locale, "overview.attemptsCount", { n: totals.attempts }),
33
- // 通过率只渲染 computeCell 算好的同一个 MetricCell(cellText 复用缺数据/coverage 角标语义),
34
- // 不从 passed/failed/errored 现场重算——与 web 面 RunOverview 同一份数据同一种读法。
35
- `${localeText(locale, "overview.passRate")} ${cellText(totals.passRate)}`,
36
- countText(locale, "composedFrom", runs),
37
- ...(latest ? [localeText(locale, "latestRun", { run: latest })] : []),
38
- ].join(" · ");
39
- const tallies = [
40
- `${localeText(locale, "verdict.passed")} ${totals.passed}`,
41
- `${localeText(locale, "verdict.failed")} ${totals.failed}`,
42
- `${localeText(locale, "verdict.errored")} ${totals.errored}`,
43
- `${localeText(locale, "verdict.skipped")} ${totals.skipped}`,
44
- totals.costUSD === null ? missingText(locale) : formatUSD(totals.costUSD),
45
- formatDurationMs(totals.durationMs),
46
- ].join(" · ");
47
- const lines = [head, tallies];
48
- for (const warning of data.warnings)
49
- lines.push(`! ${warning.message}`);
50
- return lines.join("\n");
23
+ /** verdict 计票的紧凑文案("3 passed / 1 failed"):非零判定逐个列,全部为零如实 —。 */
24
+ export function verdictTallyText(tally, locale) {
25
+ const parts = [];
26
+ for (const kind of ["passed", "failed", "errored", "skipped"]) {
27
+ if (tally[kind] > 0)
28
+ parts.push(`${tally[kind]} ${localeText(locale, `verdict.${kind}`)}`);
29
+ }
30
+ return parts.length > 0 ? parts.join(" / ") : MISSING_MARK;
31
+ }
32
+ /** ISO 时间 "YYYY-MM-DD HH:mm"(本地时区);不可解析原样返回。 */
33
+ function formatDateTimeMinute(iso) {
34
+ const date = new Date(iso);
35
+ if (Number.isNaN(date.valueOf()))
36
+ return iso;
37
+ const pad = (n) => String(n).padStart(2, "0");
38
+ return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`;
51
39
  }
52
- // ───────────────────────── GroupSummary ─────────────────────────
40
+ // ───────────────────────── ScopeSummary ─────────────────────────
53
41
  /**
54
- * 一至两行:头行是通过率(GroupSummaryData.passRate.display,不重算比例)+ experiment/eval 数 +
55
- * failed(+ errored,非零才列,与旧 GroupSelector 卡片一致)+ 总成本;第二行(有则加)是最后运行时间。
56
- * 不依赖固定网格宽度,窄终端自然换行。
42
+ * 一至两行:头行是端到端成功率(官方 MetricCell,不重算)+ experiment/eval/attempt 数 +
43
+ * `votes` 选中的那级计票 + 总成本;第二行(有则加)是快照时间窗。
57
44
  */
58
- export function groupSummaryText(data, ctx) {
45
+ export function scopeSummaryText(data, votes, ctx) {
59
46
  const locale = ctx.locale;
47
+ const tally = votes === "attempt" ? data.attemptVerdicts : data.evalVerdicts;
60
48
  const head = [
61
- `${localeText(locale, "overview.passRate")} ${cellText(data.passRate)}`,
49
+ `${localeText(locale, "scopeSummary.passRate")} ${cellText(data.endToEndPassRate, locale)}`,
62
50
  countText(locale, "overview.experiments", data.experiments),
63
51
  localeText(locale, "overview.evalsCount", { n: data.evals }),
64
- `${localeText(locale, "verdict.failed")} ${data.verdicts.failed}`,
65
- ...(data.verdicts.errored > 0 ? [`${localeText(locale, "verdict.errored")} ${data.verdicts.errored}`] : []),
66
- data.totalCostUSD === null ? missingText(locale) : formatUSD(data.totalCostUSD),
52
+ localeText(locale, "overview.attemptsCount", { n: data.attempts }),
53
+ verdictTallyText(tally, locale),
54
+ `${localeText(locale, "scopeSummary.totalCost")} ${cellText(data.totalCostUSD, locale)}`,
67
55
  ].join(" · ");
68
56
  const lines = [head];
69
- if (data.lastRunAt)
70
- lines.push(localeText(locale, "latestRun", { run: data.lastRunAt }));
57
+ if (data.range.latestStartedAt !== null) {
58
+ const from = data.range.earliestStartedAt;
59
+ const to = data.range.latestStartedAt;
60
+ lines.push(from !== null && from !== to
61
+ ? localeText(locale, "scopeSummary.range", { from: formatDateTimeMinute(from), to: formatDateTimeMinute(to) })
62
+ : formatDateTimeMinute(to));
63
+ }
71
64
  return lines.join("\n");
72
65
  }
73
- // ───────────────────────── MetricTable ─────────────────────────
74
- /** verdict 计票的紧凑文案("3 passed / 1 failed"):非零判定逐个列,全部为零如实 —。 */
75
- export function verdictTallyText(verdicts, locale) {
76
- const parts = [];
77
- for (const kind of ["passed", "failed", "errored", "skipped"]) {
78
- if (verdicts[kind] > 0)
79
- parts.push(`${verdicts[kind]} ${localeText(locale, `verdict.${kind}`)}`);
80
- }
81
- return parts.length > 0 ? parts.join(" / ") : MISSING_MARK;
66
+ // ───────────────────────── ExperimentComparison ─────────────────────────
67
+ /** 单组详情:标题行 + 缩进的散点与实验列表( web 面同一份预分区数据) */
68
+ function comparisonGroupText(group, ctx) {
69
+ const inner = Math.max(20, ctx.width - 2);
70
+ const innerCtx = {
71
+ width: inner,
72
+ locale: ctx.locale,
73
+ attemptCommand: ctx.attemptCommand,
74
+ experimentCommand: ctx.experimentCommand,
75
+ render: (node, w) => ctx.render(node, w ?? inner),
76
+ };
77
+ const body = [scatterText(group.scatter, innerCtx), experimentListText(group.experiments, innerCtx, group.key)]
78
+ .filter((block) => block.length > 0)
79
+ .join("\n\n");
80
+ return `${localeText(ctx.locale, "experimentComparison.group")} ${group.key}\n\n${indentBlock(body, " ")}`;
81
+ }
82
+ /**
83
+ * text 面:命中多个可比组时只输出组索引与可复制的单组查看命令,不倾倒全部组详情;
84
+ * 命中单组时省略索引,直接输出该组的散点与实验列表。
85
+ */
86
+ export function experimentComparisonText(data, _className, ctx) {
87
+ const locale = ctx.locale;
88
+ if (data.groups.length === 0)
89
+ return localeText(locale, "experimentComparison.empty");
90
+ if (data.groups.length === 1)
91
+ return comparisonGroupText(data.groups[0], ctx);
92
+ const columns = [
93
+ { key: "group", header: localeText(locale, "experimentComparison.group") },
94
+ { key: "experiments", header: localeText(locale, "scopeSummary.experiments"), align: "right" },
95
+ { key: "evals", header: localeText(locale, "scopeSummary.evals"), align: "right" },
96
+ { key: "passRate", header: localeText(locale, "scopeSummary.passRate"), align: "right" },
97
+ { key: "results", header: localeText(locale, "experimentComparison.results") },
98
+ { key: "cost", header: localeText(locale, "scopeSummary.totalCost"), align: "right" },
99
+ { key: "lastRun", header: localeText(locale, "experimentComparison.lastRun") },
100
+ ];
101
+ const rows = data.groups.map((group) => ({
102
+ key: group.key,
103
+ cells: {
104
+ group: group.key,
105
+ experiments: String(group.summary.experiments),
106
+ evals: String(group.summary.evals),
107
+ passRate: cellText(group.summary.endToEndPassRate, locale),
108
+ results: verdictTallyText(group.summary.evalVerdicts, locale),
109
+ cost: group.summary.totalCostUSD.value === null ? null : cellText(group.summary.totalCostUSD, locale),
110
+ lastRun: group.summary.range.latestStartedAt === null ? null : formatDateTimeMinute(group.summary.range.latestStartedAt),
111
+ },
112
+ }));
113
+ const table = renderTableText({ columns: columns, rows, locale }, ctx);
114
+ const commands = [
115
+ localeText(locale, "experimentComparison.commandsHead"),
116
+ ...data.groups.map((group) => ` ${ctx.experimentCommand(group.key)}`),
117
+ ].join("\n");
118
+ return `${table}\n\n${commands}`;
82
119
  }
120
+ // ───────────────────────── MetricTable ─────────────────────────
83
121
  export function tableText(data, ctx) {
84
122
  const locale = ctx.locale;
85
- // meta 在场时补 Model / Agent / Verdicts 列(rows: "experiment" 的榜单 parity);
86
- // 列序对齐 view 原生榜单:experiment、model、agent、指标列…、verdicts
87
- const hasMeta = data.rows.some((row) => row.meta !== undefined);
88
- const hasModel = data.rows.some((row) => row.meta?.model !== undefined);
89
- const hasVerdicts = data.rows.some((row) => row.meta?.verdicts !== undefined);
90
123
  const columns = [
91
- { key: "dimension", header: data.dimension },
92
- ...(hasMeta && hasModel ? [{ key: "model", header: localeText(locale, "table.model") }] : []),
93
- ...(hasMeta ? [{ key: "agent", header: localeText(locale, "table.agent") }] : []),
94
- // 指标列的键用序号,不用 metric name —— 维度名、"model" 这些键都在同一个命名空间里,
95
- // 指标恰好叫 "agent" 时不能把 meta 列顶掉。
124
+ { key: "dimension", header: data.rowDimension },
125
+ // 指标列的键用序号,不用 metric name —— 维度名和指标名在同一个命名空间里,可能撞键。
96
126
  ...data.columns.map((c, i) => ({ key: `metric${i}`, header: resolveMetricLabel(c.label, locale, c.key) })),
97
- ...(hasVerdicts ? [{ key: "verdicts", header: localeText(locale, "table.verdicts") }] : []),
98
127
  ];
99
128
  const rows = data.rows.map((row) => {
100
129
  const cells = { dimension: row.key };
101
- if (hasMeta && hasModel)
102
- cells.model = row.meta?.model ?? null;
103
- if (hasMeta)
104
- cells.agent = row.meta?.agent ?? null;
105
130
  data.columns.forEach((col, i) => {
106
131
  const cell = row.cells[col.key];
107
- cells[`metric${i}`] = cell ? cellText(cell) : null;
132
+ cells[`metric${i}`] = cell ? cellText(cell, locale) : null;
108
133
  });
109
- if (hasVerdicts)
110
- cells.verdicts = row.meta?.verdicts ? verdictTallyText(row.meta.verdicts, locale) : null;
111
134
  return { key: row.key, cells };
112
135
  });
113
- const table = renderTableText({ columns, rows, locale }, ctx);
114
- // rows: "experiment" 专属的行摘要(eval/attempt 数 + 最后运行时间):表格列已经挤满
115
- // dimension/model/agent/指标/verdicts,这几个数字挤进行键下面单独一行,与 web 面
116
- // 「行键下的小 sub-line」同一份信息、同一种「不进列」的取舍。
117
- const metaLines = [];
118
- for (const row of data.rows) {
119
- if (row.meta?.evals === undefined)
120
- continue;
121
- const parts = [localeText(locale, "overview.evalsCount", { n: row.meta.evals })];
122
- if (row.meta.attempts !== undefined && row.meta.attempts > row.meta.evals) {
123
- parts.push(localeText(locale, "overview.attemptsCount", { n: row.meta.attempts }));
124
- }
125
- if (row.meta.lastRunAt)
126
- parts.push(localeText(locale, "latestRun", { run: row.meta.lastRunAt }));
127
- metaLines.push(` ${row.key}: ${parts.join(" · ")}`);
128
- }
129
- const blocks = [table, ...(metaLines.length > 0 ? [metaLines.join("\n")] : [])];
130
- return blocks.join("\n\n");
136
+ return renderTableText({ columns: columns, rows, locale }, ctx);
131
137
  }
132
138
  // ───────────────────────── MetricMatrix ─────────────────────────
133
139
  export function matrixText(data, ctx) {
134
140
  // 表体全是维度键与 display,没有 chrome 文案;"next:" 是命令提示,不本地化。
141
+ const locale = ctx.locale;
135
142
  const rowKeys = [];
136
143
  const columnKeys = [];
137
144
  const byPosition = new Map();
@@ -143,20 +150,20 @@ export function matrixText(data, ctx) {
143
150
  byPosition.set(JSON.stringify([entry.row, entry.column]), entry.cell);
144
151
  }
145
152
  const columns = [
146
- { key: "dimension", header: data.rows },
153
+ { key: "dimension", header: data.rowDimension },
147
154
  ...columnKeys.map((column, i) => ({ key: `column${i}`, header: column })),
148
155
  ];
149
156
  const rows = rowKeys.map((row) => {
150
157
  const cells = { dimension: row };
151
158
  columnKeys.forEach((column, i) => {
152
159
  const cell = byPosition.get(JSON.stringify([row, column]));
153
- cells[`column${i}`] = cell ? cellText(cell) : null; // 稀疏格子在文本里以 — 呈现,不编数
160
+ cells[`column${i}`] = cell ? cellText(cell, locale) : null; // 稀疏格子在文本里以 — 呈现,不编数
154
161
  });
155
162
  return { key: row, cells };
156
163
  });
157
- const table = renderTableText({ columns, rows, locale: ctx.locale }, ctx);
164
+ const table = renderTableText({ columns: columns, rows, locale }, ctx);
158
165
  // 下钻命令:行维度是 eval 时,指向最值得看的一行(先挑有缺格的,再挑按 better 最差的)
159
- if (data.rows !== "eval" || rowKeys.length === 0)
166
+ if (data.rowDimension !== "eval" || rowKeys.length === 0)
160
167
  return table;
161
168
  const better = data.metric.better ?? "higher";
162
169
  let next;
@@ -185,7 +192,8 @@ export function matrixText(data, ctx) {
185
192
  }
186
193
  // ───────────────────────── MetricBars(矩阵数据的另一种摆法)─────────────────────────
187
194
  const BAR_WIDTH = 20;
188
- export function barsText(data) {
195
+ export function barsText(data, ctx) {
196
+ const locale = ctx.locale;
189
197
  const groupKeys = [];
190
198
  const seriesKeys = [];
191
199
  const byPosition = new Map();
@@ -200,8 +208,8 @@ export function barsText(data) {
200
208
  // 条长刻度:% 的天然域是 [0,1],其余以全图最大值为满条
201
209
  const values = data.cells.map((c) => c.cell.value).filter((v) => v !== null);
202
210
  const maxValue = values.length > 0 ? Math.max(...values) : 0;
203
- const ratioOf = (value) => data.metric.unit === "%" ? value : maxValue === 0 ? 0 : value / maxValue;
204
- const seriesWidth = Math.max(...seriesKeys.map((k) => k.length), 0);
211
+ const ratioOf = (value) => (data.metric.unit === "%" ? value : maxValue === 0 ? 0 : value / maxValue);
212
+ const seriesWidth = Math.max(...seriesKeys.map((k) => stringWidth(k)), 0);
205
213
  const lines = [];
206
214
  for (const group of groupKeys) {
207
215
  lines.push(group);
@@ -227,7 +235,7 @@ export function barsText(data) {
227
235
  lines.push(` ${label} ${MISSING_MARK}`);
228
236
  continue;
229
237
  }
230
- lines.push(` ${label} ${textBar(ratioOf(cell.value), BAR_WIDTH)} ${cellText(cell)}`);
238
+ lines.push(` ${label} ${textBar(ratioOf(cell.value), BAR_WIDTH)} ${cellText(cell, locale)}`);
231
239
  }
232
240
  }
233
241
  return lines.join("\n");
@@ -243,14 +251,18 @@ export function scoreboardText(data, ctx) {
243
251
  }
244
252
  }
245
253
  const columns = [
246
- { key: "dimension", header: data.dimension },
254
+ { key: "dimension", header: data.rowDimension },
247
255
  { key: "total", header: localeText(locale, "scoreboard.totalText") },
248
256
  ...subjectKeys.map((key, i) => ({ key: `subject${i}`, header: key })),
249
257
  ];
250
258
  const rows = data.rows.map((row) => {
259
+ const totalNotes = [
260
+ ...(row.total.notRun > 0 ? [localeText(locale, "scoreboard.notRunText", { n: row.total.notRun })] : []),
261
+ ...(row.total.unscorable > 0 ? [localeText(locale, "scoreboard.unscorableText", { n: row.total.unscorable })] : []),
262
+ ];
251
263
  const cells = {
252
264
  dimension: row.key,
253
- total: `${row.total.display}/${data.fullMarks}`,
265
+ total: `${resolveLocalizedText(row.total.display, locale)}/${data.fullMarks}${totalNotes.length > 0 ? ` ${totalNotes.join(" ")}` : ""}`,
254
266
  };
255
267
  subjectKeys.forEach((key, i) => {
256
268
  const subject = row.subjects.find((s) => s.key === key);
@@ -259,25 +271,49 @@ export function scoreboardText(data, ctx) {
259
271
  return;
260
272
  }
261
273
  const score = `${formatPlainNumber(subject.earned)}/${formatPlainNumber(subject.possible)}`;
262
- cells[`subject${i}`] =
263
- subject.missing > 0
264
- ? `${score} ${localeText(locale, "scoreboard.missingText", { n: subject.missing })}`
265
- : score;
274
+ const notes = [
275
+ ...(subject.notRun > 0 ? [localeText(locale, "scoreboard.notRunText", { n: subject.notRun })] : []),
276
+ ...(subject.unscorable > 0 ? [localeText(locale, "scoreboard.unscorableText", { n: subject.unscorable })] : []),
277
+ ];
278
+ cells[`subject${i}`] = notes.length > 0 ? `${score} ${notes.join(" ")}` : score;
266
279
  });
267
280
  return { key: row.key, cells };
268
281
  });
269
- const table = renderTableText({ columns, rows, locale }, ctx);
270
- if (data.weights.length === 0)
271
- return table;
272
- // 实际生效的权重表 —— 成绩单可审计
273
- const weights = data.weights.map((w) => `${w.prefix} ×${w.weight}`).join(" · ");
274
- return `${table}\n${localeText(locale, "scoreboard.weights")} ${weights} · ${localeText(locale, "scoreboard.othersWeight")}`;
282
+ const table = renderTableText({ columns: columns, rows, locale }, ctx);
283
+ const footnotes = [];
284
+ if (data.weights.length > 0) {
285
+ const weights = data.weights.map((w) => `${w.prefix} ×${w.weight}`).join(" · ");
286
+ footnotes.push(`${localeText(locale, "scoreboard.weights")} ${weights} · ${localeText(locale, "scoreboard.othersWeight")}`);
287
+ }
288
+ if (data.ignoredEvals > 0)
289
+ footnotes.push(countText(locale, "scoreboard.ignored", data.ignoredEvals));
290
+ return footnotes.length > 0 ? `${table}\n${footnotes.join("\n")}` : table;
275
291
  }
276
292
  // ───────────────────────── MetricScatter ─────────────────────────
277
293
  const POINT_MARKS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
278
294
  function axisLabel(col, locale) {
279
295
  return resolveMetricLabel(col.label, locale, col.key);
280
296
  }
297
+ /**
298
+ * 方向提示:轴向已随 better 反正(lower 反向),「更好」恒指向右上,文案恒为
299
+ * 「越靠右上越好」;仅当 x、y 都声明 better 时显示——任一轴未声明,组件不猜
300
+ * 「更好」朝哪边,整图无提示(返回 undefined)。
301
+ */
302
+ function betterCornerText(x, y, locale) {
303
+ if (x.better === undefined || y.better === undefined)
304
+ return undefined;
305
+ return localeText(locale, "scatter.betterUpperRight");
306
+ }
307
+ /** 轴标题行:"<x label>(越低越好) × <y label>"——better 方向如实标注,不虚构轴向。 */
308
+ function scatterHeading(data, locale) {
309
+ const withBetter = (col) => {
310
+ const label = axisLabel(col, locale);
311
+ if (col.better === undefined)
312
+ return label;
313
+ return `${label}(${localeText(locale, col.better === "lower" ? "table.lowerBetter" : "table.higherBetter")})`;
314
+ };
315
+ return `${withBetter(data.x)} × ${withBetter(data.y)}`;
316
+ }
281
317
  export function scatterText(data, ctx) {
282
318
  const locale = ctx.locale;
283
319
  const drawable = data.rows.filter((r) => r.x.value !== null && r.y.value !== null);
@@ -292,8 +328,12 @@ export function scatterText(data, ctx) {
292
328
  }
293
329
  // 点太密排不下时降级为坐标表,不硬挤
294
330
  if (drawable.length > POINT_MARKS.length || ctx.width < 44) {
295
- const table = renderCoordinateTable(drawable.map((r) => ({ key: r.key, x: r.x.display, y: r.y.display })), { key: data.points, x: axisLabel(data.x, locale), y: axisLabel(data.y, locale) });
296
- return [table, ...footnotes].join("\n");
331
+ const table = renderCoordinateTable(drawable.map((r) => ({
332
+ key: r.key,
333
+ x: resolveLocalizedText(r.x.display, locale),
334
+ y: resolveLocalizedText(r.y.display, locale),
335
+ })), { key: data.pointDimension, x: axes.x, y: axes.y });
336
+ return [scatterHeading(data, locale), "", table, ...footnotes].join("\n");
297
337
  }
298
338
  const points = drawable.map((r, i) => ({
299
339
  mark: POINT_MARKS[i],
@@ -311,20 +351,21 @@ export function scatterText(data, ctx) {
311
351
  }
312
352
  for (const list of bySeries.values())
313
353
  list.sort((a, b) => a.x - b.x);
314
- const invertX = data.x.better === "lower";
315
354
  const plot = renderCharPlot({
316
355
  width: ctx.width,
317
356
  points,
318
357
  lines: [...bySeries.values()].filter((l) => l.length > 1),
319
- xLabel: `${axisLabel(data.x, locale)}${invertX ? ` ${localeText(locale, "scatter.axisReversed")}` : ""}`,
320
- yLabel: axisLabel(data.y, locale),
358
+ xLabel: axes.x,
359
+ yLabel: axes.y,
321
360
  formatX: (v) => formatMetricValue(v, data.x.unit),
322
361
  formatY: (v) => formatMetricValue(v, data.y.unit),
323
- invertX,
362
+ // 轴方向跟随 better(与 web 面同规则):lower 反向,higher 与未声明正向。
363
+ invertX: data.x.better === "lower",
324
364
  invertY: data.y.better === "lower",
325
365
  });
326
366
  const legend = drawable.map((r, i) => `${POINT_MARKS[i]} ${r.key}`).join(" ");
327
- return [plot, "", localeText(locale, "scatter.betterUpperRight"), legend, ...footnotes].join("\n");
367
+ const hint = betterCornerText(data.x, data.y, locale);
368
+ return [scatterHeading(data, locale), plot, "", ...(hint !== undefined ? [hint] : []), legend, ...footnotes].join("\n");
328
369
  }
329
370
  // ───────────────────────── MetricLine ─────────────────────────
330
371
  export function lineText(data, ctx) {
@@ -336,6 +377,7 @@ export function lineText(data, ctx) {
336
377
  footnotes.push(countText(locale, "pointsMissing", missing));
337
378
  if (drawable.length === 0)
338
379
  return [missingText(locale), ...footnotes].join("\n");
380
+ const xLabel = resolveMetricLabel(data.x.label, locale, data.x.key);
339
381
  // 系列 → 字母;无系列 = 单系列
340
382
  const seriesKeys = [];
341
383
  for (const r of drawable) {
@@ -344,7 +386,11 @@ export function lineText(data, ctx) {
344
386
  seriesKeys.push(key);
345
387
  }
346
388
  if (seriesKeys.length > POINT_MARKS.length || ctx.width < 44) {
347
- const table = renderCoordinateTable(drawable.map((r) => ({ key: r.series ? `${r.key} (${r.series})` : r.key, x: r.xDisplay, y: r.y.display })), { key: "experiment", x: data.x.label, y: axisLabel(data.y, locale) });
389
+ const table = renderCoordinateTable(drawable.map((r) => ({
390
+ key: r.series !== undefined ? `${r.series} (${r.key})` : r.key,
391
+ x: resolveLocalizedText(r.xDisplay, locale),
392
+ y: resolveLocalizedText(r.y.display, locale),
393
+ })), { key: data.seriesDimension ?? xLabel, x: xLabel, y: axisLabel(data.y, locale) });
348
394
  return [table, ...footnotes].join("\n");
349
395
  }
350
396
  const markOf = (r) => POINT_MARKS[seriesKeys.indexOf(r.series ?? "")];
@@ -361,11 +407,10 @@ export function lineText(data, ctx) {
361
407
  width: ctx.width,
362
408
  points,
363
409
  lines: lines.filter((l) => l.length > 1),
364
- xLabel: data.x.label,
410
+ xLabel,
365
411
  yLabel: axisLabel(data.y, locale),
366
412
  formatX: (v) => formatMetricValue(v, data.x.unit),
367
413
  formatY: (v) => formatMetricValue(v, data.y.unit),
368
- invertY: data.y.better === "lower",
369
414
  });
370
415
  const legend = seriesKeys
371
416
  .map((key, i) => `${POINT_MARKS[i]} ${key === "" ? axisLabel(data.y, locale) : key}`)
@@ -374,37 +419,53 @@ export function lineText(data, ctx) {
374
419
  }
375
420
  // ───────────────────────── DeltaTable ─────────────────────────
376
421
  export function deltaText(data, ctx) {
422
+ const locale = ctx.locale;
423
+ // 0 对不是错误:明确空态并报告配对域实验数(派生形态携带;字面形态缺省按 0)。
424
+ if (data.rows.length === 0) {
425
+ return localeText(locale, "delta.empty", { experiments: data.experiments ?? 0 });
426
+ }
377
427
  const columns = [
378
- { key: "pair", header: "pair" },
379
- ...data.columns.map((c, i) => ({ key: `metric${i}`, header: resolveMetricLabel(c.label, ctx.locale, c.key) })),
428
+ { key: "pair", header: localeText(locale, "delta.pairHeader") },
429
+ ...data.columns.map((c, i) => ({ key: `metric${i}`, header: resolveMetricLabel(c.label, locale, c.key) })),
380
430
  ];
381
431
  const rows = data.rows.map((row) => {
382
- const cells = { pair: row.key };
432
+ const cells = { pair: resolveLocalizedText(row.label, locale) };
383
433
  data.columns.forEach((col, i) => {
384
434
  const cell = row.cells[col.key];
385
435
  if (!cell) {
386
436
  cells[`metric${i}`] = null;
387
437
  return;
388
438
  }
389
- const a = cell.a.value === null ? MISSING_MARK : cell.a.display;
390
- const b = cell.b.value === null ? MISSING_MARK : cell.b.display;
391
- cells[`metric${i}`] = `${a} → ${b} ${cell.display}`;
439
+ const a = cell.a.value === null ? MISSING_MARK : resolveLocalizedText(cell.a.display, locale);
440
+ const b = cell.b.value === null ? MISSING_MARK : resolveLocalizedText(cell.b.display, locale);
441
+ cells[`metric${i}`] = `${a} → ${b} ${resolveLocalizedText(cell.display, locale)}`;
392
442
  });
393
443
  return { key: row.key, cells };
394
444
  });
395
- return renderTableText({ columns, rows, locale: ctx.locale }, ctx);
445
+ return renderTableText({ columns: columns, rows, locale }, ctx);
396
446
  }
397
447
  // ───────────────────────── 实体列表(ExperimentList / EvalList / AttemptList)─────────────────────────
398
448
  //
399
449
  // 三面共用的紧凑标记:`locator✓`(判定符紧跟 locator,中间不留空格)。
400
- // ExperimentList / EvalList 逐 attempt 只列这一个标记 + 各自的原因/耗时摘要,不重复整段
401
- // niceeval show 命令;要看某个 attempt 的完整证据,agent 自己拼 `niceeval show <locator>`——
402
- // 命令模板只在 AttemptList(叶子层)展示完整断言明细时才值得,不在中间层重复。
450
+ // ExperimentList / EvalList 逐 attempt 只列这一个标记 + 各自的摘要,不重复整段
451
+ // niceeval show 命令;要看某个 attempt 的完整证据,agent 自己拼 `niceeval show <locator>`。
403
452
  function locatorBadge(item) {
404
453
  return `${item.locator}${verdictMark(item.verdict)}`;
405
454
  }
455
+ /**
456
+ * failureSummary + moreFailures 的展示形态:摘要在计算侧已按 Scoring display 契约折好,
457
+ * 这里只加 "+N more failures" 计数与宽度收口,不重算摘要。
458
+ */
459
+ function attemptReasonText(item, locale, maxChars) {
460
+ if (item.failureSummary === null)
461
+ return undefined;
462
+ const withMore = item.moreFailures > 0
463
+ ? `${item.failureSummary} · ${countText(locale, "entityList.moreFailures", item.moreFailures)}`
464
+ : item.failureSummary;
465
+ return fitFailureSummary(withMore, Math.max(24, maxChars));
466
+ }
406
467
  // ── ExperimentList ──
407
- function experimentSummaryTable(items, ctx) {
468
+ function experimentSummaryTable(items, ctx, relativeTo) {
408
469
  const locale = ctx.locale;
409
470
  const compact = ctx.width < 100;
410
471
  const columns = [
@@ -415,42 +476,50 @@ function experimentSummaryTable(items, ctx) {
415
476
  { key: "passRate", header: compact && locale === "en" ? "Pass" : localeText(locale, "experimentList.passRate"), align: "right" },
416
477
  { key: "result", header: localeText(locale, "experimentList.result") },
417
478
  { key: "tokens", header: localeText(locale, "experimentList.tokens"), align: "right" },
418
- { key: "cost", header: compact && locale === "en" ? "Cost" : localeText(locale, "experimentList.estimatedCost"), align: "right" },
479
+ { key: "cost", header: localeText(locale, "experimentList.cost"), align: "right" },
419
480
  ];
420
481
  const rows = items.map((item) => ({
421
482
  key: item.experimentId,
422
483
  cells: {
423
- experiment: item.experimentId,
484
+ experiment: experimentDisplayName(item.experimentId, relativeTo),
424
485
  model: item.model ?? localeText(locale, "experimentList.defaultModel"),
425
486
  agent: item.agent,
426
- duration: cellText(item.duration),
427
- passRate: cellText(item.passRate),
428
- result: verdictTallyText(item.verdicts, locale),
429
- tokens: cellText(item.tokens),
430
- cost: cellText(item.cost),
487
+ duration: cellText(item.durationMs, locale),
488
+ passRate: cellText(item.endToEndPassRate, locale),
489
+ result: verdictTallyText(item.evalVerdicts, locale),
490
+ tokens: cellText(item.tokens, locale),
491
+ cost: cellText(item.costUSD, locale),
431
492
  },
432
493
  }));
433
- const metadata = items.flatMap((item) => wrapDisplay(`${item.experimentId}: ${localeText(locale, "overview.evalsCount", { n: item.evals })} · ${localeText(locale, "overview.attemptsCount", { n: item.attempts })} · ${item.lastRunAt}`, Math.max(8, ctx.width - 2)).map((line) => ` ${line}`));
434
- return [renderTableText({ columns, rows, locale }, ctx), metadata.join("\n")].join("\n");
494
+ const metadata = items.flatMap((item) => wrapDisplay(`${experimentDisplayName(item.experimentId, relativeTo)}: ${localeText(locale, "overview.evalsCount", { n: item.evals })} · ${localeText(locale, "overview.attemptsCount", { n: item.attempts })} · ${item.lastRunAt}`, Math.max(8, ctx.width - 2)).map((line) => ` ${line}`));
495
+ return [renderTableText({ columns: columns, rows, locale }, ctx), metadata.join("\n")].join("\n");
435
496
  }
436
- function experimentDetailTable(item, ctx) {
497
+ function experimentDetailTable(item, ctx, relativeTo) {
437
498
  const locale = ctx.locale;
438
499
  const columns = [
439
500
  { key: "status", header: localeText(locale, "experimentList.status") },
440
501
  { key: "entity", header: localeText(locale, "experimentList.evalAttempt") },
441
- { key: "result", header: localeText(locale, "experimentList.result") },
502
+ // Result 是可扫读的失败预览,不是证据面:两行放不下的以 收口,完整值走 locator 下钻。
503
+ { key: "result", header: localeText(locale, "experimentList.result"), maxLines: 2 },
442
504
  { key: "duration", header: localeText(locale, "experimentList.duration"), align: "right" },
443
505
  { key: "cost", header: localeText(locale, "experimentList.cost"), align: "right" },
444
506
  ];
507
+ // Result 的字符预算 ≈ 两行 × 它能分到的列宽(总宽减其它列的自然宽与列距)。这里只做
508
+ // 粗预算;精确的按宽度收口由列的 maxLines 兜底。
509
+ const statusWidth = Math.max(stringWidth(localeText(locale, "experimentList.status")), ...item.evalRows.map((row) => stringWidth(`${verdictMark(row.verdict)} ${localeText(locale, `verdict.${row.verdict}`)}`)));
510
+ const entityWidth = Math.max(stringWidth(localeText(locale, "experimentList.evalAttempt")), ...item.evalRows.flatMap((row) => [stringWidth(row.evalId), ...row.attempts.map((a) => stringWidth(a.locator) + 3)]));
511
+ const fixedWidth = statusWidth + entityWidth + 8 /* duration */ + 6 /* cost */ + 3 * 4; /* 4 段列距 */
512
+ const resultBudget = Math.max(24, (ctx.width - fixedWidth) * 2);
445
513
  const rows = item.evalRows.flatMap((row) => {
514
+ // Eval 父行只承载折叠判定与题级聚合;失败摘要只在 Attempt 子行出现。
446
515
  const parent = {
447
516
  key: row.evalId,
448
517
  cells: {
449
518
  status: `${verdictMark(row.verdict)} ${localeText(locale, `verdict.${row.verdict}`)}`,
450
519
  entity: row.evalId,
451
520
  result: "",
452
- duration: "",
453
- cost: "",
521
+ duration: localeText(locale, "entityList.average", { value: cellText(row.durationMs, locale) }),
522
+ cost: localeText(locale, "entityList.average", { value: cellText(row.costUSD, locale) }),
454
523
  },
455
524
  };
456
525
  const attempts = row.attempts.map((attempt, index) => ({
@@ -458,26 +527,38 @@ function experimentDetailTable(item, ctx) {
458
527
  cells: {
459
528
  status: ` ${verdictMark(attempt.verdict)}`,
460
529
  entity: `${index === row.attempts.length - 1 ? "└─" : "├─"} ${attempt.locator}`,
461
- result: attemptItemReason(attempt) ?? MISSING_MARK,
530
+ result: attemptReasonText(attempt, locale, resultBudget) ?? MISSING_MARK,
462
531
  duration: attempt.verdict === "skipped" && attempt.durationMs === 0 ? null : formatDurationMs(attempt.durationMs),
463
- cost: attempt.costUSD === undefined ? null : formatUSD(attempt.costUSD),
532
+ cost: attempt.costUSD === null ? null : formatUSD(attempt.costUSD),
464
533
  },
465
534
  }));
466
535
  return [parent, ...attempts];
467
536
  });
468
537
  const flags = item.flags && Object.keys(item.flags).length > 0
469
- ? `${localeText(locale, "experimentList.flags")} ${Object.entries(item.flags).map(([key, value]) => `${key}=${String(value)}`).join(" · ")}`
538
+ ? `${localeText(locale, "experimentList.flags")} ${Object.entries(item.flags)
539
+ .map(([key, value]) => `${key}=${typeof value === "string" ? value : JSON.stringify(value)}`)
540
+ .join(" · ")}`
470
541
  : undefined;
471
- return [item.experimentId, flags, renderTableText({ columns, rows, locale }, ctx)].filter(Boolean).join("\n");
542
+ return [
543
+ experimentDisplayName(item.experimentId, relativeTo),
544
+ flags,
545
+ renderTableText({ columns: columns, rows, locale }, ctx),
546
+ ]
547
+ .filter(Boolean)
548
+ .join("\n");
472
549
  }
473
- export function experimentListText(items, ctx) {
550
+ export function experimentListText(items, ctx, relativeTo) {
474
551
  if (items.length === 0)
475
552
  return localeText(ctx.locale, "attemptList.empty");
476
- return [experimentSummaryTable(items, ctx), ...items.map((item) => experimentDetailTable(item, ctx))].join("\n\n");
553
+ return [
554
+ experimentSummaryTable(items, ctx, relativeTo),
555
+ ...items.map((item) => experimentDetailTable(item, ctx, relativeTo)),
556
+ ].join("\n\n");
477
557
  }
478
558
  // ── EvalList ──
479
- function evalListAttemptLine(item) {
480
- const reason = attemptItemReason(item);
559
+ function evalListAttemptLine(item, ctx) {
560
+ // 行式列表同守「Result 最多两行」:预算 = 两行终端宽,超出按尾截收口。
561
+ const reason = attemptReasonText(item, ctx.locale, ctx.width * 2 - stringWidth(locatorBadge(item)) - 6);
481
562
  return ` ${locatorBadge(item)}${reason ? ` · ${reason}` : ""}`;
482
563
  }
483
564
  export function evalListText(items, ctx) {
@@ -487,53 +568,41 @@ export function evalListText(items, ctx) {
487
568
  const blocks = items.map((item) => {
488
569
  const identity = `${item.evalId} · ${item.experimentId} · ${localeText(locale, `verdict.${item.verdict}`)}`;
489
570
  const summary = [
490
- localeText(locale, "attemptList.score", { score: cellText(item.score) }),
571
+ localeText(locale, "attemptList.score", { score: cellText(item.examScore, locale) }),
491
572
  localeText(locale, "overview.attemptsCount", { n: item.attempts.length }),
492
- `${formatDurationMs(item.duration.value ?? 0)} avg`,
493
- item.cost.value === null ? `${missingText(locale)} avg` : `${formatUSD(item.cost.value)} avg`,
573
+ localeText(locale, "entityList.average", {
574
+ value: item.durationMs.value === null ? missingText(locale) : formatDurationMs(item.durationMs.value),
575
+ }),
576
+ localeText(locale, "entityList.average", {
577
+ value: item.costUSD.value === null ? missingText(locale) : formatUSD(item.costUSD.value),
578
+ }),
494
579
  ].join(" · ");
495
- const attemptLines = item.attempts.map(evalListAttemptLine);
580
+ const attemptLines = item.attempts.map((attempt) => evalListAttemptLine(attempt, ctx));
496
581
  return [identity, ` ${summary}`, ...attemptLines].join("\n");
497
582
  });
498
583
  return blocks.join("\n\n");
499
584
  }
500
585
  // ── AttemptList ──
501
- /** 一个 AttemptListItem 的完整 text 卡片:判定符 + locator + 身份 + 耗时/成本,
502
- * 然后逐条断言(gate 与 soft 都列,与 web 面的 AttemptRow 同一份材料) */
503
- function attemptListItemText(item, ctx, locale) {
586
+ /** Attempt 比较卡片:只显示一条主失败摘要(至多两行终端宽);完整 assertions locator 下钻。 */
587
+ function attemptListItemText(item, ctx) {
504
588
  const head = [
505
589
  `${verdictMark(item.verdict)} ${item.locator}`,
506
590
  item.evalId,
507
591
  item.experimentId,
508
592
  formatDurationMs(item.durationMs),
509
- ...(item.costUSD !== undefined ? [formatUSD(item.costUSD)] : []),
593
+ ...(item.costUSD !== null ? [formatUSD(item.costUSD)] : []),
510
594
  ].join(" · ");
511
595
  const lines = [head];
512
- if (item.error) {
513
- lines.push(indentBlock(wrapDisplay(item.error, ctx.width - 4).join("\n"), " "));
514
- }
515
- for (const assertion of item.assertions) {
516
- const scoreText = assertion.threshold !== undefined
517
- ? `${formatPlainNumber(assertion.score)}/${formatPlainNumber(assertion.threshold)}`
518
- : formatPlainNumber(assertion.score);
519
- lines.push(` ${assertion.severity} ${assertion.name} · ${localeText(locale, `verdict.${assertion.passed ? "passed" : "failed"}`)}${assertion.severity === "soft" ? ` ${scoreText}` : ""}`);
520
- if (assertion.detail)
521
- lines.push(indentBlock(wrapDisplay(assertion.detail, ctx.width - 4).join("\n"), " "));
522
- if (assertion.evidence) {
523
- const limit = Math.max(240, ctx.width * 5);
524
- const evidence = assertion.evidence.length <= limit
525
- ? assertion.evidence
526
- : `${assertion.evidence.slice(0, limit)}… (${assertion.evidence.length - limit} more chars; open ${item.locator} for full evidence)`;
527
- lines.push(indentBlock(wrapDisplay(evidence, ctx.width - 6).join("\n"), " "));
528
- }
529
- }
596
+ const reason = attemptReasonText(item, ctx.locale, ctx.width * 2 - 4);
597
+ if (reason)
598
+ lines.push(` ${reason}`);
530
599
  return lines.join("\n");
531
600
  }
532
601
  export function attemptListText(items, total, ctx) {
533
602
  const locale = ctx.locale;
534
603
  if (items.length === 0)
535
604
  return localeText(locale, "attemptList.empty");
536
- const blocks = items.map((item) => attemptListItemText(item, ctx, locale));
605
+ const blocks = items.map((item) => attemptListItemText(item, ctx));
537
606
  const remaining = (total ?? items.length) - items.length;
538
607
  if (remaining > 0)
539
608
  blocks.push(localeText(locale, "attemptList.truncatedText", { n: remaining }));