niceeval 0.5.3 → 0.6.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 +55 -0
- package/dist/agents/types.d.ts +317 -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/execution-tree.d.ts +103 -0
- package/dist/o11y/otlp/parse.d.ts +3 -0
- package/dist/o11y/otlp/receiver.d.ts +21 -0
- package/dist/o11y/otlp/select.d.ts +22 -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/cost-pass-rate-comparison.d.ts +1 -0
- package/dist/report/built-ins/cost-pass-rate-comparison.js +17 -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 +178 -0
- package/dist/report/components.js +132 -0
- package/dist/report/compute.d.ts +106 -0
- package/dist/report/compute.js +618 -0
- package/dist/report/flag.d.ts +6 -0
- package/dist/report/flag.js +8 -0
- package/dist/report/format.d.ts +32 -0
- package/dist/report/format.js +126 -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 +83 -0
- package/dist/report/locale.js +160 -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 +20 -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 +10 -0
- package/dist/report/react/ExperimentList.js +15 -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 +100 -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 +393 -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 +503 -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 +420 -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/annotated-source.d.ts +61 -0
- package/dist/results/attempt-evidence.d.ts +69 -0
- package/dist/results/attempt-source.d.ts +15 -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 +9 -4
- package/docs-site/zh/concepts/assert.mdx +8 -8
- package/docs-site/zh/concepts/evals.mdx +5 -5
- package/docs-site/zh/concepts/experiment.mdx +3 -1
- package/docs-site/zh/concepts/judge.mdx +9 -9
- package/docs-site/zh/concepts/overview.mdx +2 -2
- 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 +235 -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 -0
- package/docs-site/zh/guides/fixtures.mdx +1 -1
- 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 +35 -34
- package/docs-site/zh/guides/runner.mdx +2 -2
- package/docs-site/zh/guides/sandbox-agent.mdx +10 -5
- package/docs-site/zh/guides/sandbox-providers.mdx +102 -0
- package/docs-site/zh/guides/viewing-results.mdx +199 -79
- package/docs-site/zh/guides/write-experiment.mdx +23 -1
- package/docs-site/zh/guides/write-send.mdx +2 -2
- 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 +24 -12
- 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 +7 -1
- package/docs-site/zh/reference/expect.mdx +3 -3
- package/package.json +10 -6
- package/src/agents/bub.ts +134 -43
- 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 +11 -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 +62 -2
- package/src/cli.ts +74 -47
- package/src/context/context.test.ts +20 -2
- package/src/context/context.ts +3 -0
- package/src/context/control-flow.ts +1 -1
- package/src/context/session.test.ts +92 -1
- package/src/context/session.ts +26 -1
- package/src/context/types.ts +4 -4
- package/src/define.test.ts +82 -0
- package/src/define.ts +72 -11
- package/src/expect/index.ts +5 -5
- package/src/i18n/en.ts +36 -15
- package/src/i18n/zh-CN.ts +39 -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 +643 -0
- package/src/report/built-ins/cost-pass-rate-comparison.tsx +23 -0
- package/src/report/built-ins/index.ts +2 -0
- package/src/report/components.tsx +223 -50
- package/src/report/compute.ts +358 -53
- package/src/report/dual-render.test.tsx +1125 -0
- package/src/report/flag.ts +1 -1
- package/src/report/format.ts +50 -2
- package/src/report/index.ts +48 -14
- package/src/report/load.ts +1 -1
- package/src/report/locale.ts +206 -0
- package/src/report/metrics.ts +39 -15
- package/src/report/primitives.tsx +100 -2
- package/src/report/react/AttemptList.tsx +115 -0
- package/src/report/react/DeltaTable.tsx +9 -6
- package/src/report/react/EvalList.tsx +0 -0
- package/src/report/react/ExperimentList.tsx +108 -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 +136 -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 +129 -0
- package/src/report/react/fixtures.ts +163 -34
- package/src/report/react/format.ts +1 -1
- package/src/report/react/index.tsx +22 -6
- package/src/report/react/render.test.tsx +164 -31
- package/src/report/react/styles.css +405 -213
- package/src/report/report.test.ts +403 -94
- package/src/report/report.ts +29 -11
- package/src/report/text/faces.ts +303 -95
- 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 +196 -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 +97 -15
- 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 +69 -28
- 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 +125 -94
- package/src/show/render.ts +660 -160
- package/src/show/show.test.ts +472 -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/docs-site/zh/guides/sandbox-backends.mdx +0 -75
- 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/open.ts
CHANGED
|
@@ -1,28 +1,35 @@
|
|
|
1
1
|
// openResults:扫描结果目录,返回「实验 → 快照 → eval → attempt」的类型化层次
|
|
2
|
-
// (定稿见 docs/results
|
|
2
|
+
// (定稿见 docs/feature/results/library.md「读:openResults」、docs/feature/results/architecture.md「读取规则」)。
|
|
3
3
|
//
|
|
4
4
|
// 三条铁律:
|
|
5
5
|
// - 忠实磁盘:快照与实验归组只切片,不合并、不聚合、不去重;合并/聚合永远发生在消费方。
|
|
6
|
-
// - 读不了的落盘进 skipped(三种原因),不静默丢,也不抛错(
|
|
7
|
-
// -
|
|
6
|
+
// - 读不了的落盘进 skipped(三种原因),不静默丢,也不抛错(单个坏快照不拖垮整次扫描)。
|
|
7
|
+
// - 重 artifact 全部懒加载:缺失返回 null(存在性判断被方法语义吸收),同一 handle 内记忆化。
|
|
8
8
|
|
|
9
9
|
import { readFile, readdir, stat } from "node:fs/promises";
|
|
10
|
-
import { basename, dirname, join, resolve } from "node:path";
|
|
11
|
-
import type { EvalResult
|
|
10
|
+
import { basename, dirname, join, relative, resolve, sep } from "node:path";
|
|
11
|
+
import type { EvalResult } from "../types.ts";
|
|
12
12
|
import type { O11ySummary, StreamEvent, TraceSpan } from "../types.ts";
|
|
13
|
-
import type { DiffData, SourceArtifact } from "../types.ts";
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
13
|
+
import type { AgentSetupManifest, DiffData, SourceArtifact } from "../types.ts";
|
|
14
|
+
import { RESULT_FILE, SNAPSHOT_FILE, artifactFileOf, classifySnapshot } from "./format.ts";
|
|
15
|
+
import { isNewerSnapshot, selectLatest } from "./select.ts";
|
|
16
|
+
import {
|
|
17
|
+
encodeAttemptLocator,
|
|
18
|
+
resolveAttemptLocator,
|
|
19
|
+
LocatorCollisionError,
|
|
20
|
+
type AttemptIdentity,
|
|
21
|
+
type AttemptLocator,
|
|
22
|
+
} from "./locator.ts";
|
|
16
23
|
import type {
|
|
17
24
|
ArtifactKind,
|
|
18
25
|
AttemptHandle,
|
|
19
26
|
Eval,
|
|
20
27
|
Experiment,
|
|
21
28
|
Results,
|
|
22
|
-
RunDir,
|
|
23
29
|
Selection,
|
|
24
|
-
|
|
30
|
+
SkippedDir,
|
|
25
31
|
Snapshot,
|
|
32
|
+
SnapshotMeta,
|
|
26
33
|
} from "./types.ts";
|
|
27
34
|
import { ARTIFACT_KINDS } from "./types.ts";
|
|
28
35
|
|
|
@@ -35,224 +42,332 @@ export function experimentOfSnapshot(snapshot: Snapshot): Experiment | undefined
|
|
|
35
42
|
return experimentBySnapshot.get(snapshot);
|
|
36
43
|
}
|
|
37
44
|
|
|
45
|
+
// locator → AttemptHandle 索引同样挂在 openResults() 产出的 Results 上,不进公开类型
|
|
46
|
+
// (Results 接口保持精简,索引经 resolveLocator() 这个自由函数取用,与 experimentOfSnapshot
|
|
47
|
+
// 同一种「WeakMap 记归属」模式)。openResults() 之外手工拼出来的 Results 对象查不到索引,
|
|
48
|
+
// resolveLocator() 对此的处理是「查不到 = 空索引」,一律 not-found,不抛意外错误。
|
|
49
|
+
const locatorIndexByResults = new WeakMap<Results, Map<AttemptLocator, AttemptHandle>>();
|
|
50
|
+
|
|
51
|
+
/** locator 语法合法、但索引里没有这个 key——落盘已被清理、复制时没带上,或纯粹打错。 */
|
|
52
|
+
export class LocatorNotFoundError extends Error {
|
|
53
|
+
constructor(public readonly locator: string) {
|
|
54
|
+
super(
|
|
55
|
+
`No attempt found for locator "${locator}" in this results root. It may be stale ` +
|
|
56
|
+
"(the snapshot was deleted, or copySnapshots didn't include it) or mistyped.",
|
|
57
|
+
);
|
|
58
|
+
this.name = "LocatorNotFoundError";
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** locator 字符串本身不合法(前缀 / scheme 字符 / body 字符集或长度不对)。 */
|
|
63
|
+
export class MalformedLocatorError extends Error {
|
|
64
|
+
constructor(
|
|
65
|
+
public readonly input: string,
|
|
66
|
+
public readonly reason: string,
|
|
67
|
+
) {
|
|
68
|
+
super(`"${input}" is not a valid attempt locator: ${reason}`);
|
|
69
|
+
this.name = "MalformedLocatorError";
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
38
73
|
/**
|
|
39
|
-
*
|
|
74
|
+
* 拿 CLI 位置参数里的原始 `@...` 字符串,在 `openResults()` 建好的 locator 索引里查找。
|
|
75
|
+
* 找不到 / 语法不对是两种不同的用户错误(打错 vs 过期),用两个可判别的 Error 子类分开抛,
|
|
76
|
+
* 不折叠成一句通用报错——上层(CLI)按 `instanceof` 决定提示文案。
|
|
77
|
+
*/
|
|
78
|
+
export function resolveLocator(results: Results, input: string): AttemptHandle {
|
|
79
|
+
const index = locatorIndexByResults.get(results) ?? new Map<AttemptLocator, AttemptHandle>();
|
|
80
|
+
const resolution = resolveAttemptLocator(index, input);
|
|
81
|
+
switch (resolution.kind) {
|
|
82
|
+
case "found":
|
|
83
|
+
return resolution.handle;
|
|
84
|
+
case "malformed":
|
|
85
|
+
throw new MalformedLocatorError(resolution.input, resolution.reason);
|
|
86
|
+
case "not-found":
|
|
87
|
+
throw new LocatorNotFoundError(resolution.locator);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* 扫描出的全部 attempt 建一份 locator → AttemptHandle 索引(openResults() 收尾时调一次)。
|
|
93
|
+
* 遍历顺序 = experiments(字典序)→ exp.snapshots(新→旧,buildExperiments 已排好序)→
|
|
94
|
+
* snapshot.attempts;「先遇到的赢」自然保留最新快照里的那份(--resume 携带条目在新旧两个
|
|
95
|
+
* 快照里都能扫到时,新快照排在前面先被记进索引,旧快照那份被跳过——同一份 locator 重复
|
|
96
|
+
* 出现不是撞车)。三元组(experimentId/evalId/attempt 序号)不同却撞出同一个 locator
|
|
97
|
+
* 字符串,才是真撞车,直接抛 LocatorCollisionError,不静默覆盖。
|
|
98
|
+
*/
|
|
99
|
+
function buildAttemptLocatorIndex(experiments: Experiment[]): Map<AttemptLocator, AttemptHandle> {
|
|
100
|
+
const index = new Map<AttemptLocator, AttemptHandle>();
|
|
101
|
+
for (const exp of experiments) {
|
|
102
|
+
for (const snapshot of exp.snapshots) {
|
|
103
|
+
for (const attempt of snapshot.attempts) {
|
|
104
|
+
const locator = attempt.locator;
|
|
105
|
+
if (locator === undefined) continue; // 理论上不会发生(makeAttempt 恒回填),防御性跳过
|
|
106
|
+
const existing = index.get(locator);
|
|
107
|
+
if (existing === undefined) {
|
|
108
|
+
index.set(locator, attempt);
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
if (
|
|
112
|
+
existing.experimentId !== attempt.experimentId ||
|
|
113
|
+
existing.evalId !== attempt.evalId ||
|
|
114
|
+
existing.result.attempt !== attempt.result.attempt
|
|
115
|
+
) {
|
|
116
|
+
throw new LocatorCollisionError(locator, [identityForError(existing), identityForError(attempt)]);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return index;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** LocatorCollisionError 诊断信息用:携带条目场景下这不一定是「真」身份(snapshotStartedAt
|
|
125
|
+
* 取当前所在快照的值,携带条目原本可能来自更早的快照),但足够定位是哪两个 attempt 撞的。 */
|
|
126
|
+
function identityForError(attempt: AttemptHandle): AttemptIdentity {
|
|
127
|
+
return {
|
|
128
|
+
experimentId: attempt.experimentId,
|
|
129
|
+
snapshotStartedAt: attempt.snapshot.startedAt,
|
|
130
|
+
evalId: attempt.evalId,
|
|
131
|
+
attempt: attempt.result.attempt,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
interface ScanState {
|
|
136
|
+
snapshots: Snapshot[];
|
|
137
|
+
skipped: SkippedDir[];
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* 打开结果根、实验目录、快照目录,或直接指向某个 snapshot.json(/ 历史版本 summary.json)的路径。
|
|
40
142
|
* 目录不存在返回空集合(还没跑过 eval 不是错误);任何读不了的落盘进 skipped,不抛错。
|
|
41
143
|
*/
|
|
42
144
|
export async function openResults(dir: string): Promise<Results> {
|
|
43
145
|
const target = resolve(dir);
|
|
44
|
-
const
|
|
45
|
-
const skipped: SkippedRun[] = [];
|
|
146
|
+
const state: ScanState = { snapshots: [], skipped: [] };
|
|
46
147
|
|
|
47
148
|
let targetStat;
|
|
48
149
|
try {
|
|
49
150
|
targetStat = await stat(target);
|
|
50
151
|
} catch {
|
|
51
|
-
return makeResults([],
|
|
152
|
+
return makeResults([], []);
|
|
52
153
|
}
|
|
53
154
|
|
|
54
|
-
const summaryPaths: string[] = [];
|
|
55
155
|
if (targetStat.isFile()) {
|
|
56
|
-
|
|
156
|
+
// 单文件模式:直指 snapshot.json(或历史版本 summary.json)→ 读它所在目录为快照。
|
|
157
|
+
await handleMetaFile(target, state);
|
|
57
158
|
} else {
|
|
58
|
-
|
|
59
|
-
if (await fileExists(direct)) {
|
|
60
|
-
// target 本身是 run 目录:它的子目录是 attempt 工件目录,不做 incomplete 探测。
|
|
61
|
-
summaryPaths.push(direct, ...(await findSummaryFiles(target, direct)));
|
|
62
|
-
} else {
|
|
63
|
-
// target 是结果根:逐个 immediate child 判定 —— 有 summary 的收 run;
|
|
64
|
-
// 没有 summary 但有 attempt 工件的 = crash 没收尾,进 skipped("incomplete")。
|
|
65
|
-
const entries = await readdir(target, { withFileTypes: true });
|
|
66
|
-
for (const entry of entries.filter((e) => e.isDirectory())) {
|
|
67
|
-
const childDir = join(target, entry.name);
|
|
68
|
-
const found = await findSummaryFiles(childDir);
|
|
69
|
-
if (found.length > 0) {
|
|
70
|
-
summaryPaths.push(...found);
|
|
71
|
-
} else if (await hasArtifactFiles(childDir)) {
|
|
72
|
-
skipped.push({ dir: childDir, reason: "incomplete" });
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
for (const path of summaryPaths) {
|
|
79
|
-
const outcome = await readRun(path);
|
|
80
|
-
if (outcome.kind === "run") runDirs.push(outcome.run);
|
|
81
|
-
else if (outcome.kind === "skipped") skipped.push(outcome.entry);
|
|
82
|
-
// not-a-report:无关 JSON,静默忽略。
|
|
159
|
+
await scan(target, 0, state);
|
|
83
160
|
}
|
|
84
161
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
|
|
162
|
+
state.skipped.sort((a, b) => b.dir.localeCompare(a.dir));
|
|
163
|
+
const experiments = buildExperiments(state.snapshots);
|
|
164
|
+
// 建 locator 索引:必须在全部快照扫完、Experiment 归组完成之后,返回 Results 之前——
|
|
165
|
+
// 撞车(LocatorCollisionError)在这里抛,不静默吞、不拖到消费方第一次 resolveLocator() 才发现。
|
|
166
|
+
const locatorIndex = buildAttemptLocatorIndex(experiments);
|
|
167
|
+
const results = makeResults(experiments, state.skipped);
|
|
168
|
+
locatorIndexByResults.set(results, locatorIndex);
|
|
169
|
+
return results;
|
|
92
170
|
}
|
|
93
171
|
|
|
94
|
-
function makeResults(experiments: Experiment[], skipped:
|
|
172
|
+
function makeResults(experiments: Experiment[], skipped: SkippedDir[]): Results {
|
|
95
173
|
return {
|
|
96
174
|
experiments,
|
|
97
175
|
skipped,
|
|
98
|
-
runDirs,
|
|
99
176
|
latest(opts?: { experiments?: string | string[] }): Selection {
|
|
100
177
|
return selectLatest(experiments, opts);
|
|
101
178
|
},
|
|
102
179
|
};
|
|
103
180
|
}
|
|
104
181
|
|
|
105
|
-
// ─────────────────────────
|
|
182
|
+
// ───────────────────────── 目录扫描 ─────────────────────────
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* 递归扫描:目录里直接有 snapshot.json 或 summary.json → 处理并计 found,不再向下找;
|
|
186
|
+
* 否则递归子目录;子树全部未 found 且 depth ≤ 2 且该目录(递归)含 artifact/result 文件 →
|
|
187
|
+
* 折叠成 skipped("incomplete"),计 found —— 把 attempt 级噪音折叠到实验/快照层,
|
|
188
|
+
* 旧版(v3 及更早)run 目录直接 crash 在 depth 1 也被这条规则覆盖。
|
|
189
|
+
*/
|
|
190
|
+
async function scan(dir: string, depth: number, state: ScanState): Promise<boolean> {
|
|
191
|
+
if (await hasFile(dir, SNAPSHOT_FILE)) {
|
|
192
|
+
await handleMetaFile(join(dir, SNAPSHOT_FILE), state);
|
|
193
|
+
return true;
|
|
194
|
+
}
|
|
195
|
+
if (await hasFile(dir, "summary.json")) {
|
|
196
|
+
await handleMetaFile(join(dir, "summary.json"), state);
|
|
197
|
+
return true;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
let entries;
|
|
201
|
+
try {
|
|
202
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
203
|
+
} catch {
|
|
204
|
+
return false;
|
|
205
|
+
}
|
|
106
206
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
207
|
+
let anyFound = false;
|
|
208
|
+
for (const entry of entries) {
|
|
209
|
+
if (!entry.isDirectory()) continue;
|
|
210
|
+
const found = await scan(join(dir, entry.name), depth + 1, state);
|
|
211
|
+
anyFound = anyFound || found;
|
|
212
|
+
}
|
|
111
213
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
214
|
+
if (!anyFound && depth <= 2 && (await hasArtifactOrResultFiles(dir))) {
|
|
215
|
+
state.skipped.push({ dir, reason: "incomplete" });
|
|
216
|
+
return true;
|
|
217
|
+
}
|
|
218
|
+
return anyFound;
|
|
219
|
+
}
|
|
118
220
|
|
|
221
|
+
/** 读一份元数据文件(snapshot.json 或历史版本 summary.json),按分类结果分流。 */
|
|
222
|
+
async function handleMetaFile(path: string, state: ScanState): Promise<void> {
|
|
223
|
+
const dir = dirname(path);
|
|
119
224
|
let text: string;
|
|
120
225
|
try {
|
|
121
226
|
text = await readFile(path, "utf-8");
|
|
122
227
|
} catch (e) {
|
|
123
|
-
|
|
228
|
+
state.skipped.push({ dir, reason: "malformed", detail: `cannot read file (${e instanceof Error ? e.message : String(e)})` });
|
|
229
|
+
return;
|
|
124
230
|
}
|
|
125
231
|
let raw: unknown;
|
|
126
232
|
try {
|
|
127
233
|
raw = JSON.parse(text);
|
|
128
234
|
} catch {
|
|
129
|
-
|
|
235
|
+
state.skipped.push({ dir, reason: "malformed", detail: "invalid JSON" });
|
|
236
|
+
return;
|
|
130
237
|
}
|
|
131
238
|
|
|
132
|
-
const classified =
|
|
239
|
+
const classified = classifySnapshot(raw);
|
|
133
240
|
switch (classified.kind) {
|
|
134
241
|
case "not-a-report":
|
|
135
|
-
return
|
|
242
|
+
return; // 无关 JSON,静默忽略(调用方仍把此目录计为 found,不触发 incomplete 折叠)。
|
|
136
243
|
case "malformed":
|
|
137
|
-
|
|
244
|
+
state.skipped.push({ dir, reason: "malformed", detail: classified.detail });
|
|
245
|
+
return;
|
|
138
246
|
case "incompatible":
|
|
139
|
-
|
|
247
|
+
state.skipped.push({
|
|
248
|
+
dir,
|
|
140
249
|
reason: "incompatible-version",
|
|
141
250
|
schemaVersion: classified.schemaVersion,
|
|
142
251
|
...(classified.producer ? { producer: classified.producer } : {}),
|
|
143
252
|
});
|
|
253
|
+
return;
|
|
144
254
|
case "ok": {
|
|
145
|
-
const
|
|
146
|
-
|
|
255
|
+
const snapshot = await readSnapshotDir(dir, classified.meta, state);
|
|
256
|
+
state.snapshots.push(snapshot);
|
|
257
|
+
return;
|
|
147
258
|
}
|
|
148
259
|
}
|
|
149
260
|
}
|
|
150
261
|
|
|
151
|
-
// ─────────────────────────
|
|
262
|
+
// ───────────────────────── 快照读取 ─────────────────────────
|
|
152
263
|
|
|
153
|
-
/**
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
264
|
+
/** 快照目录:递归收集全部 result.json,组装成 evals / attempts;单个 result.json 坏 JSON 不拖垮快照。 */
|
|
265
|
+
async function readSnapshotDir(dir: string, meta: SnapshotMeta, state: ScanState): Promise<Snapshot> {
|
|
266
|
+
const snapshot: Snapshot = {
|
|
267
|
+
experimentId: meta.experimentId,
|
|
268
|
+
startedAt: meta.startedAt,
|
|
269
|
+
...(meta.completedAt !== undefined ? { completedAt: meta.completedAt } : {}),
|
|
270
|
+
agent: meta.agent,
|
|
271
|
+
...(meta.model !== undefined ? { model: meta.model } : {}),
|
|
272
|
+
...(meta.experiment !== undefined ? { experiment: meta.experiment } : {}),
|
|
273
|
+
producer: meta.producer,
|
|
274
|
+
schemaVersion: meta.schemaVersion,
|
|
275
|
+
...(meta.name !== undefined ? { name: meta.name } : {}),
|
|
276
|
+
evals: [],
|
|
277
|
+
attempts: [],
|
|
278
|
+
dir,
|
|
279
|
+
...(meta.knownEvalIds?.length ? { knownEvalIds: [...meta.knownEvalIds] } : {}),
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
const resultPaths = (await findResultFiles(dir)).sort();
|
|
283
|
+
const evalsById = new Map<string, Eval>();
|
|
284
|
+
|
|
285
|
+
for (const resultPath of resultPaths) {
|
|
286
|
+
const attemptDir = dirname(resultPath);
|
|
287
|
+
let record: EvalResult;
|
|
288
|
+
try {
|
|
289
|
+
const text = await readFile(resultPath, "utf-8");
|
|
290
|
+
record = JSON.parse(text) as EvalResult;
|
|
291
|
+
} catch (e) {
|
|
292
|
+
state.skipped.push({ dir: attemptDir, reason: "malformed", detail: `invalid result.json (${e instanceof Error ? e.message : String(e)})` });
|
|
293
|
+
continue;
|
|
183
294
|
}
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
295
|
+
|
|
296
|
+
// 快照级字段拼合:「缺才补」,条目自带的值(携带条目的 startedAt)优先。
|
|
297
|
+
record.experimentId ??= meta.experimentId;
|
|
298
|
+
record.agent ??= meta.agent;
|
|
299
|
+
if (record.model === undefined && meta.model !== undefined) record.model = meta.model;
|
|
300
|
+
record.startedAt ??= meta.startedAt;
|
|
301
|
+
if (record.experiment === undefined && meta.experiment !== undefined) record.experiment = meta.experiment;
|
|
302
|
+
// locator 同理「缺才补」:niceeval 自己的 writer(schemaVersion 5 起)恒会写这个字段,
|
|
303
|
+
// 携带条目原样携带上一轮的值——只有真缺失(第三方 harness 没实现 locator,或手工构造的
|
|
304
|
+
// 落盘)才按当前身份兜底算一份;这份兜底不保证跨未来的 --resume 稳定,但至少确定性、
|
|
305
|
+
// 可解析,不比完全没有 locator 差。
|
|
306
|
+
record.locator ??= encodeAttemptLocator({
|
|
307
|
+
experimentId: record.experimentId,
|
|
308
|
+
snapshotStartedAt: meta.startedAt,
|
|
309
|
+
evalId: record.id,
|
|
310
|
+
attempt: record.attempt,
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
const attempt = makeAttempt(snapshot, dir, attemptDir, record);
|
|
314
|
+
let ev = evalsById.get(record.id);
|
|
188
315
|
if (!ev) {
|
|
189
|
-
ev = { id:
|
|
190
|
-
|
|
316
|
+
ev = { id: record.id, attempts: [] };
|
|
317
|
+
evalsById.set(record.id, ev);
|
|
191
318
|
snapshot.evals.push(ev);
|
|
192
319
|
}
|
|
193
320
|
ev.attempts.push(attempt);
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
for (const snapshot of byExperiment.values()) {
|
|
197
|
-
// attempts 平铺 = evals 逐题展开(同题的重试相邻)。
|
|
198
|
-
snapshot.attempts = snapshot.evals.flatMap((ev) => ev.attempts);
|
|
199
321
|
}
|
|
200
|
-
return [...byExperiment.values()];
|
|
201
|
-
}
|
|
202
322
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
for (const snapshot of snapshots) {
|
|
207
|
-
const group = byId.get(snapshot.experimentId);
|
|
208
|
-
if (group) group.push(snapshot);
|
|
209
|
-
else byId.set(snapshot.experimentId, [snapshot]);
|
|
323
|
+
for (const ev of snapshot.evals) {
|
|
324
|
+
// attempt 序号升序,同号按 startedAt。
|
|
325
|
+
ev.attempts.sort((a, b) => a.result.attempt - b.result.attempt || (a.result.startedAt ?? "").localeCompare(b.result.startedAt ?? ""));
|
|
210
326
|
}
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
for (const [id, group] of byId) {
|
|
214
|
-
group.sort((a, b) => b.startedAt.localeCompare(a.startedAt) || b.runDir.dir.localeCompare(a.runDir.dir));
|
|
215
|
-
// 已知并集 = 本地历史(各快照覆盖的题)∪ 各快照携带的 knownEvalIds ——
|
|
216
|
-
// 不是「优先字段」:把快照复制进已有历史的目录时,本地并集可能更大,优先字段会让分母缩水。
|
|
217
|
-
const ids = new Set<string>();
|
|
218
|
-
for (const snapshot of group) {
|
|
219
|
-
for (const ev of snapshot.evals) ids.add(ev.id);
|
|
220
|
-
for (const known of snapshot.knownEvalIds ?? []) ids.add(known);
|
|
221
|
-
}
|
|
222
|
-
const experiment: Experiment = { id, snapshots: group, latest: group[0], evalIds: [...ids].sort() };
|
|
223
|
-
for (const snapshot of group) experimentBySnapshot.set(snapshot, experiment);
|
|
224
|
-
experiments.push(experiment);
|
|
225
|
-
}
|
|
226
|
-
experiments.sort((a, b) => a.id.localeCompare(b.id));
|
|
227
|
-
return experiments;
|
|
327
|
+
snapshot.attempts = snapshot.evals.flatMap((ev) => ev.attempts);
|
|
328
|
+
return snapshot;
|
|
228
329
|
}
|
|
229
330
|
|
|
230
|
-
|
|
331
|
+
function makeAttempt(snapshot: Snapshot, snapshotDir: string, attemptDir: string, record: EvalResult): AttemptHandle {
|
|
332
|
+
// 候选 artifact 目录:本 attempt 目录为主;--resume 携带条目的 artifact 留在原快照里,
|
|
333
|
+
// artifactBase(相对结果根 = 快照目录的上两级)指向那里,作为回退。
|
|
334
|
+
const candidates: string[] = [attemptDir];
|
|
335
|
+
// sources 的去重仓库(sources/<sha256>.json)挂在「快照根」,不是 attempt 目录——每个候选
|
|
336
|
+
// attempt 目录都要配一个对应的快照根,顺序与 candidates 一一对应,lazySources 按下标取用。
|
|
337
|
+
const candidateSnapshotRoots: string[] = [snapshotDir];
|
|
338
|
+
if (record.artifactBase) {
|
|
339
|
+
const resultsRoot = dirname(dirname(snapshotDir));
|
|
340
|
+
candidates.push(resolve(resultsRoot, record.artifactBase));
|
|
341
|
+
// artifactBase 恒为 `<实验目录>/<快照目录>/<evalId 路径>/a<n>`;experimentDirOf/快照目录名
|
|
342
|
+
// 都不含 `/`,所以前两段就是原快照根,即便 evalId 自己带 `/`(多段)也不影响这个切法。
|
|
343
|
+
const [expDir, snapDirName] = record.artifactBase.split("/");
|
|
344
|
+
candidateSnapshotRoots.push(resolve(resultsRoot, expDir ?? "", snapDirName ?? ""));
|
|
345
|
+
}
|
|
231
346
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
if (result.artifactsDir) candidates.push(join(run.dir, result.artifactsDir));
|
|
237
|
-
if (result.artifactBase) candidates.push(join(dirname(run.dir), result.artifactBase));
|
|
347
|
+
const ref = {
|
|
348
|
+
snapshot: `${basename(dirname(snapshotDir))}/${basename(snapshotDir)}`,
|
|
349
|
+
attempt: relative(snapshotDir, attemptDir).split(sep).join("/"),
|
|
350
|
+
};
|
|
238
351
|
|
|
239
352
|
return {
|
|
240
|
-
evalId:
|
|
241
|
-
experimentId
|
|
242
|
-
result,
|
|
243
|
-
ref
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
353
|
+
evalId: record.id,
|
|
354
|
+
experimentId: record.experimentId!,
|
|
355
|
+
result: record,
|
|
356
|
+
ref,
|
|
357
|
+
snapshot,
|
|
358
|
+
locator: record.locator as AttemptLocator,
|
|
359
|
+
events: lazyArtifact<StreamEvent[]>(candidates, "events", record.events),
|
|
360
|
+
trace: lazyArtifact<TraceSpan[]>(candidates, "trace", record.trace),
|
|
361
|
+
o11y: lazyArtifact<O11ySummary>(candidates, "o11y", record.o11y),
|
|
362
|
+
agentSetup: lazyArtifact<AgentSetupManifest>(candidates, "agentSetup", record.agentSetup),
|
|
363
|
+
diff: lazyArtifact<DiffData>(candidates, "diff", record.diff),
|
|
364
|
+
sources: lazySources(candidates, candidateSnapshotRoots, record.sources),
|
|
250
365
|
};
|
|
251
366
|
}
|
|
252
367
|
|
|
253
368
|
/**
|
|
254
|
-
*
|
|
255
|
-
* 文件绝不读两遍。
|
|
369
|
+
* 单个 artifact 的懒加载器:缺失返回 null(不抛错);同一 handle 内记忆化,diff 这类可达百 MB 的
|
|
370
|
+
* 文件绝不读两遍。result.json 里内联了该字段时直接用(外部工具转换/全量输出的场景)。
|
|
256
371
|
* 文件存在但 JSON 损坏是真错误,抛英文错误而不是伪装成缺失;失败不缓存,允许重试。
|
|
257
372
|
*/
|
|
258
373
|
function lazyArtifact<T>(candidateDirs: string[], kind: ArtifactKind, inline: T | undefined): () => Promise<T | null> {
|
|
@@ -285,42 +400,144 @@ function lazyArtifact<T>(candidateDirs: string[], kind: ArtifactKind, inline: T
|
|
|
285
400
|
};
|
|
286
401
|
}
|
|
287
402
|
|
|
403
|
+
/** 一条 sources 引用条目(attempt 级 `sources.json` 的落盘形状,schemaVersion 5 起)。 */
|
|
404
|
+
interface SourceRef {
|
|
405
|
+
path: string;
|
|
406
|
+
sha256: string;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* sources 的懒加载器:与 lazyArtifact 同样的存在性/记忆化/坏 JSON 语义,但多一层——
|
|
411
|
+
* attempt 目录下的 `sources.json` 只是引用(`{path, sha256}[]`),真内容按 sha256 在对应
|
|
412
|
+
* 快照根的 `sources/<sha256>.json` 里(去重仓库,见 writer.ts 的 writeSourcesRef)。
|
|
413
|
+
* candidateSnapshotRoots 与 candidateDirs 下标一一对应:命中哪个候选 attempt 目录的引用,
|
|
414
|
+
* 就去哪个候选对应的快照根找仓库——本 attempt 目录对本快照根,artifactBase 回退对原快照根。
|
|
415
|
+
* 仓库里缺单条 blob(理论不该发生,引用与仓库应同时存在)如实跳过那一条,不让整个方法失败。
|
|
416
|
+
*/
|
|
417
|
+
function lazySources(
|
|
418
|
+
candidateDirs: string[],
|
|
419
|
+
candidateSnapshotRoots: string[],
|
|
420
|
+
inline: SourceArtifact[] | undefined,
|
|
421
|
+
): () => Promise<SourceArtifact[] | null> {
|
|
422
|
+
let memo: Promise<SourceArtifact[] | null> | undefined;
|
|
423
|
+
const load = async (): Promise<SourceArtifact[] | null> => {
|
|
424
|
+
if (inline !== undefined) return inline;
|
|
425
|
+
for (let i = 0; i < candidateDirs.length; i++) {
|
|
426
|
+
const refFile = join(candidateDirs[i]!, artifactFileOf("sources"));
|
|
427
|
+
let text: string;
|
|
428
|
+
try {
|
|
429
|
+
text = await readFile(refFile, "utf-8");
|
|
430
|
+
} catch (e) {
|
|
431
|
+
if (isMissingFile(e)) continue;
|
|
432
|
+
throw new Error(`Cannot read artifact ${refFile} (${e instanceof Error ? e.message : String(e)}).`);
|
|
433
|
+
}
|
|
434
|
+
let refs: SourceRef[];
|
|
435
|
+
try {
|
|
436
|
+
refs = JSON.parse(text) as SourceRef[];
|
|
437
|
+
} catch {
|
|
438
|
+
throw new Error(`Artifact ${refFile} is not valid JSON. The file may be corrupted; re-run the eval or delete this attempt directory.`);
|
|
439
|
+
}
|
|
440
|
+
const storeDir = join(candidateSnapshotRoots[i]!, "sources");
|
|
441
|
+
const out: SourceArtifact[] = [];
|
|
442
|
+
for (const ref of refs) {
|
|
443
|
+
const blobFile = join(storeDir, `${ref.sha256}.json`);
|
|
444
|
+
let blobText: string;
|
|
445
|
+
try {
|
|
446
|
+
blobText = await readFile(blobFile, "utf-8");
|
|
447
|
+
} catch (e) {
|
|
448
|
+
if (isMissingFile(e)) continue; // 仓库缺这一条(极端情况):跳过,不拖垮其它条目
|
|
449
|
+
throw new Error(`Cannot read source blob ${blobFile} (${e instanceof Error ? e.message : String(e)}).`);
|
|
450
|
+
}
|
|
451
|
+
let blob: { content: string };
|
|
452
|
+
try {
|
|
453
|
+
blob = JSON.parse(blobText) as { content: string };
|
|
454
|
+
} catch {
|
|
455
|
+
throw new Error(`Source blob ${blobFile} is not valid JSON. It may be corrupted.`);
|
|
456
|
+
}
|
|
457
|
+
out.push({ path: ref.path, content: blob.content });
|
|
458
|
+
}
|
|
459
|
+
return out;
|
|
460
|
+
}
|
|
461
|
+
return null;
|
|
462
|
+
};
|
|
463
|
+
return () => {
|
|
464
|
+
memo ??= load().catch((e: unknown) => {
|
|
465
|
+
memo = undefined;
|
|
466
|
+
throw e;
|
|
467
|
+
});
|
|
468
|
+
return memo;
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
|
|
288
472
|
function isMissingFile(e: unknown): boolean {
|
|
289
473
|
const code = (e as NodeJS.ErrnoException | null)?.code;
|
|
290
474
|
return code === "ENOENT" || code === "ENOTDIR";
|
|
291
475
|
}
|
|
292
476
|
|
|
293
|
-
// ─────────────────────────
|
|
477
|
+
// ───────────────────────── 实验归组 ─────────────────────────
|
|
478
|
+
|
|
479
|
+
/** 同一 experiment id 的历次快照归在一起;实验按 id 字典序,快照最新在前。 */
|
|
480
|
+
function buildExperiments(snapshots: Snapshot[]): Experiment[] {
|
|
481
|
+
const byId = new Map<string, Snapshot[]>();
|
|
482
|
+
for (const snapshot of snapshots) {
|
|
483
|
+
const group = byId.get(snapshot.experimentId);
|
|
484
|
+
if (group) group.push(snapshot);
|
|
485
|
+
else byId.set(snapshot.experimentId, [snapshot]);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
const experiments: Experiment[] = [];
|
|
489
|
+
for (const [id, group] of byId) {
|
|
490
|
+
group.sort((a, b) => (isNewerSnapshot(a, b) ? -1 : 1));
|
|
491
|
+
// 已知并集 = 本地历史(各快照覆盖的题)∪ 各快照携带的 knownEvalIds ——
|
|
492
|
+
// 不是「优先字段」:把快照复制进已有历史的目录时,本地并集可能更大,优先字段会让分母缩水。
|
|
493
|
+
const ids = new Set<string>();
|
|
494
|
+
for (const snapshot of group) {
|
|
495
|
+
for (const ev of snapshot.evals) ids.add(ev.id);
|
|
496
|
+
for (const known of snapshot.knownEvalIds ?? []) ids.add(known);
|
|
497
|
+
}
|
|
498
|
+
const experiment: Experiment = { id, snapshots: group, latest: group[0], evalIds: [...ids].sort() };
|
|
499
|
+
for (const snapshot of group) experimentBySnapshot.set(snapshot, experiment);
|
|
500
|
+
experiments.push(experiment);
|
|
501
|
+
}
|
|
502
|
+
experiments.sort((a, b) => a.id.localeCompare(b.id));
|
|
503
|
+
return experiments;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// ───────────────────────── 文件系统助手 ─────────────────────────
|
|
294
507
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
const
|
|
303
|
-
|
|
304
|
-
);
|
|
508
|
+
async function findResultFiles(dir: string): Promise<string[]> {
|
|
509
|
+
let entries;
|
|
510
|
+
try {
|
|
511
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
512
|
+
} catch {
|
|
513
|
+
return [];
|
|
514
|
+
}
|
|
515
|
+
const direct = entries.filter((e) => e.isFile() && e.name === RESULT_FILE).map((e) => join(dir, e.name));
|
|
516
|
+
const nested = await Promise.all(entries.filter((e) => e.isDirectory()).map((e) => findResultFiles(join(dir, e.name))));
|
|
305
517
|
return [...direct, ...nested.flat()];
|
|
306
518
|
}
|
|
307
519
|
|
|
308
|
-
/** 目录下(递归)
|
|
309
|
-
async function
|
|
310
|
-
const
|
|
311
|
-
|
|
520
|
+
/** 目录下(递归)是否存在 result.json 或任何 attempt artifact 文件 —— incomplete 判定的依据。 */
|
|
521
|
+
async function hasArtifactOrResultFiles(dir: string): Promise<boolean> {
|
|
522
|
+
const names = new Set<string>([RESULT_FILE, ...ARTIFACT_KINDS.map((kind) => artifactFileOf(kind))]);
|
|
523
|
+
let entries;
|
|
524
|
+
try {
|
|
525
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
526
|
+
} catch {
|
|
527
|
+
return false;
|
|
528
|
+
}
|
|
312
529
|
for (const entry of entries) {
|
|
313
|
-
if (entry.isFile() &&
|
|
530
|
+
if (entry.isFile() && names.has(entry.name)) return true;
|
|
314
531
|
}
|
|
315
532
|
for (const entry of entries) {
|
|
316
|
-
if (entry.isDirectory() && (await
|
|
533
|
+
if (entry.isDirectory() && (await hasArtifactOrResultFiles(join(dir, entry.name)))) return true;
|
|
317
534
|
}
|
|
318
535
|
return false;
|
|
319
536
|
}
|
|
320
537
|
|
|
321
|
-
async function
|
|
538
|
+
async function hasFile(dir: string, name: string): Promise<boolean> {
|
|
322
539
|
try {
|
|
323
|
-
return (await stat(
|
|
540
|
+
return (await stat(join(dir, name))).isFile();
|
|
324
541
|
} catch {
|
|
325
542
|
return false;
|
|
326
543
|
}
|