niceeval 0.6.1 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/INDEX.md +23 -23
- package/README.zh.md +6 -6
- package/dist/agents/types.d.ts +69 -7
- package/dist/context/types.d.ts +32 -12
- package/dist/i18n/en.d.ts +54 -0
- package/dist/i18n/zh-CN.d.ts +57 -3
- package/dist/o11y/types.d.ts +16 -2
- package/dist/report/aggregate.d.ts +32 -24
- package/dist/report/aggregate.js +158 -50
- package/dist/report/built-in/index.d.ts +2 -0
- package/dist/report/built-in/index.js +8 -0
- package/dist/report/components.d.ts +93 -160
- package/dist/report/components.js +377 -114
- package/dist/report/compute.d.ts +87 -81
- package/dist/report/compute.js +597 -417
- package/dist/report/flag.d.ts +32 -6
- package/dist/report/flag.js +92 -4
- package/dist/report/format.d.ts +19 -11
- package/dist/report/format.js +30 -13
- package/dist/report/index.d.ts +16 -16
- package/dist/report/index.js +20 -21
- package/dist/report/load.js +3 -2
- package/dist/report/locale.d.ts +57 -33
- package/dist/report/locale.js +122 -56
- package/dist/report/metrics.d.ts +23 -4
- package/dist/report/metrics.js +110 -25
- package/dist/report/primitives.d.ts +48 -15
- package/dist/report/primitives.js +135 -26
- package/dist/report/react/AttemptList.d.ts +9 -7
- package/dist/report/react/AttemptList.js +17 -10
- package/dist/report/react/DeltaTable.js +19 -18
- package/dist/report/react/EvalList.d.ts +4 -4
- package/dist/report/react/EvalList.js +0 -0
- package/dist/report/react/ExperimentComparison.d.ts +10 -0
- package/dist/report/react/ExperimentComparison.js +12 -0
- package/dist/report/react/ExperimentList.d.ts +4 -3
- package/dist/report/react/ExperimentList.js +17 -18
- package/dist/report/react/MetricBars.js +5 -4
- package/dist/report/react/MetricLine.js +12 -5
- package/dist/report/react/MetricMatrix.js +1 -1
- package/dist/report/react/MetricScatter.js +59 -28
- package/dist/report/react/MetricTable.js +2 -12
- package/dist/report/react/ScopeSummary.d.ts +10 -0
- package/dist/report/react/ScopeSummary.js +17 -0
- package/dist/report/react/Scoreboard.js +6 -6
- package/dist/report/react/cell.js +2 -2
- 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 +5 -9
- package/dist/report/react/fixtures.js +110 -147
- package/dist/report/react/index.d.ts +15 -5
- package/dist/report/react/index.js +18 -7
- package/dist/report/report.d.ts +137 -16
- package/dist/report/report.js +259 -28
- package/dist/report/text/faces.d.ts +17 -19
- package/dist/report/text/faces.js +253 -184
- package/dist/report/text/plot.js +1 -1
- package/dist/report/text/table.js +38 -7
- package/dist/report/tree.d.ts +90 -40
- package/dist/report/tree.js +252 -94
- package/dist/report/types.d.ts +247 -284
- package/dist/report/types.js +4 -3
- package/dist/report/web.d.ts +21 -5
- package/dist/report/web.js +42 -16
- package/dist/results/select.d.ts +38 -16
- package/dist/results/select.js +73 -25
- package/dist/results/types.d.ts +49 -14
- 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 +4 -2
- package/dist/shared/aggregate.js +8 -7
- 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/README.md +44 -0
- package/docs-site/zh/examples/ai-agent-application.mdx +63 -0
- package/docs-site/zh/examples/coding-agent-extensions.mdx +57 -0
- package/docs-site/zh/examples/index.mdx +50 -0
- package/docs-site/zh/{concepts → explanation}/adapter.mdx +31 -13
- package/docs-site/zh/{concepts → explanation}/assert.mdx +7 -7
- package/docs-site/zh/{concepts → explanation}/drive.mdx +8 -8
- package/docs-site/zh/{concepts → explanation}/evals.mdx +4 -4
- package/docs-site/zh/{concepts → explanation}/experiment.mdx +8 -8
- package/docs-site/zh/{concepts → explanation}/hitl.mdx +8 -8
- package/docs-site/zh/{concepts → explanation}/judge.mdx +5 -5
- package/docs-site/zh/{concepts → explanation}/overview.mdx +11 -11
- package/docs-site/zh/{guides → explanation}/runner.mdx +18 -8
- package/docs-site/zh/{concepts → explanation}/tier.mdx +6 -6
- package/docs-site/zh/{guides → how-to}/agent-feedback-loop.mdx +35 -33
- package/docs-site/zh/{guides → how-to}/authoring.mdx +35 -2
- package/docs-site/zh/{guides → how-to}/ci-integration.mdx +23 -12
- package/docs-site/zh/{guides → how-to}/connect-otel.mdx +6 -6
- package/docs-site/zh/{guides → how-to}/connect-your-agent.mdx +47 -21
- package/docs-site/zh/{guides → how-to}/custom-reports.mdx +34 -39
- package/docs-site/zh/{guides → how-to}/dataset-fanout.mdx +25 -3
- package/docs-site/zh/{guides → how-to}/experiments.mdx +12 -5
- package/docs-site/zh/how-to/publish-report.mdx +105 -0
- package/docs-site/zh/{guides → how-to}/reporters.mdx +2 -2
- package/docs-site/zh/{guides → how-to}/sandbox-agent.mdx +56 -7
- package/docs-site/zh/how-to/sandbox-providers.mdx +350 -0
- package/docs-site/zh/{guides → how-to}/scoring-guide.mdx +4 -4
- package/docs-site/zh/{guides → how-to}/viewing-results.mdx +82 -39
- package/docs-site/zh/{guides → how-to}/write-experiment.mdx +6 -4
- package/docs-site/zh/{guides → how-to}/write-send.mdx +30 -14
- package/docs-site/zh/index.mdx +24 -26
- package/docs-site/zh/introduction.mdx +8 -8
- package/docs-site/zh/reference/builtin-agents.mdx +32 -5
- package/docs-site/zh/reference/capabilities.mdx +8 -8
- package/docs-site/zh/reference/cli.mdx +40 -12
- package/docs-site/zh/reference/define-agent.mdx +58 -5
- 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/events.mdx +3 -3
- package/docs-site/zh/reference/expect.mdx +26 -1
- package/docs-site/zh/{guides → reference}/official-adapters.mdx +32 -8
- package/docs-site/zh/{guides → reference}/report-components.mdx +45 -33
- package/docs-site/zh/{guides → reference}/results-data.mdx +21 -13
- package/docs-site/zh/troubleshooting/debug-sandbox.mdx +57 -0
- package/docs-site/zh/troubleshooting/debugging.mdx +212 -0
- package/docs-site/zh/{quickstart.mdx → tutorials/quickstart.mdx} +5 -17
- package/package.json +10 -2
- 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 +13 -2
- 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/openai-compat.ts +1 -1
- 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/streaming.ts +2 -2
- package/src/agents/types.ts +71 -8
- package/src/agents/ui-message-stream.test.ts +3 -0
- package/src/cli.ts +446 -124
- package/src/context/context.test.ts +51 -12
- package/src/context/context.ts +162 -30
- package/src/context/session.test.ts +2 -1
- package/src/context/session.ts +115 -7
- 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 +81 -17
- package/src/i18n/zh-CN.ts +80 -17
- 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 +178 -61
- package/src/report/built-in/index.tsx +9 -0
- package/src/report/components.tsx +625 -279
- package/src/report/compute.ts +723 -491
- package/src/report/dual-render.test.tsx +741 -1024
- package/src/report/flag.ts +104 -12
- package/src/report/format.ts +32 -12
- package/src/report/index.ts +119 -46
- package/src/report/load.ts +3 -2
- package/src/report/locale.ts +136 -65
- package/src/report/metrics.ts +108 -25
- package/src/report/primitives.tsx +196 -45
- package/src/report/react/AttemptList.tsx +30 -43
- package/src/report/react/DeltaTable.tsx +63 -45
- package/src/report/react/EvalList.tsx +0 -0
- package/src/report/react/ExperimentComparison.tsx +73 -0
- package/src/report/react/ExperimentList.tsx +50 -32
- package/src/report/react/MetricBars.tsx +5 -4
- package/src/report/react/MetricLine.tsx +13 -8
- package/src/report/react/MetricMatrix.tsx +2 -2
- package/src/report/react/MetricScatter.tsx +86 -34
- package/src/report/react/MetricTable.tsx +4 -76
- package/src/report/react/ScopeSummary.tsx +86 -0
- package/src/report/react/Scoreboard.tsx +28 -10
- package/src/report/react/cell.tsx +2 -2
- 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 +89 -5
- package/src/report/react/fixtures.ts +114 -154
- package/src/report/react/index.tsx +24 -39
- package/src/report/react/render.test.tsx +138 -158
- package/src/report/react/styles.css +243 -82
- package/src/report/report.test.ts +779 -841
- package/src/report/report.ts +423 -41
- package/src/report/text/faces.ts +290 -193
- package/src/report/text/plot.ts +1 -1
- package/src/report/text/table.ts +44 -7
- package/src/report/tree.ts +362 -104
- package/src/report/types.ts +261 -271
- package/src/report/web.ts +63 -20
- package/src/results/annotated-source.test.ts +62 -9
- package/src/results/annotated-source.ts +64 -6
- package/src/results/attempt-evidence.test.ts +13 -11
- package/src/results/attempt-evidence.ts +20 -13
- package/src/results/attempt-source.ts +6 -3
- package/src/results/copy.ts +150 -60
- package/src/results/host-equivalence.test.ts +34 -20
- package/src/results/index.ts +12 -4
- package/src/results/locator.test.ts +1 -22
- package/src/results/open.ts +15 -5
- package/src/results/publish.ts +149 -0
- package/src/results/results.test.ts +89 -54
- package/src/results/select.ts +104 -34
- package/src/results/truncate.ts +90 -0
- package/src/results/types.ts +43 -14
- 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 +243 -37
- 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 +8 -6
- package/src/shared/types.ts +31 -0
- package/src/show/compose.ts +50 -67
- package/src/show/index.ts +127 -56
- package/src/show/render.ts +662 -131
- package/src/show/report-host.test.ts +188 -0
- package/src/show/report-host.ts +375 -0
- package/src/show/show.test.ts +320 -54
- package/src/tty-line.ts +8 -26
- package/src/util.test.ts +1 -0
- package/src/util.ts +41 -0
- package/src/view/app/App.test.tsx +69 -0
- package/src/view/app/App.tsx +144 -48
- package/src/view/app/components/AttemptModal.tsx +423 -11
- package/src/view/app/components/CodeView.tsx +41 -14
- package/src/view/app/components/CopyControls.tsx +2 -2
- package/src/view/app/i18n.ts +37 -17
- package/src/view/app/lib/attempt-route.test.ts +1 -0
- package/src/view/app/lib/verdict.ts +7 -9
- package/src/view/app/main.tsx +13 -8
- package/src/view/app/pages/{RunsPage.tsx → AttemptsPage.tsx} +6 -6
- package/src/view/app/types.ts +4 -1
- 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 +10 -3
- package/src/view/data.ts +155 -49
- package/src/view/index.ts +56 -41
- package/src/view/server.ts +37 -15
- package/src/view/shared/types.ts +34 -5
- package/src/view/styles.css +227 -0
- package/src/view/view-report.test.ts +167 -62
- package/dist/report/built-ins/experiment-comparison.d.ts +0 -1
- package/dist/report/built-ins/experiment-comparison.js +0 -13
- package/dist/report/built-ins/index.d.ts +0 -1
- package/dist/report/built-ins/index.js +0 -2
- package/dist/report/react/GroupSummary.d.ts +0 -8
- package/dist/report/react/GroupSummary.js +0 -8
- package/dist/report/react/RunOverview.d.ts +0 -8
- package/dist/report/react/RunOverview.js +0 -12
- package/docs-site/zh/example/ai-agent-application.mdx +0 -152
- package/docs-site/zh/example/claude-code-codex-plugin.mdx +0 -167
- package/docs-site/zh/example/claude-code-codex-skill.mdx +0 -152
- package/docs-site/zh/example/showcase.mdx +0 -39
- package/docs-site/zh/guides/publish-report.mdx +0 -91
- package/docs-site/zh/guides/sandbox-providers.mdx +0 -102
- package/src/report/built-in-user-parity.test.tsx +0 -640
- package/src/report/built-ins/experiment-comparison.tsx +0 -19
- package/src/report/built-ins/index.ts +0 -2
- package/src/report/react/GroupSummary.tsx +0 -66
- package/src/report/react/RunOverview.tsx +0 -109
- 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/docs-site/zh/{example/tier1-ai-sdk-v7.mdx → examples/integrations/ai-sdk-v7.mdx} +0 -0
- /package/docs-site/zh/{example/tier1-claude-sdk.mdx → examples/integrations/claude-sdk.mdx} +0 -0
- /package/docs-site/zh/{example/tier1-codex-sdk.mdx → examples/integrations/codex-sdk.mdx} +0 -0
- /package/docs-site/zh/{example/tier1-langgraph.mdx → examples/integrations/langgraph.mdx} +0 -0
- /package/docs-site/zh/{example/tier1-pi-sdk.mdx → examples/integrations/pi-sdk.mdx} +0 -0
- /package/docs-site/zh/{guides → how-to}/fixtures.mdx +0 -0
package/dist/report/compute.js
CHANGED
|
@@ -1,217 +1,152 @@
|
|
|
1
|
-
//
|
|
2
|
-
// (终值 + 渲染提示,不含公式);渲染面(web/text)只做展示。
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
// 见 components.tsx):配对打点即发现,泛化名不占顶层导出。
|
|
1
|
+
// 计算函数(*Data):ReportInput → 一份组件数据。跑在 Node 侧,产物是算好的、可序列化的
|
|
2
|
+
// 普通 JSON(终值 + 渲染提示,不含公式);渲染面(web/text)只做展示。
|
|
3
|
+
// 它们是双面组件解析面的具名形式(MetricTable / metricTableData),与组件成对导出,
|
|
4
|
+
// 只住在 niceeval/report(docs/feature/reports/library.md「数据计算与缓存边界」)。
|
|
6
5
|
//
|
|
7
6
|
// 共同约定(docs/feature/reports/architecture.md「指标聚合不变量」):
|
|
8
|
-
// - 第一参收
|
|
7
|
+
// - 第一参收 ReportInput = Scope | readonly Snapshot[];warnings 不进组件数据(宿主统一显示);
|
|
9
8
|
// - 聚合前按身份键去重(dedupeAttempts;missing-startedAt 不去重、如实保留、不透出警告);
|
|
10
9
|
// - null ≠ 0:缺数据不编数,覆盖率经 samples/total 如实暴露;
|
|
10
|
+
// - 显式传入的列表(questions / pairs / metrics)保留声明顺序,从数据发现的维度 domain
|
|
11
|
+
// 按稳定 key 字典序;
|
|
11
12
|
// - core 中立:只认 Metric / Dimension 接口,不出现具体 agent 名的分支。
|
|
13
|
+
import { comparabilityConfigOf, deepEqualJson } from "../results/select.js";
|
|
12
14
|
import { evalLevelStats, foldEvalVerdict } from "../shared/verdict.js";
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
* 内部纯函数,不导出、不进 index.ts:对外只经 `experimentRowMeta`(挑 verdicts)与
|
|
31
|
-
* `groupSummaryData`(挑全部字段,包成 `GroupSummaryData`)暴露,调用方拿不到 `Item[]`
|
|
32
|
-
* 本身,所以这里也不用担心被越权复用。
|
|
33
|
-
*/
|
|
34
|
-
function summarizeItems(items) {
|
|
35
|
-
const experimentIds = new Set();
|
|
36
|
-
for (const item of items)
|
|
37
|
-
experimentIds.add(experimentIdOf(item));
|
|
38
|
-
const byEval = new Map();
|
|
39
|
-
for (const item of items) {
|
|
40
|
-
const key = fullEvalKey(item);
|
|
41
|
-
const list = byEval.get(key);
|
|
42
|
-
if (list)
|
|
43
|
-
list.push(item);
|
|
44
|
-
else
|
|
45
|
-
byEval.set(key, [item]);
|
|
46
|
-
}
|
|
47
|
-
const stats = evalLevelStats(items.map((item) => ({ verdict: item.attempt.result.verdict, key: fullEvalKey(item) })), (r) => r.key);
|
|
48
|
-
// 折叠代表 attempt:每个已跑的 eval 挑一条与折叠判定一致的 attempt 做证据引用,
|
|
49
|
-
// skipped 的 eval 不进分母、不出证据。
|
|
50
|
-
const refs = [];
|
|
51
|
-
for (const group of byEval.values()) {
|
|
52
|
-
const verdict = foldEvalVerdict(group.map((item) => item.attempt.result));
|
|
53
|
-
if (verdict === "skipped")
|
|
54
|
-
continue;
|
|
55
|
-
const rep = group.find((item) => item.attempt.result.verdict === verdict) ?? group[0];
|
|
56
|
-
refs.push(locatorOf(rep));
|
|
57
|
-
}
|
|
58
|
-
let totalCostUSD = null;
|
|
59
|
-
for (const item of items) {
|
|
60
|
-
const cost = attemptCostUSD(item.attempt.result);
|
|
61
|
-
if (cost !== null)
|
|
62
|
-
totalCostUSD = (totalCostUSD ?? 0) + cost;
|
|
63
|
-
}
|
|
64
|
-
let lastRunAt;
|
|
65
|
-
for (const item of items) {
|
|
66
|
-
const startedAt = item.snapshot.startedAt;
|
|
67
|
-
if (lastRunAt === undefined || startedAt > lastRunAt)
|
|
68
|
-
lastRunAt = startedAt;
|
|
15
|
+
import { experimentGroupOf } from "../shared/aggregate.js";
|
|
16
|
+
import { assertUniqueMetricNames, axisValueOf, collectItems, computeCell, dimensionKey, dimensionName, evalGroupOf, evalIdOf, experimentIdOf, filterItems, fullEvalKey, groupItems, locatorOf, refDisplayKey, resolveInput, toColumn, } from "./aggregate.js";
|
|
17
|
+
import { attemptCostUSD, costUSD, durationMs, endToEndPassRate, examScore, tokens } from "./metrics.js";
|
|
18
|
+
import { formatMetricValue, formatPlainNumber, localizedDisplay } from "./format.js";
|
|
19
|
+
import { compactAssertionSummary, primaryAssertionSummary, summaryText } from "../scoring/display.js";
|
|
20
|
+
import { defineMetric } from "./metrics.js";
|
|
21
|
+
export async function metricTableData(input, options) {
|
|
22
|
+
assertUniqueMetricNames(options.columns, "metricTableData columns");
|
|
23
|
+
if (options.sort !== undefined) {
|
|
24
|
+
if (!options.columns.includes(options.sort)) {
|
|
25
|
+
throw new Error(`metricTableData sort must be one of the Metric instances passed in columns (got "${options.sort.name}"). ` +
|
|
26
|
+
"Pass the same imported instance in both places so the sorted column is visible in the table.");
|
|
27
|
+
}
|
|
28
|
+
if (options.sort.better === undefined) {
|
|
29
|
+
throw new Error(`metricTableData cannot sort by "${options.sort.name}": the metric declares no "better" direction, so there is no defined order. ` +
|
|
30
|
+
'Declare better: "higher" | "lower" on the metric, or drop sort to keep the lexicographic row order.');
|
|
31
|
+
}
|
|
69
32
|
}
|
|
70
|
-
return {
|
|
71
|
-
experiments: experimentIds.size,
|
|
72
|
-
evals: stats.evals,
|
|
73
|
-
attempts: items.length,
|
|
74
|
-
verdicts: { passed: stats.passed, failed: stats.failed, errored: stats.errored, skipped: stats.skipped },
|
|
75
|
-
refs,
|
|
76
|
-
ran: stats.passed + stats.failed + stats.errored,
|
|
77
|
-
totalCostUSD,
|
|
78
|
-
lastRunAt,
|
|
79
|
-
};
|
|
80
|
-
}
|
|
81
|
-
/**
|
|
82
|
-
* experiment 行的元信息:agent/model 身份(组内去重后拼接)+ eval 级折叠计票 + eval/attempt
|
|
83
|
-
* 数量与最后运行时间(summarizeItems,即 view 榜单 / ExperimentList 的同一套 foldEvalVerdict
|
|
84
|
-
* 口径)。其它行维度(agent/eval/自定义…)没有唯一身份,不携带。
|
|
85
|
-
*/
|
|
86
|
-
function experimentRowMeta(group) {
|
|
87
|
-
const agents = new Set();
|
|
88
|
-
const models = new Set();
|
|
89
|
-
for (const item of group) {
|
|
90
|
-
agents.add(item.attempt.result.agent);
|
|
91
|
-
const model = item.attempt.result.model ?? item.snapshot.model;
|
|
92
|
-
if (model !== undefined)
|
|
93
|
-
models.add(model);
|
|
94
|
-
}
|
|
95
|
-
const stats = summarizeItems(group);
|
|
96
|
-
return {
|
|
97
|
-
...(agents.size > 0 ? { agent: [...agents].join(", ") } : {}),
|
|
98
|
-
...(models.size > 0 ? { model: [...models].join(", ") } : {}),
|
|
99
|
-
verdicts: stats.verdicts,
|
|
100
|
-
evals: stats.evals,
|
|
101
|
-
attempts: stats.attempts,
|
|
102
|
-
...(stats.lastRunAt !== undefined ? { lastRunAt: stats.lastRunAt } : {}),
|
|
103
|
-
};
|
|
104
|
-
}
|
|
105
|
-
/**
|
|
106
|
-
* 一次 attempt 未通过的 gate 断言,原始声明顺序不变;soft 断言不参与判定,不算「失败原因」,
|
|
107
|
-
* 只影响得分,永不出现在这份列表里。`EvalList` / `ExperimentList` 的失败诊断与 `AttemptList`
|
|
108
|
-
* 的断言列表共用这同一份材料,保证同一个 attempt 在各处给出同一个原因。
|
|
109
|
-
*/
|
|
110
|
-
export function failingGateAssertions(result) {
|
|
111
|
-
return result.assertions.filter((a) => !a.passed && a.severity === "gate");
|
|
112
|
-
}
|
|
113
|
-
/**
|
|
114
|
-
* 一次 attempt 的失败原因文案,按优先级取第一个在场的:
|
|
115
|
-
* `error` → `skipReason` → 未通过的 gate 断言(原始声明顺序,`name`,detail 在场则
|
|
116
|
-
* `"name: detail"`,多条用「, 」连接)→ 都缺席则无原因(如某道题恰好没有失败信号)。
|
|
117
|
-
* soft 断言永不进入这份原因文案,soft 得分是独立概念,不与 reason 混用同一个字段。
|
|
118
|
-
*/
|
|
119
|
-
export function reasonFor(result) {
|
|
120
|
-
if (result.error !== undefined)
|
|
121
|
-
return result.error;
|
|
122
|
-
if (result.skipReason !== undefined)
|
|
123
|
-
return result.skipReason;
|
|
124
|
-
const gates = failingGateAssertions(result);
|
|
125
|
-
if (gates.length === 0)
|
|
126
|
-
return undefined;
|
|
127
|
-
return gates.map((a) => (a.detail ? `${a.name}: ${a.detail}` : a.name)).join(", ");
|
|
128
|
-
}
|
|
129
|
-
export async function tableData(input, opts) {
|
|
130
|
-
assertUniqueMetricNames(opts.columns, "MetricTable.data columns");
|
|
131
33
|
const { snapshots } = resolveInput(input);
|
|
132
|
-
const items = filterItems(collectItems(snapshots),
|
|
133
|
-
const groups = groupItems(items,
|
|
34
|
+
const items = filterItems(collectItems(snapshots), options.evals);
|
|
35
|
+
const groups = groupItems(items, options.rows);
|
|
134
36
|
const rows = [];
|
|
135
|
-
const sortCells = new Map();
|
|
136
37
|
for (const [key, group] of groups) {
|
|
137
38
|
const cells = {};
|
|
138
|
-
for (const metric of
|
|
39
|
+
for (const metric of options.columns)
|
|
139
40
|
cells[metric.name] = await computeCell(metric, group);
|
|
140
|
-
|
|
141
|
-
// sort 指标不在 columns 里时单独算一遍,只用于排序、不进输出
|
|
142
|
-
sortCells.set(key, cells[opts.sort.name] ?? (await computeCell(opts.sort, group)));
|
|
143
|
-
}
|
|
144
|
-
const meta = opts.rows === "experiment" ? experimentRowMeta(group) : {};
|
|
145
|
-
rows.push({
|
|
146
|
-
key,
|
|
147
|
-
cells,
|
|
148
|
-
...(Object.keys(meta).length > 0 ? { meta } : {}),
|
|
149
|
-
});
|
|
41
|
+
rows.push({ key, cells });
|
|
150
42
|
}
|
|
151
|
-
if (
|
|
152
|
-
const better =
|
|
43
|
+
if (options.sort) {
|
|
44
|
+
const better = options.sort.better ?? "higher";
|
|
45
|
+
const name = options.sort.name;
|
|
153
46
|
rows.sort((a, b) => {
|
|
154
|
-
const va =
|
|
155
|
-
const vb =
|
|
47
|
+
const va = a.cells[name]?.value ?? null;
|
|
48
|
+
const vb = b.cells[name]?.value ?? null;
|
|
156
49
|
if (va === null && vb === null)
|
|
157
|
-
return 0;
|
|
50
|
+
return a.key < b.key ? -1 : a.key > b.key ? 1 : 0;
|
|
158
51
|
if (va === null)
|
|
159
52
|
return 1; // 缺数据沉底
|
|
160
53
|
if (vb === null)
|
|
161
54
|
return -1;
|
|
162
|
-
|
|
55
|
+
const diff = better === "lower" ? va - vb : vb - va;
|
|
56
|
+
if (diff !== 0)
|
|
57
|
+
return diff;
|
|
58
|
+
return a.key < b.key ? -1 : a.key > b.key ? 1 : 0; // 稳定排序,同值以 key 收口
|
|
163
59
|
});
|
|
164
60
|
}
|
|
165
61
|
return {
|
|
166
|
-
|
|
167
|
-
columns:
|
|
62
|
+
rowDimension: dimensionName(options.rows),
|
|
63
|
+
columns: options.columns.map(toColumn),
|
|
168
64
|
rows,
|
|
169
65
|
};
|
|
170
66
|
}
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
//
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
}
|
|
67
|
+
export async function metricMatrixData(input, options) {
|
|
68
|
+
const { snapshots } = resolveInput(input);
|
|
69
|
+
const items = filterItems(collectItems(snapshots), options.evals);
|
|
70
|
+
// 稀疏分组:只有真有 attempt 的 (row, column) 组合成格;没有样本的格子不出现
|
|
71
|
+
const groups = new Map();
|
|
72
|
+
for (const item of items) {
|
|
73
|
+
const row = dimensionKey(options.rows, item);
|
|
74
|
+
const column = dimensionKey(options.columns, item);
|
|
75
|
+
const key = JSON.stringify([row, column]);
|
|
76
|
+
const group = groups.get(key);
|
|
77
|
+
if (group)
|
|
78
|
+
group.items.push(item);
|
|
79
|
+
else
|
|
80
|
+
groups.set(key, { row, column, items: [item] });
|
|
81
|
+
}
|
|
82
|
+
const ordered = [...groups.values()].sort((a, b) => (a.row < b.row ? -1 : a.row > b.row ? 1 : a.column < b.column ? -1 : a.column > b.column ? 1 : 0));
|
|
83
|
+
const cells = [];
|
|
84
|
+
for (const group of ordered) {
|
|
85
|
+
cells.push({ row: group.row, column: group.column, cell: await computeCell(options.cell, group.items) });
|
|
86
|
+
}
|
|
87
|
+
return {
|
|
88
|
+
rowDimension: dimensionName(options.rows),
|
|
89
|
+
columnDimension: dimensionName(options.columns),
|
|
90
|
+
metric: toColumn(options.cell),
|
|
91
|
+
cells,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
// ───────────────────────── 实体列表(experimentListData / evalListData / attemptListData)─────────────────────────
|
|
95
|
+
/**
|
|
96
|
+
* 一次 attempt 的单行结果摘要(Scoring display 契约):failed 取主失败断言摘要(不含
|
|
97
|
+
* "+N more",N 单独进 moreFailures),errored 取结构化 error 的一层摘要
|
|
98
|
+
* (phase · code · message),passed / skipped 为 null。
|
|
99
|
+
*/
|
|
100
|
+
function failureSummaryOf(result) {
|
|
101
|
+
if (result.verdict === "errored" && result.error !== undefined) {
|
|
102
|
+
const parts = [result.error.phase, result.error.code, result.error.message].filter((part) => typeof part === "string" && part.length > 0);
|
|
103
|
+
return { summary: summaryText(parts.join(" · ")), more: 0 };
|
|
104
|
+
}
|
|
105
|
+
if (result.verdict === "failed" || result.verdict === "errored") {
|
|
106
|
+
const primary = primaryAssertionSummary(result.assertions, result.verdict);
|
|
107
|
+
if (primary !== undefined) {
|
|
108
|
+
return {
|
|
109
|
+
summary: compactAssertionSummary({ ...primary, additionalFailures: 0 }),
|
|
110
|
+
more: primary.additionalFailures,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
if (result.verdict === "errored" && result.skipReason !== undefined) {
|
|
114
|
+
return { summary: summaryText(result.skipReason), more: 0 };
|
|
115
|
+
}
|
|
116
|
+
return { summary: null, more: 0 };
|
|
117
|
+
}
|
|
118
|
+
return { summary: null, more: 0 };
|
|
186
119
|
}
|
|
120
|
+
const identityRedact = (text) => text;
|
|
187
121
|
/** AttemptList / ExperimentList / EvalList 共用的叶子构造:一个 Item → 一个 AttemptListItem。 */
|
|
188
|
-
function attemptListItemOf(item, redact) {
|
|
122
|
+
async function attemptListItemOf(item, redact) {
|
|
189
123
|
const result = item.attempt.result;
|
|
190
|
-
const
|
|
124
|
+
const { summary, more } = failureSummaryOf(result);
|
|
191
125
|
return {
|
|
192
|
-
evalId: evalIdOf(item),
|
|
193
126
|
experimentId: experimentIdOf(item),
|
|
127
|
+
evalId: evalIdOf(item),
|
|
194
128
|
attempt: result.attempt,
|
|
195
129
|
agent: result.agent,
|
|
196
130
|
verdict: result.verdict,
|
|
197
|
-
|
|
198
|
-
|
|
131
|
+
failureSummary: summary === null ? null : redact(summary),
|
|
132
|
+
moreFailures: more,
|
|
133
|
+
examScore: await computeCell(examScore, [item]),
|
|
199
134
|
durationMs: result.durationMs,
|
|
200
|
-
|
|
135
|
+
costUSD: attemptCostUSD(result),
|
|
201
136
|
locator: locatorOf(item),
|
|
202
137
|
};
|
|
203
138
|
}
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
export async function attemptListData(input, opts) {
|
|
139
|
+
/** `attemptListData(input)`:每个 Attempt 一项,顺序取自 Scope 展平顺序(不重排)。 */
|
|
140
|
+
export async function attemptListData(input, options) {
|
|
207
141
|
const { snapshots } = resolveInput(input);
|
|
208
|
-
const redact =
|
|
142
|
+
const redact = options?.redact ?? identityRedact;
|
|
209
143
|
const items = collectItems(snapshots);
|
|
210
|
-
return items.map((item) => attemptListItemOf(item, redact));
|
|
144
|
+
return Promise.all(items.map((item) => attemptListItemOf(item, redact)));
|
|
211
145
|
}
|
|
212
|
-
/** `
|
|
213
|
-
export async function evalListData(input) {
|
|
146
|
+
/** `evalListData(input)`:每个 `experimentId + evalId` 一项,按 evalId 再按 experimentId 升序。 */
|
|
147
|
+
export async function evalListData(input, options) {
|
|
214
148
|
const { snapshots } = resolveInput(input);
|
|
149
|
+
const redact = options?.redact ?? identityRedact;
|
|
215
150
|
const items = collectItems(snapshots);
|
|
216
151
|
const groups = new Map();
|
|
217
152
|
for (const item of items) {
|
|
@@ -226,25 +161,45 @@ export async function evalListData(input) {
|
|
|
226
161
|
for (const group of groups.values()) {
|
|
227
162
|
const sorted = [...group].sort((a, b) => a.attempt.result.attempt - b.attempt.result.attempt);
|
|
228
163
|
const verdict = foldEvalVerdict(sorted.map((item) => item.attempt.result));
|
|
229
|
-
const
|
|
230
|
-
const attempts = sorted.map((item) => attemptListItemOf(item, identityRedact));
|
|
164
|
+
const attempts = await Promise.all(sorted.map((item) => attemptListItemOf(item, redact)));
|
|
231
165
|
out.push({
|
|
232
|
-
evalId: evalIdOf(sorted[0]),
|
|
233
166
|
experimentId: experimentIdOf(sorted[0]),
|
|
167
|
+
evalId: evalIdOf(sorted[0]),
|
|
234
168
|
verdict,
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
cost: await computeCell(costUSD, sorted),
|
|
169
|
+
examScore: await computeCell(examScore, sorted),
|
|
170
|
+
durationMs: await computeCell(durationMs, sorted),
|
|
171
|
+
costUSD: await computeCell(costUSD, sorted),
|
|
239
172
|
attempts,
|
|
240
173
|
});
|
|
241
174
|
}
|
|
242
175
|
out.sort((a, b) => a.evalId.localeCompare(b.evalId) || a.experimentId.localeCompare(b.experimentId));
|
|
243
176
|
return out;
|
|
244
177
|
}
|
|
245
|
-
/**
|
|
246
|
-
|
|
178
|
+
/**
|
|
179
|
+
* `experimentListData(input)`:每个 experiment 一项,展开到每道 Eval;初始按端到端成功率
|
|
180
|
+
* 从高到低(缺数据沉底,同分按 id)。一行只有一套 agent / model / flags 是输入约束:
|
|
181
|
+
* 宿主注入的 current() Scope 保证每个 experiment 只由可比性配置一致的快照拼成;作者自选
|
|
182
|
+
* Snapshot[] 时若同一 experiment 混入不一致的可比性配置,按完整用户反馈失败并指引——
|
|
183
|
+
* 看跨配置演化用 snapshot 维度或 MetricLine,不把两套配置拼成一行冒充单一配置。
|
|
184
|
+
*/
|
|
185
|
+
export async function experimentListData(input, options) {
|
|
247
186
|
const { snapshots } = resolveInput(input);
|
|
187
|
+
const redact = options?.redact ?? identityRedact;
|
|
188
|
+
// 可比性配置单义检查:同一 experiment 的输入快照必须共享一套可比性配置。
|
|
189
|
+
const configByExperiment = new Map();
|
|
190
|
+
for (const snapshot of snapshots) {
|
|
191
|
+
const config = comparabilityConfigOf(snapshot);
|
|
192
|
+
const existing = configByExperiment.get(snapshot.experimentId);
|
|
193
|
+
if (existing === undefined) {
|
|
194
|
+
configByExperiment.set(snapshot.experimentId, { snapshot, config });
|
|
195
|
+
}
|
|
196
|
+
else if (!deepEqualJson(existing.config, config)) {
|
|
197
|
+
throw new Error(`experimentListData got inconsistent comparability configs for experiment "${snapshot.experimentId}" ` +
|
|
198
|
+
`(snapshots ${existing.snapshot.startedAt} and ${snapshot.startedAt} differ in agent/model/reasoningEffort/flags/budget/timeoutMs/sandbox). ` +
|
|
199
|
+
"One row shows one configuration — it cannot honestly merge two. To chart evolution across configs, " +
|
|
200
|
+
'use the "snapshot" dimension or MetricLine; to show the current level, pass results.current() which selects a single config per experiment.');
|
|
201
|
+
}
|
|
202
|
+
}
|
|
248
203
|
const items = collectItems(snapshots);
|
|
249
204
|
const groups = groupItems(items, "experiment");
|
|
250
205
|
const out = [];
|
|
@@ -256,30 +211,26 @@ export async function experimentListData(input) {
|
|
|
256
211
|
for (const [evalId, evalItems] of evalGroups) {
|
|
257
212
|
const sorted = [...evalItems].sort((a, b) => a.attempt.result.attempt - b.attempt.result.attempt);
|
|
258
213
|
const verdict = foldEvalVerdict(sorted.map((item) => item.attempt.result));
|
|
259
|
-
const
|
|
260
|
-
const attempts = sorted.map((item) => attemptListItemOf(item, identityRedact));
|
|
214
|
+
const attempts = await Promise.all(sorted.map((item) => attemptListItemOf(item, redact)));
|
|
261
215
|
evalRows.push({
|
|
262
216
|
evalId,
|
|
263
217
|
verdict,
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
cost: await computeCell(costUSD, sorted),
|
|
218
|
+
durationMs: await computeCell(durationMs, sorted),
|
|
219
|
+
costUSD: await computeCell(costUSD, sorted),
|
|
267
220
|
attempts,
|
|
268
221
|
});
|
|
269
222
|
}
|
|
270
|
-
evalRows.sort((a, b) => a.evalId.localeCompare(b.evalId));
|
|
271
223
|
const experiment = newest.snapshot.experiment ?? newest.attempt.result.experiment;
|
|
224
|
+
const model = newest.attempt.result.model ?? newest.snapshot.model;
|
|
272
225
|
out.push({
|
|
273
226
|
experimentId,
|
|
274
|
-
agent: newest.snapshot.agent,
|
|
275
|
-
...(
|
|
276
|
-
? { model: newest.attempt.result.model ?? newest.snapshot.model }
|
|
277
|
-
: {}),
|
|
227
|
+
agent: newest.snapshot.agent || newest.attempt.result.agent,
|
|
228
|
+
...(model !== undefined ? { model } : {}),
|
|
278
229
|
...(experiment?.flags ? { flags: experiment.flags } : {}),
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
230
|
+
evalVerdicts: stats.verdicts,
|
|
231
|
+
endToEndPassRate: await computeCell(endToEndPassRate, group),
|
|
232
|
+
costUSD: await computeCell(costUSD, group),
|
|
233
|
+
durationMs: await computeCell(durationMs, group),
|
|
283
234
|
tokens: await computeCell(tokens, group),
|
|
284
235
|
evals: stats.evals,
|
|
285
236
|
attempts: stats.attempts,
|
|
@@ -287,314 +238,543 @@ export async function experimentListData(input) {
|
|
|
287
238
|
evalRows,
|
|
288
239
|
});
|
|
289
240
|
}
|
|
290
|
-
//
|
|
291
|
-
// 同分时按 experiment id 稳定排序。web 增强可临时重排,text 面沿用同一基准顺序。
|
|
241
|
+
// 初始态按端到端成功率(endToEndPassRate)从高到低,缺数据沉底;同分按 experiment id 稳定排序。
|
|
292
242
|
out.sort((a, b) => {
|
|
293
|
-
|
|
243
|
+
const va = a.endToEndPassRate.value;
|
|
244
|
+
const vb = b.endToEndPassRate.value;
|
|
245
|
+
if (va === null && vb === null)
|
|
294
246
|
return a.experimentId.localeCompare(b.experimentId);
|
|
295
|
-
if (
|
|
247
|
+
if (va === null)
|
|
296
248
|
return 1;
|
|
297
|
-
if (
|
|
249
|
+
if (vb === null)
|
|
298
250
|
return -1;
|
|
299
|
-
return
|
|
251
|
+
return vb - va || a.experimentId.localeCompare(b.experimentId);
|
|
300
252
|
});
|
|
301
253
|
return out;
|
|
302
254
|
}
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
255
|
+
// ───────────────────────── scopeSummaryData ─────────────────────────
|
|
256
|
+
/** costUSD 的求和投影:两级都 sum(题内多轮求和 + 跨题求和 = 全量求和),display 走 $。 */
|
|
257
|
+
const totalCostMetric = defineMetric({
|
|
258
|
+
name: "total-cost",
|
|
259
|
+
label: costUSD.label,
|
|
260
|
+
unit: "$",
|
|
261
|
+
value: costUSD.value,
|
|
262
|
+
aggregate: { perEval: "sum", acrossEvals: "sum" },
|
|
263
|
+
});
|
|
264
|
+
function tallyOf() {
|
|
265
|
+
return { passed: 0, failed: 0, errored: 0, skipped: 0 };
|
|
266
|
+
}
|
|
267
|
+
/** 一批 Item 的组级统计(experimentListData / scopeSummaryData 共用)。 */
|
|
268
|
+
function summarizeItems(items) {
|
|
269
|
+
const experimentIds = new Set();
|
|
270
|
+
for (const item of items)
|
|
271
|
+
experimentIds.add(experimentIdOf(item));
|
|
272
|
+
const stats = evalLevelStats(items.map((item) => ({ verdict: item.attempt.result.verdict, key: fullEvalKey(item) })), (r) => r.key);
|
|
273
|
+
let lastRunAt;
|
|
308
274
|
for (const item of items) {
|
|
309
|
-
const
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
const group = groups.get(key);
|
|
313
|
-
if (group)
|
|
314
|
-
group.items.push(item);
|
|
315
|
-
else
|
|
316
|
-
groups.set(key, { row, column, items: [item] });
|
|
317
|
-
}
|
|
318
|
-
const cells = [];
|
|
319
|
-
for (const group of groups.values()) {
|
|
320
|
-
cells.push({ row: group.row, column: group.column, cell: await computeCell(opts.cell, group.items) });
|
|
275
|
+
const startedAt = item.snapshot.startedAt;
|
|
276
|
+
if (lastRunAt === undefined || startedAt > lastRunAt)
|
|
277
|
+
lastRunAt = startedAt;
|
|
321
278
|
}
|
|
322
279
|
return {
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
280
|
+
experiments: experimentIds.size,
|
|
281
|
+
evals: stats.evals,
|
|
282
|
+
attempts: items.length,
|
|
283
|
+
verdicts: { passed: stats.passed, failed: stats.failed, errored: stats.errored, skipped: stats.skipped },
|
|
284
|
+
lastRunAt,
|
|
327
285
|
};
|
|
328
286
|
}
|
|
329
287
|
/**
|
|
330
|
-
*
|
|
331
|
-
*
|
|
332
|
-
*
|
|
333
|
-
* 没跑到的题挣 0 分但留在分母里,missing 如实报 —— 这是显式的考试契约,不是「null ≠ 0」的例外。
|
|
288
|
+
* `scopeSummaryData(input)`:范围摘要——快照时间窗、experiment / eval / attempt 数、
|
|
289
|
+
* 两级判定计票、端到端成功率与总成本(docs/feature/reports/library/summaries.md)。
|
|
290
|
+
* data 恒携带两级计票;成功率来自官方两级指标引擎,不从任一计票重算。
|
|
334
291
|
*/
|
|
335
|
-
export async function
|
|
292
|
+
export async function scopeSummaryData(input) {
|
|
336
293
|
const { snapshots } = resolveInput(input);
|
|
337
|
-
const
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
const match = evalPrefixPredicate(opts.evals);
|
|
341
|
-
const items = filterItems(collectItems(snapshots), opts.evals);
|
|
342
|
-
// 题集(固定分母):选中范围内、任一快照声明覆盖或实际出现过的全部题
|
|
343
|
-
const universe = new Set();
|
|
294
|
+
const items = collectItems(snapshots);
|
|
295
|
+
let earliest = null;
|
|
296
|
+
let latest = null;
|
|
344
297
|
for (const snapshot of snapshots) {
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
if (match(id))
|
|
350
|
-
universe.add(id);
|
|
298
|
+
if (earliest === null || snapshot.startedAt < earliest)
|
|
299
|
+
earliest = snapshot.startedAt;
|
|
300
|
+
if (latest === null || snapshot.startedAt > latest)
|
|
301
|
+
latest = snapshot.startedAt;
|
|
351
302
|
}
|
|
303
|
+
const stats = summarizeItems(items);
|
|
304
|
+
const attemptVerdicts = tallyOf();
|
|
352
305
|
for (const item of items)
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
const subjectOf = (id) => {
|
|
364
|
-
const known = subjectByEval.get(id);
|
|
365
|
-
if (known !== undefined)
|
|
366
|
-
return known;
|
|
367
|
-
if (subjectsDim === "eval")
|
|
368
|
-
return id;
|
|
369
|
-
if (subjectsDim === "evalGroup")
|
|
370
|
-
return evalGroupOf(id);
|
|
371
|
-
return "(unknown)";
|
|
306
|
+
attemptVerdicts[item.attempt.result.verdict] += 1;
|
|
307
|
+
return {
|
|
308
|
+
range: { earliestStartedAt: earliest, latestStartedAt: latest },
|
|
309
|
+
experiments: stats.experiments,
|
|
310
|
+
evals: stats.evals,
|
|
311
|
+
attempts: stats.attempts,
|
|
312
|
+
evalVerdicts: stats.verdicts,
|
|
313
|
+
attemptVerdicts,
|
|
314
|
+
endToEndPassRate: await computeCell(endToEndPassRate, items),
|
|
315
|
+
totalCostUSD: await computeCell(totalCostMetric, items),
|
|
372
316
|
};
|
|
317
|
+
}
|
|
318
|
+
// ───────────────────────── experimentComparisonData ─────────────────────────
|
|
319
|
+
/** 完整父路径是组键;没有父路径的 experiment 不能互相比,自己形成单例组。 */
|
|
320
|
+
export function experimentComparisonGroupKey(experimentId) {
|
|
321
|
+
return experimentGroupOf(experimentId) ?? experimentId;
|
|
322
|
+
}
|
|
323
|
+
/** 每组散点的唯一口径:默认 definition 与公开计算共用,不各写一份。 */
|
|
324
|
+
const COMPARISON_SCATTER_OPTIONS = {
|
|
325
|
+
points: "experiment",
|
|
326
|
+
series: "agent",
|
|
327
|
+
x: costUSD,
|
|
328
|
+
y: endToEndPassRate,
|
|
329
|
+
};
|
|
330
|
+
/**
|
|
331
|
+
* `experimentComparisonData(input)`:先把 input 按可比组分区(experiment id 的完整父路径),
|
|
332
|
+
* 再为每组分别计算 ScopeSummary、成本 × 端到端成功率散点和 ExperimentList——分区发生在任何
|
|
333
|
+
* 指标计算之前,组外 attempt 不可能污染该组的坐标尺度、series、成功率、成本、排序或缺数据计数。
|
|
334
|
+
*/
|
|
335
|
+
export async function experimentComparisonData(input) {
|
|
336
|
+
const { snapshots } = resolveInput(input);
|
|
337
|
+
const snapshotsByGroup = new Map();
|
|
338
|
+
for (const snapshot of snapshots) {
|
|
339
|
+
const key = experimentComparisonGroupKey(snapshot.experimentId);
|
|
340
|
+
const group = snapshotsByGroup.get(key);
|
|
341
|
+
if (group)
|
|
342
|
+
group.push(snapshot);
|
|
343
|
+
else
|
|
344
|
+
snapshotsByGroup.set(key, [snapshot]);
|
|
345
|
+
}
|
|
346
|
+
const groups = await Promise.all([...snapshotsByGroup.entries()]
|
|
347
|
+
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
|
|
348
|
+
.map(async ([key, groupSnapshots]) => {
|
|
349
|
+
const [summary, scatter, experiments] = await Promise.all([
|
|
350
|
+
scopeSummaryData(groupSnapshots),
|
|
351
|
+
metricScatterData(groupSnapshots, COMPARISON_SCATTER_OPTIONS),
|
|
352
|
+
experimentListData(groupSnapshots),
|
|
353
|
+
]);
|
|
354
|
+
return { key, summary, scatter, experiments };
|
|
355
|
+
}));
|
|
356
|
+
return { groups };
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* 固定题集分母:未跑题按 0 分计入 `notRun`,跑了但指标为 null 的题按 0 分计入 `unscorable`,
|
|
360
|
+
* 两个计数不合并——成绩单能回答「这 0 分是没去考还是考了判不了」。组件不从已观测 attempt
|
|
361
|
+
* 的并集猜分母;Scope 中题集之外的 eval 被忽略并计入 `ignoredEvals`。
|
|
362
|
+
*/
|
|
363
|
+
export async function scoreboardData(input, options) {
|
|
364
|
+
const questions = options.questions;
|
|
365
|
+
if (!Array.isArray(questions) || questions.length === 0) {
|
|
366
|
+
throw new Error("scoreboardData questions must be a non-empty list of eval ids: the fixed question set is the denominator, and an empty denominator makes no scoreboard. " +
|
|
367
|
+
"Pass the eval ids to grade, or filter your source list before passing it.");
|
|
368
|
+
}
|
|
369
|
+
const seen = new Set();
|
|
370
|
+
for (const q of questions) {
|
|
371
|
+
if (seen.has(q)) {
|
|
372
|
+
throw new Error(`scoreboardData questions contains "${q}" twice — each question is one denominator slot; remove the duplicate.`);
|
|
373
|
+
}
|
|
374
|
+
seen.add(q);
|
|
375
|
+
}
|
|
376
|
+
const fullMarks = options.fullMarks ?? 100;
|
|
377
|
+
if (!Number.isFinite(fullMarks) || fullMarks <= 0) {
|
|
378
|
+
throw new Error(`scoreboardData fullMarks must be a positive finite number (got ${String(fullMarks)}).`);
|
|
379
|
+
}
|
|
380
|
+
const weightEntries = Object.entries(options.weights ?? {});
|
|
381
|
+
for (const [prefix, weight] of weightEntries) {
|
|
382
|
+
if (prefix.length === 0) {
|
|
383
|
+
throw new Error('scoreboardData weights contains an empty prefix ""; weight prefixes must be non-empty eval id prefixes.');
|
|
384
|
+
}
|
|
385
|
+
if (!Number.isFinite(weight) || weight <= 0) {
|
|
386
|
+
throw new Error(`scoreboardData weight for prefix "${prefix}" must be a positive finite number (got ${String(weight)}).`);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
const scoreMetric = options.score ?? examScore;
|
|
390
|
+
const subjectOf = options.subject ?? evalGroupOf;
|
|
391
|
+
const { snapshots } = resolveInput(input);
|
|
392
|
+
const allItems = collectItems(snapshots);
|
|
393
|
+
const questionSet = new Set(questions);
|
|
394
|
+
const items = allItems.filter((item) => questionSet.has(evalIdOf(item)));
|
|
395
|
+
const ignored = new Set();
|
|
396
|
+
for (const item of allItems) {
|
|
397
|
+
const id = evalIdOf(item);
|
|
398
|
+
if (!questionSet.has(id))
|
|
399
|
+
ignored.add(id);
|
|
400
|
+
}
|
|
373
401
|
// 权重:最长前缀生效(排序后线性找第一个命中即最长)
|
|
374
|
-
const weights =
|
|
402
|
+
const weights = weightEntries
|
|
375
403
|
.map(([prefix, weight]) => ({ prefix, weight }))
|
|
376
404
|
.sort((a, b) => b.prefix.length - a.prefix.length);
|
|
377
405
|
const weightOf = (id) => weights.find((w) => id.startsWith(w.prefix))?.weight ?? 1;
|
|
378
|
-
const
|
|
406
|
+
const subjectByQuestion = new Map();
|
|
407
|
+
for (const id of questions) {
|
|
408
|
+
const subject = subjectOf(id);
|
|
409
|
+
if (typeof subject !== "string" || subject.length === 0) {
|
|
410
|
+
throw new Error(`scoreboardData subject("${id}") returned an empty value; every question must map to a non-empty subject name.`);
|
|
411
|
+
}
|
|
412
|
+
subjectByQuestion.set(id, subject);
|
|
413
|
+
}
|
|
414
|
+
const groups = groupItems(items, options.rows);
|
|
379
415
|
const rows = [];
|
|
380
416
|
for (const [key, group] of groups) {
|
|
381
|
-
|
|
382
|
-
const perSnapshot = new Map(); // evalId → 快照键 → 原始值
|
|
417
|
+
const byQuestion = new Map();
|
|
383
418
|
for (const item of group) {
|
|
384
|
-
const value = await evaluateMetric(scoreMetric, item.attempt);
|
|
385
|
-
if (value === null)
|
|
386
|
-
continue; // 测不了的 attempt 不进题得分;整题无样本 → missing
|
|
387
419
|
const id = evalIdOf(item);
|
|
388
|
-
const
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
perSnapshot.set(id, (bySnap = new Map()));
|
|
392
|
-
const bucket = bySnap.get(snapKey);
|
|
393
|
-
if (bucket)
|
|
394
|
-
bucket.push(value);
|
|
420
|
+
const list = byQuestion.get(id);
|
|
421
|
+
if (list)
|
|
422
|
+
list.push(item);
|
|
395
423
|
else
|
|
396
|
-
|
|
397
|
-
}
|
|
398
|
-
const perEvalAgg = scoreMetric.aggregate?.perEval ?? "mean";
|
|
399
|
-
const scoreByEval = new Map();
|
|
400
|
-
for (const [id, bySnap] of perSnapshot) {
|
|
401
|
-
const snapValues = [...bySnap.values()].map((values) => applyAggregator(perEvalAgg, values));
|
|
402
|
-
scoreByEval.set(id, snapValues.reduce((a, b) => a + b, 0) / snapValues.length);
|
|
424
|
+
byQuestion.set(id, [item]);
|
|
403
425
|
}
|
|
404
|
-
// 科目累计:固定分母 —— 没跑的题 0 分挣、留在分母、计入 missing
|
|
405
426
|
const subjects = new Map();
|
|
406
|
-
|
|
407
|
-
|
|
427
|
+
const totalRefs = new Set();
|
|
428
|
+
for (const id of questions) {
|
|
429
|
+
const subjectKey = subjectByQuestion.get(id);
|
|
408
430
|
let subject = subjects.get(subjectKey);
|
|
409
431
|
if (!subject) {
|
|
410
|
-
subjects.set(subjectKey, (subject = { key: subjectKey, earned: 0, possible: 0,
|
|
432
|
+
subjects.set(subjectKey, (subject = { key: subjectKey, earned: 0, possible: 0, questions: 0, notRun: 0, unscorable: 0, refs: new Set() }));
|
|
411
433
|
}
|
|
412
434
|
const weight = weightOf(id);
|
|
413
|
-
const got = scoreByEval.get(id);
|
|
414
|
-
subject.earned += (got ?? 0) * weight;
|
|
415
435
|
subject.possible += weight;
|
|
416
|
-
subject.
|
|
417
|
-
|
|
418
|
-
|
|
436
|
+
subject.questions += 1;
|
|
437
|
+
const questionItems = byQuestion.get(id);
|
|
438
|
+
if (questionItems === undefined) {
|
|
439
|
+
subject.notRun += 1;
|
|
440
|
+
continue;
|
|
441
|
+
}
|
|
442
|
+
for (const item of questionItems) {
|
|
443
|
+
const locator = locatorOf(item);
|
|
444
|
+
subject.refs.add(locator);
|
|
445
|
+
totalRefs.add(locator);
|
|
446
|
+
}
|
|
447
|
+
const cell = await computeCell(scoreMetric, questionItems);
|
|
448
|
+
if (cell.value === null) {
|
|
449
|
+
subject.unscorable += 1;
|
|
450
|
+
continue;
|
|
451
|
+
}
|
|
452
|
+
if (cell.value < 0 || cell.value > 1) {
|
|
453
|
+
throw new Error(`scoreboardData score metric "${scoreMetric.name}" produced ${cell.value} for eval "${id}" — scores must stay in [0, 1] so weighted totals stay auditable. Normalize the metric, or use a different score metric.`);
|
|
454
|
+
}
|
|
455
|
+
subject.earned += cell.value * weight;
|
|
419
456
|
}
|
|
420
457
|
let earned = 0;
|
|
421
458
|
let possible = 0;
|
|
459
|
+
let notRun = 0;
|
|
460
|
+
let unscorable = 0;
|
|
422
461
|
for (const subject of subjects.values()) {
|
|
423
462
|
earned += subject.earned;
|
|
424
463
|
possible += subject.possible;
|
|
464
|
+
notRun += subject.notRun;
|
|
465
|
+
unscorable += subject.unscorable;
|
|
425
466
|
}
|
|
426
467
|
const value = possible === 0 ? 0 : (fullMarks * earned) / possible;
|
|
427
|
-
rows.push({ key, total: { value, display: formatPlainNumber(value) }, subjects: [...subjects.values()] });
|
|
428
|
-
}
|
|
429
|
-
return { dimension: dimensionName(opts.rows), fullMarks, weights, rows };
|
|
430
|
-
}
|
|
431
|
-
export async function scatterData(input, opts) {
|
|
432
|
-
const { snapshots } = resolveInput(input);
|
|
433
|
-
const items = collectItems(snapshots);
|
|
434
|
-
const groups = groupItems(items, opts.points);
|
|
435
|
-
const rows = [];
|
|
436
|
-
for (const [key, group] of groups) {
|
|
437
468
|
rows.push({
|
|
438
469
|
key,
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
470
|
+
total: {
|
|
471
|
+
value,
|
|
472
|
+
display: formatPlainNumber(value),
|
|
473
|
+
notRun,
|
|
474
|
+
unscorable,
|
|
475
|
+
refs: [...totalRefs].sort(),
|
|
476
|
+
},
|
|
477
|
+
subjects: [...subjects.values()].map((subject) => ({
|
|
478
|
+
key: subject.key,
|
|
479
|
+
earned: subject.earned,
|
|
480
|
+
possible: subject.possible,
|
|
481
|
+
questions: subject.questions,
|
|
482
|
+
notRun: subject.notRun,
|
|
483
|
+
unscorable: subject.unscorable,
|
|
484
|
+
display: subjectDisplay(subject.earned, subject.possible),
|
|
485
|
+
refs: [...subject.refs].sort(),
|
|
486
|
+
})),
|
|
443
487
|
});
|
|
444
488
|
}
|
|
445
489
|
return {
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
490
|
+
rowDimension: dimensionName(options.rows),
|
|
491
|
+
questions: [...questions],
|
|
492
|
+
fullMarks,
|
|
493
|
+
weights,
|
|
494
|
+
ignoredEvals: ignored.size,
|
|
450
495
|
rows,
|
|
451
496
|
};
|
|
452
497
|
}
|
|
453
|
-
/**
|
|
454
|
-
|
|
498
|
+
/** 分科显示:earned / possible 与同尺度百分比。 */
|
|
499
|
+
function subjectDisplay(earned, possible) {
|
|
500
|
+
const ratio = possible === 0 ? 0 : earned / possible;
|
|
501
|
+
return `${formatPlainNumber(earned)}/${formatPlainNumber(possible)} (${formatMetricValue(ratio, "%")})`;
|
|
502
|
+
}
|
|
503
|
+
export async function metricScatterData(input, options) {
|
|
455
504
|
const { snapshots } = resolveInput(input);
|
|
456
|
-
const items = collectItems(snapshots);
|
|
457
|
-
const groups = groupItems(items,
|
|
505
|
+
const items = filterItems(collectItems(snapshots), options.evals);
|
|
506
|
+
const groups = groupItems(items, options.points);
|
|
458
507
|
const rows = [];
|
|
459
508
|
for (const [key, group] of groups) {
|
|
460
|
-
const x = flagAxisValue(opts.x, group[0]); // flag 是 experiment 级声明,组内一致
|
|
461
509
|
rows.push({
|
|
462
510
|
key,
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
y: await computeCell(
|
|
511
|
+
// 组内取第一条解析系列:点维度细于系列维度时(experiment ⊂ agent)天然一致
|
|
512
|
+
...(options.series ? { series: dimensionKey(options.series, group[0]) } : {}),
|
|
513
|
+
x: await computeCell(options.x, group),
|
|
514
|
+
y: await computeCell(options.y, group), // 任一轴 null 的点留在 rows 里:组件不画,但注脚要报的数就从这里数
|
|
467
515
|
});
|
|
468
516
|
}
|
|
469
517
|
return {
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
},
|
|
475
|
-
series: opts.series ? dimensionName(opts.series) : undefined,
|
|
476
|
-
y: toColumn(opts.y),
|
|
518
|
+
pointDimension: dimensionName(options.points),
|
|
519
|
+
...(options.series ? { seriesDimension: dimensionName(options.series) } : {}),
|
|
520
|
+
x: toColumn(options.x),
|
|
521
|
+
y: toColumn(options.y),
|
|
477
522
|
rows,
|
|
478
523
|
};
|
|
479
524
|
}
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
525
|
+
/**
|
|
526
|
+
* 点身份 = (series, x):落进同一桶的全部 attempt 先在各自 experiment × eval 内 perEval 聚合,
|
|
527
|
+
* 再 acrossEvals 跨题折成该点唯一的 y——聚合顺序是 (series, x, experiment, eval),同一桶里有
|
|
528
|
+
* 多个 experiment 时它们合成一个点,不画垂直来回线。前提是 x 在同一 experiment × eval 内恒定:
|
|
529
|
+
* 自定义 NumericAxis.of() 对同一 experiment × eval 的不同 attempt 返回不同值时按完整用户反馈失败。
|
|
530
|
+
* x 为 null 的 attempt 不伪造 x 值,归入该 series 的未绘制行,组件报告未绘制数量。
|
|
531
|
+
*/
|
|
532
|
+
export async function metricLineData(input, options) {
|
|
533
|
+
const { snapshots } = resolveInput(input);
|
|
534
|
+
const items = filterItems(collectItems(snapshots), options.evals);
|
|
535
|
+
// x 恒定性检查:同一 experiment × eval 内的全部 attempt 必须得到同一个 x。
|
|
536
|
+
const xByEvalKey = new Map();
|
|
537
|
+
const buckets = new Map();
|
|
492
538
|
for (const item of items) {
|
|
493
|
-
const
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
break;
|
|
505
|
-
case "skipped":
|
|
506
|
-
skipped += 1;
|
|
507
|
-
break;
|
|
539
|
+
const x = axisValueOf(options.x, item.attempt);
|
|
540
|
+
const evalKey = fullEvalKey(item);
|
|
541
|
+
const existing = xByEvalKey.get(evalKey);
|
|
542
|
+
if (existing === undefined) {
|
|
543
|
+
xByEvalKey.set(evalKey, { x, item });
|
|
544
|
+
}
|
|
545
|
+
else if (!Object.is(existing.x, x)) {
|
|
546
|
+
throw new Error(`Numeric axis "${options.x.name}" is not constant within experiment "${experimentIdOf(item)}" × eval "${evalIdOf(item)}" ` +
|
|
547
|
+
`(got ${String(existing.x)} and ${String(x)} for different attempts). A parameter axis must describe the configuration, ` +
|
|
548
|
+
"not vary per attempt — a per-attempt quantity is material for the y metric, not an x axis. " +
|
|
549
|
+
"Fix of() to read experiment-level configuration (numericFlag()/numericRunConfig() do this by construction).");
|
|
508
550
|
}
|
|
509
|
-
|
|
510
|
-
const
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
551
|
+
const series = options.series ? dimensionKey(options.series, item) : undefined;
|
|
552
|
+
const bucketKey = `${series ?? ""}${x === null ? "null" : String(x)}`;
|
|
553
|
+
const bucket = buckets.get(bucketKey);
|
|
554
|
+
if (bucket)
|
|
555
|
+
bucket.items.push(item);
|
|
556
|
+
else
|
|
557
|
+
buckets.set(bucketKey, { series, x, items: [item] });
|
|
558
|
+
}
|
|
559
|
+
const ordered = [...buckets.values()].sort((a, b) => {
|
|
560
|
+
const sa = a.series ?? "";
|
|
561
|
+
const sb = b.series ?? "";
|
|
562
|
+
if (sa !== sb)
|
|
563
|
+
return sa < sb ? -1 : 1;
|
|
564
|
+
if (a.x === null)
|
|
565
|
+
return b.x === null ? 0 : 1;
|
|
566
|
+
if (b.x === null)
|
|
567
|
+
return -1;
|
|
568
|
+
return a.x - b.x;
|
|
569
|
+
});
|
|
570
|
+
const rows = [];
|
|
571
|
+
for (const bucket of ordered) {
|
|
572
|
+
rows.push({
|
|
573
|
+
key: bucket.x === null ? "null" : String(bucket.x),
|
|
574
|
+
...(bucket.series !== undefined ? { series: bucket.series } : {}),
|
|
575
|
+
x: bucket.x,
|
|
576
|
+
xDisplay: bucket.x === null ? "—" : formatMetricValue(bucket.x, options.x.unit),
|
|
577
|
+
y: await computeCell(options.y, bucket.items),
|
|
578
|
+
});
|
|
579
|
+
}
|
|
517
580
|
return {
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
startedAt: s.startedAt,
|
|
523
|
-
})),
|
|
524
|
-
totals: {
|
|
525
|
-
evals: evalIds.size,
|
|
526
|
-
attempts: items.length,
|
|
527
|
-
passed,
|
|
528
|
-
failed,
|
|
529
|
-
errored,
|
|
530
|
-
skipped,
|
|
531
|
-
passRate: passRateCell,
|
|
532
|
-
costUSD,
|
|
533
|
-
durationMs,
|
|
581
|
+
x: {
|
|
582
|
+
key: options.x.name,
|
|
583
|
+
label: options.x.label ?? options.x.name,
|
|
584
|
+
...(options.x.unit !== undefined ? { unit: options.x.unit } : {}),
|
|
534
585
|
},
|
|
535
|
-
|
|
586
|
+
...(options.series ? { seriesDimension: dimensionName(options.series) } : {}),
|
|
587
|
+
y: toColumn(options.y),
|
|
588
|
+
rows,
|
|
536
589
|
};
|
|
537
590
|
}
|
|
538
|
-
// ─────────────────────────
|
|
591
|
+
// ───────────────────────── deltaTableData 与 pairsByFlag ─────────────────────────
|
|
539
592
|
/**
|
|
540
|
-
*
|
|
541
|
-
*
|
|
542
|
-
*
|
|
543
|
-
* 按 experiment 组前缀 filter 出来的那份)——本函数不再自己分组。
|
|
593
|
+
* 按 flag 派生 A/B 对(docs/feature/reports/library/metric-views.md「DeltaTable」):
|
|
594
|
+
* 配对域 = 同可比组 + 删除该 flag 后可比性配置深相等;a 取 baseline(缺省 = 未声明该 flag),
|
|
595
|
+
* b 侧该 flag 的每个其它取值各成一对;label 自动 `<a 末段> · <flag>=<显示键>`。
|
|
544
596
|
*/
|
|
545
|
-
export
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
const ratio = summary.ran > 0 ? summary.verdicts.passed / summary.ran : null; // 分母为 0 → 缺数据,不编 0%
|
|
550
|
-
const passRateCell = {
|
|
551
|
-
value: ratio,
|
|
552
|
-
display: ratio === null ? "—" : formatMetricValue(ratio, "%"),
|
|
553
|
-
samples: summary.ran,
|
|
554
|
-
total: summary.evals,
|
|
555
|
-
refs: summary.refs,
|
|
556
|
-
};
|
|
597
|
+
export function pairsByFlag(name, options) {
|
|
598
|
+
if (typeof name !== "string" || name.length === 0) {
|
|
599
|
+
throw new Error("pairsByFlag: name must be a non-empty string (the key declared in the experiment's flags).");
|
|
600
|
+
}
|
|
557
601
|
return {
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
verdicts: summary.verdicts,
|
|
562
|
-
passRate: passRateCell,
|
|
563
|
-
totalCostUSD: summary.totalCostUSD,
|
|
564
|
-
...(summary.lastRunAt !== undefined ? { lastRunAt: summary.lastRunAt } : {}),
|
|
602
|
+
kind: "flagPairs",
|
|
603
|
+
flag: name,
|
|
604
|
+
...(options?.baseline !== undefined ? { baseline: options.baseline } : {}),
|
|
565
605
|
};
|
|
566
606
|
}
|
|
567
|
-
|
|
568
|
-
|
|
607
|
+
function isFlagPairs(pairs) {
|
|
608
|
+
return typeof pairs === "object" && pairs !== null && !Array.isArray(pairs) && pairs.kind === "flagPairs";
|
|
609
|
+
}
|
|
610
|
+
/** experiment id 相对可比组的末段。 */
|
|
611
|
+
function experimentTail(experimentId) {
|
|
612
|
+
const slash = experimentId.lastIndexOf("/");
|
|
613
|
+
return slash === -1 ? experimentId : experimentId.slice(slash + 1);
|
|
614
|
+
}
|
|
615
|
+
/** 派生配对:同可比组 + 删除该 flag 后可比性配置深相等。返回 pair 列表与配对域实验数。 */
|
|
616
|
+
function derivePairsByFlag(snapshots, spec) {
|
|
617
|
+
// 每个 experiment 取最新快照的配置(current() Scope 天然一实验一快照)。
|
|
618
|
+
const byExperiment = new Map();
|
|
619
|
+
for (const snapshot of snapshots) {
|
|
620
|
+
const existing = byExperiment.get(snapshot.experimentId);
|
|
621
|
+
if (existing === undefined || snapshot.startedAt > existing.startedAt) {
|
|
622
|
+
byExperiment.set(snapshot.experimentId, snapshot);
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
const entries = [];
|
|
626
|
+
for (const [id, snapshot] of byExperiment) {
|
|
627
|
+
const config = comparabilityConfigOf(snapshot);
|
|
628
|
+
const flagValue = config.flags?.[spec.flag];
|
|
629
|
+
const reduced = { ...config, flags: { ...config.flags } };
|
|
630
|
+
delete reduced.flags[spec.flag];
|
|
631
|
+
const group = experimentGroupOf(id) ?? id;
|
|
632
|
+
entries.push({ id, flagValue, bucket: `${group}${JSON.stringify(sortedJson(reduced))}` });
|
|
633
|
+
}
|
|
634
|
+
const baseline = spec.baseline; // undefined = 未声明该 flag 的实验作 a
|
|
635
|
+
const buckets = new Map();
|
|
636
|
+
for (const entry of entries) {
|
|
637
|
+
const list = buckets.get(entry.bucket);
|
|
638
|
+
if (list)
|
|
639
|
+
list.push(entry);
|
|
640
|
+
else
|
|
641
|
+
buckets.set(entry.bucket, [entry]);
|
|
642
|
+
}
|
|
643
|
+
const pairs = [];
|
|
644
|
+
for (const bucket of buckets.values()) {
|
|
645
|
+
const aSide = bucket.filter((e) => deepEqualJson(e.flagValue, baseline));
|
|
646
|
+
const bSide = bucket.filter((e) => !deepEqualJson(e.flagValue, baseline));
|
|
647
|
+
for (const a of aSide) {
|
|
648
|
+
for (const b of bSide) {
|
|
649
|
+
pairs.push({
|
|
650
|
+
a: a.id,
|
|
651
|
+
b: b.id,
|
|
652
|
+
label: `${experimentTail(a.id)} · ${spec.flag}=${refDisplayKey(b.flagValue)[0]}`,
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
pairs.sort((p, q) => {
|
|
658
|
+
const ta = experimentTail(p.a);
|
|
659
|
+
const tb = experimentTail(q.a);
|
|
660
|
+
if (ta !== tb)
|
|
661
|
+
return ta < tb ? -1 : 1;
|
|
662
|
+
const la = p.label;
|
|
663
|
+
const lb = q.label;
|
|
664
|
+
return la < lb ? -1 : la > lb ? 1 : 0;
|
|
665
|
+
});
|
|
666
|
+
return { pairs, experiments: byExperiment.size };
|
|
667
|
+
}
|
|
668
|
+
/** 对象键递归排序(派生配对的 bucket 键用;undefined 字段剔除)。 */
|
|
669
|
+
function sortedJson(value) {
|
|
670
|
+
if (Array.isArray(value))
|
|
671
|
+
return value.map(sortedJson);
|
|
672
|
+
if (typeof value === "object" && value !== null) {
|
|
673
|
+
const out = {};
|
|
674
|
+
for (const key of Object.keys(value).sort()) {
|
|
675
|
+
const v = value[key];
|
|
676
|
+
if (v !== undefined)
|
|
677
|
+
out[key] = sortedJson(v);
|
|
678
|
+
}
|
|
679
|
+
return out;
|
|
680
|
+
}
|
|
681
|
+
return value;
|
|
682
|
+
}
|
|
683
|
+
export async function deltaTableData(input, options) {
|
|
684
|
+
assertUniqueMetricNames(options.metrics, "deltaTableData metrics");
|
|
685
|
+
if (!Array.isArray(options.metrics) || options.metrics.length === 0) {
|
|
686
|
+
throw new Error("deltaTableData metrics must be a non-empty tuple of Metric instances.");
|
|
687
|
+
}
|
|
569
688
|
const { snapshots } = resolveInput(input);
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
689
|
+
let pairs;
|
|
690
|
+
let experiments;
|
|
691
|
+
if (isFlagPairs(options.pairs)) {
|
|
692
|
+
if (options.by !== "experiment") {
|
|
693
|
+
throw new Error(`deltaTableData pairs came from pairsByFlag("${options.pairs.flag}"), which derives experiment A/B pairs — it only works with by: "experiment" (got by: ${JSON.stringify(dimensionName(options.by))}). Set by: "experiment", or write literal pairs for other dimensions.`);
|
|
694
|
+
}
|
|
695
|
+
const derived = derivePairsByFlag(snapshots, options.pairs);
|
|
696
|
+
pairs = derived.pairs;
|
|
697
|
+
experiments = derived.experiments;
|
|
698
|
+
}
|
|
699
|
+
else {
|
|
700
|
+
if (!Array.isArray(options.pairs)) {
|
|
701
|
+
throw new Error("deltaTableData pairs must be an array of { label, a, b } or a pairsByFlag(...) declaration.");
|
|
702
|
+
}
|
|
703
|
+
if (options.pairs.length === 0) {
|
|
704
|
+
throw new Error("deltaTableData pairs is empty — a delta table with no pairs has nothing to compare. " +
|
|
705
|
+
"Declare at least one { label, a, b } pair, or use pairsByFlag(name) to derive pairs from experiment flags.");
|
|
706
|
+
}
|
|
707
|
+
const seenLabels = new Set();
|
|
708
|
+
for (const pair of options.pairs) {
|
|
709
|
+
const labelKey = JSON.stringify(sortedJson(pair.label));
|
|
710
|
+
if (pair.label === undefined || pair.label === "" || labelKey === "{}") {
|
|
711
|
+
throw new Error(`deltaTableData pair (${pair.a} vs ${pair.b}) has an empty label; every pair needs a display label.`);
|
|
712
|
+
}
|
|
713
|
+
if (seenLabels.has(labelKey)) {
|
|
714
|
+
throw new Error(`deltaTableData pair label ${labelKey} is used twice — labels must be unique within one table.`);
|
|
715
|
+
}
|
|
716
|
+
seenLabels.add(labelKey);
|
|
717
|
+
if (pair.a === pair.b) {
|
|
718
|
+
throw new Error(`deltaTableData pair "${labelKey}" compares "${pair.a}" with itself; a and b must differ.`);
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
pairs = options.pairs;
|
|
722
|
+
}
|
|
723
|
+
const items = filterItems(collectItems(snapshots), options.evals);
|
|
724
|
+
const groups = groupItems(items, options.by);
|
|
573
725
|
const rows = [];
|
|
574
|
-
for (const pair of
|
|
575
|
-
|
|
576
|
-
const
|
|
726
|
+
for (const pair of pairs) {
|
|
727
|
+
// 精确匹配分组后的维度 key,不做前缀或模糊匹配;未命中保留 pair,对应侧格子为缺失。
|
|
728
|
+
const aItems = groups.get(pair.a) ?? [];
|
|
729
|
+
const bItems = groups.get(pair.b) ?? [];
|
|
577
730
|
const cells = {};
|
|
578
|
-
for (const metric of
|
|
731
|
+
for (const metric of options.metrics) {
|
|
579
732
|
const a = await computeCell(metric, aItems);
|
|
580
733
|
const b = await computeCell(metric, bItems);
|
|
581
|
-
const
|
|
582
|
-
cells[metric.name] = {
|
|
734
|
+
const delta = a.value === null || b.value === null ? null : b.value - a.value;
|
|
735
|
+
cells[metric.name] = {
|
|
736
|
+
a,
|
|
737
|
+
b,
|
|
738
|
+
delta,
|
|
739
|
+
display: deltaDisplay(metric, delta),
|
|
740
|
+
outcome: deltaOutcome(metric, delta),
|
|
741
|
+
};
|
|
583
742
|
}
|
|
584
743
|
rows.push({
|
|
585
|
-
key:
|
|
586
|
-
|
|
587
|
-
|
|
744
|
+
key: `${pair.a} → ${pair.b}`,
|
|
745
|
+
label: pair.label,
|
|
746
|
+
a: { key: pair.a },
|
|
747
|
+
b: { key: pair.b },
|
|
588
748
|
cells,
|
|
589
749
|
});
|
|
590
750
|
}
|
|
591
|
-
return {
|
|
751
|
+
return {
|
|
752
|
+
byDimension: dimensionName(options.by),
|
|
753
|
+
columns: options.metrics.map(toColumn),
|
|
754
|
+
...(experiments !== undefined ? { experiments } : {}),
|
|
755
|
+
rows,
|
|
756
|
+
};
|
|
592
757
|
}
|
|
593
758
|
function deltaDisplay(metric, delta) {
|
|
594
759
|
if (delta === null)
|
|
595
760
|
return "—"; // 任一侧缺数据:Δ 显示为缺,不硬算
|
|
596
761
|
if (delta === 0)
|
|
597
762
|
return "±0";
|
|
598
|
-
|
|
599
|
-
|
|
763
|
+
if (metric.display) {
|
|
764
|
+
const display = metric.display;
|
|
765
|
+
return localizedDisplay((locale) => {
|
|
766
|
+
const text = display(Math.abs(delta), locale);
|
|
767
|
+
return delta > 0 ? `+${text}` : `-${text}`;
|
|
768
|
+
});
|
|
769
|
+
}
|
|
770
|
+
const text = formatMetricValue(Math.abs(delta), metric.unit);
|
|
771
|
+
return delta > 0 ? `+${text}` : `-${text}`;
|
|
772
|
+
}
|
|
773
|
+
function deltaOutcome(metric, delta) {
|
|
774
|
+
if (delta === null)
|
|
775
|
+
return "unavailable";
|
|
776
|
+
if (delta === 0)
|
|
777
|
+
return "unchanged";
|
|
778
|
+
const better = metric.better ?? "higher";
|
|
779
|
+
return (delta > 0) === (better === "higher") ? "improved" : "regressed";
|
|
600
780
|
}
|