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
package/src/results/copy.ts
CHANGED
|
@@ -1,40 +1,39 @@
|
|
|
1
|
-
// copySnapshots:把选中快照按格式感知地复制到另一个目录(定稿见 docs/results
|
|
1
|
+
// copySnapshots:把选中快照按格式感知地复制到另一个目录(定稿见 docs/feature/results/library.md「复制与瘦身」)。
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
3
|
+
// 发布场景的原语:只带指定 artifact、只带选中快照的全部 attempt,布局知识不外泄。
|
|
4
|
+
// artifact 复制忠实于源(copyFile 原字节,不重新序列化、不消毒);snapshot.json / result.json
|
|
5
|
+
// 按选中条目重建,版本元数据保留。产物是一个标准结果根目录(同布局),openResults /
|
|
6
6
|
// `niceeval view` 直接能读。唯一随行补记的是挑选时的覆盖事实:每个复制出的快照带上
|
|
7
7
|
// knownEvalIds(复制时刻该实验已知的 eval 并集),发布目录上重新 openResults().latest(),
|
|
8
8
|
// 残缺警告被同一套机制重新算出来,不靠发布者转述。
|
|
9
9
|
|
|
10
10
|
import { copyFile, mkdir, readdir, stat, writeFile } from "node:fs/promises";
|
|
11
|
-
import { dirname, join, resolve } from "node:path";
|
|
12
|
-
import type { EvalResult
|
|
13
|
-
import { RESULTS_FORMAT
|
|
14
|
-
import { artifactFileOf,
|
|
11
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
12
|
+
import type { EvalResult } from "../types.ts";
|
|
13
|
+
import { RESULTS_FORMAT } from "../types.ts";
|
|
14
|
+
import { RESULT_FILE, SNAPSHOT_FILE, artifactFileOf, experimentDirOf } from "./format.ts";
|
|
15
15
|
import { experimentOfSnapshot } from "./open.ts";
|
|
16
|
-
import {
|
|
17
|
-
import
|
|
16
|
+
import { isNewerSnapshot } from "./select.ts";
|
|
17
|
+
import { hashEvalSource, normalizeEvalSource } from "./source-hash.ts";
|
|
18
|
+
import type { ArtifactKind, AttemptHandle, Selection, Snapshot, SnapshotMeta } from "./types.ts";
|
|
18
19
|
import { ARTIFACT_KINDS } from "./types.ts";
|
|
19
20
|
|
|
20
21
|
export interface CopySnapshotsOptions {
|
|
21
|
-
/**
|
|
22
|
+
/** 要带上的 artifact 种类;省略 = 全部六类。diff 可达百 MB,发布时常见地不带;o11y 只有几 KB,报告用 turns 这类 artifact 档指标(见 docs/feature/reports/library.md「内置指标」)时记得带上。 */
|
|
22
23
|
artifacts?: ArtifactKind[];
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
export interface CopySnapshotsResult {
|
|
26
|
-
/**
|
|
27
|
+
/** 目标结果根目录的绝对路径。 */
|
|
27
28
|
dir: string;
|
|
28
|
-
/**
|
|
29
|
-
summary: RunSummary;
|
|
30
|
-
/** 复制过程中的警告(如多个 attempt 落到同一目录);不静默。 */
|
|
29
|
+
/** 复制过程中的警告(如同一实验选中多个快照);不静默。 */
|
|
31
30
|
warnings: string[];
|
|
32
31
|
}
|
|
33
32
|
|
|
34
33
|
/**
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
34
|
+
* 把选中快照复制成 `destDir` 下的一个标准结果根目录(`<experiment-dir>/<源快照目录名>/`,
|
|
35
|
+
* 快照目录名原样保留,身份不变)。输入收 Selection 或手工挑的 Snapshot[]
|
|
36
|
+
* (与 Reports 计算函数同一输入约定)。目标目录非空即报错,不静默覆盖、不合并。
|
|
38
37
|
*/
|
|
39
38
|
export async function copySnapshots(
|
|
40
39
|
selection: Selection | Snapshot[],
|
|
@@ -58,45 +57,112 @@ export async function copySnapshots(
|
|
|
58
57
|
const dest = resolve(destDir);
|
|
59
58
|
await assertEmptyDestination(dest);
|
|
60
59
|
|
|
61
|
-
//
|
|
62
|
-
//
|
|
63
|
-
const
|
|
64
|
-
const order: string[] = [];
|
|
60
|
+
// 同一 experiment 选中多个快照 → 只带最新的那个,记 warning(无胜者逻辑:一旦落到单快照,
|
|
61
|
+
// 快照内 evalId+attempt 天然唯一)。
|
|
62
|
+
const byExperiment = new Map<string, Snapshot>();
|
|
65
63
|
const warnings: string[] = [];
|
|
66
64
|
for (const snapshot of selected) {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
winners.set(relDir, attempt);
|
|
72
|
-
order.push(relDir);
|
|
73
|
-
continue;
|
|
74
|
-
}
|
|
75
|
-
warnings.push(
|
|
76
|
-
`warning: multiple attempts map to "${relDir}" in the copied run; kept the one from the newest run. Dedupe attempts or re-select snapshots before copySnapshots to avoid this.`,
|
|
77
|
-
);
|
|
78
|
-
if (isNewerRunDir(attempt.runDir, existing.runDir)) winners.set(relDir, attempt);
|
|
65
|
+
const existing = byExperiment.get(snapshot.experimentId);
|
|
66
|
+
if (!existing) {
|
|
67
|
+
byExperiment.set(snapshot.experimentId, snapshot);
|
|
68
|
+
continue;
|
|
79
69
|
}
|
|
70
|
+
warnings.push(
|
|
71
|
+
`warning: multiple snapshots selected for experiment "${snapshot.experimentId}"; kept the newest one, dropped the rest. Dedupe with Selection.filter() or pick a single snapshot per experiment before copySnapshots to avoid this.`,
|
|
72
|
+
);
|
|
73
|
+
if (isNewerSnapshot(snapshot, existing)) byExperiment.set(snapshot.experimentId, snapshot);
|
|
80
74
|
}
|
|
81
75
|
|
|
82
|
-
// 第二趟:复制工件 + 生成瘦身条目(has* 按目标目录里真的有什么重算)。
|
|
83
76
|
await mkdir(dest, { recursive: true });
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
77
|
+
for (const snapshot of byExperiment.values()) {
|
|
78
|
+
await copyOneSnapshot(snapshot, selected, dest, kinds);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return { dir: dest, warnings };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function copyOneSnapshot(snapshot: Snapshot, selected: Snapshot[], destRoot: string, kinds: ArtifactKind[]): Promise<void> {
|
|
85
|
+
const destSnapDir = join(destRoot, experimentDirOf(snapshot.experimentId), basename(snapshot.dir));
|
|
86
|
+
await mkdir(destSnapDir, { recursive: true });
|
|
87
|
+
|
|
88
|
+
// sources 的去重仓库(sources/<sha256>.json)是快照级的:同一份源码被多个 attempt 引用时,
|
|
89
|
+
// 复制到目的地也只应该有一份——这个 Set 记录本快照已经落过盘的 hash,整快照的 attempt 共享。
|
|
90
|
+
const copiedSourceHashes = new Set<string>();
|
|
91
|
+
for (const attempt of snapshot.attempts) {
|
|
92
|
+
await copyOneAttempt(attempt, destSnapDir, kinds, copiedSourceHashes);
|
|
95
93
|
}
|
|
96
94
|
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
95
|
+
const knownEvalIds = experimentOfSnapshot(snapshot)?.evalIds ?? fallbackUnion(selected, snapshot.experimentId);
|
|
96
|
+
const meta: SnapshotMeta = {
|
|
97
|
+
format: RESULTS_FORMAT,
|
|
98
|
+
schemaVersion: snapshot.schemaVersion,
|
|
99
|
+
producer: snapshot.producer,
|
|
100
|
+
experimentId: snapshot.experimentId,
|
|
101
|
+
...(snapshot.experiment !== undefined ? { experiment: snapshot.experiment } : {}),
|
|
102
|
+
agent: snapshot.agent,
|
|
103
|
+
...(snapshot.model !== undefined ? { model: snapshot.model } : {}),
|
|
104
|
+
startedAt: snapshot.startedAt,
|
|
105
|
+
...(snapshot.completedAt !== undefined ? { completedAt: snapshot.completedAt } : {}),
|
|
106
|
+
...(knownEvalIds.length ? { knownEvalIds } : {}),
|
|
107
|
+
...(snapshot.name !== undefined ? { name: snapshot.name } : {}),
|
|
108
|
+
};
|
|
109
|
+
await writeFile(join(destSnapDir, SNAPSHOT_FILE), JSON.stringify(meta, null, 2), "utf-8");
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function copyOneAttempt(
|
|
113
|
+
attempt: AttemptHandle,
|
|
114
|
+
destSnapDir: string,
|
|
115
|
+
kinds: ArtifactKind[],
|
|
116
|
+
copiedSourceHashes: Set<string>,
|
|
117
|
+
): Promise<void> {
|
|
118
|
+
const destAttemptDir = join(destSnapDir, attempt.ref.attempt);
|
|
119
|
+
await mkdir(destAttemptDir, { recursive: true });
|
|
120
|
+
|
|
121
|
+
// sources 是唯一「两层」的 artifact(attempt 级引用 + 快照级去重仓库),不能像其它四类那样
|
|
122
|
+
// 单文件 copyFile 原字节完事——原字节只是引用,不带内容。走读取面已经会解引用+回退的
|
|
123
|
+
// attempt.sources() 拿到完整内容,再在目的地按内容哈希重新去重落盘,天然吸收 artifactBase
|
|
124
|
+
// 回退链(携带条目复制后,原快照可能不在目的地里,必须此刻把内容落到自己脚下)。
|
|
125
|
+
const genericKinds = kinds.filter((k) => k !== "sources");
|
|
126
|
+
const files = await findArtifactFiles(attempt, genericKinds);
|
|
127
|
+
await Promise.all(files.map(({ kind, source }) => copyFile(source, join(destAttemptDir, artifactFileOf(kind)))));
|
|
128
|
+
const copied = new Set(files.map((f) => f.kind));
|
|
129
|
+
|
|
130
|
+
if (kinds.includes("sources")) {
|
|
131
|
+
const wroteSources = await copySources(attempt, destSnapDir, destAttemptDir, copiedSourceHashes);
|
|
132
|
+
if (wroteSources) copied.add("sources");
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const record = slimForCopy(attempt.result, copied);
|
|
136
|
+
await writeFile(join(destAttemptDir, RESULT_FILE), JSON.stringify(record, null, 2), "utf-8");
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* sources 的复制:经 attempt.sources() 拿到已解引用的完整内容(null / 空数组 = 没有,不写任何
|
|
141
|
+
* 文件,与其它 artifact「空数据不落文件」的约定一致),按内容 sha256 写进目的快照的
|
|
142
|
+
* `sources/<sha256>.json`(同一快照内已经写过的 hash 不重写),attempt 目录下落一份引用。
|
|
143
|
+
*/
|
|
144
|
+
async function copySources(
|
|
145
|
+
attempt: AttemptHandle,
|
|
146
|
+
destSnapDir: string,
|
|
147
|
+
destAttemptDir: string,
|
|
148
|
+
copiedSourceHashes: Set<string>,
|
|
149
|
+
): Promise<boolean> {
|
|
150
|
+
const sources = await attempt.sources();
|
|
151
|
+
if (!sources || sources.length === 0) return false;
|
|
152
|
+
|
|
153
|
+
const destStoreDir = join(destSnapDir, "sources");
|
|
154
|
+
const refs: { path: string; sha256: string }[] = [];
|
|
155
|
+
for (const src of sources) {
|
|
156
|
+
const sha256 = hashEvalSource(normalizeEvalSource(src.content));
|
|
157
|
+
refs.push({ path: src.path, sha256 });
|
|
158
|
+
if (!copiedSourceHashes.has(sha256)) {
|
|
159
|
+
await mkdir(destStoreDir, { recursive: true });
|
|
160
|
+
await writeFile(join(destStoreDir, `${sha256}.json`), JSON.stringify({ content: src.content }), "utf-8");
|
|
161
|
+
copiedSourceHashes.add(sha256);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
await writeFile(join(destAttemptDir, artifactFileOf("sources")), JSON.stringify(refs), "utf-8");
|
|
165
|
+
return true;
|
|
100
166
|
}
|
|
101
167
|
|
|
102
168
|
/** 目标目录非空即报错:盘上不该出现「我没写的东西被动过」的惊讶;发布脚本要幂等就自己先清目录。 */
|
|
@@ -117,14 +183,15 @@ async function assertEmptyDestination(dest: string): Promise<void> {
|
|
|
117
183
|
}
|
|
118
184
|
}
|
|
119
185
|
|
|
120
|
-
/**
|
|
186
|
+
/** 源 artifact 定位:与读取面同一候选顺序(本 attempt 目录优先,artifactBase 回退)。 */
|
|
121
187
|
async function findArtifactFiles(
|
|
122
188
|
attempt: AttemptHandle,
|
|
123
189
|
kinds: ArtifactKind[],
|
|
124
190
|
): Promise<{ kind: ArtifactKind; source: string }[]> {
|
|
125
|
-
const candidates: string[] = [];
|
|
126
|
-
if (attempt.result.
|
|
127
|
-
|
|
191
|
+
const candidates: string[] = [join(attempt.snapshot.dir, attempt.ref.attempt)];
|
|
192
|
+
if (attempt.result.artifactBase) {
|
|
193
|
+
candidates.push(resolve(dirname(dirname(attempt.snapshot.dir)), attempt.result.artifactBase));
|
|
194
|
+
}
|
|
128
195
|
|
|
129
196
|
const found: { kind: ArtifactKind; source: string }[] = [];
|
|
130
197
|
for (const kind of kinds) {
|
|
@@ -136,88 +203,61 @@ async function findArtifactFiles(
|
|
|
136
203
|
break;
|
|
137
204
|
}
|
|
138
205
|
} catch {
|
|
139
|
-
// 缺文件跳过:某类数据为空本来就不生成对应 JSON(docs/results
|
|
206
|
+
// 缺文件跳过:某类数据为空本来就不生成对应 JSON(docs/feature/results/architecture.md)。
|
|
140
207
|
}
|
|
141
208
|
}
|
|
142
209
|
}
|
|
143
210
|
return found;
|
|
144
211
|
}
|
|
145
212
|
|
|
146
|
-
/**
|
|
147
|
-
|
|
148
|
-
|
|
213
|
+
/**
|
|
214
|
+
* 重建 attempt 记录:去掉快照级字段(agent/model/experimentId/experiment,目标 snapshot.json
|
|
215
|
+
* 已经带了)与 artifactBase(artifact 已本地化,不再需要回退指针);has* 按目标目录实际复制到的
|
|
216
|
+
* 种类重算。startedAt 是 attempt 级事实(身份键与「何时跑的」都靠它),原样保留。
|
|
217
|
+
*/
|
|
218
|
+
function slimForCopy(r: EvalResult, copied: Set<ArtifactKind>): Record<string, unknown> {
|
|
219
|
+
const {
|
|
220
|
+
agent,
|
|
221
|
+
model,
|
|
222
|
+
experimentId,
|
|
223
|
+
experiment,
|
|
224
|
+
events,
|
|
225
|
+
sources,
|
|
226
|
+
o11y,
|
|
227
|
+
trace,
|
|
228
|
+
agentSetup,
|
|
229
|
+
diff,
|
|
230
|
+
rawTranscript,
|
|
231
|
+
artifactBase,
|
|
232
|
+
hasTrace,
|
|
233
|
+
hasEvents,
|
|
234
|
+
hasSources,
|
|
235
|
+
...rest
|
|
236
|
+
} = r;
|
|
237
|
+
void agent;
|
|
238
|
+
void model;
|
|
239
|
+
void experimentId;
|
|
240
|
+
void experiment;
|
|
149
241
|
void events;
|
|
150
242
|
void sources;
|
|
151
243
|
void o11y;
|
|
152
244
|
void trace;
|
|
245
|
+
void agentSetup;
|
|
153
246
|
void diff;
|
|
154
247
|
void rawTranscript;
|
|
155
248
|
void artifactBase;
|
|
156
|
-
void
|
|
249
|
+
void hasTrace;
|
|
250
|
+
void hasEvents;
|
|
251
|
+
void hasSources;
|
|
157
252
|
return {
|
|
158
253
|
...rest,
|
|
159
|
-
artifactsDir: relDir,
|
|
160
254
|
hasEvents: copied.has("events"),
|
|
161
255
|
hasTrace: copied.has("trace"),
|
|
162
256
|
hasSources: copied.has("sources"),
|
|
163
257
|
};
|
|
164
258
|
}
|
|
165
259
|
|
|
166
|
-
|
|
167
|
-
// 版本元数据与 run 级元数据取最新源 run 的(producer 缺失的 legacy 源保持缺失,不冒充)。
|
|
168
|
-
const sourceRuns = uniqueRuns(selected);
|
|
169
|
-
let newest = sourceRuns[0];
|
|
170
|
-
for (const run of sourceRuns) if (isNewerRunDir(run, newest)) newest = run;
|
|
171
|
-
|
|
172
|
-
const counts = { passed: 0, failed: 0, skipped: 0, errored: 0 };
|
|
173
|
-
for (const r of results) counts[r.outcome] += 1;
|
|
174
|
-
|
|
175
|
-
return {
|
|
176
|
-
format: RESULTS_FORMAT,
|
|
177
|
-
schemaVersion: RESULTS_SCHEMA_VERSION,
|
|
178
|
-
...(newest.summary.producer ? { producer: newest.summary.producer } : {}),
|
|
179
|
-
...(newest.summary.name !== undefined ? { name: newest.summary.name } : {}),
|
|
180
|
-
// 顶层 agent/model 沿用 runner 的「第一个配置」姿势;快照各自的 agent 在条目里。
|
|
181
|
-
agent: selected[0].agent,
|
|
182
|
-
...(selected[0].model !== undefined ? { model: selected[0].model } : {}),
|
|
183
|
-
startedAt: newest.summary.startedAt,
|
|
184
|
-
completedAt: newest.summary.completedAt,
|
|
185
|
-
...counts,
|
|
186
|
-
// 跨 run 拼装无法还原墙钟时长,这里是选中 attempt 的耗时合计。
|
|
187
|
-
durationMs: results.reduce((total, r) => total + (r.durationMs ?? 0), 0),
|
|
188
|
-
...usageAndCost(results),
|
|
189
|
-
results,
|
|
190
|
-
outputDir: dest,
|
|
191
|
-
snapshots: snapshotsMeta(selected, newest.summary.startedAt),
|
|
192
|
-
};
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
/**
|
|
196
|
-
* 快照级元数据补记:startedAt 保留各快照自己的时刻(它们可能来自不同源 run),
|
|
197
|
-
* knownEvalIds = 复制时刻该实验已知的 eval 并集(openResults 产出的快照从所属实验取,
|
|
198
|
-
* 手工构造的快照退回「同 id 输入快照的覆盖 ∪ 携带值」)—— 残缺检测的分母随数据走。
|
|
199
|
-
*/
|
|
200
|
-
function snapshotsMeta(selected: Snapshot[], summaryStartedAt: string): NonNullable<RunSummary["snapshots"]> {
|
|
201
|
-
// 同一 experiment 被选了多个快照(未 dedupe)时取最新的那个,与工件胜者规则一致。
|
|
202
|
-
const bySnapshotId = new Map<string, Snapshot>();
|
|
203
|
-
for (const snapshot of selected) {
|
|
204
|
-
const existing = bySnapshotId.get(snapshot.experimentId);
|
|
205
|
-
if (!existing || isNewerRunDir(snapshot.runDir, existing.runDir)) {
|
|
206
|
-
bySnapshotId.set(snapshot.experimentId, snapshot);
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
const meta: NonNullable<RunSummary["snapshots"]> = {};
|
|
211
|
-
for (const [experimentId, snapshot] of bySnapshotId) {
|
|
212
|
-
const union = experimentOfSnapshot(snapshot)?.evalIds ?? fallbackUnion(selected, experimentId);
|
|
213
|
-
meta[experimentId] = {
|
|
214
|
-
...(snapshot.startedAt !== summaryStartedAt ? { startedAt: snapshot.startedAt } : {}),
|
|
215
|
-
knownEvalIds: union,
|
|
216
|
-
};
|
|
217
|
-
}
|
|
218
|
-
return meta;
|
|
219
|
-
}
|
|
220
|
-
|
|
260
|
+
/** experimentOfSnapshot 查不到归属(手工构造的 Snapshot[])时的兜底:同 id 输入快照的覆盖 ∪ 携带值。 */
|
|
221
261
|
function fallbackUnion(selected: Snapshot[], experimentId: string): string[] {
|
|
222
262
|
const ids = new Set<string>();
|
|
223
263
|
for (const snapshot of selected) {
|
|
@@ -227,28 +267,3 @@ function fallbackUnion(selected: Snapshot[], experimentId: string): string[] {
|
|
|
227
267
|
}
|
|
228
268
|
return [...ids].sort();
|
|
229
269
|
}
|
|
230
|
-
|
|
231
|
-
/** usage / 成本按选中条目重算;口径同 runner 的 summarize(没有任何 usage 就不写)。 */
|
|
232
|
-
function usageAndCost(results: EvalResult[]): Pick<RunSummary, "usage" | "estimatedCostUSD"> {
|
|
233
|
-
const usages = results.map((r) => r.usage).filter((u): u is Usage => u !== undefined);
|
|
234
|
-
const cost = results.reduce((total, r) => total + (r.estimatedCostUSD ?? 0), 0);
|
|
235
|
-
if (usages.length === 0) return { estimatedCostUSD: cost || undefined };
|
|
236
|
-
const usage: Usage = { inputTokens: 0, outputTokens: 0 };
|
|
237
|
-
for (const u of usages) {
|
|
238
|
-
usage.inputTokens += u.inputTokens;
|
|
239
|
-
usage.outputTokens += u.outputTokens;
|
|
240
|
-
for (const key of ["cacheReadTokens", "cacheWriteTokens", "requests", "costUSD"] as const) {
|
|
241
|
-
const value = u[key];
|
|
242
|
-
if (value !== undefined) usage[key] = (usage[key] ?? 0) + value;
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
return { usage, estimatedCostUSD: cost || undefined };
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
function uniqueRuns(snapshots: Snapshot[]): RunDir[] {
|
|
249
|
-
const runs: RunDir[] = [];
|
|
250
|
-
for (const snapshot of snapshots) {
|
|
251
|
-
if (!runs.includes(snapshot.runDir)) runs.push(snapshot.runDir);
|
|
252
|
-
}
|
|
253
|
-
return runs;
|
|
254
|
-
}
|
package/src/results/format.ts
CHANGED
|
@@ -1,76 +1,83 @@
|
|
|
1
|
-
// Results Format 的布局与版本知识,规则见 docs/results
|
|
1
|
+
// Results Format 的布局与版本知识,规则见 docs/feature/results/architecture.md。
|
|
2
2
|
//
|
|
3
|
-
// 按 docs/results
|
|
3
|
+
// 按 docs/feature/results/library.md,这份知识只住在本库:写入面(writer.ts / copy.ts)与
|
|
4
4
|
// 读取面(open.ts)共用这里的目录规则与版本判定;src/runner/reporters/artifacts.ts
|
|
5
5
|
// 是 writer 的薄壳,view(src/view/data.ts)经 openResults 消费,不自带布局知识。
|
|
6
6
|
|
|
7
|
-
import type { EvalResult
|
|
7
|
+
import type { EvalResult } from "../types.ts";
|
|
8
8
|
import { RESULTS_FORMAT, RESULTS_SCHEMA_VERSION } from "../types.ts";
|
|
9
|
-
import type { ArtifactKind } from "./types.ts";
|
|
9
|
+
import type { ArtifactKind, SnapshotMeta } from "./types.ts";
|
|
10
10
|
|
|
11
|
-
/** attempt
|
|
12
|
-
export
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
/** attempt 目录名:结果记录文件恒为 result.json,attempt 级判决权威落点。 */
|
|
12
|
+
export const RESULT_FILE = "result.json";
|
|
13
|
+
/** 快照元数据文件名。 */
|
|
14
|
+
export const SNAPSHOT_FILE = "snapshot.json";
|
|
15
|
+
|
|
16
|
+
/** attempt 目录(相对快照根):`<evalId>/a<attempt>`;evalId 里的 / 保留作目录层级,其余危险字符替换。 */
|
|
17
|
+
export function attemptDirOf(r: Pick<EvalResult, "id" | "attempt">): string {
|
|
15
18
|
const id = r.id.replace(/[^\w./@-]/g, "_");
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
19
|
+
return `${id}/a${r.attempt}`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** 实验目录名:experimentId 里的 / 与其它非 [\w.@-] 字符替换成 _。 */
|
|
23
|
+
export function experimentDirOf(experimentId: string): string {
|
|
24
|
+
return experimentId.replace(/[^\w.@-]/g, "_");
|
|
20
25
|
}
|
|
21
26
|
|
|
22
|
-
/**
|
|
27
|
+
/**
|
|
28
|
+
* artifact 文件名。种类名是 TS 侧的驼峰(`agentSetup`),文件名是磁盘侧的 kebab
|
|
29
|
+
* (`agent-setup.json`)——两边各自守自己的惯例,映射表是唯一的翻译点。
|
|
30
|
+
*/
|
|
31
|
+
const ARTIFACT_FILES: Record<ArtifactKind, string> = {
|
|
32
|
+
events: "events.json",
|
|
33
|
+
trace: "trace.json",
|
|
34
|
+
o11y: "o11y.json",
|
|
35
|
+
agentSetup: "agent-setup.json",
|
|
36
|
+
diff: "diff.json",
|
|
37
|
+
sources: "sources.json",
|
|
38
|
+
};
|
|
39
|
+
|
|
23
40
|
export function artifactFileOf(kind: ArtifactKind): string {
|
|
24
|
-
return
|
|
41
|
+
return ARTIFACT_FILES[kind];
|
|
25
42
|
}
|
|
26
43
|
|
|
27
|
-
/**
|
|
28
|
-
export type
|
|
29
|
-
| { kind: "ok";
|
|
30
|
-
| { kind: "incompatible"; schemaVersion: number; producer?:
|
|
44
|
+
/** snapshot.json 的版本判定结果;openResults 按它分流 ok / skipped / 静默忽略。 */
|
|
45
|
+
export type SnapshotClassification =
|
|
46
|
+
| { kind: "ok"; meta: SnapshotMeta }
|
|
47
|
+
| { kind: "incompatible"; schemaVersion: number; producer?: SnapshotMeta["producer"] }
|
|
31
48
|
| { kind: "malformed"; detail: string }
|
|
32
49
|
| { kind: "not-a-report" };
|
|
33
50
|
|
|
34
51
|
/**
|
|
35
|
-
* 版本判定与最小形状校验(docs/results
|
|
36
|
-
* - 带 format 信封:format 不是 niceeval.results → 无关 JSON;schemaVersion
|
|
37
|
-
* 不兼容(不解析、不迁移、不降级)
|
|
38
|
-
*
|
|
39
|
-
*
|
|
52
|
+
* 版本判定与最小形状校验(docs/feature/results/architecture.md「版本不匹配时的读取行为」):
|
|
53
|
+
* - 带 format 信封:format 不是 niceeval.results → 无关 JSON;schemaVersion 非数字 → malformed;
|
|
54
|
+
* schemaVersion 与当前不同 → 不兼容(不解析、不迁移、不降级),带 schemaVersion 与完整 producer;
|
|
55
|
+
* schemaVersion 相同时校验 experimentId / agent / startedAt 均为 string,不满足 → malformed。
|
|
56
|
+
* - 无信封:v1 的 run 级 summary.json(results[] 是数组且 startedAt 是 string)按 incompatible
|
|
57
|
+
* (schemaVersion 1)处理 —— 这是版本识别,不是迁移;不满足启发式的当无关 JSON 忽略。
|
|
58
|
+
* - v2/v3 的 summary.json 带 format + schemaVersion(≠ 4),自然落进 incompatible 档。
|
|
40
59
|
*/
|
|
41
|
-
export function
|
|
60
|
+
export function classifySnapshot(raw: unknown): SnapshotClassification {
|
|
42
61
|
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
43
62
|
return { kind: "malformed", detail: "not a JSON object" };
|
|
44
63
|
}
|
|
45
|
-
const data = raw as Partial<
|
|
64
|
+
const data = raw as Partial<SnapshotMeta> & { results?: unknown };
|
|
46
65
|
if (data.format !== undefined && data.format !== RESULTS_FORMAT) return { kind: "not-a-report" };
|
|
47
66
|
if (data.format === RESULTS_FORMAT) {
|
|
48
|
-
|
|
49
|
-
if (
|
|
50
|
-
if (version !== RESULTS_SCHEMA_VERSION) {
|
|
67
|
+
if (typeof data.schemaVersion !== "number") return { kind: "malformed", detail: "schemaVersion is not a number" };
|
|
68
|
+
if (data.schemaVersion !== RESULTS_SCHEMA_VERSION) {
|
|
51
69
|
// skipped 必须带完整 producer(name + version):npx 提示只对 name === "niceeval" 成立,
|
|
52
70
|
// 第三方 harness 的落盘只给裸版本号的话,消费方连做对这个分支的信息都没有。
|
|
53
|
-
return { kind: "incompatible", schemaVersion:
|
|
71
|
+
return { kind: "incompatible", schemaVersion: data.schemaVersion, producer: data.producer };
|
|
54
72
|
}
|
|
55
|
-
if (
|
|
56
|
-
return { kind: "malformed", detail: "missing
|
|
73
|
+
if (typeof data.experimentId !== "string" || typeof data.agent !== "string" || typeof data.startedAt !== "string") {
|
|
74
|
+
return { kind: "malformed", detail: "missing experimentId, agent or startedAt" };
|
|
57
75
|
}
|
|
58
|
-
return { kind: "ok",
|
|
76
|
+
return { kind: "ok", meta: data as SnapshotMeta };
|
|
59
77
|
}
|
|
60
|
-
|
|
61
|
-
|
|
78
|
+
// 无信封:引入版本信封之前的存量报告(v1 run 级 summary.json)。
|
|
79
|
+
if (Array.isArray(data.results) && typeof (data as { startedAt?: unknown }).startedAt === "string") {
|
|
80
|
+
return { kind: "incompatible", schemaVersion: 1 };
|
|
62
81
|
}
|
|
63
|
-
return { kind: "
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* 快照/去重共用的 experiment 身份键:experimentId 缺失时以 "<agent>/<model>" 合成
|
|
68
|
-
* (无 model 用 "default",与工件目录命名同一口径),synthesized 供调用方决定要不要出 warning。
|
|
69
|
-
*/
|
|
70
|
-
export function experimentKeyOf(r: Pick<EvalResult, "experimentId" | "agent" | "model">): {
|
|
71
|
-
id: string;
|
|
72
|
-
synthesized: boolean;
|
|
73
|
-
} {
|
|
74
|
-
if (r.experimentId) return { id: r.experimentId, synthesized: false };
|
|
75
|
-
return { id: `${r.agent}/${r.model ?? "default"}`, synthesized: true };
|
|
82
|
+
return { kind: "not-a-report" };
|
|
76
83
|
}
|