niceeval 0.5.3 → 0.6.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 (344) hide show
  1. package/INDEX.md +55 -0
  2. package/dist/agents/types.d.ts +317 -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/execution-tree.d.ts +103 -0
  9. package/dist/o11y/otlp/parse.d.ts +3 -0
  10. package/dist/o11y/otlp/receiver.d.ts +21 -0
  11. package/dist/o11y/otlp/select.d.ts +22 -0
  12. package/dist/o11y/otlp/turn-otel.d.ts +43 -0
  13. package/dist/o11y/types.d.ts +190 -0
  14. package/dist/report/aggregate.d.ts +57 -0
  15. package/dist/report/aggregate.js +212 -0
  16. package/dist/report/built-ins/cost-pass-rate-comparison.d.ts +1 -0
  17. package/dist/report/built-ins/cost-pass-rate-comparison.js +17 -0
  18. package/dist/report/built-ins/index.d.ts +1 -0
  19. package/dist/report/built-ins/index.js +2 -0
  20. package/dist/report/components.d.ts +178 -0
  21. package/dist/report/components.js +132 -0
  22. package/dist/report/compute.d.ts +106 -0
  23. package/dist/report/compute.js +618 -0
  24. package/dist/report/flag.d.ts +6 -0
  25. package/dist/report/flag.js +8 -0
  26. package/dist/report/format.d.ts +32 -0
  27. package/dist/report/format.js +126 -0
  28. package/dist/report/index.d.ts +19 -0
  29. package/dist/report/index.js +30 -0
  30. package/dist/report/load.d.ts +12 -0
  31. package/dist/report/load.js +56 -0
  32. package/dist/report/locale.d.ts +83 -0
  33. package/dist/report/locale.js +160 -0
  34. package/dist/report/metrics.d.ts +21 -0
  35. package/dist/report/metrics.js +115 -0
  36. package/dist/report/primitives.d.ts +64 -0
  37. package/dist/report/primitives.js +113 -0
  38. package/dist/report/react/AttemptList.d.ts +23 -0
  39. package/dist/report/react/AttemptList.js +20 -0
  40. package/dist/report/react/DeltaTable.d.ts +8 -0
  41. package/dist/report/react/DeltaTable.js +24 -0
  42. package/dist/report/react/EvalList.d.ts +10 -0
  43. package/dist/report/react/EvalList.js +0 -0
  44. package/dist/report/react/ExperimentList.d.ts +10 -0
  45. package/dist/report/react/ExperimentList.js +15 -0
  46. package/dist/report/react/GroupSummary.d.ts +8 -0
  47. package/dist/report/react/GroupSummary.js +8 -0
  48. package/dist/report/react/MetricBars.d.ts +10 -0
  49. package/dist/report/react/MetricBars.js +48 -0
  50. package/dist/report/react/MetricLine.d.ts +9 -0
  51. package/dist/report/react/MetricLine.js +68 -0
  52. package/dist/report/react/MetricMatrix.d.ts +10 -0
  53. package/dist/report/react/MetricMatrix.js +23 -0
  54. package/dist/report/react/MetricScatter.d.ts +9 -0
  55. package/dist/report/react/MetricScatter.js +100 -0
  56. package/dist/report/react/MetricTable.d.ts +11 -0
  57. package/dist/report/react/MetricTable.js +26 -0
  58. package/dist/report/react/RunOverview.d.ts +8 -0
  59. package/dist/report/react/RunOverview.js +12 -0
  60. package/dist/report/react/Scoreboard.d.ts +8 -0
  61. package/dist/report/react/Scoreboard.js +26 -0
  62. package/dist/report/react/cell.d.ts +9 -0
  63. package/dist/report/react/cell.js +9 -0
  64. package/dist/report/react/chart-math.d.ts +14 -0
  65. package/dist/report/react/chart-math.js +68 -0
  66. package/dist/report/react/colors.d.ts +14 -0
  67. package/dist/report/react/colors.js +45 -0
  68. package/dist/report/react/fixtures.d.ts +19 -0
  69. package/dist/report/react/fixtures.js +393 -0
  70. package/dist/report/react/format.d.ts +3 -0
  71. package/dist/report/react/format.js +7 -0
  72. package/dist/report/react/index.d.ts +6 -0
  73. package/dist/report/react/index.js +14 -0
  74. package/dist/report/report.d.ts +24 -0
  75. package/dist/report/report.js +49 -0
  76. package/dist/report/text/faces.d.ts +29 -0
  77. package/dist/report/text/faces.js +503 -0
  78. package/dist/report/text/layout.d.ts +75 -0
  79. package/dist/report/text/layout.js +173 -0
  80. package/dist/report/text/plot.d.ts +37 -0
  81. package/dist/report/text/plot.js +81 -0
  82. package/dist/report/text/table.d.ts +8 -0
  83. package/dist/report/text/table.js +86 -0
  84. package/dist/report/tree.d.ts +99 -0
  85. package/dist/report/tree.js +228 -0
  86. package/dist/report/types.d.ts +420 -0
  87. package/dist/report/types.js +4 -0
  88. package/dist/report/web.d.ts +15 -0
  89. package/dist/report/web.js +37 -0
  90. package/dist/results/annotated-source.d.ts +61 -0
  91. package/dist/results/attempt-evidence.d.ts +69 -0
  92. package/dist/results/attempt-source.d.ts +15 -0
  93. package/dist/results/locator.d.ts +75 -0
  94. package/dist/results/locator.js +0 -0
  95. package/dist/results/select.d.ts +45 -0
  96. package/dist/results/select.js +230 -0
  97. package/dist/results/source-hash.d.ts +9 -0
  98. package/dist/results/types.d.ts +204 -0
  99. package/dist/runner/eval-source.d.ts +22 -0
  100. package/dist/runner/fingerprint.d.ts +23 -0
  101. package/dist/runner/types.d.ts +332 -0
  102. package/dist/sandbox/docker-stream.d.ts +24 -0
  103. package/dist/sandbox/docker.d.ts +84 -0
  104. package/dist/sandbox/e2b.d.ts +31 -0
  105. package/dist/sandbox/errors.d.ts +4 -0
  106. package/dist/sandbox/local-files.d.ts +6 -0
  107. package/dist/sandbox/paths.d.ts +4 -0
  108. package/dist/sandbox/registry.d.ts +13 -0
  109. package/dist/sandbox/resolve.d.ts +40 -0
  110. package/dist/sandbox/retry.d.ts +10 -0
  111. package/dist/sandbox/shell.d.ts +11 -0
  112. package/dist/sandbox/source-files.d.ts +13 -0
  113. package/dist/sandbox/types.d.ts +203 -0
  114. package/dist/sandbox/vercel.d.ts +37 -0
  115. package/dist/scoring/types.d.ts +70 -0
  116. package/dist/shared/aggregate.d.ts +29 -0
  117. package/dist/shared/aggregate.js +55 -0
  118. package/dist/shared/types.d.ts +27 -0
  119. package/dist/shared/verdict.d.ts +27 -0
  120. package/dist/shared/verdict.js +34 -0
  121. package/dist/tty-line.d.ts +6 -0
  122. package/dist/types.d.ts +7 -0
  123. package/dist/util.d.ts +22 -0
  124. package/docs-site/zh/concepts/adapter.mdx +9 -4
  125. package/docs-site/zh/concepts/assert.mdx +8 -8
  126. package/docs-site/zh/concepts/evals.mdx +5 -5
  127. package/docs-site/zh/concepts/experiment.mdx +3 -1
  128. package/docs-site/zh/concepts/judge.mdx +9 -9
  129. package/docs-site/zh/concepts/overview.mdx +2 -2
  130. package/docs-site/zh/concepts/tier.mdx +6 -6
  131. package/docs-site/zh/example/claude-code-codex-plugin.mdx +9 -5
  132. package/docs-site/zh/example/claude-code-codex-skill.mdx +12 -7
  133. package/docs-site/zh/guides/agent-feedback-loop.mdx +235 -78
  134. package/docs-site/zh/guides/connect-otel.mdx +3 -3
  135. package/docs-site/zh/guides/connect-your-agent.mdx +2 -2
  136. package/docs-site/zh/guides/custom-reports.mdx +196 -36
  137. package/docs-site/zh/guides/experiments.mdx +7 -0
  138. package/docs-site/zh/guides/fixtures.mdx +1 -1
  139. package/docs-site/zh/guides/official-adapters.mdx +31 -9
  140. package/docs-site/zh/guides/publish-report.mdx +4 -4
  141. package/docs-site/zh/guides/report-components.mdx +194 -55
  142. package/docs-site/zh/guides/reporters.mdx +2 -2
  143. package/docs-site/zh/guides/results-data.mdx +35 -34
  144. package/docs-site/zh/guides/runner.mdx +2 -2
  145. package/docs-site/zh/guides/sandbox-agent.mdx +10 -5
  146. package/docs-site/zh/guides/sandbox-providers.mdx +102 -0
  147. package/docs-site/zh/guides/viewing-results.mdx +199 -79
  148. package/docs-site/zh/guides/write-experiment.mdx +23 -1
  149. package/docs-site/zh/guides/write-send.mdx +2 -2
  150. package/docs-site/zh/index.mdx +4 -3
  151. package/docs-site/zh/introduction.mdx +2 -1
  152. package/docs-site/zh/quickstart.mdx +5 -3
  153. package/docs-site/zh/reference/builtin-agents.mdx +38 -13
  154. package/docs-site/zh/reference/capabilities.mdx +1 -1
  155. package/docs-site/zh/reference/cli.mdx +36 -13
  156. package/docs-site/zh/reference/define-agent.mdx +24 -12
  157. package/docs-site/zh/reference/define-config.mdx +1 -1
  158. package/docs-site/zh/reference/define-eval.mdx +3 -3
  159. package/docs-site/zh/reference/events.mdx +7 -1
  160. package/docs-site/zh/reference/expect.mdx +3 -3
  161. package/package.json +10 -6
  162. package/src/agents/bub.ts +134 -43
  163. package/src/agents/claude-code.test.ts +152 -0
  164. package/src/agents/claude-code.ts +127 -13
  165. package/src/agents/codex.test.ts +192 -0
  166. package/src/agents/codex.ts +151 -12
  167. package/src/agents/index.ts +11 -3
  168. package/src/agents/manifest.ts +41 -0
  169. package/src/agents/plugin-config.test.ts +94 -0
  170. package/src/agents/shared.ts +33 -2
  171. package/src/agents/skills.test.ts +214 -0
  172. package/src/agents/skills.ts +304 -0
  173. package/src/agents/streaming.ts +5 -5
  174. package/src/agents/types.ts +62 -2
  175. package/src/cli.ts +74 -47
  176. package/src/context/context.test.ts +20 -2
  177. package/src/context/context.ts +3 -0
  178. package/src/context/control-flow.ts +1 -1
  179. package/src/context/session.test.ts +92 -1
  180. package/src/context/session.ts +26 -1
  181. package/src/context/types.ts +4 -4
  182. package/src/define.test.ts +82 -0
  183. package/src/define.ts +72 -11
  184. package/src/expect/index.ts +5 -5
  185. package/src/i18n/en.ts +36 -15
  186. package/src/i18n/zh-CN.ts +39 -21
  187. package/src/index.ts +1 -1
  188. package/src/o11y/execution-tree.test.ts +452 -0
  189. package/src/o11y/execution-tree.ts +367 -0
  190. package/src/o11y/otlp/receiver.ts +1 -1
  191. package/src/o11y/otlp/select.ts +7 -3
  192. package/src/o11y/parsers/claude-code.test.ts +175 -0
  193. package/src/o11y/parsers/claude-code.ts +32 -0
  194. package/src/o11y/types.ts +6 -1
  195. package/src/report/aggregate.ts +26 -20
  196. package/src/report/built-in-user-parity.test.tsx +643 -0
  197. package/src/report/built-ins/cost-pass-rate-comparison.tsx +23 -0
  198. package/src/report/built-ins/index.ts +2 -0
  199. package/src/report/components.tsx +223 -50
  200. package/src/report/compute.ts +358 -53
  201. package/src/report/dual-render.test.tsx +1125 -0
  202. package/src/report/flag.ts +1 -1
  203. package/src/report/format.ts +50 -2
  204. package/src/report/index.ts +48 -14
  205. package/src/report/load.ts +1 -1
  206. package/src/report/locale.ts +206 -0
  207. package/src/report/metrics.ts +39 -15
  208. package/src/report/primitives.tsx +100 -2
  209. package/src/report/react/AttemptList.tsx +115 -0
  210. package/src/report/react/DeltaTable.tsx +9 -6
  211. package/src/report/react/EvalList.tsx +0 -0
  212. package/src/report/react/ExperimentList.tsx +108 -0
  213. package/src/report/react/GroupSummary.tsx +66 -0
  214. package/src/report/react/MetricBars.tsx +24 -13
  215. package/src/report/react/MetricLine.tsx +21 -21
  216. package/src/report/react/MetricMatrix.tsx +8 -4
  217. package/src/report/react/MetricScatter.tsx +136 -96
  218. package/src/report/react/MetricTable.tsx +115 -15
  219. package/src/report/react/RunOverview.tsx +48 -26
  220. package/src/report/react/Scoreboard.tsx +15 -7
  221. package/src/report/react/cell.tsx +18 -9
  222. package/src/{view/app/lib/chart.ts → report/react/chart-math.ts} +2 -6
  223. package/src/report/react/colors.ts +24 -15
  224. package/src/report/react/enhance.js +129 -0
  225. package/src/report/react/fixtures.ts +163 -34
  226. package/src/report/react/format.ts +1 -1
  227. package/src/report/react/index.tsx +22 -6
  228. package/src/report/react/render.test.tsx +164 -31
  229. package/src/report/react/styles.css +405 -213
  230. package/src/report/report.test.ts +403 -94
  231. package/src/report/report.ts +29 -11
  232. package/src/report/text/faces.ts +303 -95
  233. package/src/report/text/layout.ts +76 -11
  234. package/src/report/text/table.ts +98 -0
  235. package/src/report/tree.ts +103 -20
  236. package/src/report/types.ts +196 -25
  237. package/src/report/web.ts +46 -19
  238. package/src/results/annotated-source.test.ts +146 -0
  239. package/src/results/annotated-source.ts +128 -0
  240. package/src/results/attempt-evidence.test.ts +158 -0
  241. package/src/results/attempt-evidence.ts +158 -0
  242. package/src/results/attempt-source.ts +48 -0
  243. package/src/results/copy.ts +154 -139
  244. package/src/results/format.ts +54 -47
  245. package/src/results/host-equivalence.test.ts +527 -0
  246. package/src/results/index.ts +56 -14
  247. package/src/results/locator.test.ts +198 -0
  248. package/src/results/locator.ts +0 -0
  249. package/src/results/open.ts +392 -175
  250. package/src/results/results.test.ts +943 -494
  251. package/src/results/select.ts +129 -23
  252. package/src/results/skipped-notice.ts +0 -0
  253. package/src/results/source-hash.ts +28 -0
  254. package/src/results/types.ts +98 -62
  255. package/src/results/writer.ts +304 -176
  256. package/src/runner/attempt.test.ts +170 -0
  257. package/src/runner/attempt.ts +97 -15
  258. package/src/runner/discover.test.ts +65 -0
  259. package/src/runner/discover.ts +6 -2
  260. package/src/runner/eval-source.test.ts +121 -0
  261. package/src/runner/eval-source.ts +45 -0
  262. package/src/runner/fingerprint.ts +54 -1
  263. package/src/runner/remote-sandbox.ts +1 -1
  264. package/src/runner/report.test.ts +6 -8
  265. package/src/runner/report.ts +5 -4
  266. package/src/runner/reporters/artifacts.ts +27 -41
  267. package/src/runner/reporters/braintrust.test.ts +3 -3
  268. package/src/runner/reporters/braintrust.ts +1 -1
  269. package/src/runner/reporters/console.ts +6 -6
  270. package/src/runner/reporters/json.ts +4 -4
  271. package/src/runner/reporters/live.test.ts +56 -0
  272. package/src/runner/reporters/live.ts +36 -10
  273. package/src/runner/reporters/quiet.test.ts +66 -0
  274. package/src/runner/reporters/quiet.ts +49 -0
  275. package/src/runner/reporters/shared.ts +5 -5
  276. package/src/runner/reporters/table.ts +17 -17
  277. package/src/runner/run.ts +61 -102
  278. package/src/runner/types.ts +43 -32
  279. package/src/sandbox/checkpoint.ts +1 -1
  280. package/src/sandbox/docker.ts +15 -2
  281. package/src/sandbox/e2b.ts +11 -5
  282. package/src/sandbox/errors.ts +13 -0
  283. package/src/sandbox/index.ts +3 -3
  284. package/src/sandbox/local-files.ts +1 -1
  285. package/src/sandbox/registry.ts +6 -5
  286. package/src/sandbox/resolve.ts +38 -23
  287. package/src/sandbox/retry.test.ts +68 -0
  288. package/src/sandbox/retry.ts +44 -0
  289. package/src/sandbox/types.ts +69 -28
  290. package/src/sandbox/vercel.ts +17 -3
  291. package/src/scoring/collector.ts +3 -3
  292. package/src/scoring/judge.ts +2 -2
  293. package/src/scoring/scoped.ts +13 -1
  294. package/src/scoring/types.ts +7 -7
  295. package/src/scoring/verdict.ts +4 -4
  296. package/src/shared/aggregate.ts +23 -3
  297. package/src/shared/types.ts +2 -2
  298. package/src/shared/{outcome.ts → verdict.ts} +15 -15
  299. package/src/show/compose.ts +12 -132
  300. package/src/show/index.ts +125 -94
  301. package/src/show/render.ts +660 -160
  302. package/src/show/show.test.ts +472 -253
  303. package/src/source-loc.ts +1 -1
  304. package/src/tty-line.ts +32 -0
  305. package/src/types.ts +2 -2
  306. package/src/util.test.ts +21 -1
  307. package/src/util.ts +5 -1
  308. package/src/view/app/App.tsx +33 -198
  309. package/src/view/app/components/AttemptModal.tsx +6 -6
  310. package/src/view/app/components/CodeView.tsx +2 -2
  311. package/src/view/app/components/CopyControls.tsx +32 -36
  312. package/src/view/app/components/SkippedRunsBanner.tsx +140 -0
  313. package/src/view/app/components/ui/badge.tsx +1 -1
  314. package/src/view/app/components/ui/dialog.tsx +2 -2
  315. package/src/view/app/i18n.ts +40 -31
  316. package/src/view/app/lib/artifact-url.ts +1 -1
  317. package/src/view/app/lib/attempt-route.test.ts +37 -47
  318. package/src/view/app/lib/attempt-route.ts +26 -26
  319. package/src/view/app/lib/rows.ts +9 -112
  320. package/src/view/app/lib/verdict.ts +25 -0
  321. package/src/view/app/main.tsx +9 -12
  322. package/src/view/app/pages/RunsPage.tsx +4 -4
  323. package/src/view/app/pages/TracesPage.tsx +3 -3
  324. package/src/view/app/shared.ts +0 -1
  325. package/src/view/app/types.ts +6 -51
  326. package/src/view/artifact-serving.test.ts +136 -0
  327. package/src/view/client-dist/app.css +1 -1
  328. package/src/view/client-dist/app.js +21 -21
  329. package/src/view/data.test.ts +214 -143
  330. package/src/view/data.ts +166 -120
  331. package/src/view/index.ts +23 -11
  332. package/src/view/server.ts +52 -17
  333. package/src/view/shared/types.ts +22 -31
  334. package/src/view/styles.css +117 -327
  335. package/src/view/view-report.test.ts +146 -98
  336. package/docs-site/zh/guides/sandbox-backends.mdx +0 -75
  337. package/src/report/default-report.tsx +0 -222
  338. package/src/report/dual-face.test.tsx +0 -527
  339. package/src/report/react/CaseList.tsx +0 -70
  340. package/src/view/app/components/CostScoreChart.tsx +0 -147
  341. package/src/view/app/components/ExperimentTable.tsx +0 -284
  342. package/src/view/app/components/GroupSelector.tsx +0 -61
  343. package/src/view/app/components/primitives.tsx +0 -43
  344. package/src/view/app/lib/outcome.ts +0 -82
@@ -1,147 +0,0 @@
1
- import { useState } from "react";
2
- import type { T } from "../shared.ts";
3
- import type { ViewRow } from "../types.ts";
4
- import { CELL_KEYS } from "../lib/rows.ts";
5
- import { formatCost, formatPercent } from "../lib/format.ts";
6
- import { layoutLabelOffsets, niceTicks, seriesColor } from "../lib/chart.ts";
7
-
8
- const WIDTH = 640;
9
- const HEIGHT = 300;
10
- const MARGIN = { top: 16, right: 24, bottom: 34, left: 46 };
11
-
12
- /** formatCost(0) 显示 "—"(表示"没测出成本"),但坐标轴原点确实就是 $0,不能沿用那个语义。 */
13
- function formatAxisCost(value: number): string {
14
- return value === 0 ? "$0" : formatCost(value);
15
- }
16
-
17
- interface ChartPoint {
18
- key: string;
19
- label: string;
20
- agent: string;
21
- model?: string;
22
- /** 官方 cost 格子的值(平均每 eval 成本,与轴标签同口径)。 */
23
- cost: number;
24
- /** 官方 pass-rate 格子的值(0–1)。 */
25
- rate: number;
26
- }
27
-
28
- /**
29
- * 一个 group 内「成本 vs 通过率」散点图:一行(一个实验/config)= 一个点。
30
- * 点位取官方 MetricTable.data 的格子值;任一轴缺数据(null)的行不画。
31
- * 只在 group 内至少两条 row 有成本时渲染,否则没有比较意义。
32
- */
33
- export function CostScoreChart({ rows, t }: { rows: ViewRow[]; t: T }) {
34
- const [hoverKey, setHoverKey] = useState<string | null>(null);
35
- const points: ChartPoint[] = [];
36
- for (const row of rows) {
37
- const cost = row.cells[CELL_KEYS.cost]?.value ?? null;
38
- const rate = row.cells[CELL_KEYS.passRate]?.value ?? null;
39
- if (cost === null || rate === null || !Number.isFinite(cost)) continue;
40
- points.push({ key: row.key, label: row.label, agent: row.agent, model: row.model, cost, rate });
41
- }
42
- if (points.length < 2) return null;
43
-
44
- const plotW = WIDTH - MARGIN.left - MARGIN.right;
45
- const plotH = HEIGHT - MARGIN.top - MARGIN.bottom;
46
-
47
- const xTicks = niceTicks(0, Math.max(...points.map((p) => p.cost)), 5);
48
- const xMax = xTicks[xTicks.length - 1] || 1;
49
-
50
- const rates = points.map((p) => p.rate);
51
- const rateMin = Math.min(...rates);
52
- const rateMax = Math.max(...rates);
53
- const pad = Math.max(0.03, (rateMax - rateMin) * 0.25);
54
- const yTicksRaw = niceTicks(Math.max(0, rateMin - pad), Math.min(1, rateMax + pad), 5);
55
- const yMin = Math.max(0, yTicksRaw[0] ?? 0);
56
- const yMax = Math.min(1, yTicksRaw[yTicksRaw.length - 1] ?? 1);
57
- const yTicks = yTicksRaw.filter((v) => v >= yMin - 1e-9 && v <= yMax + 1e-9);
58
-
59
- // X 轴反向:贵在左、便宜在右,「又好又便宜」落在右上角,离原点越远越好。
60
- const xScale = (cost: number) => MARGIN.left + (1 - cost / xMax) * plotW;
61
- const yScale = (rate: number) => MARGIN.top + (1 - (rate - yMin) / (yMax - yMin)) * plotH;
62
-
63
- const hovered = points.find((p) => p.key === hoverKey);
64
-
65
- const positions = points.map((p) => {
66
- const cx = xScale(p.cost);
67
- const cy = yScale(p.rate);
68
- const anchorLeft = cx >= MARGIN.left + plotW * 0.72;
69
- return { cx, cy, anchorLeft, width: p.label.length * 6.4 + 10 };
70
- });
71
- const labelOffsets = layoutLabelOffsets(positions);
72
-
73
- return (
74
- <div className="cost-score-chart">
75
- <div className="csc-head">{t("chart.costVsScore")}</div>
76
- <div className="csc-svg-wrap">
77
- <svg className="csc-svg" viewBox={`0 0 ${WIDTH} ${HEIGHT}`} role="img" aria-label={t("chart.costVsScore")}>
78
- <g className="csc-grid">
79
- {yTicks.map((tick) => (
80
- <line key={`gy-${tick}`} x1={MARGIN.left} x2={WIDTH - MARGIN.right} y1={yScale(tick)} y2={yScale(tick)} />
81
- ))}
82
- {xTicks.map((tick) => (
83
- <line key={`gx-${tick}`} y1={MARGIN.top} y2={HEIGHT - MARGIN.bottom} x1={xScale(tick)} x2={xScale(tick)} />
84
- ))}
85
- </g>
86
- <g className="csc-axis csc-axis-y">
87
- {yTicks.map((tick) => (
88
- <text key={`ay-${tick}`} x={MARGIN.left - 8} y={yScale(tick)} textAnchor="end" dominantBaseline="middle">
89
- {formatPercent(tick)}
90
- </text>
91
- ))}
92
- </g>
93
- <g className="csc-axis csc-axis-x">
94
- {xTicks.map((tick) => (
95
- <text key={`ax-${tick}`} x={xScale(tick)} y={HEIGHT - MARGIN.bottom + 18} textAnchor="middle">
96
- {formatAxisCost(tick)}
97
- </text>
98
- ))}
99
- <text x={MARGIN.left + plotW / 2} y={HEIGHT - 4} textAnchor="middle">
100
- {t("chart.axisCost")}
101
- </text>
102
- </g>
103
- <g className="csc-points">
104
- {points.map((p, i) => {
105
- const { cx, cy, anchorLeft } = positions[i]!;
106
- const labelY = cy + 4 + labelOffsets[i]!;
107
- const color = seriesColor(i);
108
- const labelX = cx + (anchorLeft ? -10 : 10);
109
- return (
110
- <g
111
- key={p.key}
112
- className="csc-point"
113
- tabIndex={0}
114
- role="button"
115
- aria-label={`${p.label}: ${formatCost(p.cost)}, ${formatPercent(p.rate)}`}
116
- onMouseEnter={() => setHoverKey(p.key)}
117
- onMouseLeave={() => setHoverKey((k) => (k === p.key ? null : k))}
118
- onFocus={() => setHoverKey(p.key)}
119
- onBlur={() => setHoverKey((k) => (k === p.key ? null : k))}
120
- >
121
- {/* 标签被挤开时补一条 leader line,避免脱离原点看不出对应关系。 */}
122
- {labelOffsets[i] ? <line className="csc-leader" x1={cx} y1={cy} x2={labelX} y2={labelY - 4} /> : null}
123
- <circle className="csc-hit" cx={cx} cy={cy} r={13} />
124
- <circle className="csc-dot" cx={cx} cy={cy} r={5} style={{ fill: color }} />
125
- <text x={labelX} y={labelY} textAnchor={anchorLeft ? "end" : "start"}>
126
- {p.label}
127
- </text>
128
- </g>
129
- );
130
- })}
131
- </g>
132
- </svg>
133
- {hovered ? (
134
- <div
135
- className="csc-tooltip"
136
- style={{ left: `${(xScale(hovered.cost) / WIDTH) * 100}%`, top: `${(yScale(hovered.rate) / HEIGHT) * 100}%` }}
137
- >
138
- <b>{formatPercent(hovered.rate)}</b> {t("chart.axisScore")}
139
- <div className="csc-tooltip-meta">
140
- {formatCost(hovered.cost)} · {hovered.model || hovered.agent}
141
- </div>
142
- </div>
143
- ) : null}
144
- </div>
145
- </div>
146
- );
147
- }
@@ -1,284 +0,0 @@
1
- import React, { useState } from "react";
2
- import { ChevronRight } from "lucide-react";
3
- import type { OpenModal, T } from "../shared.ts";
4
- import type { Assertion, SortKey, SortState, ViewResult, ViewRow } from "../types.ts";
5
- import { EvalGroup, failingAssertions, groupByEval, outcomeClass, outcomeLabel, outcomeSummary, reasonFor, scoresSummary } from "../lib/outcome.ts";
6
- import { CELL_KEYS, configChips } from "../lib/rows.ts";
7
- import { formatClock, formatCost, formatDateTime, formatDuration, formatTokens, totalTokens } from "../lib/format.ts";
8
- import { Kpi, SortHeader } from "./primitives.tsx";
9
- import type { MetricCell } from "../types.ts";
10
-
11
- /** 官方格子的渲染:display 已格式化;samples < total 时 title 如实报覆盖率(有 attempt 测不了这个指标)。 */
12
- function CellValue({ cell }: { cell: MetricCell | undefined }) {
13
- if (!cell || cell.value === null) return <>—</>;
14
- const partial = cell.samples < cell.total;
15
- return <span title={partial ? `${cell.samples}/${cell.total} attempts measured` : undefined}>{cell.display}</span>;
16
- }
17
-
18
- export function ExperimentTable({
19
- rows,
20
- sort,
21
- setSortKey,
22
- openRows,
23
- toggleRow,
24
- openModal,
25
- t,
26
- }: {
27
- rows: ViewRow[];
28
- sort: SortState;
29
- setSortKey: (key: SortKey) => void;
30
- openRows: Set<string>;
31
- toggleRow: (key: string) => void;
32
- openModal: OpenModal;
33
- t: T;
34
- }) {
35
- return (
36
- <div className="table-wrap">
37
- <table>
38
- <thead>
39
- <tr>
40
- <SortHeader name={t("table.experiment")} sortKey="experiment" sort={sort} onSort={setSortKey} />
41
- <SortHeader name={t("table.model")} sortKey="model" sort={sort} onSort={setSortKey} />
42
- <SortHeader name={t("table.agent")} sortKey="agent" sort={sort} onSort={setSortKey} />
43
- <SortHeader name={t("table.avgDuration")} sortKey="duration" sort={sort} onSort={setSortKey} />
44
- <SortHeader name={t("table.successRate")} sortKey="passRate" sort={sort} onSort={setSortKey} />
45
- <SortHeader name={t("table.tokens")} sortKey="tokens" sort={sort} onSort={setSortKey} />
46
- <SortHeader name={t("table.estCost")} sortKey="cost" sort={sort} onSort={setSortKey} />
47
- <th>{t("table.outcomes")}</th>
48
- </tr>
49
- </thead>
50
- <tbody>
51
- {rows.map((row: ViewRow) => (
52
- <React.Fragment key={row.key}>
53
- <ExperimentRow row={row} open={openRows.has(row.key)} onToggle={() => toggleRow(row.key)} t={t} />
54
- {openRows.has(row.key) ? <ExperimentDetail row={row} openModal={openModal} t={t} /> : null}
55
- </React.Fragment>
56
- ))}
57
- </tbody>
58
- </table>
59
- </div>
60
- );
61
- }
62
-
63
- export function ExperimentRow({ row, open, onToggle, t }: { row: ViewRow; open: boolean; onToggle: () => void; t: T }) {
64
- const passRate = row.cells[CELL_KEYS.passRate]?.value ?? null;
65
- const tone = passRate === null ? "" : passRate >= 0.8 ? "good" : passRate >= 0.5 ? "warn" : "bad";
66
- return (
67
- <tr
68
- className={`main-row${open ? " is-open" : ""}`}
69
- tabIndex={0}
70
- onClick={onToggle}
71
- onKeyDown={(e) => {
72
- if (e.key === "Enter" || e.key === " ") {
73
- e.preventDefault();
74
- onToggle();
75
- }
76
- }}
77
- >
78
- <td>
79
- <ChevronRight className="chev-icon" aria-hidden="true" />
80
- <span className="name">{row.label}</span>
81
- <div className="sub">
82
- {row.evals} {row.evals === 1 ? t("detail.evalResult") : t("detail.evalResults")}
83
- {row.runs > row.evals ? ` · ${row.runs} ${t("detail.runsUnit")}` : ""}
84
- {row.lastRunAt ? ` · ${formatDateTime(row.lastRunAt)}` : ""}
85
- </div>
86
- </td>
87
- <td>{row.model || t("config.default")}</td>
88
- <td>{row.agent}</td>
89
- {/* 官方 MetricTable.data 的格子:display 直接渲染,数字口径与 show 榜单同源。 */}
90
- <td className="num">
91
- <CellValue cell={row.cells[CELL_KEYS.duration]} />
92
- </td>
93
- <td className={`num ${tone}`}>
94
- <CellValue cell={row.cells[CELL_KEYS.passRate]} />
95
- </td>
96
- <td className="num">
97
- <CellValue cell={row.cells[CELL_KEYS.tokens]} />
98
- </td>
99
- <td className="num">
100
- <CellValue cell={row.cells[CELL_KEYS.cost]} />
101
- </td>
102
- <td>
103
- <span className="pill">{outcomeSummary(row, t)}</span>
104
- </td>
105
- </tr>
106
- );
107
- }
108
-
109
- export function ExperimentDetail({ row, openModal, t }: { row: ViewRow; openModal: OpenModal; t: T }) {
110
- const totalDuration = (row.results ?? []).reduce((sum: number, r: ViewResult) => sum + (r.durationMs || 0), 0);
111
- const sampleResult =
112
- row.results?.find((r: ViewResult) => r.outcome === "errored") ||
113
- row.results?.find((r: ViewResult) => r.outcome === "failed") ||
114
- row.results?.[0] ||
115
- {};
116
- const evalGroups = groupByEval(row.results ?? []).sort((a, b) => a.id.localeCompare(b.id));
117
- return (
118
- <tr className="detail-row">
119
- <td className="detail-cell" colSpan={8}>
120
- <div className="detail">
121
- <div className="config-strip">
122
- {configChips(row, t).map(([label, value]) => (
123
- <span className="config-chip" key={label}>
124
- <span>{label}</span>
125
- <b>{value}</b>
126
- </span>
127
- ))}
128
- </div>
129
- <div className="detail-kpis">
130
- <Kpi label={t("detail.evals")} value={row.evals} />
131
- <Kpi label={t("detail.passed")} value={row.passed} className="good" />
132
- <Kpi label={t("detail.failed")} value={row.failed} className={row.failed ? "bad" : ""} />
133
- <Kpi label={t("detail.errored")} value={row.errored} className={row.errored ? "infra-err" : ""} />
134
- {row.runs > row.evals ? <Kpi label={t("detail.runs")} value={row.runs} /> : null}
135
- <Kpi label={t("detail.totalTime")} value={formatDuration(totalDuration)} />
136
- <Kpi label={t("detail.totalCost")} value={formatCost(row.totalCostUSD)} />
137
- <Kpi label={t("detail.ran")} value={formatDateTime(row.lastRunAt)} title={row.lastRunAt || ""} />
138
- </div>
139
- <h3>{t("detail.evaluationAttempts")}</h3>
140
- <div className="eval-list">
141
- <div className="eval-grid-head">
142
- <span>{t("detail.status")}</span>
143
- <span>{t("detail.eval")}</span>
144
- <span>{t("detail.reason")}</span>
145
- <span>{t("detail.time")}</span>
146
- <span>{t("table.tokens")}</span>
147
- <span>{t("table.estCost")}</span>
148
- <span>{t("detail.run")}</span>
149
- </div>
150
- {evalGroups.map((group) => (
151
- <EvalRow key={`${group.experimentId ?? ""}-${group.id}`} group={group} openModal={openModal} t={t} />
152
- ))}
153
- </div>
154
- <details className="raw-details">
155
- <summary>
156
- {t("detail.rawSample")} <span className="raw-note">{t("detail.rawNote")}</span>
157
- </summary>
158
- <pre>{JSON.stringify(sampleResult, null, 2)}</pre>
159
- </details>
160
- </div>
161
- </td>
162
- </tr>
163
- );
164
- }
165
-
166
- /**
167
- * 一个 eval 一行:多轮(runs>1)折叠成单行摘要,点开展开各轮 attempt;单轮直接就是那条 attempt。
168
- * Run 列显示 通过轮数 / 总轮数(如 0/3、2/3),让 earlyExit 重试和 flaky 一眼可见。
169
- */
170
- export function EvalRow({ group, openModal, t }: { group: EvalGroup; openModal: OpenModal; t: T }) {
171
- const [open, setOpen] = useState(false);
172
- const n = group.attempts.length;
173
-
174
- if (n === 1) {
175
- return <Attempt result={group.attempts[0]!} totalRuns={1} openModal={openModal} t={t} />;
176
- }
177
-
178
- // 代表轮:取与 eval 判决相同的第一条,用它的原因/分数做折叠行摘要。
179
- const rep = group.attempts.find((a) => a.outcome === group.outcome) ?? group.attempts[0]!;
180
- const gates = failingAssertions(rep);
181
- const reason = reasonFor(rep, gates) || (group.outcome === "passed" ? scoresSummary(rep.assertions || []) : "");
182
- const totalDuration = group.attempts.reduce((s, a) => s + (a.durationMs || 0), 0);
183
- const totalTok = group.attempts.reduce((s, a) => s + totalTokens(a.usage), 0);
184
- const totalCost = group.attempts.reduce((s, a) => s + (a.estimatedCostUSD || 0), 0);
185
- const toggle = () => setOpen((v) => !v);
186
-
187
- return (
188
- <>
189
- <div
190
- className={`eval-item eval-item-clickable eval-group${open ? " is-open" : ""}`}
191
- role="button"
192
- tabIndex={0}
193
- aria-expanded={open}
194
- onClick={toggle}
195
- onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); toggle(); } }}
196
- >
197
- <span className="attempt-status">
198
- <span className={outcomeClass(group.outcome)}>{outcomeLabel(group.outcome, t)}</span>
199
- </span>
200
- <span className="eval-id">
201
- <ChevronRight className="chev-icon" aria-hidden="true" />
202
- {group.id}
203
- </span>
204
- <div className="assertions-cell">
205
- <span className="assertions" title={reason || undefined}>
206
- {reason || <span className="reason-empty">—</span>}
207
- </span>
208
- </div>
209
- <span className="num">{formatDuration(totalDuration)}</span>
210
- <span className="num">{formatTokens(totalTok)}</span>
211
- <span className="num">{formatCost(totalCost)}</span>
212
- <span className="num run-ratio" title={`${group.passedAttempts}/${n} ${t("detail.passed")}`}>
213
- {group.passedAttempts}/{n}
214
- </span>
215
- </div>
216
- {open ? (
217
- <div className="eval-attempts">
218
- {group.attempts.map((a) => (
219
- <Attempt key={`${a.id}-${a.attempt}`} result={a} totalRuns={n} openModal={openModal} t={t} />
220
- ))}
221
- </div>
222
- ) : null}
223
- </>
224
- );
225
- }
226
-
227
- export function Attempt({ result, totalRuns, openModal, t }: { result: ViewResult; totalRuns: number; openModal: OpenModal; t: T }) {
228
- const outcome = result.outcome;
229
- const gates = failingAssertions(result);
230
- const reason = reasonFor(result, gates);
231
- const allAssertions = result.assertions || [];
232
- const hasScores = allAssertions.some((a: Assertion) => a.score !== undefined && a.score !== null);
233
- const hasBody = result.hasEvents || result.hasTrace || hasScores;
234
-
235
- const inlineScores = !reason && outcome === "passed" ? scoresSummary(allAssertions) : "";
236
- const displayReason = reason || inlineScores;
237
-
238
- const handleOpen = () => openModal(result);
239
-
240
- const cells = (
241
- <>
242
- <span className="attempt-status">
243
- <span className={outcomeClass(outcome)}>{outcomeLabel(outcome, t)}</span>
244
- </span>
245
- <span className="eval-id">{result.id}</span>
246
- <div className="assertions-cell">
247
- <span
248
- className={`assertions${hasBody ? " assertions-link" : ""}`}
249
- title={displayReason || undefined}
250
- onClick={hasBody ? (e) => { e.stopPropagation(); handleOpen(); } : undefined}
251
- >
252
- {displayReason || <span className="reason-empty">—</span>}
253
- </span>
254
- </div>
255
- <span className="num">
256
- {formatDuration(result.durationMs)}
257
- {result.startedAt ? <small className="ran-at">{formatClock(result.startedAt)}</small> : null}
258
- </span>
259
- <span className="num">{formatTokens(totalTokens(result.usage))}</span>
260
- <span className="num">{formatCost(result.estimatedCostUSD)}</span>
261
- <span className="num" title={`attempt ${result.attempt + 1} of ${totalRuns}`}>
262
- #{result.attempt + 1}
263
- </span>
264
- </>
265
- );
266
-
267
- if (!hasBody) {
268
- return <div className="eval-item">{cells}</div>;
269
- }
270
-
271
- return (
272
- <div
273
- className="eval-item eval-item-clickable"
274
- role="button"
275
- tabIndex={0}
276
- onClick={handleOpen}
277
- onKeyDown={(e) => {
278
- if (e.key === "Enter" || e.key === " ") { e.preventDefault(); handleOpen(); }
279
- }}
280
- >
281
- {cells}
282
- </div>
283
- );
284
- }
@@ -1,61 +0,0 @@
1
- import type { T } from "../shared.ts";
2
- import type { ViewResult, ViewRow } from "../types.ts";
3
- import { evalPassRate, groupByEval } from "../lib/outcome.ts";
4
- import { formatCost, formatDateTime, formatPercent } from "../lib/format.ts";
5
-
6
- export function GroupSelector({
7
- groupMap,
8
- selectedGroup,
9
- onSelect,
10
- t,
11
- }: {
12
- groupMap: Map<string, ViewRow[]>;
13
- selectedGroup: string | null;
14
- onSelect: (group: string) => void;
15
- t: T;
16
- }) {
17
- if (!groupMap.size) return <div id="group-selector" className="group-selector" />;
18
- return (
19
- <div id="group-selector" className="group-selector">
20
- {[...groupMap.keys()].sort().map((group) => {
21
- const groupRows = groupMap.get(group) ?? [];
22
- const allResults = groupRows.flatMap((r: ViewRow) => r.results ?? []);
23
- const evalGroups = groupByEval(allResults); // 卡片计票按 eval,不按 attempt
24
- const failed = evalGroups.filter((g) => g.outcome === "failed").length;
25
- const errored = evalGroups.filter((g) => g.outcome === "errored").length;
26
- const passRate = evalPassRate(allResults);
27
- const tone = passRate >= 0.8 ? "good" : passRate >= 0.5 ? "warn" : "bad";
28
- const totalCost = groupRows.reduce((s: number, r: ViewRow) => s + (r.totalCostUSD || 0), 0);
29
- const lastRun = groupRows
30
- .map((r: ViewRow) => r.lastRunAt)
31
- .filter((value): value is string => Boolean(value))
32
- .sort()
33
- .at(-1);
34
- const selected = selectedGroup === group;
35
- return (
36
- <div
37
- key={group}
38
- className={`group-card${selected ? " is-selected" : ""}`}
39
- tabIndex={0}
40
- role="button"
41
- onClick={() => onSelect(group)}
42
- onKeyDown={(e) => {
43
- if (e.key === "Enter" || e.key === " ") {
44
- e.preventDefault();
45
- onSelect(group);
46
- }
47
- }}
48
- >
49
- <div className="group-card-name">{group}</div>
50
- <div className={`group-card-rate ${tone}`}>{formatPercent(passRate)}</div>
51
- <div className="group-card-meta">
52
- {groupRows.length} {groupRows.length === 1 ? t("detail.evalResult") : t("detail.evalResults")} · {failed} {t("outcome.failed")}
53
- {errored ? ` · ${errored} ${t("outcome.errored")}` : ""} · {formatCost(totalCost)}
54
- </div>
55
- {lastRun ? <div className="group-card-time">{formatDateTime(lastRun)}</div> : null}
56
- </div>
57
- );
58
- })}
59
- </div>
60
- );
61
- }
@@ -1,43 +0,0 @@
1
- import type { ReactNode } from "react";
2
- import type { SortKey, SortState } from "../types.ts";
3
-
4
- export function Metric({ label, value }: { label: string; value: string }) {
5
- return (
6
- <div className="metric">
7
- <div className="label">{label}</div>
8
- <div className="value">{value}</div>
9
- </div>
10
- );
11
- }
12
-
13
- export function SortHeader({
14
- name,
15
- sortKey,
16
- sort,
17
- onSort,
18
- }: {
19
- name: string;
20
- sortKey: SortKey;
21
- sort: SortState;
22
- onSort: (key: SortKey) => void;
23
- }) {
24
- const sorted = sort.key === sortKey ? (sort.dir === 1 ? "asc" : "desc") : undefined;
25
- return (
26
- <th>
27
- <button data-sorted={sorted} onClick={() => onSort(sortKey)}>
28
- {name}
29
- </button>
30
- </th>
31
- );
32
- }
33
-
34
- export function Kpi({ label, value, className = "", title }: { label: string; value: ReactNode; className?: string; title?: string }) {
35
- return (
36
- <div className="detail-kpi">
37
- <span>{label}</span>
38
- <b className={className} title={title}>
39
- {value}
40
- </b>
41
- </div>
42
- );
43
- }
@@ -1,82 +0,0 @@
1
- import type { Assertion, Outcome, ViewResult, ViewRow } from "../types.ts";
2
- import type { T } from "../shared.ts";
3
- import { formatScore } from "./format.ts";
4
-
5
- // 折叠口径与 server 聚合共用一份实现,见 src/shared/outcome.ts。
6
- import { foldEvalOutcome } from "../../../shared/outcome.ts";
7
-
8
- export { foldEvalOutcome };
9
-
10
- export interface EvalGroup {
11
- id: string;
12
- experimentId?: string;
13
- outcome: Outcome;
14
- attempts: ViewResult[];
15
- passedAttempts: number;
16
- }
17
-
18
- /** 把一批 attempt 按 (experimentId, eval id) 折叠成「每个 eval 一行」,内部 attempt 按轮次排序。 */
19
- export function groupByEval(results: ViewResult[]): EvalGroup[] {
20
- const byEval = new Map<string, ViewResult[]>();
21
- for (const r of results) {
22
- const key = `${r.experimentId ?? ""}|||${r.id}`;
23
- byEval.set(key, [...(byEval.get(key) ?? []), r]);
24
- }
25
- return [...byEval.values()].map((attempts) => {
26
- const sorted = [...attempts].sort((a, b) => a.attempt - b.attempt);
27
- return {
28
- id: sorted[0]!.id,
29
- experimentId: sorted[0]!.experimentId,
30
- outcome: foldEvalOutcome(sorted),
31
- attempts: sorted,
32
- passedAttempts: sorted.filter((a) => a.outcome === "passed").length,
33
- };
34
- });
35
- }
36
-
37
- /** 成功率按 eval 计票:折叠后通过的 eval 占已跑(非 skipped)eval 的比例。 */
38
- export function evalPassRate(results: ViewResult[]): number {
39
- const ran = groupByEval(results).filter((g) => g.outcome !== "skipped");
40
- return ran.length ? ran.filter((g) => g.outcome === "passed").length / ran.length : 0;
41
- }
42
-
43
- export function outcomeClass(outcome: Outcome): string {
44
- return outcome === "passed" ? "good" : outcome === "errored" ? "infra-err" : outcome === "failed" ? "bad" : "warn";
45
- }
46
-
47
- export function outcomeLabel(outcome: Outcome, t: T): string {
48
- if (outcome === "passed") return t("status.pass");
49
- if (outcome === "failed") return t("status.fail");
50
- if (outcome === "errored") return t("status.error");
51
- if (outcome === "skipped") return t("status.skipped");
52
- return outcome || "—";
53
- }
54
-
55
- // Only gate-severity failures are eval "failure reasons"; soft failures show as scores
56
- export function failingAssertions(result: ViewResult): Assertion[] {
57
- return (result.assertions || []).filter((a: Assertion) => !a.passed && a.severity === "gate");
58
- }
59
-
60
- export function reasonFor(result: ViewResult, failedGates: Assertion[]): string {
61
- if (result.error) return result.error;
62
- if (result.skipReason) return result.skipReason;
63
- return failedGates.map((a: Assertion) => (a.detail ? `${a.name}: ${a.detail}` : a.name)).join(", ");
64
- }
65
-
66
- export function scoresSummary(assertions: Assertion[]): string {
67
- const scored = (assertions || []).filter((a: Assertion) => a.score !== undefined && a.score !== null);
68
- if (!scored.length) return "";
69
- return scored
70
- .map((a: Assertion) => {
71
- const s = formatScore(a.score);
72
- return a.threshold !== undefined ? `${a.name} ${s}/${formatScore(a.threshold)}` : `${a.name} ${s}`;
73
- })
74
- .join(" · ");
75
- }
76
-
77
- export function outcomeSummary(row: ViewRow, t: T): string {
78
- const parts = [`${row.passed} ${t("outcome.passed")}`, `${row.failed} ${t("outcome.failed")}`];
79
- if (row.errored) parts.push(`${row.errored} ${t("outcome.errored")}`);
80
- if (row.skipped) parts.push(`${row.skipped} ${t("outcome.skipped")}`);
81
- return parts.join(" / ");
82
- }