niceeval 0.5.4 → 0.6.1
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 +55 -0
- package/dist/agents/types.d.ts +321 -0
- package/dist/context/types.d.ts +335 -0
- package/dist/i18n/core.d.ts +11 -0
- package/dist/i18n/en.d.ts +181 -0
- package/dist/i18n/index.d.ts +5 -0
- package/dist/i18n/zh-CN.d.ts +183 -0
- package/dist/o11y/otlp/parse.d.ts +3 -0
- package/dist/o11y/otlp/receiver.d.ts +21 -0
- package/dist/o11y/otlp/turn-otel.d.ts +43 -0
- package/dist/o11y/types.d.ts +190 -0
- package/dist/report/aggregate.d.ts +57 -0
- package/dist/report/aggregate.js +212 -0
- package/dist/report/built-ins/experiment-comparison.d.ts +1 -0
- package/dist/report/built-ins/experiment-comparison.js +13 -0
- package/dist/report/built-ins/index.d.ts +1 -0
- package/dist/report/built-ins/index.js +2 -0
- package/dist/report/components.d.ts +180 -0
- package/dist/report/components.js +132 -0
- package/dist/report/compute.d.ts +106 -0
- package/dist/report/compute.js +600 -0
- package/dist/report/flag.d.ts +6 -0
- package/dist/report/flag.js +8 -0
- package/dist/report/format.d.ts +26 -0
- package/dist/report/format.js +109 -0
- package/dist/report/index.d.ts +19 -0
- package/dist/report/index.js +30 -0
- package/dist/report/load.d.ts +12 -0
- package/dist/report/load.js +56 -0
- package/dist/report/locale.d.ts +94 -0
- package/dist/report/locale.js +183 -0
- package/dist/report/metrics.d.ts +21 -0
- package/dist/report/metrics.js +115 -0
- package/dist/report/primitives.d.ts +64 -0
- package/dist/report/primitives.js +113 -0
- package/dist/report/react/AttemptList.d.ts +23 -0
- package/dist/report/react/AttemptList.js +19 -0
- package/dist/report/react/DeltaTable.d.ts +8 -0
- package/dist/report/react/DeltaTable.js +24 -0
- package/dist/report/react/EvalList.d.ts +10 -0
- package/dist/report/react/EvalList.js +0 -0
- package/dist/report/react/ExperimentList.d.ts +11 -0
- package/dist/report/react/ExperimentList.js +67 -0
- package/dist/report/react/GroupSummary.d.ts +8 -0
- package/dist/report/react/GroupSummary.js +8 -0
- package/dist/report/react/MetricBars.d.ts +10 -0
- package/dist/report/react/MetricBars.js +48 -0
- package/dist/report/react/MetricLine.d.ts +9 -0
- package/dist/report/react/MetricLine.js +68 -0
- package/dist/report/react/MetricMatrix.d.ts +10 -0
- package/dist/report/react/MetricMatrix.js +23 -0
- package/dist/report/react/MetricScatter.d.ts +9 -0
- package/dist/report/react/MetricScatter.js +96 -0
- package/dist/report/react/MetricTable.d.ts +11 -0
- package/dist/report/react/MetricTable.js +26 -0
- package/dist/report/react/RunOverview.d.ts +8 -0
- package/dist/report/react/RunOverview.js +12 -0
- package/dist/report/react/Scoreboard.d.ts +8 -0
- package/dist/report/react/Scoreboard.js +26 -0
- package/dist/report/react/cell.d.ts +9 -0
- package/dist/report/react/cell.js +9 -0
- package/dist/report/react/chart-math.d.ts +14 -0
- package/dist/report/react/chart-math.js +68 -0
- package/dist/report/react/colors.d.ts +14 -0
- package/dist/report/react/colors.js +45 -0
- package/dist/report/react/fixtures.d.ts +19 -0
- package/dist/report/react/fixtures.js +398 -0
- package/dist/report/react/format.d.ts +3 -0
- package/dist/report/react/format.js +7 -0
- package/dist/report/react/index.d.ts +6 -0
- package/dist/report/react/index.js +14 -0
- package/dist/report/report.d.ts +24 -0
- package/dist/report/report.js +49 -0
- package/dist/report/text/faces.d.ts +29 -0
- package/dist/report/text/faces.js +541 -0
- package/dist/report/text/layout.d.ts +75 -0
- package/dist/report/text/layout.js +173 -0
- package/dist/report/text/plot.d.ts +37 -0
- package/dist/report/text/plot.js +81 -0
- package/dist/report/text/table.d.ts +8 -0
- package/dist/report/text/table.js +86 -0
- package/dist/report/tree.d.ts +99 -0
- package/dist/report/tree.js +228 -0
- package/dist/report/types.d.ts +408 -0
- package/dist/report/types.js +4 -0
- package/dist/report/web.d.ts +15 -0
- package/dist/report/web.js +37 -0
- package/dist/results/locator.d.ts +75 -0
- package/dist/results/locator.js +0 -0
- package/dist/results/select.d.ts +45 -0
- package/dist/results/select.js +230 -0
- package/dist/results/source-hash.d.ts +9 -0
- package/dist/results/types.d.ts +204 -0
- package/dist/runner/eval-source.d.ts +22 -0
- package/dist/runner/fingerprint.d.ts +23 -0
- package/dist/runner/types.d.ts +332 -0
- package/dist/sandbox/docker-stream.d.ts +24 -0
- package/dist/sandbox/docker.d.ts +84 -0
- package/dist/sandbox/e2b.d.ts +31 -0
- package/dist/sandbox/errors.d.ts +4 -0
- package/dist/sandbox/local-files.d.ts +6 -0
- package/dist/sandbox/paths.d.ts +4 -0
- package/dist/sandbox/registry.d.ts +13 -0
- package/dist/sandbox/resolve.d.ts +40 -0
- package/dist/sandbox/retry.d.ts +10 -0
- package/dist/sandbox/shell.d.ts +11 -0
- package/dist/sandbox/source-files.d.ts +13 -0
- package/dist/sandbox/types.d.ts +203 -0
- package/dist/sandbox/vercel.d.ts +37 -0
- package/dist/scoring/types.d.ts +70 -0
- package/dist/shared/aggregate.d.ts +29 -0
- package/dist/shared/aggregate.js +55 -0
- package/dist/shared/types.d.ts +27 -0
- package/dist/shared/verdict.d.ts +27 -0
- package/dist/shared/verdict.js +34 -0
- package/dist/tty-line.d.ts +6 -0
- package/dist/types.d.ts +7 -0
- package/dist/util.d.ts +22 -0
- package/docs-site/zh/concepts/adapter.mdx +7 -7
- package/docs-site/zh/concepts/assert.mdx +19 -18
- package/docs-site/zh/concepts/evals.mdx +12 -11
- package/docs-site/zh/concepts/experiment.mdx +2 -2
- package/docs-site/zh/concepts/judge.mdx +9 -9
- package/docs-site/zh/concepts/overview.mdx +3 -3
- package/docs-site/zh/concepts/tier.mdx +6 -6
- package/docs-site/zh/example/claude-code-codex-plugin.mdx +9 -5
- package/docs-site/zh/example/claude-code-codex-skill.mdx +12 -7
- package/docs-site/zh/guides/agent-feedback-loop.mdx +237 -78
- package/docs-site/zh/guides/connect-otel.mdx +3 -3
- package/docs-site/zh/guides/connect-your-agent.mdx +2 -2
- package/docs-site/zh/guides/custom-reports.mdx +196 -36
- package/docs-site/zh/guides/experiments.mdx +7 -1
- package/docs-site/zh/guides/fixtures.mdx +4 -2
- package/docs-site/zh/guides/official-adapters.mdx +31 -9
- package/docs-site/zh/guides/publish-report.mdx +4 -4
- package/docs-site/zh/guides/report-components.mdx +194 -55
- package/docs-site/zh/guides/reporters.mdx +2 -2
- package/docs-site/zh/guides/results-data.mdx +61 -34
- package/docs-site/zh/guides/runner.mdx +2 -2
- package/docs-site/zh/guides/sandbox-agent.mdx +3 -2
- package/docs-site/zh/guides/{sandbox-backends.mdx → sandbox-providers.mdx} +12 -12
- package/docs-site/zh/guides/viewing-results.mdx +200 -79
- package/docs-site/zh/guides/write-experiment.mdx +9 -1
- package/docs-site/zh/guides/write-send.mdx +4 -3
- package/docs-site/zh/index.mdx +4 -3
- package/docs-site/zh/introduction.mdx +2 -1
- package/docs-site/zh/quickstart.mdx +5 -3
- package/docs-site/zh/reference/builtin-agents.mdx +38 -13
- package/docs-site/zh/reference/capabilities.mdx +1 -1
- package/docs-site/zh/reference/cli.mdx +36 -13
- package/docs-site/zh/reference/define-agent.mdx +15 -13
- package/docs-site/zh/reference/define-config.mdx +1 -1
- package/docs-site/zh/reference/define-eval.mdx +3 -3
- package/docs-site/zh/reference/events.mdx +9 -3
- package/docs-site/zh/reference/expect.mdx +13 -8
- package/package.json +10 -6
- package/src/agents/bub.ts +129 -42
- package/src/agents/claude-code.test.ts +152 -0
- package/src/agents/claude-code.ts +127 -13
- package/src/agents/codex.test.ts +192 -0
- package/src/agents/codex.ts +151 -12
- package/src/agents/index.ts +9 -3
- package/src/agents/manifest.ts +41 -0
- package/src/agents/plugin-config.test.ts +94 -0
- package/src/agents/shared.ts +33 -2
- package/src/agents/skills.test.ts +214 -0
- package/src/agents/skills.ts +304 -0
- package/src/agents/streaming.ts +5 -5
- package/src/agents/types.ts +59 -3
- package/src/cli.ts +74 -47
- package/src/context/context.test.ts +20 -2
- package/src/context/control-flow.ts +1 -1
- package/src/context/session.test.ts +92 -1
- package/src/context/session.ts +23 -1
- package/src/context/types.ts +4 -4
- package/src/define.test.ts +8 -8
- package/src/define.ts +11 -11
- package/src/expect/index.ts +5 -5
- package/src/i18n/en.ts +33 -15
- package/src/i18n/zh-CN.ts +37 -21
- package/src/index.ts +1 -1
- package/src/o11y/execution-tree.test.ts +452 -0
- package/src/o11y/execution-tree.ts +367 -0
- package/src/o11y/otlp/receiver.ts +1 -1
- package/src/o11y/otlp/select.ts +7 -3
- package/src/o11y/parsers/claude-code.test.ts +175 -0
- package/src/o11y/parsers/claude-code.ts +32 -0
- package/src/o11y/types.ts +6 -1
- package/src/report/aggregate.ts +26 -20
- package/src/report/built-in-user-parity.test.tsx +640 -0
- package/src/report/built-ins/experiment-comparison.tsx +19 -0
- package/src/report/built-ins/index.ts +2 -0
- package/src/report/components.tsx +225 -50
- package/src/report/compute.ts +335 -54
- package/src/report/dual-render.test.tsx +1129 -0
- package/src/report/flag.ts +1 -1
- package/src/report/format.ts +35 -2
- package/src/report/index.ts +47 -14
- package/src/report/load.ts +1 -1
- package/src/report/locale.ts +229 -0
- package/src/report/metrics.ts +39 -15
- package/src/report/primitives.tsx +100 -2
- package/src/report/react/AttemptList.tsx +113 -0
- package/src/report/react/DeltaTable.tsx +9 -6
- package/src/report/react/EvalList.tsx +0 -0
- package/src/report/react/ExperimentList.tsx +220 -0
- package/src/report/react/GroupSummary.tsx +66 -0
- package/src/report/react/MetricBars.tsx +24 -13
- package/src/report/react/MetricLine.tsx +21 -21
- package/src/report/react/MetricMatrix.tsx +8 -4
- package/src/report/react/MetricScatter.tsx +126 -96
- package/src/report/react/MetricTable.tsx +115 -15
- package/src/report/react/RunOverview.tsx +48 -26
- package/src/report/react/Scoreboard.tsx +15 -7
- package/src/report/react/cell.tsx +18 -9
- package/src/{view/app/lib/chart.ts → report/react/chart-math.ts} +2 -6
- package/src/report/react/colors.ts +24 -15
- package/src/report/react/enhance.js +168 -0
- package/src/report/react/fixtures.ts +169 -34
- package/src/report/react/format.ts +1 -1
- package/src/report/react/index.tsx +21 -6
- package/src/report/react/render.test.tsx +180 -31
- package/src/report/react/styles.css +432 -213
- package/src/report/report.test.ts +398 -94
- package/src/report/report.ts +29 -11
- package/src/report/text/faces.ts +344 -96
- package/src/report/text/layout.ts +76 -11
- package/src/report/text/table.ts +98 -0
- package/src/report/tree.ts +103 -20
- package/src/report/types.ts +184 -25
- package/src/report/web.ts +46 -19
- package/src/results/annotated-source.test.ts +146 -0
- package/src/results/annotated-source.ts +128 -0
- package/src/results/attempt-evidence.test.ts +158 -0
- package/src/results/attempt-evidence.ts +158 -0
- package/src/results/attempt-source.ts +48 -0
- package/src/results/copy.ts +154 -139
- package/src/results/format.ts +54 -47
- package/src/results/host-equivalence.test.ts +527 -0
- package/src/results/index.ts +56 -14
- package/src/results/locator.test.ts +198 -0
- package/src/results/locator.ts +0 -0
- package/src/results/open.ts +392 -175
- package/src/results/results.test.ts +943 -494
- package/src/results/select.ts +129 -23
- package/src/results/skipped-notice.ts +0 -0
- package/src/results/source-hash.ts +28 -0
- package/src/results/types.ts +98 -62
- package/src/results/writer.ts +304 -176
- package/src/runner/attempt.test.ts +170 -0
- package/src/runner/attempt.ts +48 -13
- package/src/runner/discover.test.ts +65 -0
- package/src/runner/discover.ts +6 -2
- package/src/runner/eval-source.test.ts +121 -0
- package/src/runner/eval-source.ts +45 -0
- package/src/runner/fingerprint.ts +54 -1
- package/src/runner/remote-sandbox.ts +1 -1
- package/src/runner/report.test.ts +6 -8
- package/src/runner/report.ts +5 -4
- package/src/runner/reporters/artifacts.ts +27 -41
- package/src/runner/reporters/braintrust.test.ts +3 -3
- package/src/runner/reporters/braintrust.ts +1 -1
- package/src/runner/reporters/console.ts +6 -6
- package/src/runner/reporters/json.ts +4 -4
- package/src/runner/reporters/live.test.ts +56 -0
- package/src/runner/reporters/live.ts +36 -10
- package/src/runner/reporters/quiet.test.ts +66 -0
- package/src/runner/reporters/quiet.ts +49 -0
- package/src/runner/reporters/shared.ts +5 -5
- package/src/runner/reporters/table.ts +17 -17
- package/src/runner/run.ts +61 -102
- package/src/runner/types.ts +43 -32
- package/src/sandbox/checkpoint.ts +1 -1
- package/src/sandbox/docker.ts +15 -2
- package/src/sandbox/e2b.ts +11 -5
- package/src/sandbox/errors.ts +13 -0
- package/src/sandbox/index.ts +3 -3
- package/src/sandbox/local-files.ts +1 -1
- package/src/sandbox/registry.ts +6 -5
- package/src/sandbox/resolve.ts +38 -23
- package/src/sandbox/retry.test.ts +68 -0
- package/src/sandbox/retry.ts +44 -0
- package/src/sandbox/types.ts +24 -24
- package/src/sandbox/vercel.ts +17 -3
- package/src/scoring/collector.ts +3 -3
- package/src/scoring/judge.ts +2 -2
- package/src/scoring/scoped.ts +13 -1
- package/src/scoring/types.ts +7 -7
- package/src/scoring/verdict.ts +4 -4
- package/src/shared/aggregate.ts +23 -3
- package/src/shared/types.ts +2 -2
- package/src/shared/{outcome.ts → verdict.ts} +15 -15
- package/src/show/compose.ts +12 -132
- package/src/show/index.ts +119 -95
- package/src/show/render.ts +462 -159
- package/src/show/show.test.ts +471 -253
- package/src/source-loc.ts +1 -1
- package/src/tty-line.ts +32 -0
- package/src/types.ts +2 -2
- package/src/util.test.ts +21 -1
- package/src/util.ts +5 -1
- package/src/view/app/App.tsx +33 -198
- package/src/view/app/components/AttemptModal.tsx +6 -6
- package/src/view/app/components/CodeView.tsx +2 -2
- package/src/view/app/components/CopyControls.tsx +32 -36
- package/src/view/app/components/SkippedRunsBanner.tsx +140 -0
- package/src/view/app/components/ui/badge.tsx +1 -1
- package/src/view/app/components/ui/dialog.tsx +2 -2
- package/src/view/app/i18n.ts +40 -31
- package/src/view/app/lib/artifact-url.ts +1 -1
- package/src/view/app/lib/attempt-route.test.ts +37 -47
- package/src/view/app/lib/attempt-route.ts +26 -26
- package/src/view/app/lib/rows.ts +9 -112
- package/src/view/app/lib/verdict.ts +25 -0
- package/src/view/app/main.tsx +9 -12
- package/src/view/app/pages/RunsPage.tsx +4 -4
- package/src/view/app/pages/TracesPage.tsx +3 -3
- package/src/view/app/shared.ts +0 -1
- package/src/view/app/types.ts +6 -51
- package/src/view/artifact-serving.test.ts +136 -0
- package/src/view/client-dist/app.css +1 -1
- package/src/view/client-dist/app.js +21 -21
- package/src/view/data.test.ts +214 -143
- package/src/view/data.ts +166 -120
- package/src/view/index.ts +23 -11
- package/src/view/server.ts +52 -17
- package/src/view/shared/types.ts +22 -31
- package/src/view/styles.css +117 -327
- package/src/view/view-report.test.ts +146 -98
- package/src/report/default-report.tsx +0 -222
- package/src/report/dual-face.test.tsx +0 -527
- package/src/report/react/CaseList.tsx +0 -70
- package/src/view/app/components/CostScoreChart.tsx +0 -147
- package/src/view/app/components/ExperimentTable.tsx +0 -284
- package/src/view/app/components/GroupSelector.tsx +0 -61
- package/src/view/app/components/primitives.tsx +0 -43
- package/src/view/app/lib/outcome.ts +0 -82
|
@@ -0,0 +1,600 @@
|
|
|
1
|
+
// 计算函数:Selection → 一份组件数据。跑在 Node 侧,产物是算好的、可序列化的普通 JSON
|
|
2
|
+
// (终值 + 渲染提示,不含公式);渲染面(web/text)只做展示。
|
|
3
|
+
//
|
|
4
|
+
// 这些函数不做顶层导出,而是挂在对应组件上(MetricTable.data / Scoreboard.data …,
|
|
5
|
+
// 见 components.tsx):配对打点即发现,泛化名不占顶层导出。
|
|
6
|
+
//
|
|
7
|
+
// 共同约定(docs/feature/reports/architecture.md「指标聚合不变量」):
|
|
8
|
+
// - 第一参收 Selection | Snapshot[];收 Selection 时 warnings 随行进 OverviewData;
|
|
9
|
+
// - 聚合前按身份键去重(dedupeAttempts;missing-startedAt 不去重、如实保留、不透出警告);
|
|
10
|
+
// - null ≠ 0:缺数据不编数,覆盖率经 samples/total 如实暴露;
|
|
11
|
+
// - core 中立:只认 Metric / Dimension 接口,不出现具体 agent 名的分支。
|
|
12
|
+
import { evalLevelStats, foldEvalVerdict } from "../shared/verdict.js";
|
|
13
|
+
import { applyAggregator, assertUniqueMetricNames, collectItems, computeCell, dimensionKey, dimensionName, displayValue, evalGroupOf, evalIdOf, evalPrefixPredicate, evaluateMetric, experimentIdOf, filterItems, groupItems, flagAxisValue, locatorOf, resolveInput, snapshotKeyOf, toColumn, } from "./aggregate.js";
|
|
14
|
+
import { attemptCostUSD, costUSD, durationMs, examScore, passRate, tokens } from "./metrics.js";
|
|
15
|
+
import { formatMetricValue, formatPlainNumber } from "./format.js";
|
|
16
|
+
// 一组 Item 的 eval 全身份键:experimentId + eval id。单 experiment 场景(如 experimentRowMeta,
|
|
17
|
+
// 一组本就只有一个 experimentId)下退化为只按 eval id 折叠,与旧行为一致;多 experiment 场景
|
|
18
|
+
// (GroupSummary 的组可能跨多个 experiment)下避免两个 experiment 里同名 eval 被误合并成一道题。
|
|
19
|
+
// 分隔符是 NUL(同 aggregate.ts 的 KEY_SEP 手法):不会出现在 eval id / experimentId 里,拼接键不串味。
|
|
20
|
+
const GROUP_KEY_SEP = "\u0000";
|
|
21
|
+
function fullEvalKey(item) {
|
|
22
|
+
return `${experimentIdOf(item)}${GROUP_KEY_SEP}${evalIdOf(item)}`;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* 一批 Item 的组级统计:eval 级折叠计票(evalLevelStats,与 view 榜单 / `TableRowMeta.verdicts`
|
|
26
|
+
* 同一套 foldEvalVerdict 口径,按完整身份键折叠)、experiment/eval/attempt 数量、总成本
|
|
27
|
+
* (null-safe 求和)、最后运行时间(组内快照 startedAt 的最大值)。`experimentRowMeta` 与
|
|
28
|
+
* `groupSummaryData` 共用这一份实现,不各自拼装 evalLevelStats。
|
|
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;
|
|
69
|
+
}
|
|
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
|
+
const { snapshots } = resolveInput(input);
|
|
132
|
+
const items = filterItems(collectItems(snapshots), opts.evals);
|
|
133
|
+
const groups = groupItems(items, opts.rows);
|
|
134
|
+
const rows = [];
|
|
135
|
+
const sortCells = new Map();
|
|
136
|
+
for (const [key, group] of groups) {
|
|
137
|
+
const cells = {};
|
|
138
|
+
for (const metric of opts.columns)
|
|
139
|
+
cells[metric.name] = await computeCell(metric, group);
|
|
140
|
+
if (opts.sort) {
|
|
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
|
+
});
|
|
150
|
+
}
|
|
151
|
+
if (opts.sort) {
|
|
152
|
+
const better = opts.sort.better ?? "higher";
|
|
153
|
+
rows.sort((a, b) => {
|
|
154
|
+
const va = sortCells.get(a.key)?.value ?? null;
|
|
155
|
+
const vb = sortCells.get(b.key)?.value ?? null;
|
|
156
|
+
if (va === null && vb === null)
|
|
157
|
+
return 0;
|
|
158
|
+
if (va === null)
|
|
159
|
+
return 1; // 缺数据沉底
|
|
160
|
+
if (vb === null)
|
|
161
|
+
return -1;
|
|
162
|
+
return better === "lower" ? va - vb : vb - va;
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
return {
|
|
166
|
+
dimension: dimensionName(opts.rows),
|
|
167
|
+
columns: opts.columns.map(toColumn),
|
|
168
|
+
rows,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
// ───────────────────────── ExperimentList.data / EvalList.data / AttemptList.data ─────────────────────────
|
|
172
|
+
//
|
|
173
|
+
// 三个实体列表逐级下钻(experiment → experimentId × eval → attempt),固定展示实体事实,
|
|
174
|
+
// 没有列配置;过滤是报告作者对返回数组调用 .filter()/.slice() 的事,不进这里
|
|
175
|
+
// (docs/feature/reports/library.md「实体列表」)。AttemptListItem 是三者共用的叶子形状——
|
|
176
|
+
// ExperimentList / EvalList 的下钻数组直接复用它,不各自精简一份。
|
|
177
|
+
/** 自由文本(error / 断言 detail / evidence)的发布消毒钩子;身份字段(name/severity/loc)不经它。 */
|
|
178
|
+
function redactAssertions(assertions, redact) {
|
|
179
|
+
if (assertions.length === 0)
|
|
180
|
+
return assertions;
|
|
181
|
+
return assertions.map((a) => ({
|
|
182
|
+
...a,
|
|
183
|
+
...(a.detail !== undefined ? { detail: redact(a.detail) } : {}),
|
|
184
|
+
...(a.evidence !== undefined ? { evidence: redact(a.evidence) } : {}),
|
|
185
|
+
}));
|
|
186
|
+
}
|
|
187
|
+
/** AttemptList / ExperimentList / EvalList 共用的叶子构造:一个 Item → 一个 AttemptListItem。 */
|
|
188
|
+
function attemptListItemOf(item, redact) {
|
|
189
|
+
const result = item.attempt.result;
|
|
190
|
+
const cost = attemptCostUSD(result);
|
|
191
|
+
return {
|
|
192
|
+
evalId: evalIdOf(item),
|
|
193
|
+
experimentId: experimentIdOf(item),
|
|
194
|
+
attempt: result.attempt,
|
|
195
|
+
agent: result.agent,
|
|
196
|
+
verdict: result.verdict,
|
|
197
|
+
...(result.error !== undefined ? { error: redact(result.error) } : {}),
|
|
198
|
+
assertions: redactAssertions(result.assertions, redact),
|
|
199
|
+
durationMs: result.durationMs,
|
|
200
|
+
...(cost !== null ? { costUSD: cost } : {}),
|
|
201
|
+
locator: locatorOf(item),
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
const identityRedact = (text) => text;
|
|
205
|
+
/** `AttemptList.data(selection)`:每个 Attempt 一项,顺序取自 Selection 展平顺序(不重排)。 */
|
|
206
|
+
export async function attemptListData(input, opts) {
|
|
207
|
+
const { snapshots } = resolveInput(input);
|
|
208
|
+
const redact = opts?.redact ?? identityRedact;
|
|
209
|
+
const items = collectItems(snapshots);
|
|
210
|
+
return items.map((item) => attemptListItemOf(item, redact));
|
|
211
|
+
}
|
|
212
|
+
/** `EvalList.data(selection)`:每个 `experimentId + evalId` 一项,按 evalId 再按 experimentId 升序。 */
|
|
213
|
+
export async function evalListData(input) {
|
|
214
|
+
const { snapshots } = resolveInput(input);
|
|
215
|
+
const items = collectItems(snapshots);
|
|
216
|
+
const groups = new Map();
|
|
217
|
+
for (const item of items) {
|
|
218
|
+
const key = fullEvalKey(item);
|
|
219
|
+
const list = groups.get(key);
|
|
220
|
+
if (list)
|
|
221
|
+
list.push(item);
|
|
222
|
+
else
|
|
223
|
+
groups.set(key, [item]);
|
|
224
|
+
}
|
|
225
|
+
const out = [];
|
|
226
|
+
for (const group of groups.values()) {
|
|
227
|
+
const sorted = [...group].sort((a, b) => a.attempt.result.attempt - b.attempt.result.attempt);
|
|
228
|
+
const verdict = foldEvalVerdict(sorted.map((item) => item.attempt.result));
|
|
229
|
+
const representative = sorted.find((item) => item.attempt.result.verdict === verdict) ?? sorted[0];
|
|
230
|
+
const attempts = sorted.map((item) => attemptListItemOf(item, identityRedact));
|
|
231
|
+
out.push({
|
|
232
|
+
evalId: evalIdOf(sorted[0]),
|
|
233
|
+
experimentId: experimentIdOf(sorted[0]),
|
|
234
|
+
verdict,
|
|
235
|
+
reason: reasonFor(representative.attempt.result),
|
|
236
|
+
score: await computeCell(examScore, sorted),
|
|
237
|
+
duration: await computeCell(durationMs, sorted),
|
|
238
|
+
cost: await computeCell(costUSD, sorted),
|
|
239
|
+
attempts,
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
out.sort((a, b) => a.evalId.localeCompare(b.evalId) || a.experimentId.localeCompare(b.experimentId));
|
|
243
|
+
return out;
|
|
244
|
+
}
|
|
245
|
+
/** `ExperimentList.data(selection)`:每个 experiment 一项,按 experimentId 升序;展开到每道 Eval。 */
|
|
246
|
+
export async function experimentListData(input) {
|
|
247
|
+
const { snapshots } = resolveInput(input);
|
|
248
|
+
const items = collectItems(snapshots);
|
|
249
|
+
const groups = groupItems(items, "experiment");
|
|
250
|
+
const out = [];
|
|
251
|
+
for (const [experimentId, group] of groups) {
|
|
252
|
+
const stats = summarizeItems(group);
|
|
253
|
+
const newest = [...group].sort((a, b) => b.snapshot.startedAt.localeCompare(a.snapshot.startedAt))[0];
|
|
254
|
+
const evalGroups = groupItems(group, "eval");
|
|
255
|
+
const evalRows = [];
|
|
256
|
+
for (const [evalId, evalItems] of evalGroups) {
|
|
257
|
+
const sorted = [...evalItems].sort((a, b) => a.attempt.result.attempt - b.attempt.result.attempt);
|
|
258
|
+
const verdict = foldEvalVerdict(sorted.map((item) => item.attempt.result));
|
|
259
|
+
const representative = sorted.find((item) => item.attempt.result.verdict === verdict) ?? sorted[0];
|
|
260
|
+
const attempts = sorted.map((item) => attemptListItemOf(item, identityRedact));
|
|
261
|
+
evalRows.push({
|
|
262
|
+
evalId,
|
|
263
|
+
verdict,
|
|
264
|
+
reason: reasonFor(representative.attempt.result),
|
|
265
|
+
duration: await computeCell(durationMs, sorted),
|
|
266
|
+
cost: await computeCell(costUSD, sorted),
|
|
267
|
+
attempts,
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
evalRows.sort((a, b) => a.evalId.localeCompare(b.evalId));
|
|
271
|
+
const experiment = newest.snapshot.experiment ?? newest.attempt.result.experiment;
|
|
272
|
+
out.push({
|
|
273
|
+
experimentId,
|
|
274
|
+
agent: newest.snapshot.agent,
|
|
275
|
+
...((newest.attempt.result.model ?? newest.snapshot.model) !== undefined
|
|
276
|
+
? { model: newest.attempt.result.model ?? newest.snapshot.model }
|
|
277
|
+
: {}),
|
|
278
|
+
...(experiment?.flags ? { flags: experiment.flags } : {}),
|
|
279
|
+
verdicts: stats.verdicts,
|
|
280
|
+
passRate: await computeCell(passRate, group),
|
|
281
|
+
cost: await computeCell(costUSD, group),
|
|
282
|
+
duration: await computeCell(durationMs, group),
|
|
283
|
+
tokens: await computeCell(tokens, group),
|
|
284
|
+
evals: stats.evals,
|
|
285
|
+
attempts: stats.attempts,
|
|
286
|
+
lastRunAt: stats.lastRunAt,
|
|
287
|
+
evalRows,
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
// ExperimentList 是默认实验比较表:初始态按成功率从高到低,缺数据沉底;
|
|
291
|
+
// 同分时按 experiment id 稳定排序。web 增强可临时重排,text 面沿用同一基准顺序。
|
|
292
|
+
out.sort((a, b) => {
|
|
293
|
+
if (a.passRate.value === null && b.passRate.value === null)
|
|
294
|
+
return a.experimentId.localeCompare(b.experimentId);
|
|
295
|
+
if (a.passRate.value === null)
|
|
296
|
+
return 1;
|
|
297
|
+
if (b.passRate.value === null)
|
|
298
|
+
return -1;
|
|
299
|
+
return b.passRate.value - a.passRate.value || a.experimentId.localeCompare(b.experimentId);
|
|
300
|
+
});
|
|
301
|
+
return out;
|
|
302
|
+
}
|
|
303
|
+
export async function matrixData(input, opts) {
|
|
304
|
+
const { snapshots } = resolveInput(input);
|
|
305
|
+
const items = filterItems(collectItems(snapshots), opts.evals);
|
|
306
|
+
// 稀疏分组:只有真有 attempt 的 (row, column) 组合成格;没有样本的格子不出现
|
|
307
|
+
const groups = new Map();
|
|
308
|
+
for (const item of items) {
|
|
309
|
+
const row = dimensionKey(opts.rows, item);
|
|
310
|
+
const column = dimensionKey(opts.columns, item);
|
|
311
|
+
const key = JSON.stringify([row, column]);
|
|
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) });
|
|
321
|
+
}
|
|
322
|
+
return {
|
|
323
|
+
rows: dimensionName(opts.rows),
|
|
324
|
+
columns: dimensionName(opts.columns),
|
|
325
|
+
metric: toColumn(opts.cell),
|
|
326
|
+
cells,
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* 逐题分值制,分母对所有被打分者恒定:
|
|
331
|
+
* 题分值 = 命中的权重(默认 1) 题得分 = score 指标的题级值(perEval 折叠后)
|
|
332
|
+
* 总分 = fullMarks × Σ(题得分 × 题分值) / Σ(题分值) Σ 遍历选中范围内全部题
|
|
333
|
+
* 没跑到的题挣 0 分但留在分母里,missing 如实报 —— 这是显式的考试契约,不是「null ≠ 0」的例外。
|
|
334
|
+
*/
|
|
335
|
+
export async function scoreboardData(input, opts) {
|
|
336
|
+
const { snapshots } = resolveInput(input);
|
|
337
|
+
const fullMarks = opts.fullMarks ?? 100;
|
|
338
|
+
const scoreMetric = opts.score ?? examScore;
|
|
339
|
+
const subjectsDim = opts.subjects ?? "evalGroup";
|
|
340
|
+
const match = evalPrefixPredicate(opts.evals);
|
|
341
|
+
const items = filterItems(collectItems(snapshots), opts.evals);
|
|
342
|
+
// 题集(固定分母):选中范围内、任一快照声明覆盖或实际出现过的全部题
|
|
343
|
+
const universe = new Set();
|
|
344
|
+
for (const snapshot of snapshots) {
|
|
345
|
+
for (const e of snapshot.evals)
|
|
346
|
+
if (match(e.id))
|
|
347
|
+
universe.add(e.id);
|
|
348
|
+
for (const id of snapshot.knownEvalIds ?? [])
|
|
349
|
+
if (match(id))
|
|
350
|
+
universe.add(id);
|
|
351
|
+
}
|
|
352
|
+
for (const item of items)
|
|
353
|
+
universe.add(evalIdOf(item));
|
|
354
|
+
const sortedUniverse = [...universe].sort();
|
|
355
|
+
// 每题的科目:先从任一 attempt 解析(自定义 subjects 维度也能算);
|
|
356
|
+
// 全程无 attempt 的题按内置规则兜底,自定义维度无从计算时如实标 "(unknown)"
|
|
357
|
+
const subjectByEval = new Map();
|
|
358
|
+
for (const item of items) {
|
|
359
|
+
const id = evalIdOf(item);
|
|
360
|
+
if (!subjectByEval.has(id))
|
|
361
|
+
subjectByEval.set(id, dimensionKey(subjectsDim, item));
|
|
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)";
|
|
372
|
+
};
|
|
373
|
+
// 权重:最长前缀生效(排序后线性找第一个命中即最长)
|
|
374
|
+
const weights = Object.entries(opts.weights ?? {})
|
|
375
|
+
.map(([prefix, weight]) => ({ prefix, weight }))
|
|
376
|
+
.sort((a, b) => b.prefix.length - a.prefix.length);
|
|
377
|
+
const weightOf = (id) => weights.find((w) => id.startsWith(w.prefix))?.weight ?? 1;
|
|
378
|
+
const groups = groupItems(items, opts.rows);
|
|
379
|
+
const rows = [];
|
|
380
|
+
for (const [key, group] of groups) {
|
|
381
|
+
// 题得分:perEval 折叠(同 eval × 快照 内);同题出现在多个快照时取快照级值的均值
|
|
382
|
+
const perSnapshot = new Map(); // evalId → 快照键 → 原始值
|
|
383
|
+
for (const item of group) {
|
|
384
|
+
const value = await evaluateMetric(scoreMetric, item.attempt);
|
|
385
|
+
if (value === null)
|
|
386
|
+
continue; // 测不了的 attempt 不进题得分;整题无样本 → missing
|
|
387
|
+
const id = evalIdOf(item);
|
|
388
|
+
const snapKey = snapshotKeyOf(item.snapshot);
|
|
389
|
+
let bySnap = perSnapshot.get(id);
|
|
390
|
+
if (!bySnap)
|
|
391
|
+
perSnapshot.set(id, (bySnap = new Map()));
|
|
392
|
+
const bucket = bySnap.get(snapKey);
|
|
393
|
+
if (bucket)
|
|
394
|
+
bucket.push(value);
|
|
395
|
+
else
|
|
396
|
+
bySnap.set(snapKey, [value]);
|
|
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);
|
|
403
|
+
}
|
|
404
|
+
// 科目累计:固定分母 —— 没跑的题 0 分挣、留在分母、计入 missing
|
|
405
|
+
const subjects = new Map();
|
|
406
|
+
for (const id of sortedUniverse) {
|
|
407
|
+
const subjectKey = subjectOf(id);
|
|
408
|
+
let subject = subjects.get(subjectKey);
|
|
409
|
+
if (!subject) {
|
|
410
|
+
subjects.set(subjectKey, (subject = { key: subjectKey, earned: 0, possible: 0, evals: 0, missing: 0 }));
|
|
411
|
+
}
|
|
412
|
+
const weight = weightOf(id);
|
|
413
|
+
const got = scoreByEval.get(id);
|
|
414
|
+
subject.earned += (got ?? 0) * weight;
|
|
415
|
+
subject.possible += weight;
|
|
416
|
+
subject.evals += 1;
|
|
417
|
+
if (got === undefined)
|
|
418
|
+
subject.missing += 1;
|
|
419
|
+
}
|
|
420
|
+
let earned = 0;
|
|
421
|
+
let possible = 0;
|
|
422
|
+
for (const subject of subjects.values()) {
|
|
423
|
+
earned += subject.earned;
|
|
424
|
+
possible += subject.possible;
|
|
425
|
+
}
|
|
426
|
+
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
|
+
rows.push({
|
|
438
|
+
key,
|
|
439
|
+
// 组内取第一条解析系列:点维度细于系列维度时(experiment ⊂ agent)天然一致
|
|
440
|
+
series: opts.series ? dimensionKey(opts.series, group[0]) : undefined,
|
|
441
|
+
x: await computeCell(opts.x, group),
|
|
442
|
+
y: await computeCell(opts.y, group), // 任一轴 null 的点留在 rows 里:组件不画,但注脚要报的数就从这里数
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
return {
|
|
446
|
+
points: dimensionName(opts.points),
|
|
447
|
+
series: opts.series ? dimensionName(opts.series) : undefined,
|
|
448
|
+
x: toColumn(opts.x),
|
|
449
|
+
y: toColumn(opts.y),
|
|
450
|
+
rows,
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
/** 每个点 = 一个 experiment 的聚合;同系列的点按 x 排序连线(排序在组件面,数据保持分组序)。 */
|
|
454
|
+
export async function lineData(input, opts) {
|
|
455
|
+
const { snapshots } = resolveInput(input);
|
|
456
|
+
const items = collectItems(snapshots);
|
|
457
|
+
const groups = groupItems(items, "experiment");
|
|
458
|
+
const rows = [];
|
|
459
|
+
for (const [key, group] of groups) {
|
|
460
|
+
const x = flagAxisValue(opts.x, group[0]); // flag 是 experiment 级声明,组内一致
|
|
461
|
+
rows.push({
|
|
462
|
+
key,
|
|
463
|
+
series: opts.series ? dimensionKey(opts.series, group[0]) : undefined,
|
|
464
|
+
x,
|
|
465
|
+
xDisplay: x === null ? "" : formatMetricValue(x, opts.x.unit),
|
|
466
|
+
y: await computeCell(opts.y, group),
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
return {
|
|
470
|
+
x: {
|
|
471
|
+
key: opts.x.name,
|
|
472
|
+
label: typeof opts.x.label === "string" ? opts.x.label : opts.x.name,
|
|
473
|
+
unit: opts.x.unit,
|
|
474
|
+
},
|
|
475
|
+
series: opts.series ? dimensionName(opts.series) : undefined,
|
|
476
|
+
y: toColumn(opts.y),
|
|
477
|
+
rows,
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
// ───────────────────────── RunOverview.data ─────────────────────────
|
|
481
|
+
/** Selection 的 warnings 随行进 OverviewData,RunOverview 直接渲染 —— 诚实不靠使用者记得接线。 */
|
|
482
|
+
export async function overviewData(input) {
|
|
483
|
+
const { snapshots, warnings } = resolveInput(input);
|
|
484
|
+
const items = collectItems(snapshots);
|
|
485
|
+
const evalIds = new Set();
|
|
486
|
+
let passed = 0;
|
|
487
|
+
let failed = 0;
|
|
488
|
+
let errored = 0;
|
|
489
|
+
let skipped = 0;
|
|
490
|
+
let durationMs = 0;
|
|
491
|
+
let costUSD = null; // 任一 attempt 报了成本才有;全缺 = null,不编 0
|
|
492
|
+
for (const item of items) {
|
|
493
|
+
const result = item.attempt.result;
|
|
494
|
+
evalIds.add(evalIdOf(item));
|
|
495
|
+
switch (result.verdict) {
|
|
496
|
+
case "passed":
|
|
497
|
+
passed += 1;
|
|
498
|
+
break;
|
|
499
|
+
case "failed":
|
|
500
|
+
failed += 1;
|
|
501
|
+
break;
|
|
502
|
+
case "errored":
|
|
503
|
+
errored += 1;
|
|
504
|
+
break;
|
|
505
|
+
case "skipped":
|
|
506
|
+
skipped += 1;
|
|
507
|
+
break;
|
|
508
|
+
}
|
|
509
|
+
durationMs += result.durationMs;
|
|
510
|
+
const cost = attemptCostUSD(result);
|
|
511
|
+
if (cost !== null)
|
|
512
|
+
costUSD = (costUSD ?? 0) + cost;
|
|
513
|
+
}
|
|
514
|
+
// 通过率的唯一官方口径:两级聚合(computeCell),不是从上面四个 verdict 计票现场重算——
|
|
515
|
+
// 一道题内 attempt 部分通过要算部分 credit,不是二元投票。
|
|
516
|
+
const passRateCell = await computeCell(passRate, items);
|
|
517
|
+
return {
|
|
518
|
+
snapshots: snapshots.map((s) => ({
|
|
519
|
+
experimentId: s.experimentId,
|
|
520
|
+
agent: s.agent,
|
|
521
|
+
model: s.model,
|
|
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,
|
|
534
|
+
},
|
|
535
|
+
warnings: [...warnings],
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
// ───────────────────────── GroupSummary.data ─────────────────────────
|
|
539
|
+
/**
|
|
540
|
+
* 一组 experiment 的摘要:experiment/eval/attempt 数量、eval 级折叠计票、通过率(旧
|
|
541
|
+
* `GroupSelector` 卡片口径,见 summarizeItems)、总成本(null-safe 求和)、最后运行时间
|
|
542
|
+
* (组内快照 startedAt 最大值)。`input` 就是调用方已经收窄好的组 Selection(如自定义报告
|
|
543
|
+
* 按 experiment 组前缀 filter 出来的那份)——本函数不再自己分组。
|
|
544
|
+
*/
|
|
545
|
+
export async function groupSummaryData(input) {
|
|
546
|
+
const { snapshots } = resolveInput(input);
|
|
547
|
+
const items = collectItems(snapshots);
|
|
548
|
+
const summary = summarizeItems(items);
|
|
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
|
+
};
|
|
557
|
+
return {
|
|
558
|
+
experiments: summary.experiments,
|
|
559
|
+
evals: summary.evals,
|
|
560
|
+
attempts: summary.attempts,
|
|
561
|
+
verdicts: summary.verdicts,
|
|
562
|
+
passRate: passRateCell,
|
|
563
|
+
totalCostUSD: summary.totalCostUSD,
|
|
564
|
+
...(summary.lastRunAt !== undefined ? { lastRunAt: summary.lastRunAt } : {}),
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
export async function deltaData(input, opts) {
|
|
568
|
+
assertUniqueMetricNames(opts.metrics, "DeltaTable.data metrics");
|
|
569
|
+
const { snapshots } = resolveInput(input);
|
|
570
|
+
const items = collectItems(snapshots);
|
|
571
|
+
// 一侧的键既匹配 experiment id 也匹配快照键 —— 与 "snapshot" 维度同一格式,不另造对比语义
|
|
572
|
+
const sideItems = (key) => items.filter((item) => experimentIdOf(item) === key || snapshotKeyOf(item.snapshot) === key);
|
|
573
|
+
const rows = [];
|
|
574
|
+
for (const pair of opts.pairs) {
|
|
575
|
+
const aItems = sideItems(pair.a);
|
|
576
|
+
const bItems = sideItems(pair.b);
|
|
577
|
+
const cells = {};
|
|
578
|
+
for (const metric of opts.metrics) {
|
|
579
|
+
const a = await computeCell(metric, aItems);
|
|
580
|
+
const b = await computeCell(metric, bItems);
|
|
581
|
+
const d = a.value === null || b.value === null ? null : b.value - a.value;
|
|
582
|
+
cells[metric.name] = { a, b, delta: d, display: deltaDisplay(metric, d) };
|
|
583
|
+
}
|
|
584
|
+
rows.push({
|
|
585
|
+
key: pair.label ?? `${pair.a} vs ${pair.b}`,
|
|
586
|
+
a: { experimentId: pair.a },
|
|
587
|
+
b: { experimentId: pair.b },
|
|
588
|
+
cells,
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
return { columns: opts.metrics.map(toColumn), rows };
|
|
592
|
+
}
|
|
593
|
+
function deltaDisplay(metric, delta) {
|
|
594
|
+
if (delta === null)
|
|
595
|
+
return "—"; // 任一侧缺数据:Δ 显示为缺,不硬算
|
|
596
|
+
if (delta === 0)
|
|
597
|
+
return "±0";
|
|
598
|
+
const text = displayValue(metric, delta); // 负号由格式化自带
|
|
599
|
+
return delta > 0 ? `+${text}` : text;
|
|
600
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// flag():把 experiment 声明的 flags 当维度或轴(docs/feature/reports/library.md「维度与 flags」)。
|
|
2
|
+
// 变量来自配置,不来自命名 —— 报告不解析 experiment id 字符串抠变量。
|
|
3
|
+
export function flag(name, opts) {
|
|
4
|
+
if (typeof name !== "string" || name.length === 0) {
|
|
5
|
+
throw new Error("flag: name must be a non-empty string (the key declared in the experiment's flags).");
|
|
6
|
+
}
|
|
7
|
+
return { kind: "flag", name, label: opts?.label, unit: opts?.unit };
|
|
8
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { AssertionResult, Verdict } from "../types.ts";
|
|
2
|
+
export declare function formatMetricValue(value: number, unit?: string): string;
|
|
3
|
+
/** 无单位纯数字(scoreboard 总分等):一位小数,去尾零。 */
|
|
4
|
+
export declare function formatPlainNumber(value: number): string;
|
|
5
|
+
/** 全 null / 无样本的统一文案。绝不画 0(docs/feature/reports/architecture.md「指标聚合不变量」)。 */
|
|
6
|
+
export declare const MISSING_TEXT = "no data";
|
|
7
|
+
/** 毫秒 → 人读耗时("850ms" / "1.2s" / "4m 20s" / "1h 4m")。 */
|
|
8
|
+
export declare function formatDurationMs(ms: number): string;
|
|
9
|
+
/** 美元金额;小额保留更多位数,不四舍五入成 $0.00 的假零。 */
|
|
10
|
+
export declare function formatUSD(usd: number): string;
|
|
11
|
+
/** 0..1 的比率 → 整数百分比。 */
|
|
12
|
+
export declare function formatPercent(ratio: number): string;
|
|
13
|
+
/** passed / failed / errored / skipped 的判定符。 */
|
|
14
|
+
export declare function verdictMark(verdict: Verdict): string;
|
|
15
|
+
/**
|
|
16
|
+
* 一个 AttemptListItem 的失败原因摘要,按优先级取第一个在场的:`error` → 未通过的 gate 断言
|
|
17
|
+
* (原始声明顺序,`name`,detail 在场则 `"name: detail"`,多条用「, 」连接)→ 缺席。与
|
|
18
|
+
* compute.ts 的 `reasonFor(EvalResult)` 同一口径,只是输入换成瘦身后的 AttemptListItem——
|
|
19
|
+
* 没有 `skipReason` 字段(AttemptListItem 不携带),skipped 的 attempt 因此没有原因摘要,
|
|
20
|
+
* 这与它们本来就不该出现在「为什么失败」列表里的事实一致。EvalList / ExperimentList 的
|
|
21
|
+
* 逐 attempt 徽标行用它给每个 attempt 自己的原因(而不是复用整道题的代表原因)。
|
|
22
|
+
*/
|
|
23
|
+
export declare function attemptItemReason(item: {
|
|
24
|
+
error?: string;
|
|
25
|
+
assertions: AssertionResult[];
|
|
26
|
+
}): string | undefined;
|