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.
- package/dist/agents/types.d.ts +67 -5
- package/dist/context/types.d.ts +32 -12
- package/dist/i18n/en.d.ts +54 -0
- package/dist/i18n/zh-CN.d.ts +55 -1
- package/dist/o11y/types.d.ts +16 -2
- package/dist/report/aggregate.d.ts +5 -3
- package/dist/report/aggregate.js +32 -5
- package/dist/report/built-ins/experiment-comparison.d.ts +39 -1
- package/dist/report/built-ins/experiment-comparison.js +116 -10
- package/dist/report/built-ins/index.d.ts +1 -0
- package/dist/report/built-ins/index.js +1 -1
- package/dist/report/components.d.ts +8 -2
- package/dist/report/components.js +3 -3
- package/dist/report/compute.d.ts +11 -18
- package/dist/report/compute.js +54 -34
- package/dist/report/flag.d.ts +16 -1
- package/dist/report/flag.js +19 -1
- package/dist/report/format.d.ts +16 -8
- package/dist/report/format.js +27 -12
- package/dist/report/index.d.ts +4 -3
- package/dist/report/index.js +5 -4
- package/dist/report/locale.d.ts +11 -2
- package/dist/report/locale.js +23 -5
- package/dist/report/metrics.d.ts +13 -1
- package/dist/report/metrics.js +65 -14
- package/dist/report/primitives.d.ts +6 -0
- package/dist/report/react/AttemptList.d.ts +2 -2
- package/dist/report/react/AttemptList.js +5 -6
- package/dist/report/react/EvalList.d.ts +1 -1
- package/dist/report/react/EvalList.js +0 -0
- package/dist/report/react/ExperimentComparison.d.ts +8 -0
- package/dist/report/react/ExperimentComparison.js +11 -0
- package/dist/report/react/ExperimentList.d.ts +2 -1
- package/dist/report/react/ExperimentList.js +8 -10
- package/dist/report/react/MetricScatter.js +5 -11
- package/dist/report/react/chart-math.d.ts +23 -6
- package/dist/report/react/chart-math.js +71 -19
- package/dist/report/react/fixtures.d.ts +3 -3
- package/dist/report/react/fixtures.js +21 -14
- package/dist/report/report.d.ts +5 -1
- package/dist/report/report.js +6 -2
- package/dist/report/text/faces.d.ts +1 -1
- package/dist/report/text/faces.js +42 -41
- package/dist/report/text/table.js +36 -5
- package/dist/report/types.d.ts +39 -21
- package/dist/results/types.d.ts +11 -0
- package/dist/runner/feedback/sink.d.ts +110 -0
- package/dist/runner/types.d.ts +513 -22
- package/dist/sandbox/docker.d.ts +23 -2
- package/dist/sandbox/e2b.d.ts +15 -1
- package/dist/sandbox/errors.d.ts +30 -3
- package/dist/sandbox/io-retry.d.ts +17 -0
- package/dist/sandbox/registry.d.ts +2 -0
- package/dist/sandbox/resolve.d.ts +18 -5
- package/dist/sandbox/retry.d.ts +11 -1
- package/dist/sandbox/types.d.ts +39 -5
- package/dist/sandbox/vercel.d.ts +7 -1
- package/dist/scoring/coverage.d.ts +30 -0
- package/dist/scoring/display.d.ts +21 -0
- package/dist/scoring/display.js +120 -0
- package/dist/scoring/types.d.ts +103 -20
- package/dist/shared/aggregate.d.ts +1 -0
- package/dist/shared/aggregate.js +3 -3
- package/dist/shared/types.d.ts +28 -0
- package/dist/tty-line.d.ts +0 -4
- package/dist/util.d.ts +23 -0
- package/docs-site/zh/concepts/adapter.mdx +22 -4
- package/docs-site/zh/concepts/experiment.mdx +1 -1
- package/docs-site/zh/concepts/overview.mdx +6 -6
- package/docs-site/zh/guides/agent-feedback-loop.mdx +28 -26
- package/docs-site/zh/guides/authoring.mdx +33 -0
- package/docs-site/zh/guides/ci-integration.mdx +23 -12
- package/docs-site/zh/guides/connect-your-agent.mdx +29 -3
- package/docs-site/zh/guides/custom-reports.mdx +29 -34
- package/docs-site/zh/guides/dataset-fanout.mdx +25 -3
- package/docs-site/zh/guides/debug-sandbox.mdx +57 -0
- package/docs-site/zh/guides/debugging.mdx +210 -0
- package/docs-site/zh/guides/experiments.mdx +10 -3
- package/docs-site/zh/guides/official-adapters.mdx +26 -2
- package/docs-site/zh/guides/publish-report.mdx +30 -16
- package/docs-site/zh/guides/report-components.mdx +42 -30
- package/docs-site/zh/guides/reporters.mdx +2 -2
- package/docs-site/zh/guides/results-data.mdx +17 -9
- package/docs-site/zh/guides/runner.mdx +17 -7
- package/docs-site/zh/guides/sandbox-agent.mdx +56 -7
- package/docs-site/zh/guides/sandbox-providers.mdx +257 -9
- package/docs-site/zh/guides/scoring-guide.mdx +4 -4
- package/docs-site/zh/guides/viewing-results.mdx +79 -36
- package/docs-site/zh/guides/write-experiment.mdx +5 -3
- package/docs-site/zh/guides/write-send.mdx +17 -1
- package/docs-site/zh/index.mdx +1 -1
- package/docs-site/zh/reference/builtin-agents.mdx +27 -0
- package/docs-site/zh/reference/capabilities.mdx +2 -2
- package/docs-site/zh/reference/cli.mdx +33 -7
- package/docs-site/zh/reference/define-agent.mdx +57 -4
- package/docs-site/zh/reference/define-config.mdx +1 -1
- package/docs-site/zh/reference/define-eval.mdx +42 -9
- package/docs-site/zh/reference/expect.mdx +26 -1
- package/package.json +5 -1
- package/src/agents/ai-sdk-otel.test.ts +1 -0
- package/src/agents/ai-sdk.test.ts +3 -0
- package/src/agents/ai-sdk.ts +3 -0
- package/src/agents/bub-install-spec.test.ts +34 -0
- package/src/agents/bub-install-spec.ts +32 -0
- package/src/agents/bub.ts +31 -32
- package/src/agents/claude-code.test.ts +130 -9
- package/src/agents/claude-code.ts +76 -4
- package/src/agents/codex.test.ts +189 -40
- package/src/agents/codex.ts +155 -14
- package/src/agents/coding-cli-versions.test.ts +15 -0
- package/src/agents/coding-cli-versions.ts +3 -0
- package/src/agents/index.ts +11 -0
- package/src/agents/langgraph.test.ts +204 -0
- package/src/agents/langgraph.ts +495 -0
- package/src/agents/marketplace.ts +85 -0
- package/src/agents/native-config.test.ts +179 -0
- package/src/agents/native-config.ts +267 -0
- package/src/agents/openai-compat.test.ts +1 -0
- package/src/agents/openclaw.test.ts +31 -0
- package/src/agents/openclaw.ts +171 -0
- package/src/agents/plugin-config.test.ts +1 -0
- package/src/agents/sdk-streams.test.ts +79 -0
- package/src/agents/sdk-streams.ts +55 -10
- package/src/agents/skills.test.ts +1 -0
- package/src/agents/streaming.test.ts +3 -9
- package/src/agents/types.ts +68 -5
- package/src/agents/ui-message-stream.test.ts +3 -0
- package/src/cli.ts +411 -108
- package/src/context/context.test.ts +51 -12
- package/src/context/context.ts +161 -29
- package/src/context/session.test.ts +1 -0
- package/src/context/session.ts +114 -6
- package/src/context/types.ts +30 -12
- package/src/define.test.ts +13 -8
- package/src/define.ts +25 -4
- package/src/expect/index.ts +53 -23
- package/src/i18n/en.ts +64 -2
- package/src/i18n/zh-CN.ts +65 -3
- package/src/o11y/cost.test.ts +1 -0
- package/src/o11y/execution-tree.test.ts +1 -20
- package/src/o11y/otlp/mappers/claude-code.test.ts +1 -0
- package/src/o11y/otlp/parse.test.ts +1 -0
- package/src/o11y/otlp/turn-otel.test.ts +1 -0
- package/src/o11y/parsers/bub.test.ts +1 -0
- package/src/o11y/parsers/claude-code.test.ts +1 -34
- package/src/o11y/parsers/openclaw.test.ts +154 -0
- package/src/o11y/parsers/openclaw.ts +310 -0
- package/src/o11y/prices.json +746 -311
- package/src/o11y/tool-names.test.ts +1 -0
- package/src/o11y/types.ts +16 -2
- package/src/report/aggregate.ts +34 -5
- package/src/report/built-in-user-parity.test.tsx +110 -153
- package/src/report/built-ins/experiment-comparison.tsx +173 -13
- package/src/report/built-ins/index.ts +6 -1
- package/src/report/components.tsx +9 -3
- package/src/report/compute.ts +70 -40
- package/src/report/dual-render.test.tsx +194 -67
- package/src/report/flag.ts +30 -2
- package/src/report/format.ts +35 -11
- package/src/report/index.ts +22 -4
- package/src/report/locale.ts +25 -5
- package/src/report/metrics.ts +67 -14
- package/src/report/primitives.tsx +6 -0
- package/src/report/react/AttemptList.tsx +6 -31
- package/src/report/react/EvalList.tsx +0 -0
- package/src/report/react/ExperimentComparison.tsx +68 -0
- package/src/report/react/ExperimentList.tsx +15 -9
- package/src/report/react/MetricScatter.tsx +12 -14
- package/src/report/react/chart-math.test.ts +85 -0
- package/src/report/react/chart-math.ts +101 -22
- package/src/report/react/enhance.js +33 -1
- package/src/report/react/fixtures.ts +24 -17
- package/src/report/react/render.test.tsx +9 -64
- package/src/report/react/styles.css +73 -2
- package/src/report/report.test.ts +306 -98
- package/src/report/report.ts +6 -2
- package/src/report/text/faces.ts +47 -43
- package/src/report/text/table.ts +42 -5
- package/src/report/types.ts +41 -21
- package/src/results/annotated-source.test.ts +62 -9
- package/src/results/annotated-source.ts +64 -6
- package/src/results/attempt-evidence.test.ts +9 -7
- package/src/results/attempt-evidence.ts +15 -8
- package/src/results/attempt-source.ts +6 -3
- package/src/results/copy.ts +145 -55
- package/src/results/host-equivalence.test.ts +8 -6
- package/src/results/index.ts +2 -0
- package/src/results/locator.test.ts +1 -22
- package/src/results/open.ts +7 -1
- package/src/results/publish.ts +149 -0
- package/src/results/results.test.ts +85 -51
- package/src/results/truncate.ts +90 -0
- package/src/results/types.ts +7 -0
- package/src/results/writer.ts +31 -13
- package/src/runner/attempt.test.ts +138 -7
- package/src/runner/attempt.ts +603 -104
- package/src/runner/discover.test.ts +47 -0
- package/src/runner/discover.ts +36 -2
- package/src/runner/eval-source.test.ts +1 -27
- package/src/runner/feedback/agent.test.ts +504 -0
- package/src/runner/feedback/agent.ts +409 -0
- package/src/runner/feedback/ci.test.ts +562 -0
- package/src/runner/feedback/ci.ts +401 -0
- package/src/runner/feedback/coordinator.test.ts +317 -0
- package/src/runner/feedback/coordinator.ts +397 -0
- package/src/runner/feedback/failure.ts +40 -0
- package/src/runner/feedback/human.test.ts +616 -0
- package/src/runner/feedback/human.ts +535 -0
- package/src/runner/feedback/index.ts +66 -0
- package/src/runner/feedback/io.ts +78 -0
- package/src/runner/feedback/profile.test.ts +50 -0
- package/src/runner/feedback/profile.ts +58 -0
- package/src/runner/feedback/reducer.test.ts +395 -0
- package/src/runner/feedback/reducer.ts +260 -0
- package/src/runner/feedback/renderer.ts +82 -0
- package/src/runner/feedback/sink.ts +203 -0
- package/src/runner/feedback/testing.ts +106 -0
- package/src/runner/ledger.test.ts +230 -0
- package/src/runner/ledger.ts +329 -0
- package/src/runner/report.test.ts +128 -3
- package/src/runner/report.ts +33 -9
- package/src/runner/reporters/artifacts.ts +8 -2
- package/src/runner/reporters/braintrust.test.ts +8 -7
- package/src/runner/reporters/braintrust.ts +9 -2
- package/src/runner/reporters/index.ts +2 -2
- package/src/runner/reporters/json.test.ts +162 -0
- package/src/runner/reporters/json.ts +35 -8
- package/src/runner/reporters/shared.ts +1 -5
- package/src/runner/run.test.ts +760 -3
- package/src/runner/run.ts +242 -36
- package/src/runner/sandbox-prep.ts +3 -42
- package/src/runner/timing.ts +158 -0
- package/src/runner/types.ts +518 -22
- package/src/sandbox/checkpoint.test.ts +55 -0
- package/src/sandbox/checkpoint.ts +29 -8
- package/src/sandbox/cli-commands.ts +407 -0
- package/src/sandbox/docker.ts +115 -16
- package/src/sandbox/e2b-agent-template.test.ts +56 -0
- package/src/sandbox/e2b-agent-template.ts +94 -0
- package/src/sandbox/e2b.ts +74 -9
- package/src/sandbox/errors.ts +111 -4
- package/src/sandbox/index.ts +2 -0
- package/src/sandbox/io-retry.test.ts +58 -0
- package/src/sandbox/io-retry.ts +45 -0
- package/src/sandbox/keep-registry.test.ts +86 -0
- package/src/sandbox/keep-registry.ts +142 -0
- package/src/sandbox/keep.ts +178 -0
- package/src/sandbox/paths.test.ts +1 -0
- package/src/sandbox/paths.ts +19 -8
- package/src/sandbox/registry.ts +20 -3
- package/src/sandbox/resolve.ts +76 -11
- package/src/sandbox/retry.test.ts +70 -0
- package/src/sandbox/retry.ts +46 -4
- package/src/sandbox/types.ts +44 -6
- package/src/sandbox/vercel.ts +43 -20
- package/src/scoring/collector.ts +60 -17
- package/src/scoring/coverage.ts +95 -0
- package/src/scoring/diff.ts +81 -0
- package/src/scoring/display.test.ts +121 -0
- package/src/scoring/display.ts +133 -0
- package/src/scoring/evidence.test.ts +189 -0
- package/src/scoring/judge.test.ts +142 -0
- package/src/scoring/judge.ts +15 -18
- package/src/scoring/scoped.ts +217 -50
- package/src/scoring/types.ts +117 -20
- package/src/scoring/verdict.ts +16 -4
- package/src/shared/aggregate.ts +3 -2
- package/src/shared/types.ts +31 -0
- package/src/show/compose.ts +2 -2
- package/src/show/index.ts +21 -1
- package/src/show/render.ts +619 -104
- package/src/show/show.test.ts +235 -19
- package/src/tty-line.ts +8 -26
- package/src/util.test.ts +1 -0
- package/src/util.ts +41 -0
- package/src/view/app/components/AttemptModal.tsx +153 -2
- package/src/view/app/components/CodeView.tsx +32 -11
- package/src/view/app/components/CopyControls.tsx +2 -2
- package/src/view/app/i18n.ts +6 -0
- package/src/view/app/lib/attempt-route.test.ts +1 -0
- package/src/view/app/lib/verdict.ts +7 -9
- package/src/view/artifact-serving.test.ts +2 -1
- package/src/view/client-dist/app.css +1 -1
- package/src/view/client-dist/app.js +17 -17
- package/src/view/data.test.ts +1 -0
- package/src/view/data.ts +11 -1
- package/src/view/index.ts +11 -0
- package/src/view/server.ts +2 -0
- package/src/view/styles.css +3 -0
- package/src/view/view-report.test.ts +6 -5
- package/src/runner/reporters/console.ts +0 -70
- package/src/runner/reporters/live.test.ts +0 -56
- package/src/runner/reporters/live.ts +0 -247
- package/src/runner/reporters/quiet.test.ts +0 -66
- package/src/runner/reporters/quiet.ts +0 -49
- package/src/runner/reporters/table.ts +0 -277
package/src/report/locale.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// 只覆盖组件自带的固定文案(verdict 词、缺数据、覆盖率角标、注脚、占位符等);
|
|
3
3
|
// 数据本身(display、维度键、warnings 的 message)不经这里。
|
|
4
4
|
// 刻意不 import src/i18n/(CLI 专用字典,locale 来源与 key 面完全不同);
|
|
5
|
-
// zh-CN
|
|
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": "
|
|
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": "
|
|
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": "总分",
|
package/src/report/metrics.ts
CHANGED
|
@@ -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)
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
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
|
|
2
|
-
// (assertions
|
|
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
|
-
|
|
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
|
-
{
|
|
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-
|
|
83
|
-
{
|
|
84
|
-
|
|
85
|
-
|
|
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))}>
|
|
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 刻度 + 网格线;每个点直接标注(
|
|
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 {
|
|
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
|
-
//
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
|
|
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
|
|
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
|
-
{/*
|
|
215
|
-
{
|
|
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={
|
|
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
|
+
});
|