niceeval 0.6.2 → 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 (216) hide show
  1. package/INDEX.md +23 -23
  2. package/README.zh.md +6 -6
  3. package/dist/agents/types.d.ts +2 -2
  4. package/dist/i18n/zh-CN.d.ts +3 -3
  5. package/dist/report/aggregate.d.ts +32 -26
  6. package/dist/report/aggregate.js +157 -76
  7. package/dist/report/built-in/index.d.ts +2 -0
  8. package/dist/report/built-in/index.js +8 -0
  9. package/dist/report/components.d.ts +91 -164
  10. package/dist/report/components.js +377 -114
  11. package/dist/report/compute.d.ts +86 -73
  12. package/dist/report/compute.js +592 -432
  13. package/dist/report/flag.d.ts +28 -17
  14. package/dist/report/flag.js +86 -16
  15. package/dist/report/format.d.ts +11 -11
  16. package/dist/report/format.js +17 -15
  17. package/dist/report/index.d.ts +16 -17
  18. package/dist/report/index.js +20 -22
  19. package/dist/report/load.js +3 -2
  20. package/dist/report/locale.d.ts +49 -34
  21. package/dist/report/locale.js +106 -58
  22. package/dist/report/metrics.d.ts +10 -3
  23. package/dist/report/metrics.js +46 -12
  24. package/dist/report/primitives.d.ts +42 -15
  25. package/dist/report/primitives.js +135 -26
  26. package/dist/report/react/AttemptList.d.ts +10 -8
  27. package/dist/report/react/AttemptList.js +18 -10
  28. package/dist/report/react/DeltaTable.js +19 -18
  29. package/dist/report/react/EvalList.d.ts +3 -3
  30. package/dist/report/react/EvalList.js +0 -0
  31. package/dist/report/react/ExperimentComparison.d.ts +4 -2
  32. package/dist/report/react/ExperimentComparison.js +5 -4
  33. package/dist/report/react/ExperimentList.d.ts +3 -3
  34. package/dist/report/react/ExperimentList.js +16 -15
  35. package/dist/report/react/MetricBars.js +5 -4
  36. package/dist/report/react/MetricLine.js +12 -5
  37. package/dist/report/react/MetricMatrix.js +1 -1
  38. package/dist/report/react/MetricScatter.js +54 -17
  39. package/dist/report/react/MetricTable.js +2 -12
  40. package/dist/report/react/ScopeSummary.d.ts +10 -0
  41. package/dist/report/react/ScopeSummary.js +17 -0
  42. package/dist/report/react/Scoreboard.js +6 -6
  43. package/dist/report/react/cell.js +2 -2
  44. package/dist/report/react/fixtures.d.ts +5 -9
  45. package/dist/report/react/fixtures.js +105 -149
  46. package/dist/report/react/index.d.ts +15 -5
  47. package/dist/report/react/index.js +18 -7
  48. package/dist/report/report.d.ts +137 -20
  49. package/dist/report/report.js +261 -34
  50. package/dist/report/text/faces.d.ts +17 -19
  51. package/dist/report/text/faces.js +225 -157
  52. package/dist/report/text/plot.js +1 -1
  53. package/dist/report/text/table.js +2 -2
  54. package/dist/report/tree.d.ts +90 -40
  55. package/dist/report/tree.js +252 -94
  56. package/dist/report/types.d.ts +245 -300
  57. package/dist/report/types.js +4 -3
  58. package/dist/report/web.d.ts +21 -5
  59. package/dist/report/web.js +42 -16
  60. package/dist/results/select.d.ts +38 -16
  61. package/dist/results/select.js +73 -25
  62. package/dist/results/types.d.ts +38 -14
  63. package/dist/shared/aggregate.d.ts +3 -2
  64. package/dist/shared/aggregate.js +5 -4
  65. package/docs-site/zh/README.md +44 -0
  66. package/docs-site/zh/examples/ai-agent-application.mdx +63 -0
  67. package/docs-site/zh/examples/coding-agent-extensions.mdx +57 -0
  68. package/docs-site/zh/examples/index.mdx +50 -0
  69. package/docs-site/zh/{concepts → explanation}/adapter.mdx +11 -11
  70. package/docs-site/zh/{concepts → explanation}/assert.mdx +7 -7
  71. package/docs-site/zh/{concepts → explanation}/drive.mdx +8 -8
  72. package/docs-site/zh/{concepts → explanation}/evals.mdx +4 -4
  73. package/docs-site/zh/{concepts → explanation}/experiment.mdx +8 -8
  74. package/docs-site/zh/{concepts → explanation}/hitl.mdx +8 -8
  75. package/docs-site/zh/{concepts → explanation}/judge.mdx +5 -5
  76. package/docs-site/zh/{concepts → explanation}/overview.mdx +5 -5
  77. package/docs-site/zh/{guides → explanation}/runner.mdx +1 -1
  78. package/docs-site/zh/{concepts → explanation}/tier.mdx +6 -6
  79. package/docs-site/zh/{guides → how-to}/agent-feedback-loop.mdx +7 -7
  80. package/docs-site/zh/{guides → how-to}/authoring.mdx +2 -2
  81. package/docs-site/zh/{guides → how-to}/connect-otel.mdx +6 -6
  82. package/docs-site/zh/{guides → how-to}/connect-your-agent.mdx +18 -18
  83. package/docs-site/zh/{guides → how-to}/custom-reports.mdx +6 -6
  84. package/docs-site/zh/{guides → how-to}/experiments.mdx +3 -3
  85. package/docs-site/zh/{guides → how-to}/publish-report.mdx +2 -2
  86. package/docs-site/zh/{guides → how-to}/sandbox-agent.mdx +2 -2
  87. package/docs-site/zh/{guides → how-to}/sandbox-providers.mdx +1 -1
  88. package/docs-site/zh/{guides → how-to}/viewing-results.mdx +6 -6
  89. package/docs-site/zh/{guides → how-to}/write-experiment.mdx +3 -3
  90. package/docs-site/zh/{guides → how-to}/write-send.mdx +13 -13
  91. package/docs-site/zh/index.mdx +23 -25
  92. package/docs-site/zh/introduction.mdx +8 -8
  93. package/docs-site/zh/reference/builtin-agents.mdx +5 -5
  94. package/docs-site/zh/reference/capabilities.mdx +6 -6
  95. package/docs-site/zh/reference/cli.mdx +9 -7
  96. package/docs-site/zh/reference/define-agent.mdx +1 -1
  97. package/docs-site/zh/reference/events.mdx +3 -3
  98. package/docs-site/zh/{guides → reference}/official-adapters.mdx +7 -7
  99. package/docs-site/zh/{guides → reference}/report-components.mdx +5 -5
  100. package/docs-site/zh/{guides → reference}/results-data.mdx +5 -5
  101. package/docs-site/zh/{guides → troubleshooting}/debug-sandbox.mdx +2 -2
  102. package/docs-site/zh/{guides → troubleshooting}/debugging.mdx +4 -2
  103. package/docs-site/zh/{quickstart.mdx → tutorials/quickstart.mdx} +5 -17
  104. package/package.json +6 -2
  105. package/src/agents/index.ts +2 -2
  106. package/src/agents/openai-compat.ts +1 -1
  107. package/src/agents/streaming.ts +2 -2
  108. package/src/agents/types.ts +3 -3
  109. package/src/cli.ts +42 -23
  110. package/src/context/context.ts +1 -1
  111. package/src/context/session.test.ts +1 -1
  112. package/src/context/session.ts +1 -1
  113. package/src/i18n/en.ts +18 -16
  114. package/src/i18n/zh-CN.ts +16 -15
  115. package/src/report/aggregate.ts +175 -87
  116. package/src/report/built-in/index.tsx +9 -0
  117. package/src/report/components.tsx +625 -285
  118. package/src/report/compute.ts +717 -515
  119. package/src/report/dual-render.test.tsx +738 -1148
  120. package/src/report/flag.ts +97 -33
  121. package/src/report/format.ts +18 -22
  122. package/src/report/index.ts +113 -58
  123. package/src/report/load.ts +3 -2
  124. package/src/report/locale.ts +120 -69
  125. package/src/report/metrics.ts +42 -12
  126. package/src/report/primitives.tsx +190 -45
  127. package/src/report/react/AttemptList.tsx +32 -20
  128. package/src/report/react/DeltaTable.tsx +63 -45
  129. package/src/report/react/EvalList.tsx +0 -0
  130. package/src/report/react/ExperimentComparison.tsx +12 -7
  131. package/src/report/react/ExperimentList.tsx +38 -26
  132. package/src/report/react/MetricBars.tsx +5 -4
  133. package/src/report/react/MetricLine.tsx +13 -8
  134. package/src/report/react/MetricMatrix.tsx +2 -2
  135. package/src/report/react/MetricScatter.tsx +74 -20
  136. package/src/report/react/MetricTable.tsx +4 -76
  137. package/src/report/react/ScopeSummary.tsx +86 -0
  138. package/src/report/react/Scoreboard.tsx +28 -10
  139. package/src/report/react/cell.tsx +2 -2
  140. package/src/report/react/enhance.js +57 -5
  141. package/src/report/react/fixtures.ts +109 -156
  142. package/src/report/react/index.tsx +24 -39
  143. package/src/report/react/render.test.tsx +139 -104
  144. package/src/report/react/styles.css +181 -91
  145. package/src/report/report.test.ts +761 -1031
  146. package/src/report/report.ts +425 -47
  147. package/src/report/text/faces.ts +257 -164
  148. package/src/report/text/plot.ts +1 -1
  149. package/src/report/text/table.ts +2 -2
  150. package/src/report/tree.ts +362 -104
  151. package/src/report/types.ts +257 -287
  152. package/src/report/web.ts +63 -20
  153. package/src/results/attempt-evidence.test.ts +4 -4
  154. package/src/results/attempt-evidence.ts +5 -5
  155. package/src/results/copy.ts +6 -6
  156. package/src/results/host-equivalence.test.ts +26 -14
  157. package/src/results/index.ts +10 -4
  158. package/src/results/open.ts +8 -4
  159. package/src/results/results.test.ts +4 -3
  160. package/src/results/select.ts +104 -34
  161. package/src/results/types.ts +36 -14
  162. package/src/runner/feedback/human.test.ts +1 -1
  163. package/src/runner/run.ts +1 -1
  164. package/src/sandbox/cli-commands.ts +2 -2
  165. package/src/scoring/judge.test.ts +1 -1
  166. package/src/shared/aggregate.ts +5 -4
  167. package/src/show/compose.ts +50 -67
  168. package/src/show/index.ts +107 -56
  169. package/src/show/render.ts +43 -27
  170. package/src/show/report-host.test.ts +188 -0
  171. package/src/show/report-host.ts +375 -0
  172. package/src/show/show.test.ts +86 -36
  173. package/src/view/app/App.test.tsx +69 -0
  174. package/src/view/app/App.tsx +144 -48
  175. package/src/view/app/components/AttemptModal.tsx +324 -63
  176. package/src/view/app/components/CodeView.tsx +10 -4
  177. package/src/view/app/i18n.ts +31 -17
  178. package/src/view/app/main.tsx +13 -8
  179. package/src/view/app/pages/{RunsPage.tsx → AttemptsPage.tsx} +6 -6
  180. package/src/view/app/types.ts +4 -1
  181. package/src/view/client-dist/app.css +1 -1
  182. package/src/view/client-dist/app.js +14 -14
  183. package/src/view/data.test.ts +9 -3
  184. package/src/view/data.ts +145 -49
  185. package/src/view/index.ts +48 -44
  186. package/src/view/server.ts +35 -15
  187. package/src/view/shared/types.ts +34 -5
  188. package/src/view/styles.css +224 -0
  189. package/src/view/view-report.test.ts +161 -57
  190. package/dist/report/built-ins/experiment-comparison.d.ts +0 -39
  191. package/dist/report/built-ins/experiment-comparison.js +0 -119
  192. package/dist/report/built-ins/index.d.ts +0 -2
  193. package/dist/report/built-ins/index.js +0 -2
  194. package/dist/report/react/GroupSummary.d.ts +0 -8
  195. package/dist/report/react/GroupSummary.js +0 -8
  196. package/dist/report/react/RunOverview.d.ts +0 -8
  197. package/dist/report/react/RunOverview.js +0 -12
  198. package/docs-site/zh/example/ai-agent-application.mdx +0 -152
  199. package/docs-site/zh/example/claude-code-codex-plugin.mdx +0 -167
  200. package/docs-site/zh/example/claude-code-codex-skill.mdx +0 -152
  201. package/docs-site/zh/example/showcase.mdx +0 -39
  202. package/src/report/built-in-user-parity.test.tsx +0 -597
  203. package/src/report/built-ins/experiment-comparison.tsx +0 -179
  204. package/src/report/built-ins/index.ts +0 -7
  205. package/src/report/react/GroupSummary.tsx +0 -66
  206. package/src/report/react/RunOverview.tsx +0 -109
  207. /package/docs-site/zh/{example/tier1-ai-sdk-v7.mdx → examples/integrations/ai-sdk-v7.mdx} +0 -0
  208. /package/docs-site/zh/{example/tier1-claude-sdk.mdx → examples/integrations/claude-sdk.mdx} +0 -0
  209. /package/docs-site/zh/{example/tier1-codex-sdk.mdx → examples/integrations/codex-sdk.mdx} +0 -0
  210. /package/docs-site/zh/{example/tier1-langgraph.mdx → examples/integrations/langgraph.mdx} +0 -0
  211. /package/docs-site/zh/{example/tier1-pi-sdk.mdx → examples/integrations/pi-sdk.mdx} +0 -0
  212. /package/docs-site/zh/{guides → how-to}/ci-integration.mdx +0 -0
  213. /package/docs-site/zh/{guides → how-to}/dataset-fanout.mdx +0 -0
  214. /package/docs-site/zh/{guides → how-to}/fixtures.mdx +0 -0
  215. /package/docs-site/zh/{guides → how-to}/reporters.mdx +0 -0
  216. /package/docs-site/zh/{guides → how-to}/scoring-guide.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, experimentDisplayName, formatDurationMs, formatMetricValue, formatPlainNumber, formatUSD, verdictMark, } from "../format.js";
8
- import { countText, localeText, resolveMetricLabel } from "../locale.js";
9
- import { padDisplay, stringWidth, 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,35 +419,51 @@ 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
- // 比较列表都不内联完整断言或命令模板。
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
468
  function experimentSummaryTable(items, ctx, relativeTo) {
408
469
  const locale = ctx.locale;
@@ -415,7 +476,7 @@ function experimentSummaryTable(items, ctx, relativeTo) {
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,
@@ -423,15 +484,15 @@ function experimentSummaryTable(items, ctx, relativeTo) {
423
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
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}`));
434
- return [renderTableText({ columns, rows, locale }, ctx), metadata.join("\n")].join("\n");
495
+ return [renderTableText({ columns: columns, rows, locale }, ctx), metadata.join("\n")].join("\n");
435
496
  }
436
497
  function experimentDetailTable(item, ctx, relativeTo) {
437
498
  const locale = ctx.locale;
@@ -444,20 +505,21 @@ function experimentDetailTable(item, ctx, relativeTo) {
444
505
  { key: "cost", header: localeText(locale, "experimentList.cost"), align: "right" },
445
506
  ];
446
507
  // Result 的字符预算 ≈ 两行 × 它能分到的列宽(总宽减其它列的自然宽与列距)。这里只做
447
- // 优先级让位(标题先截、received 最后截)的粗预算;精确的按宽度收口由列的 maxLines 兜底。
508
+ // 粗预算;精确的按宽度收口由列的 maxLines 兜底。
448
509
  const statusWidth = Math.max(stringWidth(localeText(locale, "experimentList.status")), ...item.evalRows.map((row) => stringWidth(`${verdictMark(row.verdict)} ${localeText(locale, `verdict.${row.verdict}`)}`)));
449
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)]));
450
511
  const fixedWidth = statusWidth + entityWidth + 8 /* duration */ + 6 /* cost */ + 3 * 4; /* 4 段列距 */
451
512
  const resultBudget = Math.max(24, (ctx.width - fixedWidth) * 2);
452
513
  const rows = item.evalRows.flatMap((row) => {
514
+ // Eval 父行只承载折叠判定与题级聚合;失败摘要只在 Attempt 子行出现。
453
515
  const parent = {
454
516
  key: row.evalId,
455
517
  cells: {
456
518
  status: `${verdictMark(row.verdict)} ${localeText(locale, `verdict.${row.verdict}`)}`,
457
519
  entity: row.evalId,
458
520
  result: "",
459
- duration: localeText(locale, "entityList.average", { value: cellText(row.duration) }),
460
- cost: localeText(locale, "entityList.average", { value: cellText(row.cost) }),
521
+ duration: localeText(locale, "entityList.average", { value: cellText(row.durationMs, locale) }),
522
+ cost: localeText(locale, "entityList.average", { value: cellText(row.costUSD, locale) }),
461
523
  },
462
524
  };
463
525
  const attempts = row.attempts.map((attempt, index) => ({
@@ -465,17 +527,23 @@ function experimentDetailTable(item, ctx, relativeTo) {
465
527
  cells: {
466
528
  status: ` ${verdictMark(attempt.verdict)}`,
467
529
  entity: `${index === row.attempts.length - 1 ? "└─" : "├─"} ${attempt.locator}`,
468
- result: attemptItemReason(attempt, resultBudget) ?? MISSING_MARK,
530
+ result: attemptReasonText(attempt, locale, resultBudget) ?? MISSING_MARK,
469
531
  duration: attempt.verdict === "skipped" && attempt.durationMs === 0 ? null : formatDurationMs(attempt.durationMs),
470
- cost: attempt.costUSD === undefined ? null : formatUSD(attempt.costUSD),
532
+ cost: attempt.costUSD === null ? null : formatUSD(attempt.costUSD),
471
533
  },
472
534
  }));
473
535
  return [parent, ...attempts];
474
536
  });
475
537
  const flags = item.flags && Object.keys(item.flags).length > 0
476
- ? `${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(" · ")}`
477
541
  : undefined;
478
- return [experimentDisplayName(item.experimentId, relativeTo), flags, renderTableText({ columns, rows, locale }, ctx)]
542
+ return [
543
+ experimentDisplayName(item.experimentId, relativeTo),
544
+ flags,
545
+ renderTableText({ columns: columns, rows, locale }, ctx),
546
+ ]
479
547
  .filter(Boolean)
480
548
  .join("\n");
481
549
  }
@@ -489,8 +557,8 @@ export function experimentListText(items, ctx, relativeTo) {
489
557
  }
490
558
  // ── EvalList ──
491
559
  function evalListAttemptLine(item, ctx) {
492
- // 行式列表同守「Result 最多两行」:预算 = 两行终端宽,超出由 fit 投影按优先级让位。
493
- const reason = attemptItemReason(item, Math.max(24, ctx.width * 2 - stringWidth(locatorBadge(item)) - 6));
560
+ // 行式列表同守「Result 最多两行」:预算 = 两行终端宽,超出按尾截收口。
561
+ const reason = attemptReasonText(item, ctx.locale, ctx.width * 2 - stringWidth(locatorBadge(item)) - 6);
494
562
  return ` ${locatorBadge(item)}${reason ? ` · ${reason}` : ""}`;
495
563
  }
496
564
  export function evalListText(items, ctx) {
@@ -500,13 +568,13 @@ export function evalListText(items, ctx) {
500
568
  const blocks = items.map((item) => {
501
569
  const identity = `${item.evalId} · ${item.experimentId} · ${localeText(locale, `verdict.${item.verdict}`)}`;
502
570
  const summary = [
503
- localeText(locale, "attemptList.score", { score: cellText(item.score) }),
571
+ localeText(locale, "attemptList.score", { score: cellText(item.examScore, locale) }),
504
572
  localeText(locale, "overview.attemptsCount", { n: item.attempts.length }),
505
573
  localeText(locale, "entityList.average", {
506
- value: item.duration.value === null ? missingText(locale) : formatDurationMs(item.duration.value),
574
+ value: item.durationMs.value === null ? missingText(locale) : formatDurationMs(item.durationMs.value),
507
575
  }),
508
576
  localeText(locale, "entityList.average", {
509
- value: item.cost.value === null ? missingText(locale) : formatUSD(item.cost.value),
577
+ value: item.costUSD.value === null ? missingText(locale) : formatUSD(item.costUSD.value),
510
578
  }),
511
579
  ].join(" · ");
512
580
  const attemptLines = item.attempts.map((attempt) => evalListAttemptLine(attempt, ctx));
@@ -515,17 +583,17 @@ export function evalListText(items, ctx) {
515
583
  return blocks.join("\n\n");
516
584
  }
517
585
  // ── AttemptList ──
518
- /** Attempt 比较卡片:只显示一条主失败摘要(至多两行终端宽);完整 assertions 走 locator 下钻。 */
586
+ /** Attempt 比较卡片:只显示一条主失败摘要(至多两行终端宽);完整 assertions 走 locator 下钻。 */
519
587
  function attemptListItemText(item, ctx) {
520
588
  const head = [
521
589
  `${verdictMark(item.verdict)} ${item.locator}`,
522
590
  item.evalId,
523
591
  item.experimentId,
524
592
  formatDurationMs(item.durationMs),
525
- ...(item.costUSD !== undefined ? [formatUSD(item.costUSD)] : []),
593
+ ...(item.costUSD !== null ? [formatUSD(item.costUSD)] : []),
526
594
  ].join(" · ");
527
595
  const lines = [head];
528
- const reason = attemptItemReason(item, Math.max(24, ctx.width * 2 - 4));
596
+ const reason = attemptReasonText(item, ctx.locale, ctx.width * 2 - 4);
529
597
  if (reason)
530
598
  lines.push(` ${reason}`);
531
599
  return lines.join("\n");