niceeval 0.5.4 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (337) hide show
  1. package/INDEX.md +55 -0
  2. package/dist/agents/types.d.ts +321 -0
  3. package/dist/context/types.d.ts +335 -0
  4. package/dist/i18n/core.d.ts +11 -0
  5. package/dist/i18n/en.d.ts +181 -0
  6. package/dist/i18n/index.d.ts +5 -0
  7. package/dist/i18n/zh-CN.d.ts +183 -0
  8. package/dist/o11y/otlp/parse.d.ts +3 -0
  9. package/dist/o11y/otlp/receiver.d.ts +21 -0
  10. package/dist/o11y/otlp/turn-otel.d.ts +43 -0
  11. package/dist/o11y/types.d.ts +190 -0
  12. package/dist/report/aggregate.d.ts +57 -0
  13. package/dist/report/aggregate.js +212 -0
  14. package/dist/report/built-ins/experiment-comparison.d.ts +1 -0
  15. package/dist/report/built-ins/experiment-comparison.js +13 -0
  16. package/dist/report/built-ins/index.d.ts +1 -0
  17. package/dist/report/built-ins/index.js +2 -0
  18. package/dist/report/components.d.ts +180 -0
  19. package/dist/report/components.js +132 -0
  20. package/dist/report/compute.d.ts +106 -0
  21. package/dist/report/compute.js +600 -0
  22. package/dist/report/flag.d.ts +6 -0
  23. package/dist/report/flag.js +8 -0
  24. package/dist/report/format.d.ts +26 -0
  25. package/dist/report/format.js +109 -0
  26. package/dist/report/index.d.ts +19 -0
  27. package/dist/report/index.js +30 -0
  28. package/dist/report/load.d.ts +12 -0
  29. package/dist/report/load.js +56 -0
  30. package/dist/report/locale.d.ts +94 -0
  31. package/dist/report/locale.js +183 -0
  32. package/dist/report/metrics.d.ts +21 -0
  33. package/dist/report/metrics.js +115 -0
  34. package/dist/report/primitives.d.ts +64 -0
  35. package/dist/report/primitives.js +113 -0
  36. package/dist/report/react/AttemptList.d.ts +23 -0
  37. package/dist/report/react/AttemptList.js +19 -0
  38. package/dist/report/react/DeltaTable.d.ts +8 -0
  39. package/dist/report/react/DeltaTable.js +24 -0
  40. package/dist/report/react/EvalList.d.ts +10 -0
  41. package/dist/report/react/EvalList.js +0 -0
  42. package/dist/report/react/ExperimentList.d.ts +11 -0
  43. package/dist/report/react/ExperimentList.js +67 -0
  44. package/dist/report/react/GroupSummary.d.ts +8 -0
  45. package/dist/report/react/GroupSummary.js +8 -0
  46. package/dist/report/react/MetricBars.d.ts +10 -0
  47. package/dist/report/react/MetricBars.js +48 -0
  48. package/dist/report/react/MetricLine.d.ts +9 -0
  49. package/dist/report/react/MetricLine.js +68 -0
  50. package/dist/report/react/MetricMatrix.d.ts +10 -0
  51. package/dist/report/react/MetricMatrix.js +23 -0
  52. package/dist/report/react/MetricScatter.d.ts +9 -0
  53. package/dist/report/react/MetricScatter.js +96 -0
  54. package/dist/report/react/MetricTable.d.ts +11 -0
  55. package/dist/report/react/MetricTable.js +26 -0
  56. package/dist/report/react/RunOverview.d.ts +8 -0
  57. package/dist/report/react/RunOverview.js +12 -0
  58. package/dist/report/react/Scoreboard.d.ts +8 -0
  59. package/dist/report/react/Scoreboard.js +26 -0
  60. package/dist/report/react/cell.d.ts +9 -0
  61. package/dist/report/react/cell.js +9 -0
  62. package/dist/report/react/chart-math.d.ts +14 -0
  63. package/dist/report/react/chart-math.js +68 -0
  64. package/dist/report/react/colors.d.ts +14 -0
  65. package/dist/report/react/colors.js +45 -0
  66. package/dist/report/react/fixtures.d.ts +19 -0
  67. package/dist/report/react/fixtures.js +398 -0
  68. package/dist/report/react/format.d.ts +3 -0
  69. package/dist/report/react/format.js +7 -0
  70. package/dist/report/react/index.d.ts +6 -0
  71. package/dist/report/react/index.js +14 -0
  72. package/dist/report/report.d.ts +24 -0
  73. package/dist/report/report.js +49 -0
  74. package/dist/report/text/faces.d.ts +29 -0
  75. package/dist/report/text/faces.js +541 -0
  76. package/dist/report/text/layout.d.ts +75 -0
  77. package/dist/report/text/layout.js +173 -0
  78. package/dist/report/text/plot.d.ts +37 -0
  79. package/dist/report/text/plot.js +81 -0
  80. package/dist/report/text/table.d.ts +8 -0
  81. package/dist/report/text/table.js +86 -0
  82. package/dist/report/tree.d.ts +99 -0
  83. package/dist/report/tree.js +228 -0
  84. package/dist/report/types.d.ts +408 -0
  85. package/dist/report/types.js +4 -0
  86. package/dist/report/web.d.ts +15 -0
  87. package/dist/report/web.js +37 -0
  88. package/dist/results/locator.d.ts +75 -0
  89. package/dist/results/locator.js +0 -0
  90. package/dist/results/select.d.ts +45 -0
  91. package/dist/results/select.js +230 -0
  92. package/dist/results/source-hash.d.ts +9 -0
  93. package/dist/results/types.d.ts +204 -0
  94. package/dist/runner/eval-source.d.ts +22 -0
  95. package/dist/runner/fingerprint.d.ts +23 -0
  96. package/dist/runner/types.d.ts +332 -0
  97. package/dist/sandbox/docker-stream.d.ts +24 -0
  98. package/dist/sandbox/docker.d.ts +84 -0
  99. package/dist/sandbox/e2b.d.ts +31 -0
  100. package/dist/sandbox/errors.d.ts +4 -0
  101. package/dist/sandbox/local-files.d.ts +6 -0
  102. package/dist/sandbox/paths.d.ts +4 -0
  103. package/dist/sandbox/registry.d.ts +13 -0
  104. package/dist/sandbox/resolve.d.ts +40 -0
  105. package/dist/sandbox/retry.d.ts +10 -0
  106. package/dist/sandbox/shell.d.ts +11 -0
  107. package/dist/sandbox/source-files.d.ts +13 -0
  108. package/dist/sandbox/types.d.ts +203 -0
  109. package/dist/sandbox/vercel.d.ts +37 -0
  110. package/dist/scoring/types.d.ts +70 -0
  111. package/dist/shared/aggregate.d.ts +29 -0
  112. package/dist/shared/aggregate.js +55 -0
  113. package/dist/shared/types.d.ts +27 -0
  114. package/dist/shared/verdict.d.ts +27 -0
  115. package/dist/shared/verdict.js +34 -0
  116. package/dist/tty-line.d.ts +6 -0
  117. package/dist/types.d.ts +7 -0
  118. package/dist/util.d.ts +22 -0
  119. package/docs-site/zh/concepts/adapter.mdx +7 -7
  120. package/docs-site/zh/concepts/assert.mdx +19 -18
  121. package/docs-site/zh/concepts/evals.mdx +12 -11
  122. package/docs-site/zh/concepts/experiment.mdx +2 -2
  123. package/docs-site/zh/concepts/judge.mdx +9 -9
  124. package/docs-site/zh/concepts/overview.mdx +3 -3
  125. package/docs-site/zh/concepts/tier.mdx +6 -6
  126. package/docs-site/zh/example/claude-code-codex-plugin.mdx +9 -5
  127. package/docs-site/zh/example/claude-code-codex-skill.mdx +12 -7
  128. package/docs-site/zh/guides/agent-feedback-loop.mdx +237 -78
  129. package/docs-site/zh/guides/connect-otel.mdx +3 -3
  130. package/docs-site/zh/guides/connect-your-agent.mdx +2 -2
  131. package/docs-site/zh/guides/custom-reports.mdx +196 -36
  132. package/docs-site/zh/guides/experiments.mdx +7 -1
  133. package/docs-site/zh/guides/fixtures.mdx +4 -2
  134. package/docs-site/zh/guides/official-adapters.mdx +31 -9
  135. package/docs-site/zh/guides/publish-report.mdx +4 -4
  136. package/docs-site/zh/guides/report-components.mdx +194 -55
  137. package/docs-site/zh/guides/reporters.mdx +2 -2
  138. package/docs-site/zh/guides/results-data.mdx +61 -34
  139. package/docs-site/zh/guides/runner.mdx +2 -2
  140. package/docs-site/zh/guides/sandbox-agent.mdx +3 -2
  141. package/docs-site/zh/guides/{sandbox-backends.mdx → sandbox-providers.mdx} +12 -12
  142. package/docs-site/zh/guides/viewing-results.mdx +200 -79
  143. package/docs-site/zh/guides/write-experiment.mdx +9 -1
  144. package/docs-site/zh/guides/write-send.mdx +4 -3
  145. package/docs-site/zh/index.mdx +4 -3
  146. package/docs-site/zh/introduction.mdx +2 -1
  147. package/docs-site/zh/quickstart.mdx +5 -3
  148. package/docs-site/zh/reference/builtin-agents.mdx +38 -13
  149. package/docs-site/zh/reference/capabilities.mdx +1 -1
  150. package/docs-site/zh/reference/cli.mdx +36 -13
  151. package/docs-site/zh/reference/define-agent.mdx +15 -13
  152. package/docs-site/zh/reference/define-config.mdx +1 -1
  153. package/docs-site/zh/reference/define-eval.mdx +3 -3
  154. package/docs-site/zh/reference/events.mdx +9 -3
  155. package/docs-site/zh/reference/expect.mdx +13 -8
  156. package/package.json +10 -6
  157. package/src/agents/bub.ts +129 -42
  158. package/src/agents/claude-code.test.ts +152 -0
  159. package/src/agents/claude-code.ts +127 -13
  160. package/src/agents/codex.test.ts +192 -0
  161. package/src/agents/codex.ts +151 -12
  162. package/src/agents/index.ts +9 -3
  163. package/src/agents/manifest.ts +41 -0
  164. package/src/agents/plugin-config.test.ts +94 -0
  165. package/src/agents/shared.ts +33 -2
  166. package/src/agents/skills.test.ts +214 -0
  167. package/src/agents/skills.ts +304 -0
  168. package/src/agents/streaming.ts +5 -5
  169. package/src/agents/types.ts +59 -3
  170. package/src/cli.ts +74 -47
  171. package/src/context/context.test.ts +20 -2
  172. package/src/context/control-flow.ts +1 -1
  173. package/src/context/session.test.ts +92 -1
  174. package/src/context/session.ts +23 -1
  175. package/src/context/types.ts +4 -4
  176. package/src/define.test.ts +8 -8
  177. package/src/define.ts +11 -11
  178. package/src/expect/index.ts +5 -5
  179. package/src/i18n/en.ts +33 -15
  180. package/src/i18n/zh-CN.ts +37 -21
  181. package/src/index.ts +1 -1
  182. package/src/o11y/execution-tree.test.ts +452 -0
  183. package/src/o11y/execution-tree.ts +367 -0
  184. package/src/o11y/otlp/receiver.ts +1 -1
  185. package/src/o11y/otlp/select.ts +7 -3
  186. package/src/o11y/parsers/claude-code.test.ts +175 -0
  187. package/src/o11y/parsers/claude-code.ts +32 -0
  188. package/src/o11y/types.ts +6 -1
  189. package/src/report/aggregate.ts +26 -20
  190. package/src/report/built-in-user-parity.test.tsx +640 -0
  191. package/src/report/built-ins/experiment-comparison.tsx +19 -0
  192. package/src/report/built-ins/index.ts +2 -0
  193. package/src/report/components.tsx +225 -50
  194. package/src/report/compute.ts +335 -54
  195. package/src/report/dual-render.test.tsx +1129 -0
  196. package/src/report/flag.ts +1 -1
  197. package/src/report/format.ts +35 -2
  198. package/src/report/index.ts +47 -14
  199. package/src/report/load.ts +1 -1
  200. package/src/report/locale.ts +229 -0
  201. package/src/report/metrics.ts +39 -15
  202. package/src/report/primitives.tsx +100 -2
  203. package/src/report/react/AttemptList.tsx +113 -0
  204. package/src/report/react/DeltaTable.tsx +9 -6
  205. package/src/report/react/EvalList.tsx +0 -0
  206. package/src/report/react/ExperimentList.tsx +220 -0
  207. package/src/report/react/GroupSummary.tsx +66 -0
  208. package/src/report/react/MetricBars.tsx +24 -13
  209. package/src/report/react/MetricLine.tsx +21 -21
  210. package/src/report/react/MetricMatrix.tsx +8 -4
  211. package/src/report/react/MetricScatter.tsx +126 -96
  212. package/src/report/react/MetricTable.tsx +115 -15
  213. package/src/report/react/RunOverview.tsx +48 -26
  214. package/src/report/react/Scoreboard.tsx +15 -7
  215. package/src/report/react/cell.tsx +18 -9
  216. package/src/{view/app/lib/chart.ts → report/react/chart-math.ts} +2 -6
  217. package/src/report/react/colors.ts +24 -15
  218. package/src/report/react/enhance.js +168 -0
  219. package/src/report/react/fixtures.ts +169 -34
  220. package/src/report/react/format.ts +1 -1
  221. package/src/report/react/index.tsx +21 -6
  222. package/src/report/react/render.test.tsx +180 -31
  223. package/src/report/react/styles.css +432 -213
  224. package/src/report/report.test.ts +398 -94
  225. package/src/report/report.ts +29 -11
  226. package/src/report/text/faces.ts +344 -96
  227. package/src/report/text/layout.ts +76 -11
  228. package/src/report/text/table.ts +98 -0
  229. package/src/report/tree.ts +103 -20
  230. package/src/report/types.ts +184 -25
  231. package/src/report/web.ts +46 -19
  232. package/src/results/annotated-source.test.ts +146 -0
  233. package/src/results/annotated-source.ts +128 -0
  234. package/src/results/attempt-evidence.test.ts +158 -0
  235. package/src/results/attempt-evidence.ts +158 -0
  236. package/src/results/attempt-source.ts +48 -0
  237. package/src/results/copy.ts +154 -139
  238. package/src/results/format.ts +54 -47
  239. package/src/results/host-equivalence.test.ts +527 -0
  240. package/src/results/index.ts +56 -14
  241. package/src/results/locator.test.ts +198 -0
  242. package/src/results/locator.ts +0 -0
  243. package/src/results/open.ts +392 -175
  244. package/src/results/results.test.ts +943 -494
  245. package/src/results/select.ts +129 -23
  246. package/src/results/skipped-notice.ts +0 -0
  247. package/src/results/source-hash.ts +28 -0
  248. package/src/results/types.ts +98 -62
  249. package/src/results/writer.ts +304 -176
  250. package/src/runner/attempt.test.ts +170 -0
  251. package/src/runner/attempt.ts +48 -13
  252. package/src/runner/discover.test.ts +65 -0
  253. package/src/runner/discover.ts +6 -2
  254. package/src/runner/eval-source.test.ts +121 -0
  255. package/src/runner/eval-source.ts +45 -0
  256. package/src/runner/fingerprint.ts +54 -1
  257. package/src/runner/remote-sandbox.ts +1 -1
  258. package/src/runner/report.test.ts +6 -8
  259. package/src/runner/report.ts +5 -4
  260. package/src/runner/reporters/artifacts.ts +27 -41
  261. package/src/runner/reporters/braintrust.test.ts +3 -3
  262. package/src/runner/reporters/braintrust.ts +1 -1
  263. package/src/runner/reporters/console.ts +6 -6
  264. package/src/runner/reporters/json.ts +4 -4
  265. package/src/runner/reporters/live.test.ts +56 -0
  266. package/src/runner/reporters/live.ts +36 -10
  267. package/src/runner/reporters/quiet.test.ts +66 -0
  268. package/src/runner/reporters/quiet.ts +49 -0
  269. package/src/runner/reporters/shared.ts +5 -5
  270. package/src/runner/reporters/table.ts +17 -17
  271. package/src/runner/run.ts +61 -102
  272. package/src/runner/types.ts +43 -32
  273. package/src/sandbox/checkpoint.ts +1 -1
  274. package/src/sandbox/docker.ts +15 -2
  275. package/src/sandbox/e2b.ts +11 -5
  276. package/src/sandbox/errors.ts +13 -0
  277. package/src/sandbox/index.ts +3 -3
  278. package/src/sandbox/local-files.ts +1 -1
  279. package/src/sandbox/registry.ts +6 -5
  280. package/src/sandbox/resolve.ts +38 -23
  281. package/src/sandbox/retry.test.ts +68 -0
  282. package/src/sandbox/retry.ts +44 -0
  283. package/src/sandbox/types.ts +24 -24
  284. package/src/sandbox/vercel.ts +17 -3
  285. package/src/scoring/collector.ts +3 -3
  286. package/src/scoring/judge.ts +2 -2
  287. package/src/scoring/scoped.ts +13 -1
  288. package/src/scoring/types.ts +7 -7
  289. package/src/scoring/verdict.ts +4 -4
  290. package/src/shared/aggregate.ts +23 -3
  291. package/src/shared/types.ts +2 -2
  292. package/src/shared/{outcome.ts → verdict.ts} +15 -15
  293. package/src/show/compose.ts +12 -132
  294. package/src/show/index.ts +119 -95
  295. package/src/show/render.ts +462 -159
  296. package/src/show/show.test.ts +471 -253
  297. package/src/source-loc.ts +1 -1
  298. package/src/tty-line.ts +32 -0
  299. package/src/types.ts +2 -2
  300. package/src/util.test.ts +21 -1
  301. package/src/util.ts +5 -1
  302. package/src/view/app/App.tsx +33 -198
  303. package/src/view/app/components/AttemptModal.tsx +6 -6
  304. package/src/view/app/components/CodeView.tsx +2 -2
  305. package/src/view/app/components/CopyControls.tsx +32 -36
  306. package/src/view/app/components/SkippedRunsBanner.tsx +140 -0
  307. package/src/view/app/components/ui/badge.tsx +1 -1
  308. package/src/view/app/components/ui/dialog.tsx +2 -2
  309. package/src/view/app/i18n.ts +40 -31
  310. package/src/view/app/lib/artifact-url.ts +1 -1
  311. package/src/view/app/lib/attempt-route.test.ts +37 -47
  312. package/src/view/app/lib/attempt-route.ts +26 -26
  313. package/src/view/app/lib/rows.ts +9 -112
  314. package/src/view/app/lib/verdict.ts +25 -0
  315. package/src/view/app/main.tsx +9 -12
  316. package/src/view/app/pages/RunsPage.tsx +4 -4
  317. package/src/view/app/pages/TracesPage.tsx +3 -3
  318. package/src/view/app/shared.ts +0 -1
  319. package/src/view/app/types.ts +6 -51
  320. package/src/view/artifact-serving.test.ts +136 -0
  321. package/src/view/client-dist/app.css +1 -1
  322. package/src/view/client-dist/app.js +21 -21
  323. package/src/view/data.test.ts +214 -143
  324. package/src/view/data.ts +166 -120
  325. package/src/view/index.ts +23 -11
  326. package/src/view/server.ts +52 -17
  327. package/src/view/shared/types.ts +22 -31
  328. package/src/view/styles.css +117 -327
  329. package/src/view/view-report.test.ts +146 -98
  330. package/src/report/default-report.tsx +0 -222
  331. package/src/report/dual-face.test.tsx +0 -527
  332. package/src/report/react/CaseList.tsx +0 -70
  333. package/src/view/app/components/CostScoreChart.tsx +0 -147
  334. package/src/view/app/components/ExperimentTable.tsx +0 -284
  335. package/src/view/app/components/GroupSelector.tsx +0 -61
  336. package/src/view/app/components/primitives.tsx +0 -43
  337. package/src/view/app/lib/outcome.ts +0 -82
@@ -2,10 +2,15 @@
2
2
  // 输出形态照 docs-site/zh/guides/report-components.mdx 的示例块;与 web 面共守
3
3
  // 诚实契约:排序随 better、samples < total 角标、缺数据 — 不补 0、截断报剩余。
4
4
  // 零 react、零 IO、纯同步 —— 这是 text 宿主不需要 react-dom 的那一半。
5
+ // chrome 文案(注脚、verdict 词、截断提示)经 ctx.locale 查 locale 字典,
6
+ // 默认 en 与历史输出逐字一致;数据(display、键、warnings message)不本地化。
5
7
 
6
8
  import type {
7
- CaseListData,
9
+ AttemptListItem,
8
10
  DeltaData,
11
+ EvalListItem,
12
+ ExperimentListItem,
13
+ GroupSummaryData,
9
14
  LineData,
10
15
  MatrixData,
11
16
  MetricColumn,
@@ -15,12 +20,27 @@ import type {
15
20
  TableData,
16
21
  } from "../types.ts";
17
22
  import type { TextContext } from "../tree.ts";
18
- import { MISSING_TEXT, formatDurationMs, formatMetricValue, formatPlainNumber, formatUSD } from "../format.ts";
19
- import { indentBlock, padDisplay, renderAlignedRows, textBar, wrapDisplay } from "./layout.ts";
23
+ import type { TableColumn, TableRow } from "../primitives.tsx";
24
+ import {
25
+ attemptItemReason,
26
+ formatDurationMs,
27
+ formatMetricValue,
28
+ formatPlainNumber,
29
+ formatUSD,
30
+ verdictMark,
31
+ } from "../format.ts";
32
+ import { countText, localeText, resolveMetricLabel, type ReportLocale } from "../locale.ts";
33
+ import { indentBlock, padDisplay, textBar, wrapDisplay } from "./layout.ts";
34
+ import { renderTableText } from "./table.ts";
20
35
  import { renderCharPlot, renderCoordinateTable, type PlotPoint } from "./plot.ts";
21
36
 
22
37
  const MISSING_MARK = "—";
23
38
 
39
+ /** 缺数据文案随 locale(en = "no data",与 MISSING_TEXT 一致)。 */
40
+ function missingText(locale: ReportLocale): string {
41
+ return localeText(locale, "cell.missing");
42
+ }
43
+
24
44
  /** 格子的文本形态:缺数据 —,覆盖不全带 samples/total 角标。 */
25
45
  export function cellText(cell: { value: number | null; display: string; samples: number; total: number }): string {
26
46
  if (cell.value === null) return MISSING_MARK;
@@ -29,23 +49,27 @@ export function cellText(cell: { value: number | null; display: string; samples:
29
49
 
30
50
  // ───────────────────────── RunOverview ─────────────────────────
31
51
 
32
- export function overviewText(data: OverviewData): string {
52
+ export function overviewText(data: OverviewData, ctx: TextContext): string {
33
53
  const { totals, snapshots } = data;
54
+ const locale = ctx.locale;
34
55
  const runs = new Set(snapshots.map((s) => s.startedAt)).size;
35
56
  const latest = snapshots.map((s) => s.startedAt).sort().at(-1);
36
57
  const head = [
37
- `${snapshots.length} ${snapshots.length === 1 ? "experiment" : "experiments"}`,
38
- `${totals.evals} evals`,
39
- `${totals.attempts} attempts`,
40
- `composed from ${runs} ${runs === 1 ? "run" : "runs"}`,
41
- ...(latest ? [`latest ${latest}`] : []),
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 })] : []),
42
66
  ].join(" · ");
43
67
  const tallies = [
44
- `passed ${totals.passed}`,
45
- `failed ${totals.failed}`,
46
- `errored ${totals.errored}`,
47
- `skipped ${totals.skipped}`,
48
- totals.costUSD === null ? MISSING_TEXT : formatUSD(totals.costUSD),
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),
49
73
  formatDurationMs(totals.durationMs),
50
74
  ].join(" · ");
51
75
  const lines = [head, tallies];
@@ -53,23 +77,93 @@ export function overviewText(data: OverviewData): string {
53
77
  return lines.join("\n");
54
78
  }
55
79
 
80
+ // ───────────────────────── GroupSummary ─────────────────────────
81
+
82
+ /**
83
+ * 一至两行:头行是通过率(GroupSummaryData.passRate.display,不重算比例)+ experiment/eval 数 +
84
+ * failed(+ errored,非零才列,与旧 GroupSelector 卡片一致)+ 总成本;第二行(有则加)是最后运行时间。
85
+ * 不依赖固定网格宽度,窄终端自然换行。
86
+ */
87
+ export function groupSummaryText(data: GroupSummaryData, ctx: TextContext): string {
88
+ const locale = ctx.locale;
89
+ const head = [
90
+ `${localeText(locale, "overview.passRate")} ${cellText(data.passRate)}`,
91
+ countText(locale, "overview.experiments", data.experiments),
92
+ 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),
96
+ ].join(" · ");
97
+ const lines = [head];
98
+ if (data.lastRunAt) lines.push(localeText(locale, "latestRun", { run: data.lastRunAt }));
99
+ return lines.join("\n");
100
+ }
101
+
56
102
  // ───────────────────────── MetricTable ─────────────────────────
57
103
 
58
- export function tableText(data: TableData): string {
59
- const header = [data.dimension, ...data.columns.map((c) => c.label)];
60
- const rows = data.rows.map((row) => [
61
- row.key,
62
- ...data.columns.map((col) => {
104
+ /** verdict 计票的紧凑文案("3 passed / 1 failed"):非零判定逐个列,全部为零如实 —。 */
105
+ export function verdictTallyText(
106
+ verdicts: NonNullable<NonNullable<TableData["rows"][number]["meta"]>["verdicts"]>,
107
+ locale: ReportLocale,
108
+ ): 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;
114
+ }
115
+
116
+ export function tableText(data: TableData, ctx: TextContext): string {
117
+ 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
+ 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 列顶掉。
129
+ ...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
+ ];
132
+ const rows: TableRow[] = data.rows.map((row) => {
133
+ 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
+ data.columns.forEach((col, i) => {
63
137
  const cell = (row.cells as Record<string, TableData["rows"][number]["cells"][string]>)[col.key];
64
- return cell ? cellText(cell) : MISSING_MARK;
65
- }),
66
- ]);
67
- return renderAlignedRows([header, ...rows]);
138
+ cells[`metric${i}`] = cell ? cellText(cell) : null;
139
+ });
140
+ if (hasVerdicts) cells.verdicts = row.meta?.verdicts ? verdictTallyText(row.meta.verdicts, locale) : null;
141
+ return { key: row.key, cells };
142
+ });
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");
68
161
  }
69
162
 
70
163
  // ───────────────────────── MetricMatrix ─────────────────────────
71
164
 
72
- export function matrixText(data: MatrixData): string {
165
+ export function matrixText(data: MatrixData, ctx: TextContext): string {
166
+ // 表体全是维度键与 display,没有 chrome 文案;"next:" 是命令提示,不本地化。
73
167
  const rowKeys: string[] = [];
74
168
  const columnKeys: string[] = [];
75
169
  const byPosition = new Map<string, MatrixData["cells"][number]["cell"]>();
@@ -78,15 +172,19 @@ export function matrixText(data: MatrixData): string {
78
172
  if (!columnKeys.includes(entry.column)) columnKeys.push(entry.column);
79
173
  byPosition.set(JSON.stringify([entry.row, entry.column]), entry.cell);
80
174
  }
81
- const header = [data.rows, ...columnKeys];
82
- const rows = rowKeys.map((row) => [
83
- row,
84
- ...columnKeys.map((column) => {
175
+ const columns: TableColumn[] = [
176
+ { key: "dimension", header: data.rows },
177
+ ...columnKeys.map((column, i) => ({ key: `column${i}`, header: column })),
178
+ ];
179
+ const rows: TableRow[] = rowKeys.map((row) => {
180
+ const cells: Record<string, string | null> = { dimension: row };
181
+ columnKeys.forEach((column, i) => {
85
182
  const cell = byPosition.get(JSON.stringify([row, column]));
86
- return cell ? cellText(cell) : MISSING_MARK; // 稀疏格子在文本里以 — 呈现,不编数
87
- }),
88
- ]);
89
- const table = renderAlignedRows([header, ...rows]);
183
+ cells[`column${i}`] = cell ? cellText(cell) : null; // 稀疏格子在文本里以 — 呈现,不编数
184
+ });
185
+ return { key: row, cells };
186
+ });
187
+ const table = renderTableText({ columns, rows, locale: ctx.locale }, ctx);
90
188
 
91
189
  // 下钻命令:行维度是 eval 时,指向最值得看的一行(先挑有缺格的,再挑按 better 最差的)
92
190
  if (data.rows !== "eval" || rowKeys.length === 0) return table;
@@ -166,54 +264,70 @@ export function barsText(data: MatrixData): string {
166
264
 
167
265
  // ───────────────────────── Scoreboard ─────────────────────────
168
266
 
169
- export function scoreboardText(data: ScoreboardData): string {
267
+ export function scoreboardText(data: ScoreboardData, ctx: TextContext): string {
268
+ const locale = ctx.locale;
170
269
  const subjectKeys: string[] = [];
171
270
  for (const row of data.rows) {
172
271
  for (const subject of row.subjects) {
173
272
  if (!subjectKeys.includes(subject.key)) subjectKeys.push(subject.key);
174
273
  }
175
274
  }
176
- const header = [data.dimension, "total", ...subjectKeys];
177
- const rows = data.rows.map((row) => [
178
- row.key,
179
- `${row.total.display}/${data.fullMarks}`,
180
- ...subjectKeys.map((key) => {
275
+ const columns: TableColumn[] = [
276
+ { key: "dimension", header: data.dimension },
277
+ { key: "total", header: localeText(locale, "scoreboard.totalText") },
278
+ ...subjectKeys.map((key, i) => ({ key: `subject${i}`, header: key })),
279
+ ];
280
+ const rows: TableRow[] = data.rows.map((row) => {
281
+ const cells: Record<string, string | null> = {
282
+ dimension: row.key,
283
+ total: `${row.total.display}/${data.fullMarks}`,
284
+ };
285
+ subjectKeys.forEach((key, i) => {
181
286
  const subject = row.subjects.find((s) => s.key === key);
182
- if (!subject) return MISSING_MARK;
287
+ if (!subject) {
288
+ cells[`subject${i}`] = null;
289
+ return;
290
+ }
183
291
  const score = `${formatPlainNumber(subject.earned)}/${formatPlainNumber(subject.possible)}`;
184
- return subject.missing > 0 ? `${score} (${subject.missing} missing)` : score;
185
- }),
186
- ]);
187
- const table = renderAlignedRows([header, ...rows]);
292
+ cells[`subject${i}`] =
293
+ subject.missing > 0
294
+ ? `${score} ${localeText(locale, "scoreboard.missingText", { n: subject.missing })}`
295
+ : score;
296
+ });
297
+ return { key: row.key, cells };
298
+ });
299
+ const table = renderTableText({ columns, rows, locale }, ctx);
188
300
  if (data.weights.length === 0) return table;
189
301
  // 实际生效的权重表 —— 成绩单可审计
190
302
  const weights = data.weights.map((w) => `${w.prefix} ×${w.weight}`).join(" · ");
191
- return `${table}\nweights: ${weights} · others ×1`;
303
+ return `${table}\n${localeText(locale, "scoreboard.weights")} ${weights} · ${localeText(locale, "scoreboard.othersWeight")}`;
192
304
  }
193
305
 
194
306
  // ───────────────────────── MetricScatter ─────────────────────────
195
307
 
196
308
  const POINT_MARKS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
197
309
 
198
- function axisLabel(col: MetricColumn): string {
199
- return col.label;
310
+ function axisLabel(col: MetricColumn, locale: ReportLocale): string {
311
+ return resolveMetricLabel(col.label, locale, col.key);
200
312
  }
201
313
 
202
314
  export function scatterText(data: ScatterData, ctx: TextContext): string {
315
+ const locale = ctx.locale;
203
316
  const drawable = data.rows.filter((r) => r.x.value !== null && r.y.value !== null);
204
317
  const missing = data.rows.length - drawable.length;
205
318
  const footnotes: string[] = [];
206
- if (missing > 0) footnotes.push(`${missing} ${missing === 1 ? "point" : "points"} missing data`);
319
+ if (missing > 0) footnotes.push(countText(locale, "pointsMissing", missing));
207
320
 
321
+ const axes = { x: axisLabel(data.x, locale), y: axisLabel(data.y, locale) };
322
+ // 0 个可画点:x/y 指标没有可用数据(与 web 面同一事实)。
208
323
  if (drawable.length === 0) {
209
- return [MISSING_TEXT, ...footnotes].join("\n");
324
+ return [localeText(locale, "scatter.noData", axes), ...footnotes].join("\n");
210
325
  }
211
-
212
326
  // 点太密排不下时降级为坐标表,不硬挤
213
327
  if (drawable.length > POINT_MARKS.length || ctx.width < 44) {
214
328
  const table = renderCoordinateTable(
215
329
  drawable.map((r) => ({ key: r.key, x: r.x.display, y: r.y.display })),
216
- { key: data.points, x: axisLabel(data.x), y: axisLabel(data.y) },
330
+ { key: data.points, x: axisLabel(data.x, locale), y: axisLabel(data.y, locale) },
217
331
  );
218
332
  return [table, ...footnotes].join("\n");
219
333
  }
@@ -238,26 +352,27 @@ export function scatterText(data: ScatterData, ctx: TextContext): string {
238
352
  width: ctx.width,
239
353
  points,
240
354
  lines: [...bySeries.values()].filter((l) => l.length > 1),
241
- xLabel: `${axisLabel(data.x)}${invertX ? " (axis reversed: right = better)" : ""}`,
242
- yLabel: axisLabel(data.y),
355
+ xLabel: `${axisLabel(data.x, locale)}${invertX ? ` ${localeText(locale, "scatter.axisReversed")}` : ""}`,
356
+ yLabel: axisLabel(data.y, locale),
243
357
  formatX: (v) => formatMetricValue(v, data.x.unit),
244
358
  formatY: (v) => formatMetricValue(v, data.y.unit),
245
359
  invertX,
246
360
  invertY: data.y.better === "lower",
247
361
  });
248
362
  const legend = drawable.map((r, i) => `${POINT_MARKS[i]} ${r.key}`).join(" ");
249
- return [plot, "", `better upper right`, legend, ...footnotes].join("\n");
363
+ return [plot, "", localeText(locale, "scatter.betterUpperRight"), legend, ...footnotes].join("\n");
250
364
  }
251
365
 
252
366
  // ───────────────────────── MetricLine ─────────────────────────
253
367
 
254
368
  export function lineText(data: LineData, ctx: TextContext): string {
369
+ const locale = ctx.locale;
255
370
  const drawable = data.rows.filter((r) => r.x !== null && r.y.value !== null);
256
371
  const missing = data.rows.length - drawable.length;
257
372
  const footnotes: string[] = [];
258
- if (missing > 0) footnotes.push(`${missing} ${missing === 1 ? "point" : "points"} missing data`);
373
+ if (missing > 0) footnotes.push(countText(locale, "pointsMissing", missing));
259
374
 
260
- if (drawable.length === 0) return [MISSING_TEXT, ...footnotes].join("\n");
375
+ if (drawable.length === 0) return [missingText(locale), ...footnotes].join("\n");
261
376
 
262
377
  // 系列 → 字母;无系列 = 单系列
263
378
  const seriesKeys: string[] = [];
@@ -269,7 +384,7 @@ export function lineText(data: LineData, ctx: TextContext): string {
269
384
  if (seriesKeys.length > POINT_MARKS.length || ctx.width < 44) {
270
385
  const table = renderCoordinateTable(
271
386
  drawable.map((r) => ({ key: r.series ? `${r.key} (${r.series})` : r.key, x: r.xDisplay, y: r.y.display })),
272
- { key: "experiment", x: data.x.label, y: axisLabel(data.y) },
387
+ { key: "experiment", x: data.x.label, y: axisLabel(data.y, locale) },
273
388
  );
274
389
  return [table, ...footnotes].join("\n");
275
390
  }
@@ -292,65 +407,198 @@ export function lineText(data: LineData, ctx: TextContext): string {
292
407
  points,
293
408
  lines: lines.filter((l) => l.length > 1),
294
409
  xLabel: data.x.label,
295
- yLabel: axisLabel(data.y),
410
+ yLabel: axisLabel(data.y, locale),
296
411
  formatX: (v) => formatMetricValue(v, data.x.unit),
297
412
  formatY: (v) => formatMetricValue(v, data.y.unit),
298
413
  invertY: data.y.better === "lower",
299
414
  });
300
415
  const legend = seriesKeys
301
- .map((key, i) => `${POINT_MARKS[i]} ${key === "" ? data.y.label : key}`)
416
+ .map((key, i) => `${POINT_MARKS[i]} ${key === "" ? axisLabel(data.y, locale) : key}`)
302
417
  .join(" ");
303
418
  return [plot, "", legend, ...footnotes].join("\n");
304
419
  }
305
420
 
306
421
  // ───────────────────────── DeltaTable ─────────────────────────
307
422
 
308
- export function deltaText(data: DeltaData): string {
309
- const header = ["pair", ...data.columns.map((c) => c.label)];
310
- const rows = data.rows.map((row) => [
311
- row.key,
312
- ...data.columns.map((col) => {
423
+ export function deltaText(data: DeltaData, ctx: TextContext): string {
424
+ 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) })),
427
+ ];
428
+ const rows: TableRow[] = data.rows.map((row) => {
429
+ const cells: Record<string, string | null> = { pair: row.key };
430
+ data.columns.forEach((col, i) => {
313
431
  const cell = (row.cells as Record<string, DeltaData["rows"][number]["cells"][string]>)[col.key];
314
- if (!cell) return MISSING_MARK;
432
+ if (!cell) {
433
+ cells[`metric${i}`] = null;
434
+ return;
435
+ }
315
436
  const a = cell.a.value === null ? MISSING_MARK : cell.a.display;
316
437
  const b = cell.b.value === null ? MISSING_MARK : cell.b.display;
317
- return `${a} → ${b} ${cell.display}`;
318
- }),
319
- ]);
320
- return renderAlignedRows([header, ...rows]);
438
+ cells[`metric${i}`] = `${a} → ${b} ${cell.display}`;
439
+ });
440
+ return { key: row.key, cells };
441
+ });
442
+ return renderTableText({ columns, rows, locale: ctx.locale }, ctx);
321
443
  }
322
444
 
323
- // ───────────────────────── CaseList ─────────────────────────
445
+ // ───────────────────────── 实体列表(ExperimentList / EvalList / AttemptList)─────────────────────────
446
+ //
447
+ // 三面共用的紧凑标记:`locator✓`(判定符紧跟 locator,中间不留空格)。
448
+ // ExperimentList / EvalList 逐 attempt 只列这一个标记 + 各自的原因/耗时摘要,不重复整段
449
+ // niceeval show 命令;要看某个 attempt 的完整证据,agent 自己拼 `niceeval show <locator>`——
450
+ // 命令模板只在 AttemptList(叶子层)展示完整断言明细时才值得,不在中间层重复。
324
451
 
325
- export function caseListText(data: CaseListData, ctx: TextContext): string {
326
- if (data.rows.length === 0) return "No failed or errored attempts";
327
- const lines: string[] = [];
328
- for (const row of data.rows) {
329
- const head = [
330
- `✗ ${row.eval}`,
331
- row.experimentId,
332
- row.outcome,
333
- formatDurationMs(row.durationMs),
334
- ...(row.costUSD !== undefined ? [formatUSD(row.costUSD)] : []),
452
+ function locatorBadge(item: { locator: string; verdict: AttemptListItem["verdict"] }): string {
453
+ return `${item.locator}${verdictMark(item.verdict)}`;
454
+ }
455
+
456
+ // ── ExperimentList ──
457
+
458
+ function experimentSummaryTable(items: ExperimentListItem[], ctx: TextContext): string {
459
+ const locale = ctx.locale;
460
+ const compact = ctx.width < 100;
461
+ const columns: TableColumn[] = [
462
+ { key: "experiment", header: compact && locale === "en" ? "Exp." : localeText(locale, "experimentList.experiment") },
463
+ { key: "model", header: localeText(locale, "table.model") },
464
+ { key: "agent", header: localeText(locale, "table.agent") },
465
+ { key: "duration", header: compact && locale === "en" ? "Avg" : localeText(locale, "experimentList.avgDuration"), align: "right" },
466
+ { key: "passRate", header: compact && locale === "en" ? "Pass" : localeText(locale, "experimentList.passRate"), align: "right" },
467
+ { key: "result", header: localeText(locale, "experimentList.result") },
468
+ { key: "tokens", header: localeText(locale, "experimentList.tokens"), align: "right" },
469
+ { key: "cost", header: compact && locale === "en" ? "Cost" : localeText(locale, "experimentList.estimatedCost"), align: "right" },
470
+ ];
471
+ const rows: TableRow[] = items.map((item) => ({
472
+ key: item.experimentId,
473
+ cells: {
474
+ experiment: item.experimentId,
475
+ model: item.model ?? localeText(locale, "experimentList.defaultModel"),
476
+ 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),
482
+ },
483
+ }));
484
+ const metadata = items.flatMap((item) =>
485
+ wrapDisplay(
486
+ `${item.experimentId}: ${localeText(locale, "overview.evalsCount", { n: item.evals })} · ${localeText(locale, "overview.attemptsCount", { n: item.attempts })} · ${item.lastRunAt}`,
487
+ Math.max(8, ctx.width - 2),
488
+ ).map((line) => ` ${line}`),
489
+ );
490
+ return [renderTableText({ columns, rows, locale }, ctx), metadata.join("\n")].join("\n");
491
+ }
492
+
493
+ function experimentDetailTable(item: ExperimentListItem, ctx: TextContext): string {
494
+ const locale = ctx.locale;
495
+ const columns: TableColumn[] = [
496
+ { key: "status", header: localeText(locale, "experimentList.status") },
497
+ { key: "entity", header: localeText(locale, "experimentList.evalAttempt") },
498
+ { key: "result", header: localeText(locale, "experimentList.result") },
499
+ { key: "duration", header: localeText(locale, "experimentList.duration"), align: "right" },
500
+ { key: "cost", header: localeText(locale, "experimentList.cost"), align: "right" },
501
+ ];
502
+ const rows: TableRow[] = item.evalRows.flatMap((row) => {
503
+ const parent: TableRow = {
504
+ key: row.evalId,
505
+ cells: {
506
+ status: `${verdictMark(row.verdict)} ${localeText(locale, `verdict.${row.verdict}`)}`,
507
+ entity: row.evalId,
508
+ result: "",
509
+ duration: "",
510
+ cost: "",
511
+ },
512
+ };
513
+ const attempts: TableRow[] = row.attempts.map((attempt, index) => ({
514
+ key: attempt.locator,
515
+ cells: {
516
+ status: ` ${verdictMark(attempt.verdict)}`,
517
+ entity: `${index === row.attempts.length - 1 ? "└─" : "├─"} ${attempt.locator}`,
518
+ result: attemptItemReason(attempt) ?? MISSING_MARK,
519
+ duration: attempt.verdict === "skipped" && attempt.durationMs === 0 ? null : formatDurationMs(attempt.durationMs),
520
+ cost: attempt.costUSD === undefined ? null : formatUSD(attempt.costUSD),
521
+ },
522
+ }));
523
+ return [parent, ...attempts];
524
+ });
525
+ 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(" · ")}`
527
+ : undefined;
528
+ return [item.experimentId, flags, renderTableText({ columns, rows, locale }, ctx)].filter(Boolean).join("\n");
529
+ }
530
+
531
+ export function experimentListText(items: ExperimentListItem[], ctx: TextContext): string {
532
+ if (items.length === 0) return localeText(ctx.locale, "attemptList.empty");
533
+ return [experimentSummaryTable(items, ctx), ...items.map((item) => experimentDetailTable(item, ctx))].join("\n\n");
534
+ }
535
+
536
+ // ── EvalList ──
537
+
538
+ function evalListAttemptLine(item: AttemptListItem): string {
539
+ const reason = attemptItemReason(item);
540
+ return ` ${locatorBadge(item)}${reason ? ` · ${reason}` : ""}`;
541
+ }
542
+
543
+ export function evalListText(items: EvalListItem[], ctx: TextContext): string {
544
+ const locale = ctx.locale;
545
+ if (items.length === 0) return localeText(locale, "attemptList.empty");
546
+ const blocks = items.map((item) => {
547
+ const identity = `${item.evalId} · ${item.experimentId} · ${localeText(locale, `verdict.${item.verdict}`)}`;
548
+ const summary = [
549
+ localeText(locale, "attemptList.score", { score: cellText(item.score) }),
550
+ 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`,
335
553
  ].join(" · ");
336
- lines.push(head);
337
- if (row.error) {
338
- lines.push(indentBlock(wrapDisplay(row.error, ctx.width - 4).join("\n"), " "));
339
- }
340
- for (const assertion of row.failedAssertions) {
341
- const summary = assertion.detail
342
- ? `${assertion.name} ${assertion.detail}`
343
- : `${assertion.name} — score ${assertion.score}`;
344
- lines.push(indentBlock(wrapDisplay(summary, ctx.width - 4).join("\n"), " "));
345
- if (assertion.evidence) {
346
- lines.push(indentBlock(wrapDisplay(assertion.evidence, ctx.width - 6).join("\n"), " "));
347
- }
348
- }
349
- lines.push(` → niceeval show ${row.eval}`);
554
+ const attemptLines = item.attempts.map(evalListAttemptLine);
555
+ return [identity, ` ${summary}`, ...attemptLines].join("\n");
556
+ });
557
+ return blocks.join("\n\n");
558
+ }
559
+
560
+ // ── AttemptList ──
561
+
562
+ /** 一个 AttemptListItem 的完整 text 卡片:判定符 + locator + 身份 + 耗时/成本,
563
+ * 然后逐条断言(gate 与 soft 都列,与 web 面的 AttemptRow 同一份材料) */
564
+ function attemptListItemText(item: AttemptListItem, ctx: TextContext, locale: ReportLocale): string {
565
+ const head = [
566
+ `${verdictMark(item.verdict)} ${item.locator}`,
567
+ item.evalId,
568
+ item.experimentId,
569
+ formatDurationMs(item.durationMs),
570
+ ...(item.costUSD !== undefined ? [formatUSD(item.costUSD)] : []),
571
+ ].join(" · ");
572
+ const lines = [head];
573
+ if (item.error) {
574
+ lines.push(indentBlock(wrapDisplay(item.error, ctx.width - 4).join("\n"), " "));
350
575
  }
351
- if (data.truncated > 0) {
352
- lines.push("");
353
- lines.push(`(${data.truncated} more not shown)`);
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
+ }
354
593
  }
355
594
  return lines.join("\n");
356
595
  }
596
+
597
+ export function attemptListText(items: AttemptListItem[], total: number | undefined, ctx: TextContext): string {
598
+ const locale = ctx.locale;
599
+ if (items.length === 0) return localeText(locale, "attemptList.empty");
600
+ const blocks = items.map((item) => attemptListItemText(item, ctx, locale));
601
+ const remaining = (total ?? items.length) - items.length;
602
+ if (remaining > 0) blocks.push(localeText(locale, "attemptList.truncatedText", { n: remaining }));
603
+ return blocks.join("\n\n");
604
+ }