niceeval 0.6.1 → 0.6.2

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 (296) hide show
  1. package/dist/agents/types.d.ts +67 -5
  2. package/dist/context/types.d.ts +32 -12
  3. package/dist/i18n/en.d.ts +54 -0
  4. package/dist/i18n/zh-CN.d.ts +55 -1
  5. package/dist/o11y/types.d.ts +16 -2
  6. package/dist/report/aggregate.d.ts +5 -3
  7. package/dist/report/aggregate.js +32 -5
  8. package/dist/report/built-ins/experiment-comparison.d.ts +39 -1
  9. package/dist/report/built-ins/experiment-comparison.js +116 -10
  10. package/dist/report/built-ins/index.d.ts +1 -0
  11. package/dist/report/built-ins/index.js +1 -1
  12. package/dist/report/components.d.ts +8 -2
  13. package/dist/report/components.js +3 -3
  14. package/dist/report/compute.d.ts +11 -18
  15. package/dist/report/compute.js +54 -34
  16. package/dist/report/flag.d.ts +16 -1
  17. package/dist/report/flag.js +19 -1
  18. package/dist/report/format.d.ts +16 -8
  19. package/dist/report/format.js +27 -12
  20. package/dist/report/index.d.ts +4 -3
  21. package/dist/report/index.js +5 -4
  22. package/dist/report/locale.d.ts +11 -2
  23. package/dist/report/locale.js +23 -5
  24. package/dist/report/metrics.d.ts +13 -1
  25. package/dist/report/metrics.js +65 -14
  26. package/dist/report/primitives.d.ts +6 -0
  27. package/dist/report/react/AttemptList.d.ts +2 -2
  28. package/dist/report/react/AttemptList.js +5 -6
  29. package/dist/report/react/EvalList.d.ts +1 -1
  30. package/dist/report/react/EvalList.js +0 -0
  31. package/dist/report/react/ExperimentComparison.d.ts +8 -0
  32. package/dist/report/react/ExperimentComparison.js +11 -0
  33. package/dist/report/react/ExperimentList.d.ts +2 -1
  34. package/dist/report/react/ExperimentList.js +8 -10
  35. package/dist/report/react/MetricScatter.js +5 -11
  36. package/dist/report/react/chart-math.d.ts +23 -6
  37. package/dist/report/react/chart-math.js +71 -19
  38. package/dist/report/react/fixtures.d.ts +3 -3
  39. package/dist/report/react/fixtures.js +21 -14
  40. package/dist/report/report.d.ts +5 -1
  41. package/dist/report/report.js +6 -2
  42. package/dist/report/text/faces.d.ts +1 -1
  43. package/dist/report/text/faces.js +42 -41
  44. package/dist/report/text/table.js +36 -5
  45. package/dist/report/types.d.ts +39 -21
  46. package/dist/results/types.d.ts +11 -0
  47. package/dist/runner/feedback/sink.d.ts +110 -0
  48. package/dist/runner/types.d.ts +513 -22
  49. package/dist/sandbox/docker.d.ts +23 -2
  50. package/dist/sandbox/e2b.d.ts +15 -1
  51. package/dist/sandbox/errors.d.ts +30 -3
  52. package/dist/sandbox/io-retry.d.ts +17 -0
  53. package/dist/sandbox/registry.d.ts +2 -0
  54. package/dist/sandbox/resolve.d.ts +18 -5
  55. package/dist/sandbox/retry.d.ts +11 -1
  56. package/dist/sandbox/types.d.ts +39 -5
  57. package/dist/sandbox/vercel.d.ts +7 -1
  58. package/dist/scoring/coverage.d.ts +30 -0
  59. package/dist/scoring/display.d.ts +21 -0
  60. package/dist/scoring/display.js +120 -0
  61. package/dist/scoring/types.d.ts +103 -20
  62. package/dist/shared/aggregate.d.ts +1 -0
  63. package/dist/shared/aggregate.js +3 -3
  64. package/dist/shared/types.d.ts +28 -0
  65. package/dist/tty-line.d.ts +0 -4
  66. package/dist/util.d.ts +23 -0
  67. package/docs-site/zh/concepts/adapter.mdx +22 -4
  68. package/docs-site/zh/concepts/experiment.mdx +1 -1
  69. package/docs-site/zh/concepts/overview.mdx +6 -6
  70. package/docs-site/zh/guides/agent-feedback-loop.mdx +28 -26
  71. package/docs-site/zh/guides/authoring.mdx +33 -0
  72. package/docs-site/zh/guides/ci-integration.mdx +23 -12
  73. package/docs-site/zh/guides/connect-your-agent.mdx +29 -3
  74. package/docs-site/zh/guides/custom-reports.mdx +29 -34
  75. package/docs-site/zh/guides/dataset-fanout.mdx +25 -3
  76. package/docs-site/zh/guides/debug-sandbox.mdx +57 -0
  77. package/docs-site/zh/guides/debugging.mdx +210 -0
  78. package/docs-site/zh/guides/experiments.mdx +10 -3
  79. package/docs-site/zh/guides/official-adapters.mdx +26 -2
  80. package/docs-site/zh/guides/publish-report.mdx +30 -16
  81. package/docs-site/zh/guides/report-components.mdx +42 -30
  82. package/docs-site/zh/guides/reporters.mdx +2 -2
  83. package/docs-site/zh/guides/results-data.mdx +17 -9
  84. package/docs-site/zh/guides/runner.mdx +17 -7
  85. package/docs-site/zh/guides/sandbox-agent.mdx +56 -7
  86. package/docs-site/zh/guides/sandbox-providers.mdx +257 -9
  87. package/docs-site/zh/guides/scoring-guide.mdx +4 -4
  88. package/docs-site/zh/guides/viewing-results.mdx +79 -36
  89. package/docs-site/zh/guides/write-experiment.mdx +5 -3
  90. package/docs-site/zh/guides/write-send.mdx +17 -1
  91. package/docs-site/zh/index.mdx +1 -1
  92. package/docs-site/zh/reference/builtin-agents.mdx +27 -0
  93. package/docs-site/zh/reference/capabilities.mdx +2 -2
  94. package/docs-site/zh/reference/cli.mdx +33 -7
  95. package/docs-site/zh/reference/define-agent.mdx +57 -4
  96. package/docs-site/zh/reference/define-config.mdx +1 -1
  97. package/docs-site/zh/reference/define-eval.mdx +42 -9
  98. package/docs-site/zh/reference/expect.mdx +26 -1
  99. package/package.json +5 -1
  100. package/src/agents/ai-sdk-otel.test.ts +1 -0
  101. package/src/agents/ai-sdk.test.ts +3 -0
  102. package/src/agents/ai-sdk.ts +3 -0
  103. package/src/agents/bub-install-spec.test.ts +34 -0
  104. package/src/agents/bub-install-spec.ts +32 -0
  105. package/src/agents/bub.ts +31 -32
  106. package/src/agents/claude-code.test.ts +130 -9
  107. package/src/agents/claude-code.ts +76 -4
  108. package/src/agents/codex.test.ts +189 -40
  109. package/src/agents/codex.ts +155 -14
  110. package/src/agents/coding-cli-versions.test.ts +15 -0
  111. package/src/agents/coding-cli-versions.ts +3 -0
  112. package/src/agents/index.ts +11 -0
  113. package/src/agents/langgraph.test.ts +204 -0
  114. package/src/agents/langgraph.ts +495 -0
  115. package/src/agents/marketplace.ts +85 -0
  116. package/src/agents/native-config.test.ts +179 -0
  117. package/src/agents/native-config.ts +267 -0
  118. package/src/agents/openai-compat.test.ts +1 -0
  119. package/src/agents/openclaw.test.ts +31 -0
  120. package/src/agents/openclaw.ts +171 -0
  121. package/src/agents/plugin-config.test.ts +1 -0
  122. package/src/agents/sdk-streams.test.ts +79 -0
  123. package/src/agents/sdk-streams.ts +55 -10
  124. package/src/agents/skills.test.ts +1 -0
  125. package/src/agents/streaming.test.ts +3 -9
  126. package/src/agents/types.ts +68 -5
  127. package/src/agents/ui-message-stream.test.ts +3 -0
  128. package/src/cli.ts +411 -108
  129. package/src/context/context.test.ts +51 -12
  130. package/src/context/context.ts +161 -29
  131. package/src/context/session.test.ts +1 -0
  132. package/src/context/session.ts +114 -6
  133. package/src/context/types.ts +30 -12
  134. package/src/define.test.ts +13 -8
  135. package/src/define.ts +25 -4
  136. package/src/expect/index.ts +53 -23
  137. package/src/i18n/en.ts +64 -2
  138. package/src/i18n/zh-CN.ts +65 -3
  139. package/src/o11y/cost.test.ts +1 -0
  140. package/src/o11y/execution-tree.test.ts +1 -20
  141. package/src/o11y/otlp/mappers/claude-code.test.ts +1 -0
  142. package/src/o11y/otlp/parse.test.ts +1 -0
  143. package/src/o11y/otlp/turn-otel.test.ts +1 -0
  144. package/src/o11y/parsers/bub.test.ts +1 -0
  145. package/src/o11y/parsers/claude-code.test.ts +1 -34
  146. package/src/o11y/parsers/openclaw.test.ts +154 -0
  147. package/src/o11y/parsers/openclaw.ts +310 -0
  148. package/src/o11y/prices.json +746 -311
  149. package/src/o11y/tool-names.test.ts +1 -0
  150. package/src/o11y/types.ts +16 -2
  151. package/src/report/aggregate.ts +34 -5
  152. package/src/report/built-in-user-parity.test.tsx +110 -153
  153. package/src/report/built-ins/experiment-comparison.tsx +173 -13
  154. package/src/report/built-ins/index.ts +6 -1
  155. package/src/report/components.tsx +9 -3
  156. package/src/report/compute.ts +70 -40
  157. package/src/report/dual-render.test.tsx +194 -67
  158. package/src/report/flag.ts +30 -2
  159. package/src/report/format.ts +35 -11
  160. package/src/report/index.ts +22 -4
  161. package/src/report/locale.ts +25 -5
  162. package/src/report/metrics.ts +67 -14
  163. package/src/report/primitives.tsx +6 -0
  164. package/src/report/react/AttemptList.tsx +6 -31
  165. package/src/report/react/EvalList.tsx +0 -0
  166. package/src/report/react/ExperimentComparison.tsx +68 -0
  167. package/src/report/react/ExperimentList.tsx +15 -9
  168. package/src/report/react/MetricScatter.tsx +12 -14
  169. package/src/report/react/chart-math.test.ts +85 -0
  170. package/src/report/react/chart-math.ts +101 -22
  171. package/src/report/react/enhance.js +33 -1
  172. package/src/report/react/fixtures.ts +24 -17
  173. package/src/report/react/render.test.tsx +9 -64
  174. package/src/report/react/styles.css +73 -2
  175. package/src/report/report.test.ts +306 -98
  176. package/src/report/report.ts +6 -2
  177. package/src/report/text/faces.ts +47 -43
  178. package/src/report/text/table.ts +42 -5
  179. package/src/report/types.ts +41 -21
  180. package/src/results/annotated-source.test.ts +62 -9
  181. package/src/results/annotated-source.ts +64 -6
  182. package/src/results/attempt-evidence.test.ts +9 -7
  183. package/src/results/attempt-evidence.ts +15 -8
  184. package/src/results/attempt-source.ts +6 -3
  185. package/src/results/copy.ts +145 -55
  186. package/src/results/host-equivalence.test.ts +8 -6
  187. package/src/results/index.ts +2 -0
  188. package/src/results/locator.test.ts +1 -22
  189. package/src/results/open.ts +7 -1
  190. package/src/results/publish.ts +149 -0
  191. package/src/results/results.test.ts +85 -51
  192. package/src/results/truncate.ts +90 -0
  193. package/src/results/types.ts +7 -0
  194. package/src/results/writer.ts +31 -13
  195. package/src/runner/attempt.test.ts +138 -7
  196. package/src/runner/attempt.ts +603 -104
  197. package/src/runner/discover.test.ts +47 -0
  198. package/src/runner/discover.ts +36 -2
  199. package/src/runner/eval-source.test.ts +1 -27
  200. package/src/runner/feedback/agent.test.ts +504 -0
  201. package/src/runner/feedback/agent.ts +409 -0
  202. package/src/runner/feedback/ci.test.ts +562 -0
  203. package/src/runner/feedback/ci.ts +401 -0
  204. package/src/runner/feedback/coordinator.test.ts +317 -0
  205. package/src/runner/feedback/coordinator.ts +397 -0
  206. package/src/runner/feedback/failure.ts +40 -0
  207. package/src/runner/feedback/human.test.ts +616 -0
  208. package/src/runner/feedback/human.ts +535 -0
  209. package/src/runner/feedback/index.ts +66 -0
  210. package/src/runner/feedback/io.ts +78 -0
  211. package/src/runner/feedback/profile.test.ts +50 -0
  212. package/src/runner/feedback/profile.ts +58 -0
  213. package/src/runner/feedback/reducer.test.ts +395 -0
  214. package/src/runner/feedback/reducer.ts +260 -0
  215. package/src/runner/feedback/renderer.ts +82 -0
  216. package/src/runner/feedback/sink.ts +203 -0
  217. package/src/runner/feedback/testing.ts +106 -0
  218. package/src/runner/ledger.test.ts +230 -0
  219. package/src/runner/ledger.ts +329 -0
  220. package/src/runner/report.test.ts +128 -3
  221. package/src/runner/report.ts +33 -9
  222. package/src/runner/reporters/artifacts.ts +8 -2
  223. package/src/runner/reporters/braintrust.test.ts +8 -7
  224. package/src/runner/reporters/braintrust.ts +9 -2
  225. package/src/runner/reporters/index.ts +2 -2
  226. package/src/runner/reporters/json.test.ts +162 -0
  227. package/src/runner/reporters/json.ts +35 -8
  228. package/src/runner/reporters/shared.ts +1 -5
  229. package/src/runner/run.test.ts +760 -3
  230. package/src/runner/run.ts +242 -36
  231. package/src/runner/sandbox-prep.ts +3 -42
  232. package/src/runner/timing.ts +158 -0
  233. package/src/runner/types.ts +518 -22
  234. package/src/sandbox/checkpoint.test.ts +55 -0
  235. package/src/sandbox/checkpoint.ts +29 -8
  236. package/src/sandbox/cli-commands.ts +407 -0
  237. package/src/sandbox/docker.ts +115 -16
  238. package/src/sandbox/e2b-agent-template.test.ts +56 -0
  239. package/src/sandbox/e2b-agent-template.ts +94 -0
  240. package/src/sandbox/e2b.ts +74 -9
  241. package/src/sandbox/errors.ts +111 -4
  242. package/src/sandbox/index.ts +2 -0
  243. package/src/sandbox/io-retry.test.ts +58 -0
  244. package/src/sandbox/io-retry.ts +45 -0
  245. package/src/sandbox/keep-registry.test.ts +86 -0
  246. package/src/sandbox/keep-registry.ts +142 -0
  247. package/src/sandbox/keep.ts +178 -0
  248. package/src/sandbox/paths.test.ts +1 -0
  249. package/src/sandbox/paths.ts +19 -8
  250. package/src/sandbox/registry.ts +20 -3
  251. package/src/sandbox/resolve.ts +76 -11
  252. package/src/sandbox/retry.test.ts +70 -0
  253. package/src/sandbox/retry.ts +46 -4
  254. package/src/sandbox/types.ts +44 -6
  255. package/src/sandbox/vercel.ts +43 -20
  256. package/src/scoring/collector.ts +60 -17
  257. package/src/scoring/coverage.ts +95 -0
  258. package/src/scoring/diff.ts +81 -0
  259. package/src/scoring/display.test.ts +121 -0
  260. package/src/scoring/display.ts +133 -0
  261. package/src/scoring/evidence.test.ts +189 -0
  262. package/src/scoring/judge.test.ts +142 -0
  263. package/src/scoring/judge.ts +15 -18
  264. package/src/scoring/scoped.ts +217 -50
  265. package/src/scoring/types.ts +117 -20
  266. package/src/scoring/verdict.ts +16 -4
  267. package/src/shared/aggregate.ts +3 -2
  268. package/src/shared/types.ts +31 -0
  269. package/src/show/compose.ts +2 -2
  270. package/src/show/index.ts +21 -1
  271. package/src/show/render.ts +619 -104
  272. package/src/show/show.test.ts +235 -19
  273. package/src/tty-line.ts +8 -26
  274. package/src/util.test.ts +1 -0
  275. package/src/util.ts +41 -0
  276. package/src/view/app/components/AttemptModal.tsx +153 -2
  277. package/src/view/app/components/CodeView.tsx +32 -11
  278. package/src/view/app/components/CopyControls.tsx +2 -2
  279. package/src/view/app/i18n.ts +6 -0
  280. package/src/view/app/lib/attempt-route.test.ts +1 -0
  281. package/src/view/app/lib/verdict.ts +7 -9
  282. package/src/view/artifact-serving.test.ts +2 -1
  283. package/src/view/client-dist/app.css +1 -1
  284. package/src/view/client-dist/app.js +17 -17
  285. package/src/view/data.test.ts +1 -0
  286. package/src/view/data.ts +11 -1
  287. package/src/view/index.ts +11 -0
  288. package/src/view/server.ts +2 -0
  289. package/src/view/styles.css +3 -0
  290. package/src/view/view-report.test.ts +6 -5
  291. package/src/runner/reporters/console.ts +0 -70
  292. package/src/runner/reporters/live.test.ts +0 -56
  293. package/src/runner/reporters/live.ts +0 -247
  294. package/src/runner/reporters/quiet.test.ts +0 -66
  295. package/src/runner/reporters/quiet.ts +0 -49
  296. package/src/runner/reporters/table.ts +0 -277
@@ -24,7 +24,7 @@ declare const en: {
24
24
  readonly "table.attempt": "attempt";
25
25
  readonly "experimentList.experiment": "Experiment";
26
26
  readonly "experimentList.avgDuration": "Avg duration";
27
- readonly "experimentList.passRate": "Pass rate";
27
+ readonly "experimentList.passRate": "End-to-end pass rate";
28
28
  readonly "experimentList.tokens": "Tokens";
29
29
  readonly "experimentList.estimatedCost": "Est. cost";
30
30
  readonly "experimentList.result": "Result";
@@ -35,13 +35,14 @@ declare const en: {
35
35
  readonly "experimentList.filterPlaceholder": "Filter experiments…";
36
36
  readonly "experimentList.defaultModel": "default";
37
37
  readonly "experimentList.flags": "Flags";
38
+ readonly "entityList.average": "{value} avg";
38
39
  /** <Table> 压到下限仍放不下时,从右侧丢列并如实报数。 */
39
40
  readonly "table.columnsHidden.one": "({n} more column not shown)";
40
41
  readonly "table.columnsHidden.other": "({n} more columns not shown)";
41
42
  readonly "overview.snapshots": "Snapshots";
42
43
  readonly "overview.evals": "Evals";
43
44
  readonly "overview.attempts": "attempts";
44
- readonly "overview.passRate": "Pass rate";
45
+ readonly "overview.passRate": "End-to-end pass rate";
45
46
  readonly "overview.totalCost": "Total cost";
46
47
  readonly "overview.totalDuration": "Total duration";
47
48
  readonly "overview.source": "Source: {n} snapshots";
@@ -51,6 +52,13 @@ declare const en: {
51
52
  readonly "overview.attemptsCount": "{n} attempts";
52
53
  /** GroupSummary 的 KPI dt 标签(其余字段复用 verdict.* / overview.* 已有 key)。 */
53
54
  readonly "groupSummary.experiments": "Experiments";
55
+ readonly "experimentComparison.groups": "Experiment groups";
56
+ readonly "experimentComparison.group": "Experiment group";
57
+ readonly "experimentComparison.results": "Results";
58
+ readonly "experimentComparison.lastRun": "Last run";
59
+ readonly "experimentComparison.selectGroup": "Select experiment group {group}";
60
+ readonly "experimentComparison.command": "Details: {command}";
61
+ readonly "experimentComparison.empty": "No experiment groups";
54
62
  readonly "composedFrom.one": "composed from {n} run";
55
63
  readonly "composedFrom.other": "composed from {n} runs";
56
64
  readonly latestRun: "latest {run}";
@@ -65,6 +73,7 @@ declare const en: {
65
73
  readonly "attemptList.truncated": "and {n} more not shown";
66
74
  readonly "attemptList.truncatedText": "({n} more not shown)";
67
75
  readonly "attemptList.score": "score {score}";
76
+ readonly "attemptList.unavailable": "unavailable";
68
77
  readonly "attemptList.details": "details";
69
78
  readonly "scoreboard.total": "Total";
70
79
  readonly "scoreboard.totalText": "total";
@@ -2,7 +2,7 @@
2
2
  // 只覆盖组件自带的固定文案(verdict 词、缺数据、覆盖率角标、注脚、占位符等);
3
3
  // 数据本身(display、维度键、warnings 的 message)不经这里。
4
4
  // 刻意不 import src/i18n/(CLI 专用字典,locale 来源与 key 面完全不同);
5
- // zh-CN 译法与 src/view/app/i18n.ts 现有词保持一致(成功率/平均耗时/预估成本…)。
5
+ // zh-CN 译法与默认 Reports 契约保持一致(端到端成功率/平均耗时/预估成本…)。
6
6
  export const DEFAULT_REPORT_LOCALE = "en";
7
7
  const en = {
8
8
  "verdict.passed": "passed",
@@ -27,7 +27,7 @@ const en = {
27
27
  "table.attempt": "attempt",
28
28
  "experimentList.experiment": "Experiment",
29
29
  "experimentList.avgDuration": "Avg duration",
30
- "experimentList.passRate": "Pass rate",
30
+ "experimentList.passRate": "End-to-end pass rate",
31
31
  "experimentList.tokens": "Tokens",
32
32
  "experimentList.estimatedCost": "Est. cost",
33
33
  "experimentList.result": "Result",
@@ -38,13 +38,14 @@ const en = {
38
38
  "experimentList.filterPlaceholder": "Filter experiments…",
39
39
  "experimentList.defaultModel": "default",
40
40
  "experimentList.flags": "Flags",
41
+ "entityList.average": "{value} avg",
41
42
  /** <Table> 压到下限仍放不下时,从右侧丢列并如实报数。 */
42
43
  "table.columnsHidden.one": "({n} more column not shown)",
43
44
  "table.columnsHidden.other": "({n} more columns not shown)",
44
45
  "overview.snapshots": "Snapshots",
45
46
  "overview.evals": "Evals",
46
47
  "overview.attempts": "attempts",
47
- "overview.passRate": "Pass rate",
48
+ "overview.passRate": "End-to-end pass rate",
48
49
  "overview.totalCost": "Total cost",
49
50
  "overview.totalDuration": "Total duration",
50
51
  "overview.source": "Source: {n} snapshots",
@@ -54,6 +55,13 @@ const en = {
54
55
  "overview.attemptsCount": "{n} attempts",
55
56
  /** GroupSummary 的 KPI dt 标签(其余字段复用 verdict.* / overview.* 已有 key)。 */
56
57
  "groupSummary.experiments": "Experiments",
58
+ "experimentComparison.groups": "Experiment groups",
59
+ "experimentComparison.group": "Experiment group",
60
+ "experimentComparison.results": "Results",
61
+ "experimentComparison.lastRun": "Last run",
62
+ "experimentComparison.selectGroup": "Select experiment group {group}",
63
+ "experimentComparison.command": "Details: {command}",
64
+ "experimentComparison.empty": "No experiment groups",
57
65
  "composedFrom.one": "composed from {n} run",
58
66
  "composedFrom.other": "composed from {n} runs",
59
67
  "latestRun": "latest {run}",
@@ -68,6 +76,7 @@ const en = {
68
76
  "attemptList.truncated": "and {n} more not shown",
69
77
  "attemptList.truncatedText": "({n} more not shown)",
70
78
  "attemptList.score": "score {score}",
79
+ "attemptList.unavailable": "unavailable",
71
80
  "attemptList.details": "details",
72
81
  "scoreboard.total": "Total",
73
82
  "scoreboard.totalText": "total",
@@ -103,7 +112,7 @@ const zhCN = {
103
112
  "table.attempt": "Attempt",
104
113
  "experimentList.experiment": "实验",
105
114
  "experimentList.avgDuration": "平均耗时",
106
- "experimentList.passRate": "成功率",
115
+ "experimentList.passRate": "端到端成功率",
107
116
  "experimentList.tokens": "Tokens",
108
117
  "experimentList.estimatedCost": "预估成本",
109
118
  "experimentList.result": "结果",
@@ -114,12 +123,13 @@ const zhCN = {
114
123
  "experimentList.filterPlaceholder": "筛选实验…",
115
124
  "experimentList.defaultModel": "默认",
116
125
  "experimentList.flags": "Flags",
126
+ "entityList.average": "平均 {value}",
117
127
  "table.columnsHidden.one": "(还有 {n} 列未列出)",
118
128
  "table.columnsHidden.other": "(还有 {n} 列未列出)",
119
129
  "overview.snapshots": "快照",
120
130
  "overview.evals": "题目",
121
131
  "overview.attempts": "尝试",
122
- "overview.passRate": "通过率",
132
+ "overview.passRate": "端到端成功率",
123
133
  "overview.totalCost": "总成本",
124
134
  "overview.totalDuration": "总耗时",
125
135
  "overview.source": "数据来源:{n} 个快照",
@@ -128,6 +138,13 @@ const zhCN = {
128
138
  "overview.evalsCount": "{n} 道题",
129
139
  "overview.attemptsCount": "{n} 次 attempt",
130
140
  "groupSummary.experiments": "实验数",
141
+ "experimentComparison.groups": "实验组",
142
+ "experimentComparison.group": "实验组",
143
+ "experimentComparison.results": "结果",
144
+ "experimentComparison.lastRun": "最后运行",
145
+ "experimentComparison.selectGroup": "选择实验组 {group}",
146
+ "experimentComparison.command": "查看:{command}",
147
+ "experimentComparison.empty": "没有实验组",
131
148
  "composedFrom.one": "合成自 {n} 个 run",
132
149
  "composedFrom.other": "合成自 {n} 个 run",
133
150
  "latestRun": "最新 {run}",
@@ -141,6 +158,7 @@ const zhCN = {
141
158
  "attemptList.truncated": "还有 {n} 条未列出",
142
159
  "attemptList.truncatedText": "(还有 {n} 条未列出)",
143
160
  "attemptList.score": "得分 {score}",
161
+ "attemptList.unavailable": "评不了",
144
162
  "attemptList.details": "详情",
145
163
  "scoreboard.total": "总分",
146
164
  "scoreboard.totalText": "总分",
@@ -8,7 +8,19 @@ import type { Metric } from "./types.ts";
8
8
  export declare function defineMetric<const Name extends string>(def: Metric<Name>): Metric<Name>;
9
9
  /** 单 attempt 成本:网关实测(usage.costUSD)优先于价格表估算(estimatedCostUSD);都缺 → null,不编 0。 */
10
10
  export declare function attemptCostUSD(result: EvalResult): number | null;
11
- export declare const passRate: Metric<"pass-rate">;
11
+ /**
12
+ * 条件答题质量:passed = 1,failed = 0,errored 记 null 不进分母。
13
+ * 这是「已形成可信判定」条件下的诊断指标,不能简称默认成功率
14
+ * (docs/feature/reports/library.md「内置指标」)。
15
+ */
16
+ export declare const taskPassRate: Metric<"task-pass-rate">;
17
+ /** 执行可靠性:跑到可判定(passed / failed)= 1,errored = 0;skipped → null。 */
18
+ export declare const executionReliability: Metric<"execution-reliability">;
19
+ /**
20
+ * 端到端合成:passed = 1,failed / errored = 0;哪边拖累用
21
+ * taskPassRate / executionReliability 拆开看。
22
+ */
23
+ export declare const endToEndPassRate: Metric<"end-to-end-pass-rate">;
12
24
  export declare const examScore: Metric<"exam-score">;
13
25
  export declare const durationMs: Metric<"duration">;
14
26
  export declare const tokens: Metric<"tokens">;
@@ -1,15 +1,18 @@
1
1
  // defineMetric 与内置指标。
2
2
  //
3
3
  // null ≠ 0:null = 此 attempt 测不了这个指标(不进聚合);0 = 测了,结果是零(照常进)。
4
- // 哪个 verdict 落哪边必须显式表态,内置指标按 docs/feature/reports/library.md「内置指标」的表格:
4
+ // 哪个 verdict 落哪边必须显式表态,内置指标按 docs/feature/reports/library.md「内置指标」的表格。
5
+ // 三个通过率指标把「Agent 答错」与「基建没跑起来」拆开,不互相伪装:
5
6
  //
6
- // 指标(name) skipped errored failed passed better
7
- // passRate(pass-rate) null 0 0 1 higher
8
- // examScore(exam-score) null 0 0 soft 均分 higher
9
- // durationMs(duration) null 实测 实测 实测 lower
10
- // tokens(tokens) null 实测;无 usage→null 同左 同左 lower
11
- // costUSD(cost) null 同上 同左 同左 lower
12
- // turns(turns) null 实测;o11y 缺失→null 同左 同左 lower
7
+ // 指标(name) skipped errored failed passed better
8
+ // taskPassRate(task-pass-rate) null null 0 1 higher
9
+ // executionReliability(execution-reliability) null 0 1 1 higher
10
+ // endToEndPassRate(end-to-end-pass-rate) null 0 0 1 higher
11
+ // examScore(exam-score) null 0 0 soft 均分 higher
12
+ // durationMs(duration) null 实测 实测 实测 lower
13
+ // tokens(tokens) null 实测;无 usage→null 同左 同左 lower
14
+ // costUSD(cost) null 同上 同左 同左 lower
15
+ // turns(turns) null 实测;o11y 缺失→null 同左 同左 lower
13
16
  //
14
17
  // 两档指标(docs/feature/reports/library.md「内置指标」):以上除 turns 外全部只读 attempt.result
15
18
  // 的瘦身字段——任何 producer、任何 copySnapshots artifacts 选择都算得出,内置报告
@@ -33,10 +36,56 @@ export function defineMetric(def) {
33
36
  export function attemptCostUSD(result) {
34
37
  return result.usage?.costUSD ?? result.estimatedCostUSD ?? null;
35
38
  }
36
- export const passRate = defineMetric({
37
- name: "pass-rate",
38
- label: { en: "Pass rate", "zh-CN": "成功率" },
39
- description: "Share of evals that passed (skipped attempts excluded).",
39
+ /**
40
+ * 条件答题质量:passed = 1,failed = 0,errored 记 null 不进分母。
41
+ * 这是「已形成可信判定」条件下的诊断指标,不能简称默认成功率
42
+ * (docs/feature/reports/library.md「内置指标」)
43
+ */
44
+ export const taskPassRate = defineMetric({
45
+ name: "task-pass-rate",
46
+ label: { en: "Task pass rate", "zh-CN": "可判定任务通过率" },
47
+ description: "Conditional task quality among attempts that formed a trustworthy verdict: passed = 1, failed = 0; errored is null.",
48
+ better: "higher",
49
+ unit: "%",
50
+ value(a) {
51
+ switch (a.result.verdict) {
52
+ case "passed":
53
+ return 1;
54
+ case "failed":
55
+ return 0;
56
+ default:
57
+ // errored = 没形成可信判定 → null 不进这个条件指标;skipped 同为 null。
58
+ return null;
59
+ }
60
+ },
61
+ });
62
+ /** 执行可靠性:跑到可判定(passed / failed)= 1,errored = 0;skipped → null。 */
63
+ export const executionReliability = defineMetric({
64
+ name: "execution-reliability",
65
+ label: { en: "Execution reliability", "zh-CN": "执行可靠性" },
66
+ description: "Execution reliability: reached a trustworthy verdict (passed / failed) = 1, errored = 0.",
67
+ better: "higher",
68
+ unit: "%",
69
+ value(a) {
70
+ switch (a.result.verdict) {
71
+ case "passed":
72
+ case "failed":
73
+ return 1;
74
+ case "errored":
75
+ return 0;
76
+ default:
77
+ return null; // skipped
78
+ }
79
+ },
80
+ });
81
+ /**
82
+ * 端到端合成:passed = 1,failed / errored = 0;哪边拖累用
83
+ * taskPassRate / executionReliability 拆开看。
84
+ */
85
+ export const endToEndPassRate = defineMetric({
86
+ name: "end-to-end-pass-rate",
87
+ label: { en: "End-to-end pass rate", "zh-CN": "端到端成功率" },
88
+ description: "End-to-end composite: passed = 1, failed / errored = 0. Split blame with taskPassRate and executionReliability.",
40
89
  better: "higher",
41
90
  unit: "%",
42
91
  value: (a) => a.result.verdict === "skipped" ? null : a.result.verdict === "passed" ? 1 : 0,
@@ -56,10 +105,12 @@ export const examScore = defineMetric({
56
105
  // failed 同理得 0:--strict 下被翻成 failed 的哪怕 soft 分不低也是 0(报告不重新判卷)。
57
106
  if (verdict !== "passed")
58
107
  return 0;
59
- const soft = assertions.filter((x) => x.severity === "soft");
108
+ // unavailable 没有分数:不计入均分分母(评不了 0 分;非 optional 的 unavailable
109
+ // 早已把 verdict 拖成 errored,走不到这个分支)。
110
+ const soft = assertions.filter((x) => x.severity === "soft" && x.outcome !== "unavailable");
60
111
  if (soft.length === 0)
61
112
  return 1;
62
- return soft.reduce((sum, x) => sum + x.score, 0) / soft.length;
113
+ return soft.reduce((sum, x) => sum + (x.outcome === "unavailable" ? 0 : x.score), 0) / soft.length;
63
114
  },
64
115
  });
65
116
  export const durationMs = defineMetric({
@@ -30,6 +30,12 @@ export interface TableColumn {
30
30
  header: string;
31
31
  /** 对齐方向,默认 `"left"`;`"right"` 按显示宽度右对齐,数字列用。 */
32
32
  align?: ColumnAlign;
33
+ /**
34
+ * text 面:单元格折行后的最大物理行数,放不下的部分以 `…` 收口;省略则不限行数。
35
+ * 摘要类列(如比较列表的 Result)用它保证「格子是可扫读的预览」;完整值在下钻面。
36
+ * web 面不消费——网页的高度约束是组件自己的 CSS 决定。
37
+ */
38
+ maxLines?: number;
33
39
  }
34
40
  /** 一行的数据:身份键、已格式化的格子、可选的 attempt locator。 */
35
41
  export interface TableRow {
@@ -7,8 +7,8 @@ export declare function AttemptLocatorBadge({ item, attemptHref, }: {
7
7
  item: Pick<AttemptListItem, "locator" | "verdict">;
8
8
  attemptHref: (locator: AttemptLocator) => string;
9
9
  }): ReactElement;
10
- /** 一条 Attempt 的完整卡片:AttemptList 自己的一项,也是 EvalList 展开区里的一行。 */
11
- export declare function AttemptRow({ item, attemptHref, locale, }: {
10
+ /** 一条 Attempt 的比较卡片;完整 assertions 通过 locator 下钻,不在列表内展开。 */
11
+ export declare function AttemptRow({ item, attemptHref, }: {
12
12
  item: AttemptListItem;
13
13
  attemptHref: (locator: AttemptLocator) => string;
14
14
  locale?: ReportLocale;
@@ -1,17 +1,16 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { DEFAULT_REPORT_LOCALE, localeText } from "../locale.js";
3
+ import { attemptItemReason } from "../format.js";
3
4
  import { colorClassForKey } from "./colors.js";
4
5
  import { cx, formatDurationMs, formatUSD, verdictMark } from "./format.js";
5
6
  /** locator + 判定符的普通 <a>,AttemptList/EvalList/ExperimentList 共用。 */
6
7
  export function AttemptLocatorBadge({ item, attemptHref, }) {
7
8
  return (_jsxs("a", { className: cx("nre-locator", `nre-verdict-${item.verdict}`), href: attemptHref(item.locator), children: [item.locator, _jsx("span", { className: "nre-locator-mark", children: verdictMark(item.verdict) })] }));
8
9
  }
9
- function AssertionRow({ assertion, locale }) {
10
- return (_jsxs("li", { className: cx("nre-assertion", `nre-assertion-${assertion.severity}`, !assertion.passed && "nre-assertion-failed"), children: [_jsx("span", { className: "nre-assertion-severity", children: assertion.severity }), _jsx("span", { className: "nre-assertion-name", children: assertion.name }), _jsx("span", { className: "nre-assertion-score", children: localeText(locale, "attemptList.score", { score: assertion.score }) }), (assertion.detail || assertion.evidence) && (_jsxs("details", { className: "nre-assertion-more", children: [_jsx("summary", { children: localeText(locale, "attemptList.details") }), assertion.detail && _jsx("p", { className: "nre-assertion-detail", children: assertion.detail }), assertion.evidence && _jsx("blockquote", { className: "nre-assertion-evidence", children: assertion.evidence })] }))] }));
11
- }
12
- /** 一条 Attempt 的完整卡片:AttemptList 自己的一项,也是 EvalList 展开区里的一行。 */
13
- export function AttemptRow({ item, attemptHref, locale = DEFAULT_REPORT_LOCALE, }) {
14
- return (_jsxs("li", { className: cx("nre-attempt", `nre-attempt-${item.verdict}`), children: [_jsxs("div", { className: "nre-attempt-head", children: [_jsx(AttemptLocatorBadge, { item: item, attemptHref: attemptHref }), _jsx("span", { className: "nre-attempt-eval", children: item.evalId }), _jsx("span", { className: "nre-attempt-experiment", children: item.experimentId }), _jsx("span", { className: cx("nre-attempt-agent", "nre-key", colorClassForKey(item.agent)), children: item.agent }), _jsx("span", { className: "nre-attempt-duration", children: formatDurationMs(item.durationMs) }), item.costUSD !== undefined && _jsx("span", { className: "nre-attempt-cost", children: formatUSD(item.costUSD) })] }), item.error && _jsx("p", { className: "nre-attempt-error", children: item.error }), item.assertions.length > 0 && (_jsx("ul", { className: "nre-assertions", children: item.assertions.map((a, i) => (_jsx(AssertionRow, { assertion: a, locale: locale }, i))) }))] }));
10
+ /** 一条 Attempt 的比较卡片;完整 assertions 通过 locator 下钻,不在列表内展开。 */
11
+ export function AttemptRow({ item, attemptHref, }) {
12
+ const reason = attemptItemReason(item);
13
+ return (_jsxs("li", { className: cx("nre-attempt", `nre-attempt-${item.verdict}`), children: [_jsxs("div", { className: "nre-attempt-head", children: [_jsx(AttemptLocatorBadge, { item: item, attemptHref: attemptHref }), _jsx("span", { className: "nre-attempt-eval", children: item.evalId }), _jsx("span", { className: "nre-attempt-experiment", children: item.experimentId }), _jsx("span", { className: cx("nre-attempt-agent", "nre-key", colorClassForKey(item.agent)), children: item.agent }), _jsx("span", { className: "nre-attempt-duration", children: formatDurationMs(item.durationMs) }), item.costUSD !== undefined && _jsx("span", { className: "nre-attempt-cost", children: formatUSD(item.costUSD) })] }), reason && _jsx("p", { className: "nre-attempt-result", children: reason })] }));
15
14
  }
16
15
  export function AttemptList({ items, total, attemptHref, className, locale = DEFAULT_REPORT_LOCALE, }) {
17
16
  const remaining = (total ?? items.length) - items.length;
@@ -1,6 +1,6 @@
1
1
  import type { ReactElement } from "react";
2
- import type { EvalListItem } from "../types.ts";
3
2
  import type { AttemptLocator } from "../../results/locator.ts";
3
+ import type { EvalListItem } from "../types.ts";
4
4
  import { type ReportLocale } from "../locale.ts";
5
5
  export declare function EvalList({ items, attemptHref, className, locale, }: {
6
6
  items: EvalListItem[];
Binary file
@@ -0,0 +1,8 @@
1
+ import type { ReactElement } from "react";
2
+ import type { ExperimentComparisonData } from "../built-ins/experiment-comparison.tsx";
3
+ import { type ReportLocale } from "../locale.ts";
4
+ export declare function ExperimentComparisonView({ data, className, locale, }: {
5
+ data: ExperimentComparisonData;
6
+ className?: string;
7
+ locale?: ReportLocale;
8
+ }): ReactElement;
@@ -0,0 +1,11 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { DEFAULT_REPORT_LOCALE, localeText } from "../locale.js";
3
+ import { ExperimentList, MetricScatter } from "../components.js";
4
+ import { GroupSummary } from "./GroupSummary.js";
5
+ import { cx } from "./format.js";
6
+ export function ExperimentComparisonView({ data, className, locale = DEFAULT_REPORT_LOCALE, }) {
7
+ if (data.groups.length === 0) {
8
+ return (_jsx("div", { className: cx("nre", "nre-experiment-comparison", className), children: _jsx("p", { className: "nre-experiment-groups-empty", children: localeText(locale, "experimentComparison.empty") }) }));
9
+ }
10
+ return (_jsxs("div", { className: cx("nre", "nre-experiment-comparison", className), "data-nre-experiment-groups": true, children: [_jsx("div", { className: "nre-experiment-group-tabs", role: "tablist", "aria-label": localeText(locale, "experimentComparison.groups"), children: data.groups.map((group, index) => (_jsxs("div", { className: "nre-experiment-group-tab", role: "tab", tabIndex: index === 0 ? 0 : -1, "aria-selected": index === 0 ? "true" : "false", "aria-label": localeText(locale, "experimentComparison.selectGroup", { group: group.key }), "data-nre-experiment-group-select": index, children: [_jsx("strong", { className: "nre-experiment-group-name", children: group.key }), _jsx(GroupSummary, { data: group.summary, locale: locale })] }, group.key))) }), _jsx("div", { className: "nre-experiment-group-panels", children: data.groups.map((group, index) => (_jsxs("details", { className: "nre-experiment-group-panel", "data-nre-experiment-group-panel": index, role: "tabpanel", open: index === 0, children: [_jsx("summary", { children: group.key }), _jsx(MetricScatter, { data: group.scatter, locale: locale }), _jsx(ExperimentList, { items: group.experiments, filter: true, locale: locale, relativeTo: group.key })] }, group.key))) })] }));
11
+ }
@@ -2,10 +2,11 @@ import type { ReactElement } from "react";
2
2
  import type { AttemptLocator } from "../../results/locator.ts";
3
3
  import type { ExperimentListItem } from "../types.ts";
4
4
  import { type ReportLocale } from "../locale.ts";
5
- export declare function ExperimentList({ items, attemptHref, filter, className, locale, }: {
5
+ export declare function ExperimentList({ items, attemptHref, filter, className, locale, relativeTo, }: {
6
6
  items: ExperimentListItem[];
7
7
  attemptHref: (locator: AttemptLocator) => string;
8
8
  filter?: boolean;
9
9
  className?: string;
10
10
  locale?: ReportLocale;
11
+ relativeTo?: string;
11
12
  }): ReactElement;
@@ -1,5 +1,5 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { attemptItemReason } from "../format.js";
2
+ import { attemptItemReason, experimentDisplayName } from "../format.js";
3
3
  import { DEFAULT_REPORT_LOCALE, localeText } from "../locale.js";
4
4
  import { AttemptLocatorBadge } from "./AttemptList.js";
5
5
  import { MetricCellView } from "./cell.js";
@@ -37,21 +37,19 @@ function ExperimentAttemptRow({ attempt, last, attemptHref, }) {
37
37
  return (_jsxs("li", { className: cx("nre-experiment-attempt-row", `nre-eval-${attempt.verdict}`), children: [_jsx("span", { className: "nre-attempt-branch", "aria-hidden": "true", children: last ? "└─" : "├─" }), _jsx("span", { className: "nre-eval-attempt-badges", children: _jsx(AttemptLocatorBadge, { item: attempt, attemptHref: attemptHref }) }), _jsxs("span", { className: "nre-eval-attempt-metrics", children: [formatDurationMs(attempt.durationMs), attempt.costUSD !== undefined && _jsxs(_Fragment, { children: [" \u00B7 ", formatUSD(attempt.costUSD)] })] }), _jsx("span", { className: "nre-eval-reason", children: reason ?? "—" })] }));
38
38
  }
39
39
  function EvalAttempts({ row, attemptHref, locale, }) {
40
- return (_jsxs("li", { className: "nre-experiment-eval", children: [_jsxs("div", { className: cx("nre-experiment-eval-header", `nre-eval-${row.verdict}`), children: [_jsx("span", { className: cx("nre-eval-verdict", `nre-verdict-${row.verdict}`), children: verdictMark(row.verdict) }), _jsx("span", { className: "nre-eval-id", children: row.evalId }), _jsx("span", { className: "nre-eval-attempt-count", children: localeText(locale, "overview.attemptsCount", { n: row.attempts.length }) }), _jsx("span", { className: "nre-eval-summary", children: row.verdict === "passed"
41
- ? [formatDurationMs(row.duration.value ?? 0), row.cost.value === null ? undefined : formatUSD(row.cost.value)]
42
- .filter((value) => value !== undefined)
43
- .join(" · ")
44
- : row.reason })] }), _jsx("ul", { className: "nre-experiment-attempts", children: row.attempts.map((attempt, index) => (_jsx(ExperimentAttemptRow, { attempt: attempt, last: index === row.attempts.length - 1, attemptHref: attemptHref }, attempt.locator))) })] }));
40
+ const duration = row.duration.value === null ? localeText(locale, "cell.missing") : formatDurationMs(row.duration.value);
41
+ const cost = row.cost.value === null ? localeText(locale, "cell.missing") : formatUSD(row.cost.value);
42
+ return (_jsxs("li", { className: "nre-experiment-eval", children: [_jsxs("div", { className: cx("nre-experiment-eval-header", `nre-eval-${row.verdict}`), children: [_jsx("span", { className: cx("nre-eval-verdict", `nre-verdict-${row.verdict}`), children: verdictMark(row.verdict) }), _jsx("span", { className: "nre-eval-id", children: row.evalId }), _jsx("span", { className: "nre-eval-attempt-count", children: localeText(locale, "overview.attemptsCount", { n: row.attempts.length }) }), _jsxs("span", { className: "nre-eval-rollup", children: [localeText(locale, "entityList.average", { value: duration }), " · ", localeText(locale, "entityList.average", { value: cost })] })] }), _jsx("ul", { className: "nre-experiment-attempts", children: row.attempts.map((attempt, index) => (_jsx(ExperimentAttemptRow, { attempt: attempt, last: index === row.attempts.length - 1, attemptHref: attemptHref }, attempt.locator))) })] }));
45
43
  }
46
44
  function Flags({ flags, locale }) {
47
45
  if (!flags || Object.keys(flags).length === 0)
48
46
  return null;
49
47
  return (_jsxs("div", { className: "nre-experiment-flags", children: [_jsx("span", { children: localeText(locale, "experimentList.flags") }), Object.entries(flags).map(([key, value]) => (_jsxs("b", { children: [key, "=", String(value)] }, key)))] }));
50
48
  }
51
- function ExperimentRow({ item, attemptHref, locale, }) {
52
- return (_jsxs("details", { className: "nre-experiment-entry", children: [_jsxs("summary", { className: "nre-experiment-summary", children: [_jsxs("span", { className: "nre-experiment-name", "data-sort-value": item.experimentId, children: [_jsx("b", { className: cx("nre-experiment-id", "nre-key", colorClassForKey(item.experimentId)), children: item.experimentId }), _jsxs("small", { children: [localeText(locale, "overview.evalsCount", { n: item.evals }), item.attempts > item.evals ? ` · ${localeText(locale, "overview.attemptsCount", { n: item.attempts })}` : "", ` · ${formatDate(item.lastRunAt, locale)}`] })] }), _jsx("span", { "data-sort-value": item.model ?? "", children: item.model ?? localeText(locale, "experimentList.defaultModel") }), _jsx("span", { "data-sort-value": item.agent, children: item.agent }), _jsx("span", { className: "nre-num", "data-sort-value": item.duration.value ?? "", children: _jsx(MetricCellView, { cell: item.duration, locale: locale }) }), _jsx("span", { className: cx("nre-num", passRateTone(item.passRate.value)), "data-sort-value": item.passRate.value ?? "", children: _jsx(MetricCellView, { cell: item.passRate, locale: locale }) }), _jsx("span", { className: "nre-num", "data-sort-value": item.tokens.value ?? "", children: _jsx(MetricCellView, { cell: item.tokens, locale: locale }) }), _jsx("span", { className: "nre-num", "data-sort-value": item.cost.value ?? "", children: _jsx(MetricCellView, { cell: item.cost, locale: locale }) }), _jsx("span", { "data-sort-value": item.verdicts.passed, children: _jsx(VerdictSummary, { item: item, locale: locale }) })] }), _jsxs("div", { className: "nre-experiment-detail", children: [_jsx(Flags, { flags: item.flags, locale: locale }), _jsx("ul", { className: "nre-experiment-evals", children: item.evalRows.map((row) => (_jsx(EvalAttempts, { row: row, attemptHref: attemptHref, locale: locale }, row.evalId))) })] })] }));
49
+ function ExperimentRow({ item, attemptHref, locale, relativeTo, }) {
50
+ return (_jsxs("details", { className: "nre-experiment-entry", children: [_jsxs("summary", { className: "nre-experiment-summary", children: [_jsxs("span", { className: "nre-experiment-name", "data-sort-value": item.experimentId, children: [_jsx("b", { className: cx("nre-experiment-id", "nre-key", colorClassForKey(item.experimentId)), children: experimentDisplayName(item.experimentId, relativeTo) }), _jsxs("small", { children: [localeText(locale, "overview.evalsCount", { n: item.evals }), item.attempts > item.evals ? ` · ${localeText(locale, "overview.attemptsCount", { n: item.attempts })}` : "", ` · ${formatDate(item.lastRunAt, locale)}`] })] }), _jsx("span", { "data-sort-value": item.model ?? "", children: item.model ?? localeText(locale, "experimentList.defaultModel") }), _jsx("span", { "data-sort-value": item.agent, children: item.agent }), _jsx("span", { className: "nre-num", "data-sort-value": item.duration.value ?? "", children: _jsx(MetricCellView, { cell: item.duration, locale: locale }) }), _jsx("span", { className: cx("nre-num", passRateTone(item.passRate.value)), "data-sort-value": item.passRate.value ?? "", children: _jsx(MetricCellView, { cell: item.passRate, locale: locale }) }), _jsx("span", { className: "nre-num", "data-sort-value": item.tokens.value ?? "", children: _jsx(MetricCellView, { cell: item.tokens, locale: locale }) }), _jsx("span", { className: "nre-num", "data-sort-value": item.cost.value ?? "", children: _jsx(MetricCellView, { cell: item.cost, locale: locale }) }), _jsx("span", { "data-sort-value": item.verdicts.passed, children: _jsx(VerdictSummary, { item: item, locale: locale }) })] }), _jsxs("div", { className: "nre-experiment-detail", children: [_jsx(Flags, { flags: item.flags, locale: locale }), _jsx("ul", { className: "nre-experiment-evals", children: item.evalRows.map((row) => (_jsx(EvalAttempts, { row: row, attemptHref: attemptHref, locale: locale }, row.evalId))) })] })] }));
53
51
  }
54
- export function ExperimentList({ items, attemptHref, filter = false, className, locale = DEFAULT_REPORT_LOCALE, }) {
52
+ export function ExperimentList({ items, attemptHref, filter = false, className, locale = DEFAULT_REPORT_LOCALE, relativeTo, }) {
55
53
  const labels = [
56
54
  localeText(locale, "experimentList.experiment"),
57
55
  localeText(locale, "table.model"),
@@ -62,6 +60,6 @@ export function ExperimentList({ items, attemptHref, filter = false, className,
62
60
  localeText(locale, "experimentList.estimatedCost"),
63
61
  localeText(locale, "experimentList.result"),
64
62
  ];
65
- const board = (_jsxs("div", { className: "nre-experiment-table", children: [_jsx("div", { className: "nre-experiment-head", children: labels.map((label, index) => (_jsx("button", { type: "button", "data-nre-experiment-sort": index, className: index === 4 ? "nre-sort-desc" : undefined, children: label }, label))) }), items.length === 0 && _jsx("p", { className: "nre-experiment-list-empty", children: localeText(locale, "attemptList.empty") }), items.map((item) => (_jsx(ExperimentRow, { item: item, attemptHref: attemptHref, locale: locale }, item.experimentId)))] }));
63
+ const board = (_jsxs("div", { className: "nre-experiment-table", children: [_jsx("div", { className: "nre-experiment-head", children: labels.map((label, index) => (_jsx("button", { type: "button", "data-nre-experiment-sort": index, className: index === 4 ? "nre-sort-desc" : undefined, children: label }, label))) }), items.length === 0 && _jsx("p", { className: "nre-experiment-list-empty", children: localeText(locale, "attemptList.empty") }), items.map((item) => (_jsx(ExperimentRow, { item: item, attemptHref: attemptHref, locale: locale, relativeTo: relativeTo }, item.experimentId)))] }));
66
64
  return (_jsxs("div", { className: cx("nre", "nre-experiment-list", filter && "nre-experiment-table-wrap", className), children: [filter && (_jsx("input", { className: "nre-filter", "data-nre-experiment-filter": "", type: "search", placeholder: localeText(locale, "experimentList.filterPlaceholder") })), board] }));
67
65
  }
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { formatMetricValue } from "../format.js";
3
3
  import { DEFAULT_REPORT_LOCALE, countText, localeText, resolveMetricLabel } from "../locale.js";
4
- import { layoutLabelOffsets, niceTicks } from "./chart-math.js";
4
+ import { niceTicks, placePointLabels } from "./chart-math.js";
5
5
  import { colorClassForKey, seriesClassForKey } from "./colors.js";
6
6
  import { cx } from "./format.js";
7
7
  const WIDTH = 640;
@@ -74,22 +74,16 @@ export function MetricScatter({ data, pointHref, className, locale = DEFAULT_REP
74
74
  }
75
75
  for (const list of bySeries.values())
76
76
  list.sort((a, b) => a.xValue - b.xValue);
77
- // 直接标签的防撞布局:靠右的点把标签锚到左侧,重叠的往下推、补 leader line
78
- const positions = points.map((p) => {
79
- const anchorLeft = p.px >= MARGIN.left + PLOT_W * 0.72;
80
- return { cx: p.px, cy: p.py, anchorLeft, width: p.label.length * 6.4 + 10 };
81
- });
82
- const labelOffsets = layoutLabelOffsets(positions);
77
+ // 直接标签的候选位择优布局:锚向、避让方向都由布局按空间决定,画布边界含边距
78
+ const labels = placePointLabels(points.map((p) => ({ cx: p.px, cy: p.py, width: p.label.length * 6.4 + 10 })), { x0: 2, y0: 2, x1: WIDTH - 2, y1: HEIGHT - 2 });
83
79
  return (_jsxs("figure", { className: cx("nre", "nre-metric-scatter", className), children: [_jsxs("svg", { className: "nre-scatter-svg", viewBox: `0 0 ${WIDTH} ${HEIGHT}`, role: "img", "aria-label": `${axisLabel(xLabel, data.x)} × ${axisLabel(yLabel, data.y)}`, children: [_jsxs("g", { className: "nre-scatter-grid", children: [yScale.ticks.map((tick) => (_jsx("line", { x1: MARGIN.left, x2: MARGIN.left + PLOT_W, y1: yScale.scale(tick), y2: yScale.scale(tick) }, `gy${tick}`))), xScale.ticks.map((tick) => (_jsx("line", { y1: MARGIN.top, y2: MARGIN.top + PLOT_H, x1: xScale.scale(tick), x2: xScale.scale(tick) }, `gx${tick}`)))] }), _jsx("text", { className: "nre-scatter-better-hint", x: MARGIN.left + PLOT_W - 6, y: MARGIN.top + 14, textAnchor: "end", children: localeText(locale, "scatter.betterHint") }), _jsx("g", { className: "nre-scatter-axis nre-scatter-axis-y", children: yScale.ticks.map((tick) => (_jsx("text", { className: "nre-scatter-tick", x: MARGIN.left - 8, y: yScale.scale(tick) + 3, textAnchor: "end", children: formatMetricValue(tick, data.y.unit) }, `ay${tick}`))) }), _jsx("g", { className: "nre-scatter-axis nre-scatter-axis-x", children: xScale.ticks.map((tick) => (_jsx("text", { className: "nre-scatter-tick", x: xScale.scale(tick), y: MARGIN.top + PLOT_H + 16, textAnchor: "middle", children: formatMetricValue(tick, data.x.unit) }, `ax${tick}`))) }), _jsx("text", { className: "nre-scatter-xlabel", x: MARGIN.left + PLOT_W / 2, y: HEIGHT - 8, textAnchor: "middle", children: axisLabel(xLabel, data.x) }), _jsx("text", { className: "nre-scatter-ylabel", x: 14, y: MARGIN.top + PLOT_H / 2, textAnchor: "middle", transform: `rotate(-90 14 ${MARGIN.top + PLOT_H / 2})`, children: axisLabel(yLabel, data.y) }), seriesOrder.map((series) => {
84
80
  const list = bySeries.get(series);
85
81
  if (list.length < 2)
86
82
  return null;
87
83
  return (_jsx("polyline", { className: cx("nre-scatter-line", seriesClassForKey(series)), "data-series": series, points: list.map((p) => `${p.px},${p.py}`).join(" "), fill: "none" }, series));
88
84
  }), points.map((p, i) => {
89
- const { anchorLeft } = positions[i];
90
- const labelX = p.px + (anchorLeft ? -10 : 10);
91
- const labelY = p.py + 4 + labelOffsets[i];
92
- const group = (_jsxs("g", { className: cx("nre-scatter-point", p.series !== undefined ? seriesClassForKey(p.series) : "nre-series-none"), "data-key": p.key, children: [_jsx("title", { children: p.title }), labelOffsets[i] !== 0 && _jsx("line", { className: "nre-leader", x1: p.px, y1: p.py, x2: labelX, y2: labelY - 4 }), _jsx("circle", { className: "nre-scatter-hit", cx: p.px, cy: p.py, r: 12 }), _jsx("circle", { className: "nre-scatter-dot", cx: p.px, cy: p.py, r: 4.5 }), _jsx("text", { className: "nre-scatter-point-label", x: labelX, y: labelY, textAnchor: anchorLeft ? "end" : "start", children: p.label })] }));
85
+ const label = labels[i];
86
+ const group = (_jsxs("g", { className: cx("nre-scatter-point", p.series !== undefined ? seriesClassForKey(p.series) : "nre-series-none"), "data-key": p.key, children: [_jsx("title", { children: p.title }), label.leader && _jsx("line", { className: "nre-leader", x1: p.px, y1: p.py, x2: label.x, y2: label.y - 4 }), _jsx("circle", { className: "nre-scatter-hit", cx: p.px, cy: p.py, r: 12 }), _jsx("circle", { className: "nre-scatter-dot", cx: p.px, cy: p.py, r: 4.5 }), _jsx("text", { className: "nre-scatter-point-label", x: label.x, y: label.y, textAnchor: label.anchor, children: p.label })] }));
93
87
  const row = drawableRows[i];
94
88
  return pointHref ? (_jsx("a", { className: "nre-scatter-point-link", href: pointHref(row), children: group }, p.key)) : (_jsx("g", { children: group }, p.key));
95
89
  })] }), seriesOrder.length > 0 && (_jsx("figcaption", { className: "nre-scatter-legend", children: seriesOrder.map((series) => (_jsx("span", { className: cx("nre-legend-key", "nre-key", colorClassForKey(series)), children: series }, series))) })), missingNote] }));
@@ -1,14 +1,31 @@
1
1
  /** [min, max] 区间上生成 count 个左右的“整齐”刻度(Heckbert nice-numbers)。 */
2
2
  export declare function niceTicks(min: number, max: number, count?: number): number[];
3
- export interface LabelInput {
3
+ export interface PointLabelInput {
4
4
  cx: number;
5
5
  cy: number;
6
+ /** 标签文本的估算宽度(px)。 */
6
7
  width: number;
7
- /** true = 文字锚在点左侧(text-anchor: end),false = 锚在右侧(text-anchor: start)。 */
8
- anchorLeft: boolean;
8
+ }
9
+ /** 标签允许占用的画布范围(含坐标轴边距,标签可以借用空白边距)。 */
10
+ export interface LabelBounds {
11
+ x0: number;
12
+ y0: number;
13
+ x1: number;
14
+ y1: number;
15
+ }
16
+ export interface PlacedPointLabel {
17
+ /** <text> 的锚点坐标(y 为文字基线)。 */
18
+ x: number;
19
+ y: number;
20
+ anchor: "start" | "end" | "middle";
21
+ /** 标签不在点左右紧邻位:渲染层补 leader line 连回原点。 */
22
+ leader: boolean;
9
23
  }
10
24
  /**
11
- * 贪心地把互相重叠的直接标签往下推开(按 y 排序、逐个查重叠再下移),点本身位置不变。
12
- * 散点常见同分不同价的聚簇,这一步避免标签叠成一团糊字。
25
+ * 散点直接标签的候选位择优布局(docs/feature/reports/library.md「MetricScatter」):
26
+ * 每个标签在点四周由近及远的候选环上打分——与已放置标签的重叠、与任何数据点的重叠、
27
+ * 越出画布的面积、离点距离逐项累加,取代价最小的候选。存在无冲突候选时标签必不遮点、
28
+ * 不叠标签、不越界;全候选冲突时取重叠最小者,绝不丢标签。重合点簇因此向不同方向散开,
29
+ * 而不是单向堆叠压到下方的数据点上。
13
30
  */
14
- export declare function layoutLabelOffsets(items: LabelInput[], lineHeight?: number, padX?: number): number[];
31
+ export declare function placePointLabels(points: PointLabelInput[], bounds: LabelBounds): PlacedPointLabel[];