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
@@ -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
 
7
7
  /** 报告渲染的 locale;默认 "en"(text 面缺省即 en,`niceeval show` 输出不变)。 */
8
8
  export type ReportLocale = "en" | "zh-CN";
@@ -34,7 +34,7 @@ const en = {
34
34
  "table.attempt": "attempt",
35
35
  "experimentList.experiment": "Experiment",
36
36
  "experimentList.avgDuration": "Avg duration",
37
- "experimentList.passRate": "Pass rate",
37
+ "experimentList.passRate": "End-to-end pass rate",
38
38
  "experimentList.tokens": "Tokens",
39
39
  "experimentList.estimatedCost": "Est. cost",
40
40
  "experimentList.result": "Result",
@@ -45,6 +45,7 @@ const en = {
45
45
  "experimentList.filterPlaceholder": "Filter experiments…",
46
46
  "experimentList.defaultModel": "default",
47
47
  "experimentList.flags": "Flags",
48
+ "entityList.average": "{value} avg",
48
49
  /** <Table> 压到下限仍放不下时,从右侧丢列并如实报数。 */
49
50
  "table.columnsHidden.one": "({n} more column not shown)",
50
51
  "table.columnsHidden.other": "({n} more columns not shown)",
@@ -52,7 +53,7 @@ const en = {
52
53
  "overview.snapshots": "Snapshots",
53
54
  "overview.evals": "Evals",
54
55
  "overview.attempts": "attempts",
55
- "overview.passRate": "Pass rate",
56
+ "overview.passRate": "End-to-end pass rate",
56
57
  "overview.totalCost": "Total cost",
57
58
  "overview.totalDuration": "Total duration",
58
59
  "overview.source": "Source: {n} snapshots",
@@ -64,6 +65,14 @@ const en = {
64
65
  /** GroupSummary 的 KPI dt 标签(其余字段复用 verdict.* / overview.* 已有 key)。 */
65
66
  "groupSummary.experiments": "Experiments",
66
67
 
68
+ "experimentComparison.groups": "Experiment groups",
69
+ "experimentComparison.group": "Experiment group",
70
+ "experimentComparison.results": "Results",
71
+ "experimentComparison.lastRun": "Last run",
72
+ "experimentComparison.selectGroup": "Select experiment group {group}",
73
+ "experimentComparison.command": "Details: {command}",
74
+ "experimentComparison.empty": "No experiment groups",
75
+
67
76
  "composedFrom.one": "composed from {n} run",
68
77
  "composedFrom.other": "composed from {n} runs",
69
78
  "latestRun": "latest {run}",
@@ -80,6 +89,7 @@ const en = {
80
89
  "attemptList.truncated": "and {n} more not shown",
81
90
  "attemptList.truncatedText": "({n} more not shown)",
82
91
  "attemptList.score": "score {score}",
92
+ "attemptList.unavailable": "unavailable",
83
93
  "attemptList.details": "details",
84
94
 
85
95
  "scoreboard.total": "Total",
@@ -123,7 +133,7 @@ const zhCN: Record<ReportMessageKey, string> = {
123
133
  "table.attempt": "Attempt",
124
134
  "experimentList.experiment": "实验",
125
135
  "experimentList.avgDuration": "平均耗时",
126
- "experimentList.passRate": "成功率",
136
+ "experimentList.passRate": "端到端成功率",
127
137
  "experimentList.tokens": "Tokens",
128
138
  "experimentList.estimatedCost": "预估成本",
129
139
  "experimentList.result": "结果",
@@ -134,13 +144,14 @@ const zhCN: Record<ReportMessageKey, string> = {
134
144
  "experimentList.filterPlaceholder": "筛选实验…",
135
145
  "experimentList.defaultModel": "默认",
136
146
  "experimentList.flags": "Flags",
147
+ "entityList.average": "平均 {value}",
137
148
  "table.columnsHidden.one": "(还有 {n} 列未列出)",
138
149
  "table.columnsHidden.other": "(还有 {n} 列未列出)",
139
150
 
140
151
  "overview.snapshots": "快照",
141
152
  "overview.evals": "题目",
142
153
  "overview.attempts": "尝试",
143
- "overview.passRate": "通过率",
154
+ "overview.passRate": "端到端成功率",
144
155
  "overview.totalCost": "总成本",
145
156
  "overview.totalDuration": "总耗时",
146
157
  "overview.source": "数据来源:{n} 个快照",
@@ -151,6 +162,14 @@ const zhCN: Record<ReportMessageKey, string> = {
151
162
 
152
163
  "groupSummary.experiments": "实验数",
153
164
 
165
+ "experimentComparison.groups": "实验组",
166
+ "experimentComparison.group": "实验组",
167
+ "experimentComparison.results": "结果",
168
+ "experimentComparison.lastRun": "最后运行",
169
+ "experimentComparison.selectGroup": "选择实验组 {group}",
170
+ "experimentComparison.command": "查看:{command}",
171
+ "experimentComparison.empty": "没有实验组",
172
+
154
173
  "composedFrom.one": "合成自 {n} 个 run",
155
174
  "composedFrom.other": "合成自 {n} 个 run",
156
175
  "latestRun": "最新 {run}",
@@ -166,6 +185,7 @@ const zhCN: Record<ReportMessageKey, string> = {
166
185
  "attemptList.truncated": "还有 {n} 条未列出",
167
186
  "attemptList.truncatedText": "(还有 {n} 条未列出)",
168
187
  "attemptList.score": "得分 {score}",
188
+ "attemptList.unavailable": "评不了",
169
189
  "attemptList.details": "详情",
170
190
 
171
191
  "scoreboard.total": "总分",
@@ -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 选择都算得出,内置报告
@@ -39,10 +42,58 @@ export function attemptCostUSD(result: EvalResult): number | null {
39
42
  return result.usage?.costUSD ?? result.estimatedCostUSD ?? null;
40
43
  }
41
44
 
42
- export const passRate = defineMetric({
43
- name: "pass-rate",
44
- label: { en: "Pass rate", "zh-CN": "成功率" },
45
- description: "Share of evals that passed (skipped attempts excluded).",
45
+ /**
46
+ * 条件答题质量:passed = 1,failed = 0,errored 记 null 不进分母。
47
+ * 这是「已形成可信判定」条件下的诊断指标,不能简称默认成功率
48
+ * (docs/feature/reports/library.md「内置指标」)
49
+ */
50
+ export const taskPassRate = defineMetric({
51
+ name: "task-pass-rate",
52
+ label: { en: "Task pass rate", "zh-CN": "可判定任务通过率" },
53
+ description: "Conditional task quality among attempts that formed a trustworthy verdict: passed = 1, failed = 0; errored is null.",
54
+ better: "higher",
55
+ unit: "%",
56
+ value(a) {
57
+ switch (a.result.verdict) {
58
+ case "passed":
59
+ return 1;
60
+ case "failed":
61
+ return 0;
62
+ default:
63
+ // errored = 没形成可信判定 → null 不进这个条件指标;skipped 同为 null。
64
+ return null;
65
+ }
66
+ },
67
+ });
68
+
69
+ /** 执行可靠性:跑到可判定(passed / failed)= 1,errored = 0;skipped → null。 */
70
+ export const executionReliability = defineMetric({
71
+ name: "execution-reliability",
72
+ label: { en: "Execution reliability", "zh-CN": "执行可靠性" },
73
+ description: "Execution reliability: reached a trustworthy verdict (passed / failed) = 1, errored = 0.",
74
+ better: "higher",
75
+ unit: "%",
76
+ value(a) {
77
+ switch (a.result.verdict) {
78
+ case "passed":
79
+ case "failed":
80
+ return 1;
81
+ case "errored":
82
+ return 0;
83
+ default:
84
+ return null; // skipped
85
+ }
86
+ },
87
+ });
88
+
89
+ /**
90
+ * 端到端合成:passed = 1,failed / errored = 0;哪边拖累用
91
+ * taskPassRate / executionReliability 拆开看。
92
+ */
93
+ export const endToEndPassRate = defineMetric({
94
+ name: "end-to-end-pass-rate",
95
+ label: { en: "End-to-end pass rate", "zh-CN": "端到端成功率" },
96
+ description: "End-to-end composite: passed = 1, failed / errored = 0. Split blame with taskPassRate and executionReliability.",
46
97
  better: "higher",
47
98
  unit: "%",
48
99
  value: (a) =>
@@ -62,9 +113,11 @@ export const examScore = defineMetric({
62
113
  // 字面实现会让条件空真成立、崩溃反而得满分 —— 交白卷是 0 分,不是缺数据,更不是满分。
63
114
  // failed 同理得 0:--strict 下被翻成 failed 的哪怕 soft 分不低也是 0(报告不重新判卷)。
64
115
  if (verdict !== "passed") return 0;
65
- const soft = assertions.filter((x) => x.severity === "soft");
116
+ // unavailable 没有分数:不计入均分分母(评不了 0 分;非 optional 的 unavailable
117
+ // 早已把 verdict 拖成 errored,走不到这个分支)。
118
+ const soft = assertions.filter((x) => x.severity === "soft" && x.outcome !== "unavailable");
66
119
  if (soft.length === 0) return 1;
67
- return soft.reduce((sum, x) => sum + x.score, 0) / soft.length;
120
+ return soft.reduce((sum, x) => sum + (x.outcome === "unavailable" ? 0 : x.score), 0) / soft.length;
68
121
  },
69
122
  });
70
123
 
@@ -128,6 +128,12 @@ export interface TableColumn {
128
128
  header: string;
129
129
  /** 对齐方向,默认 `"left"`;`"right"` 按显示宽度右对齐,数字列用。 */
130
130
  align?: ColumnAlign;
131
+ /**
132
+ * text 面:单元格折行后的最大物理行数,放不下的部分以 `…` 收口;省略则不限行数。
133
+ * 摘要类列(如比较列表的 Result)用它保证「格子是可扫读的预览」;完整值在下钻面。
134
+ * web 面不消费——网页的高度约束是组件自己的 CSS 决定。
135
+ */
136
+ maxLines?: number;
131
137
  }
132
138
 
133
139
  /** 一行的数据:身份键、已格式化的格子、可选的 attempt locator。 */
@@ -1,5 +1,5 @@
1
- // AttemptList:实体列表的叶子层——每项一个 Attempt,固定展示判定、断言、error、Judge 评语
2
- // (assertions detail/evidence)与证据引用(locator)。它不预设只看失败,
1
+ // AttemptList:实体列表的叶子层——每项一个 Attempt,固定展示判定、一条主失败摘要或 error
2
+ // 摘要与证据引用(locator)。完整 assertions / evidence 只经 locator 下钻。它不预设只看失败,
3
3
  // 报告作者过滤 items、用 .slice() 限量,total 让渲染面如实报告剩余数量,不静默截断。
4
4
  // ExperimentList / EvalList 的下钻数组是同一个 AttemptListItem[],这里的渲染逻辑因此
5
5
  // 也是它们展开区里"逐条 attempt"那一层的唯一实现(通过 AttemptRow 导出复用,不重写一遍)。
@@ -8,6 +8,7 @@ import type { ReactElement } from "react";
8
8
  import type { AttemptListItem } from "../types.ts";
9
9
  import type { AttemptLocator } from "../../results/locator.ts";
10
10
  import { DEFAULT_REPORT_LOCALE, localeText, type ReportLocale } from "../locale.ts";
11
+ import { attemptItemReason } from "../format.ts";
11
12
  import { colorClassForKey } from "./colors.ts";
12
13
  import { cx, formatDurationMs, formatUSD, verdictMark } from "./format.ts";
13
14
 
@@ -30,35 +31,16 @@ export function AttemptLocatorBadge({
30
31
  );
31
32
  }
32
33
 
33
- function AssertionRow({ assertion, locale }: { assertion: AttemptListItem["assertions"][number]; locale: ReportLocale }): ReactElement {
34
- return (
35
- <li className={cx("nre-assertion", `nre-assertion-${assertion.severity}`, !assertion.passed && "nre-assertion-failed")}>
36
- <span className="nre-assertion-severity">{assertion.severity}</span>
37
- <span className="nre-assertion-name">{assertion.name}</span>
38
- <span className="nre-assertion-score">
39
- {localeText(locale, "attemptList.score", { score: assertion.score })}
40
- </span>
41
- {(assertion.detail || assertion.evidence) && (
42
- <details className="nre-assertion-more">
43
- <summary>{localeText(locale, "attemptList.details")}</summary>
44
- {assertion.detail && <p className="nre-assertion-detail">{assertion.detail}</p>}
45
- {assertion.evidence && <blockquote className="nre-assertion-evidence">{assertion.evidence}</blockquote>}
46
- </details>
47
- )}
48
- </li>
49
- );
50
- }
51
-
52
- /** 一条 Attempt 的完整卡片:AttemptList 自己的一项,也是 EvalList 展开区里的一行。 */
34
+ /** 一条 Attempt 的比较卡片;完整 assertions 通过 locator 下钻,不在列表内展开。 */
53
35
  export function AttemptRow({
54
36
  item,
55
37
  attemptHref,
56
- locale = DEFAULT_REPORT_LOCALE,
57
38
  }: {
58
39
  item: AttemptListItem;
59
40
  attemptHref: (locator: AttemptLocator) => string;
60
41
  locale?: ReportLocale;
61
42
  }): ReactElement {
43
+ const reason = attemptItemReason(item);
62
44
  return (
63
45
  <li className={cx("nre-attempt", `nre-attempt-${item.verdict}`)}>
64
46
  <div className="nre-attempt-head">
@@ -70,14 +52,7 @@ export function AttemptRow({
70
52
  <span className="nre-attempt-duration">{formatDurationMs(item.durationMs)}</span>
71
53
  {item.costUSD !== undefined && <span className="nre-attempt-cost">{formatUSD(item.costUSD)}</span>}
72
54
  </div>
73
- {item.error && <p className="nre-attempt-error">{item.error}</p>}
74
- {item.assertions.length > 0 && (
75
- <ul className="nre-assertions">
76
- {item.assertions.map((a, i) => (
77
- <AssertionRow key={i} assertion={a} locale={locale} />
78
- ))}
79
- </ul>
80
- )}
55
+ {reason && <p className="nre-attempt-result">{reason}</p>}
81
56
  </li>
82
57
  );
83
58
  }
Binary file
@@ -0,0 +1,68 @@
1
+ // ExperimentComparison 的 web 面:完整持有所有可比组,组选择只切换已经独立计算好的 panel。
2
+ // 静态 HTML 用原生 <details> 保留每组完整内容;enhance.js 再把上方摘要卡变成单选切换。
3
+
4
+ import type { ReactElement } from "react";
5
+ import type { ExperimentComparisonData } from "../built-ins/experiment-comparison.tsx";
6
+ import { DEFAULT_REPORT_LOCALE, localeText, type ReportLocale } from "../locale.ts";
7
+ import { ExperimentList, MetricScatter } from "../components.tsx";
8
+ import { GroupSummary } from "./GroupSummary.tsx";
9
+ import { cx } from "./format.ts";
10
+
11
+ export function ExperimentComparisonView({
12
+ data,
13
+ className,
14
+ locale = DEFAULT_REPORT_LOCALE,
15
+ }: {
16
+ data: ExperimentComparisonData;
17
+ className?: string;
18
+ locale?: ReportLocale;
19
+ }): ReactElement {
20
+ if (data.groups.length === 0) {
21
+ return (
22
+ <div className={cx("nre", "nre-experiment-comparison", className)}>
23
+ <p className="nre-experiment-groups-empty">{localeText(locale, "experimentComparison.empty")}</p>
24
+ </div>
25
+ );
26
+ }
27
+
28
+ return (
29
+ <div className={cx("nre", "nre-experiment-comparison", className)} data-nre-experiment-groups>
30
+ <div
31
+ className="nre-experiment-group-tabs"
32
+ role="tablist"
33
+ aria-label={localeText(locale, "experimentComparison.groups")}
34
+ >
35
+ {data.groups.map((group, index) => (
36
+ <div
37
+ key={group.key}
38
+ className="nre-experiment-group-tab"
39
+ role="tab"
40
+ tabIndex={index === 0 ? 0 : -1}
41
+ aria-selected={index === 0 ? "true" : "false"}
42
+ aria-label={localeText(locale, "experimentComparison.selectGroup", { group: group.key })}
43
+ data-nre-experiment-group-select={index}
44
+ >
45
+ <strong className="nre-experiment-group-name">{group.key}</strong>
46
+ <GroupSummary data={group.summary} locale={locale} />
47
+ </div>
48
+ ))}
49
+ </div>
50
+
51
+ <div className="nre-experiment-group-panels">
52
+ {data.groups.map((group, index) => (
53
+ <details
54
+ key={group.key}
55
+ className="nre-experiment-group-panel"
56
+ data-nre-experiment-group-panel={index}
57
+ role="tabpanel"
58
+ open={index === 0}
59
+ >
60
+ <summary>{group.key}</summary>
61
+ <MetricScatter data={group.scatter} locale={locale} />
62
+ <ExperimentList items={group.experiments} filter locale={locale} relativeTo={group.key} />
63
+ </details>
64
+ ))}
65
+ </div>
66
+ </div>
67
+ );
68
+ }
@@ -5,7 +5,7 @@
5
5
  import type { ReactElement } from "react";
6
6
  import type { AttemptLocator } from "../../results/locator.ts";
7
7
  import type { AttemptListItem, ExperimentListEvalRow, ExperimentListItem } from "../types.ts";
8
- import { attemptItemReason } from "../format.ts";
8
+ import { attemptItemReason, experimentDisplayName } from "../format.ts";
9
9
  import { DEFAULT_REPORT_LOCALE, localeText, type ReportLocale } from "../locale.ts";
10
10
  import { AttemptLocatorBadge } from "./AttemptList.tsx";
11
11
  import { MetricCellView } from "./cell.tsx";
@@ -73,18 +73,18 @@ function EvalAttempts({
73
73
  attemptHref: (locator: AttemptLocator) => string;
74
74
  locale: ReportLocale;
75
75
  }): ReactElement {
76
+ const duration = row.duration.value === null ? localeText(locale, "cell.missing") : formatDurationMs(row.duration.value);
77
+ const cost = row.cost.value === null ? localeText(locale, "cell.missing") : formatUSD(row.cost.value);
76
78
  return (
77
79
  <li className="nre-experiment-eval">
78
80
  <div className={cx("nre-experiment-eval-header", `nre-eval-${row.verdict}`)}>
79
81
  <span className={cx("nre-eval-verdict", `nre-verdict-${row.verdict}`)}>{verdictMark(row.verdict)}</span>
80
82
  <span className="nre-eval-id">{row.evalId}</span>
81
83
  <span className="nre-eval-attempt-count">{localeText(locale, "overview.attemptsCount", { n: row.attempts.length })}</span>
82
- <span className="nre-eval-summary">
83
- {row.verdict === "passed"
84
- ? [formatDurationMs(row.duration.value ?? 0), row.cost.value === null ? undefined : formatUSD(row.cost.value)]
85
- .filter((value): value is string => value !== undefined)
86
- .join(" · ")
87
- : row.reason}
84
+ <span className="nre-eval-rollup">
85
+ {localeText(locale, "entityList.average", { value: duration })}
86
+ {" · "}
87
+ {localeText(locale, "entityList.average", { value: cost })}
88
88
  </span>
89
89
  </div>
90
90
  <ul className="nre-experiment-attempts">
@@ -117,16 +117,20 @@ function ExperimentRow({
117
117
  item,
118
118
  attemptHref,
119
119
  locale,
120
+ relativeTo,
120
121
  }: {
121
122
  item: ExperimentListItem;
122
123
  attemptHref: (locator: AttemptLocator) => string;
123
124
  locale: ReportLocale;
125
+ relativeTo?: string;
124
126
  }): ReactElement {
125
127
  return (
126
128
  <details className="nre-experiment-entry">
127
129
  <summary className="nre-experiment-summary">
128
130
  <span className="nre-experiment-name" data-sort-value={item.experimentId}>
129
- <b className={cx("nre-experiment-id", "nre-key", colorClassForKey(item.experimentId))}>{item.experimentId}</b>
131
+ <b className={cx("nre-experiment-id", "nre-key", colorClassForKey(item.experimentId))}>
132
+ {experimentDisplayName(item.experimentId, relativeTo)}
133
+ </b>
130
134
  <small>
131
135
  {localeText(locale, "overview.evalsCount", { n: item.evals })}
132
136
  {item.attempts > item.evals ? ` · ${localeText(locale, "overview.attemptsCount", { n: item.attempts })}` : ""}
@@ -167,12 +171,14 @@ export function ExperimentList({
167
171
  filter = false,
168
172
  className,
169
173
  locale = DEFAULT_REPORT_LOCALE,
174
+ relativeTo,
170
175
  }: {
171
176
  items: ExperimentListItem[];
172
177
  attemptHref: (locator: AttemptLocator) => string;
173
178
  filter?: boolean;
174
179
  className?: string;
175
180
  locale?: ReportLocale;
181
+ relativeTo?: string;
176
182
  }): ReactElement {
177
183
  const labels = [
178
184
  localeText(locale, "experimentList.experiment"),
@@ -200,7 +206,7 @@ export function ExperimentList({
200
206
  </div>
201
207
  {items.length === 0 && <p className="nre-experiment-list-empty">{localeText(locale, "attemptList.empty")}</p>}
202
208
  {items.map((item) => (
203
- <ExperimentRow key={item.experimentId} item={item} attemptHref={attemptHref} locale={locale} />
209
+ <ExperimentRow key={item.experimentId} item={item} attemptHref={attemptHref} locale={locale} relativeTo={relativeTo} />
204
210
  ))}
205
211
  </div>
206
212
  );
@@ -1,6 +1,7 @@
1
1
  // MetricScatter:质量 × 成本 frontier 的积木,内联 SVG、零图表库、零 hooks。
2
2
  // 轴向随 better:"lower" 的轴反向画,「好」的角落恒在右上(成本轴 $20 → $0 就是这么来的);
3
- // niceTicks 刻度 + 网格线;每个点直接标注(防撞布局,被挤开时补 leader line);
3
+ // niceTicks 刻度 + 网格线;每个点直接标注(placePointLabels 候选位择优:避开其它标签、
4
+ // 数据点与画布边界,离开左右紧邻位时补 leader line);
4
5
  // 同系列的点按 x 值排序连线,系列图例列在图下。x 或 y 为 null 的点不画,
5
6
  // 底部注脚如实报「n 个点缺数据」;hover 信息退化为 SVG <title>,不 hydrate 也在
6
7
  // (enhance.js 在场时升级为样式化 tooltip)。配色走类名(nre-series-cN)由 CSS 上色,
@@ -10,7 +11,7 @@ import type { ReactElement } from "react";
10
11
  import type { MetricColumn, ScatterData } from "../types.ts";
11
12
  import { formatMetricValue } from "../format.ts";
12
13
  import { DEFAULT_REPORT_LOCALE, countText, localeText, resolveMetricLabel, type ReportLocale } from "../locale.ts";
13
- import { layoutLabelOffsets, niceTicks } from "./chart-math.ts";
14
+ import { niceTicks, placePointLabels } from "./chart-math.ts";
14
15
  import { colorClassForKey, seriesClassForKey } from "./colors.ts";
15
16
  import { cx } from "./format.ts";
16
17
 
@@ -124,12 +125,11 @@ export function MetricScatter({
124
125
  }
125
126
  for (const list of bySeries.values()) list.sort((a, b) => a.xValue - b.xValue);
126
127
 
127
- // 直接标签的防撞布局:靠右的点把标签锚到左侧,重叠的往下推、补 leader line
128
- const positions = points.map((p) => {
129
- const anchorLeft = p.px >= MARGIN.left + PLOT_W * 0.72;
130
- return { cx: p.px, cy: p.py, anchorLeft, width: p.label.length * 6.4 + 10 };
131
- });
132
- const labelOffsets = layoutLabelOffsets(positions);
128
+ // 直接标签的候选位择优布局:锚向、避让方向都由布局按空间决定,画布边界含边距
129
+ const labels = placePointLabels(
130
+ points.map((p) => ({ cx: p.px, cy: p.py, width: p.label.length * 6.4 + 10 })),
131
+ { x0: 2, y0: 2, x1: WIDTH - 2, y1: HEIGHT - 2 },
132
+ );
133
133
 
134
134
  return (
135
135
  <figure className={cx("nre", "nre-metric-scatter", className)}>
@@ -202,20 +202,18 @@ export function MetricScatter({
202
202
  {/* 点:g 内带 <title>(无 JS 的原生 hover)、直接标签与 leader line;
203
203
  pointHref 时包普通 <a>,静态导出也能下钻 */}
204
204
  {points.map((p, i) => {
205
- const { anchorLeft } = positions[i];
206
- const labelX = p.px + (anchorLeft ? -10 : 10);
207
- const labelY = p.py + 4 + labelOffsets[i];
205
+ const label = labels[i];
208
206
  const group = (
209
207
  <g
210
208
  className={cx("nre-scatter-point", p.series !== undefined ? seriesClassForKey(p.series) : "nre-series-none")}
211
209
  data-key={p.key}
212
210
  >
213
211
  <title>{p.title}</title>
214
- {/* 标签被挤开时补一条 leader line,避免脱离原点看不出对应关系 */}
215
- {labelOffsets[i] !== 0 && <line className="nre-leader" x1={p.px} y1={p.py} x2={labelX} y2={labelY - 4} />}
212
+ {/* 标签不在紧邻位时补一条 leader line,避免脱离原点看不出对应关系 */}
213
+ {label.leader && <line className="nre-leader" x1={p.px} y1={p.py} x2={label.x} y2={label.y - 4} />}
216
214
  <circle className="nre-scatter-hit" cx={p.px} cy={p.py} r={12} />
217
215
  <circle className="nre-scatter-dot" cx={p.px} cy={p.py} r={4.5} />
218
- <text className="nre-scatter-point-label" x={labelX} y={labelY} textAnchor={anchorLeft ? "end" : "start"}>
216
+ <text className="nre-scatter-point-label" x={label.x} y={label.y} textAnchor={label.anchor}>
219
217
  {p.label}
220
218
  </text>
221
219
  </g>
@@ -0,0 +1,85 @@
1
+ // cases: docs/engineering/unit-tests/reports/cases.md
2
+ // 分区「MetricScatter 点标签布局(web 面)」:对 placePointLabels 直接断言标签框与点框的几何关系,不经 HTML。
3
+
4
+ import { describe, expect, it } from "vitest";
5
+ import { placePointLabels, type LabelBounds, type PlacedPointLabel, type PointLabelInput } from "./chart-math.ts";
6
+
7
+ const BOUNDS: LabelBounds = { x0: 2, y0: 2, x1: 638, y1: 358 };
8
+ const WIDTH = 90;
9
+
10
+ interface Box {
11
+ x0: number;
12
+ y0: number;
13
+ x1: number;
14
+ y1: number;
15
+ }
16
+
17
+ // 与契约声明的标签框一致:基线上方 10px、下方 2px,锚向决定水平延伸方向
18
+ function labelBox(l: PlacedPointLabel, width: number): Box {
19
+ const x0 = l.anchor === "start" ? l.x : l.anchor === "end" ? l.x - width : l.x - width / 2;
20
+ return { x0, y0: l.y - 10, x1: x0 + width, y1: l.y + 2 };
21
+ }
22
+
23
+ function dotBox(p: PointLabelInput): Box {
24
+ return { x0: p.cx - 6, y0: p.cy - 6, x1: p.cx + 6, y1: p.cy + 6 };
25
+ }
26
+
27
+ function overlaps(a: Box, b: Box): boolean {
28
+ return a.x0 < b.x1 && a.x1 > b.x0 && a.y0 < b.y1 && a.y1 > b.y0;
29
+ }
30
+
31
+ describe("placePointLabels", () => {
32
+ it("近重合点簇:标签框两两不叠、不压任何数据点(只向下推的级联会把第三个标签推到下方点上)", () => {
33
+ // 三点近重合 + 右下方另一点:down-only 布局把第三个标签推到 y∈[122,134]、x≥310,
34
+ // 正好压住 (330,128) 的点框;候选位择优必须避开
35
+ const points: PointLabelInput[] = [
36
+ { cx: 300, cy: 100, width: WIDTH },
37
+ { cx: 303, cy: 102, width: WIDTH },
38
+ { cx: 298, cy: 104, width: WIDTH },
39
+ { cx: 330, cy: 128, width: WIDTH },
40
+ ];
41
+ const labels = placePointLabels(points, BOUNDS);
42
+ const boxes = labels.map((l) => labelBox(l, WIDTH));
43
+
44
+ for (let i = 0; i < boxes.length; i++) {
45
+ for (let j = i + 1; j < boxes.length; j++) {
46
+ expect(overlaps(boxes[i], boxes[j]), `标签 ${i} 与标签 ${j} 重叠`).toBe(false);
47
+ }
48
+ for (let j = 0; j < points.length; j++) {
49
+ expect(overlaps(boxes[i], dotBox(points[j])), `标签 ${i} 压住点 ${j}`).toBe(false);
50
+ }
51
+ }
52
+ // 离开左右紧邻位的标签必须带 leader 标记,渲染层据此连回原点
53
+ for (let i = 0; i < labels.length; i++) {
54
+ const adjacent =
55
+ labels[i].y === points[i].cy + 4 &&
56
+ ((labels[i].anchor === "start" && labels[i].x === points[i].cx + 10) ||
57
+ (labels[i].anchor === "end" && labels[i].x === points[i].cx - 10));
58
+ expect(labels[i].leader, `标签 ${i} 的 leader 标记与位置不符`).toBe(!adjacent);
59
+ }
60
+ });
61
+
62
+ it("无冲突取右侧紧邻位不带 leader;右缘点锚到左侧紧邻位且标签框不越出画布", () => {
63
+ const sparse = placePointLabels(
64
+ [
65
+ { cx: 100, cy: 100, width: WIDTH },
66
+ { cx: 400, cy: 250, width: WIDTH },
67
+ ],
68
+ BOUNDS,
69
+ );
70
+ for (const [i, l] of sparse.entries()) {
71
+ expect(l.anchor).toBe("start");
72
+ expect(l.x).toBe([100, 400][i] + 10);
73
+ expect(l.leader).toBe(false);
74
+ }
75
+
76
+ const [edge] = placePointLabels([{ cx: 630, cy: 200, width: WIDTH }], BOUNDS);
77
+ const box = labelBox(edge, WIDTH);
78
+ expect(edge.anchor).toBe("end");
79
+ expect(edge.leader).toBe(false);
80
+ expect(box.x0).toBeGreaterThanOrEqual(BOUNDS.x0);
81
+ expect(box.x1).toBeLessThanOrEqual(BOUNDS.x1);
82
+ expect(box.y0).toBeGreaterThanOrEqual(BOUNDS.y0);
83
+ expect(box.y1).toBeLessThanOrEqual(BOUNDS.y1);
84
+ });
85
+ });