niceeval 0.11.1 → 0.11.3
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 +1 -0
- package/README.md +4 -2
- package/README.zh.md +4 -2
- package/dist/agents/types.d.ts +2 -2
- package/dist/o11y/types.d.ts +3 -3
- package/dist/report/assets/colors.d.ts +1 -1
- package/dist/report/assets/colors.js +1 -1
- package/dist/report/components/attempt-detail/AttemptSource.js +1 -2
- package/dist/report/components/attempt-detail/compute.d.ts +4 -4
- package/dist/report/components/attempt-detail/compute.js +6 -6
- package/dist/report/components/attempt-detail/faces.d.ts +1 -1
- package/dist/report/components/attempt-detail/faces.js +3 -3
- package/dist/report/components/entity-lists/AttemptList.d.ts +3 -3
- package/dist/report/components/entity-lists/AttemptList.js +3 -3
- package/dist/report/components/entity-lists/ExperimentList.js +2 -2
- package/dist/report/components/entity-lists/compute.js +1 -1
- package/dist/report/components/entity-lists/faces.js +1 -1
- package/dist/report/components/entity-lists/index.d.ts +1 -1
- package/dist/report/components/entity-lists/index.js +1 -1
- package/dist/report/components/metric-views/chart-math.d.ts +2 -2
- package/dist/report/components/metric-views/chart-math.js +2 -2
- package/dist/report/components/metric-views/compute.d.ts +2 -3
- package/dist/report/components/metric-views/compute.js +3 -4
- package/dist/report/components/shared.d.ts +1 -1
- package/dist/report/components/site-components/compute.d.ts +5 -5
- package/dist/report/components/site-components/compute.js +6 -6
- package/dist/report/components/site-components/index.d.ts +7 -7
- package/dist/report/components/site-components/index.js +7 -7
- package/dist/report/components/site-components/scope-warnings.d.ts +1 -1
- package/dist/report/components/site-components/scope-warnings.js +3 -3
- package/dist/report/components/site-components/snapshot-diagnostics.js +1 -1
- package/dist/report/components/summaries/compute.d.ts +1 -1
- package/dist/report/components/summaries/compute.js +2 -2
- package/dist/report/components/summaries/faces.js +1 -1
- package/dist/report/components/summaries/index.d.ts +1 -1
- package/dist/report/components/summaries/index.js +1 -1
- package/dist/report/index.js +1 -1
- package/dist/report/model/aggregate.d.ts +2 -2
- package/dist/report/model/aggregate.js +2 -2
- package/dist/report/model/format.d.ts +2 -2
- package/dist/report/model/format.js +2 -2
- package/dist/report/model/metrics.js +3 -3
- package/dist/report/model/types.d.ts +11 -12
- package/dist/results/locator.js +1 -1
- package/dist/results/select.d.ts +4 -4
- package/dist/results/select.js +5 -5
- package/dist/results/types.d.ts +8 -8
- package/dist/runner/types.d.ts +14 -14
- package/dist/sandbox/resolve.d.ts +1 -1
- package/dist/sandbox/types.d.ts +1 -1
- package/docs-site/zh/reference/builtin-agents.mdx +21 -0
- package/docs-site/zh/reference/define-agent.mdx +1 -1
- package/docs-site/zh/reference/report-components.mdx +156 -74
- package/docs-site/zh/tutorials/custom-reports.mdx +112 -22
- package/docs-site/zh/tutorials/sandbox-agent.mdx +3 -4
- package/docs-site/zh/tutorials/sandbox-providers.mdx +15 -15
- package/docs-site/zh/tutorials/theming.mdx +169 -0
- package/package.json +2 -1
- package/src/agents/ai-sdk.test.ts +1 -1
- package/src/agents/bub-install-spec.ts +26 -8
- package/src/agents/bub.ts +40 -15
- package/src/agents/builtin.ts +11 -1
- package/src/agents/coding-cli-versions.ts +67 -0
- package/src/agents/hermes.ts +235 -0
- package/src/agents/index.ts +4 -0
- package/src/agents/langgraph.test.ts +1 -1
- package/src/agents/manifest.ts +1 -1
- package/src/agents/openai-compat.test.ts +1 -1
- package/src/agents/openclaw.ts +103 -24
- package/src/agents/opencode.ts +183 -0
- package/src/agents/sdk-streams.test.ts +1 -1
- package/src/agents/shared.ts +13 -2
- package/src/agents/types.ts +2 -2
- package/src/context/session.test.ts +1 -1
- package/src/context/session.ts +1 -1
- package/src/o11y/cost.ts +1 -1
- package/src/o11y/parsers/bub.test.ts +1 -1
- package/src/o11y/parsers/bub.ts +1 -1
- package/src/o11y/parsers/codex.test.ts +1 -1
- package/src/o11y/parsers/codex.ts +1 -1
- package/src/o11y/parsers/hermes.ts +198 -0
- package/src/o11y/parsers/openclaw.ts +25 -3
- package/src/o11y/parsers/opencode.ts +295 -0
- package/src/o11y/types.ts +3 -3
- package/src/report/assets/colors.ts +1 -1
- package/src/report/assets/styles.css +17 -15
- package/src/report/components/attempt-detail/AttemptAssertions.tsx +1 -1
- package/src/report/components/attempt-detail/AttemptConversation.tsx +1 -1
- package/src/report/components/attempt-detail/AttemptDiagnostics.tsx +1 -1
- package/src/report/components/attempt-detail/AttemptDiff.tsx +1 -1
- package/src/report/components/attempt-detail/AttemptError.tsx +1 -1
- package/src/report/components/attempt-detail/AttemptFixPrompt.tsx +1 -1
- package/src/report/components/attempt-detail/AttemptSource.tsx +2 -3
- package/src/report/components/attempt-detail/AttemptSummary.tsx +1 -1
- package/src/report/components/attempt-detail/AttemptTimeline.tsx +1 -1
- package/src/report/components/attempt-detail/AttemptTrace.tsx +1 -1
- package/src/report/components/attempt-detail/UsageTable.tsx +1 -1
- package/src/report/components/attempt-detail/attempt-components.test.tsx +1 -1
- package/src/report/components/attempt-detail/compute.ts +6 -6
- package/src/report/components/attempt-detail/faces.ts +3 -3
- package/src/report/components/attempt-detail/index.tsx +1 -1
- package/src/report/components/entity-lists/AttemptList.tsx +3 -3
- package/src/report/components/entity-lists/ExperimentList.tsx +2 -2
- package/src/report/components/entity-lists/compute.ts +1 -1
- package/src/report/components/entity-lists/faces.ts +1 -1
- package/src/report/components/entity-lists/index.tsx +1 -1
- package/src/report/components/metric-views/DeltaTable.tsx +1 -2
- package/src/report/components/metric-views/StabilityMatrix.tsx +1 -1
- package/src/report/components/metric-views/chart-math.test.ts +1 -1
- package/src/report/components/metric-views/chart-math.ts +2 -2
- package/src/report/components/metric-views/compute.ts +3 -4
- package/src/report/components/shared.ts +1 -1
- package/src/report/components/site-components/CopyFixPrompt.tsx +1 -1
- package/src/report/components/site-components/HeroCard.tsx +1 -1
- package/src/report/components/site-components/PoweredBy.tsx +1 -1
- package/src/report/components/site-components/ScopeWarnings.tsx +1 -1
- package/src/report/components/site-components/SnapshotDiagnostics.tsx +1 -1
- package/src/report/components/site-components/TraceWaterfall.tsx +1 -1
- package/src/report/components/site-components/compute.ts +6 -6
- package/src/report/components/site-components/index.tsx +7 -7
- package/src/report/components/site-components/scope-warnings.ts +3 -3
- package/src/report/components/site-components/snapshot-diagnostics.ts +1 -1
- package/src/report/components/summaries/ScopeSummary.tsx +2 -2
- package/src/report/components/summaries/compute.ts +2 -2
- package/src/report/components/summaries/faces.ts +1 -1
- package/src/report/components/summaries/index.tsx +1 -1
- package/src/report/index.ts +1 -1
- package/src/report/model/aggregate.ts +2 -2
- package/src/report/model/format.ts +2 -2
- package/src/report/model/metrics.ts +3 -3
- package/src/report/model/types.ts +12 -13
- package/src/results/annotated-source.test.ts +1 -1
- package/src/results/attempt-evidence.test.ts +1 -1
- package/src/results/copy.ts +3 -3
- package/src/results/format.ts +3 -3
- package/src/results/host-equivalence.test.ts +6 -6
- package/src/results/index.ts +1 -1
- package/src/results/locator.test.ts +2 -2
- package/src/results/locator.ts +1 -1
- package/src/results/open.ts +2 -2
- package/src/results/publish.ts +1 -1
- package/src/results/results.test.ts +6 -323
- package/src/results/select.test.ts +438 -0
- package/src/results/select.ts +6 -6
- package/src/results/skipped-notice.ts +1 -1
- package/src/results/truncate.ts +2 -2
- package/src/results/types.ts +9 -9
- package/src/results/writer.ts +4 -4
- package/src/runner/attempt.test.ts +2 -2
- package/src/runner/attempt.ts +6 -6
- package/src/runner/eval-source.test.ts +1 -1
- package/src/runner/eval-source.ts +1 -1
- package/src/runner/reporters/artifacts.ts +1 -1
- package/src/runner/run.ts +2 -2
- package/src/runner/timing.ts +1 -1
- package/src/runner/types.ts +14 -14
- package/src/sandbox/docker-agent-image.ts +36 -0
- package/src/sandbox/e2b-agent-template.test.ts +6 -21
- package/src/sandbox/e2b-agent-template.ts +44 -11
- package/src/sandbox/index.ts +8 -0
- package/src/sandbox/official-baselines.test.ts +175 -0
- package/src/sandbox/resolve.ts +1 -1
- package/src/sandbox/types.ts +1 -1
- package/src/scoring/diff.ts +1 -1
- package/src/scoring/types.ts +1 -1
- package/src/shared/facts.ts +1 -1
- package/src/show/index.ts +1 -1
- package/src/show/render.ts +1 -1
- package/src/show/show.test.ts +3 -3
- package/src/view/app/App.tsx +3 -1
- package/src/view/client-dist/app.css +1 -1
- package/src/view/client-dist/app.js +1 -1
- package/src/view/data.test.ts +2 -2
- package/src/view/data.ts +1 -1
- package/src/view/site-base.test.ts +51 -0
- package/src/view/site.ts +16 -0
- package/src/view/styles.css +1 -2
- package/src/view/template.html +1 -0
- package/src/view/view-report.test.ts +1 -1
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
// cases: docs/engineering/testing/unit/sample.md
|
|
2
|
+
// 选择层(niceeval/sample)的单测:在临时目录里构造最小落盘树,直接调选择器,
|
|
3
|
+
// 覆盖定稿契约(docs/feature/sample/library.md):两个选择口径、覆盖缺口(coverage)、
|
|
4
|
+
// 时效与 fresh、Sample 的删减语义与四面同步重算、身份键去重、警告 kind 全集。
|
|
5
|
+
// fixture 的目录名/artifact 路径手写(不 import 库的路径函数),让测试独立于实现充当口径基准。
|
|
6
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
7
|
+
import { mkdir, mkdtemp, readdir, readFile, rm, stat, writeFile } from "node:fs/promises";
|
|
8
|
+
import { tmpdir } from "node:os";
|
|
9
|
+
import { basename, join } from "node:path";
|
|
10
|
+
import {
|
|
11
|
+
RESULTS_FORMAT,
|
|
12
|
+
RESULTS_SCHEMA_VERSION,
|
|
13
|
+
copySnapshots,
|
|
14
|
+
createResultsWriter,
|
|
15
|
+
dedupeAttempts,
|
|
16
|
+
openResults,
|
|
17
|
+
resolveLocator,
|
|
18
|
+
loadAnnotatedEvalSource,
|
|
19
|
+
LocatorNotFoundError,
|
|
20
|
+
MalformedLocatorError,
|
|
21
|
+
LocatorCollisionError,
|
|
22
|
+
encodeAttemptLocator,
|
|
23
|
+
type AttemptHandle,
|
|
24
|
+
type EvalResult,
|
|
25
|
+
type ResultsWriter,
|
|
26
|
+
type Results,
|
|
27
|
+
type Snapshot,
|
|
28
|
+
type SnapshotMeta,
|
|
29
|
+
} from "./index.ts";
|
|
30
|
+
|
|
31
|
+
// ───────────────────────── fixture 工具 ─────────────────────────
|
|
32
|
+
|
|
33
|
+
const roots: string[] = [];
|
|
34
|
+
async function makeRoot(): Promise<string> {
|
|
35
|
+
const root = await mkdtemp(join(tmpdir(), "niceeval-results-"));
|
|
36
|
+
roots.push(root);
|
|
37
|
+
return root;
|
|
38
|
+
}
|
|
39
|
+
afterEach(async () => {
|
|
40
|
+
await Promise.all(roots.splice(0).map((r) => rm(r, { recursive: true, force: true })));
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
/** 测试便利:封口一个 writer 已声明的全部 Snapshot(finish() 现在是 SnapshotWriter 的方法,
|
|
44
|
+
* 每个 Snapshot 各自只能封一次;多数测试不关心逐个 Snapshot 分别封口,只要「全部封完」)。 */
|
|
45
|
+
async function finishAll(writer: ResultsWriter): Promise<void> {
|
|
46
|
+
const snapshots = await writer.snapshotWriters();
|
|
47
|
+
await Promise.all(snapshots.map(({ writer: snap }) => snap.finish()));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function meta(over: { experimentId: string; agent: string; startedAt: string } & Partial<SnapshotMeta>): SnapshotMeta {
|
|
51
|
+
return {
|
|
52
|
+
format: RESULTS_FORMAT,
|
|
53
|
+
schemaVersion: RESULTS_SCHEMA_VERSION,
|
|
54
|
+
producer: { name: "niceeval", version: "0.3.0" },
|
|
55
|
+
...over,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function record(over: { id: string; attempt: number } & Record<string, unknown>): EvalResult {
|
|
60
|
+
return { verdict: "passed", durationMs: 1000, assertions: [], ...over } as unknown as EvalResult;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function writeSnapshot(root: string, expDir: string, snapDirName: string, m: SnapshotMeta): Promise<string> {
|
|
64
|
+
const dir = join(root, expDir, snapDirName);
|
|
65
|
+
await mkdir(dir, { recursive: true });
|
|
66
|
+
await writeFile(join(dir, "snapshot.json"), JSON.stringify(m, null, 2), "utf-8");
|
|
67
|
+
return dir;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function writeResultFile(snapDir: string, relAttemptDir: string, r: unknown): Promise<string> {
|
|
71
|
+
const dir = join(snapDir, relAttemptDir);
|
|
72
|
+
await mkdir(dir, { recursive: true });
|
|
73
|
+
const path = join(dir, "result.json");
|
|
74
|
+
await writeFile(path, JSON.stringify(r, null, 2), "utf-8");
|
|
75
|
+
return path;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function writeArtifactFile(snapDir: string, relAttemptDir: string, file: string, data: unknown): Promise<string> {
|
|
79
|
+
const dir = join(snapDir, relAttemptDir);
|
|
80
|
+
await mkdir(dir, { recursive: true });
|
|
81
|
+
const path = join(dir, file);
|
|
82
|
+
await writeFile(path, JSON.stringify(data), "utf-8");
|
|
83
|
+
return path;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function exists(path: string): Promise<boolean> {
|
|
87
|
+
try {
|
|
88
|
+
await stat(path);
|
|
89
|
+
return true;
|
|
90
|
+
} catch {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ───────────────────────── latest() Selection 与警告 ─────────────────────────
|
|
96
|
+
|
|
97
|
+
describe("results.latest() · Selection", () => {
|
|
98
|
+
it("每个实验取最新快照;experiments 前缀过滤同 CLI 语义(尾斜杠等价)", async () => {
|
|
99
|
+
const root = await makeRoot();
|
|
100
|
+
await writeSnapshot(root, "mid_a", "s1", meta({ experimentId: "mid/a", agent: "bub", startedAt: "2026-07-01T08:00:00.000Z", completedAt: "2026-07-01T08:10:00.000Z" }));
|
|
101
|
+
await writeSnapshot(root, "mid_b", "s1", meta({ experimentId: "mid/b", agent: "codex", startedAt: "2026-07-01T08:00:00.000Z", completedAt: "2026-07-01T08:10:00.000Z" }));
|
|
102
|
+
const tuesday = await writeSnapshot(root, "mid_b", "s2", meta({ experimentId: "mid/b", agent: "codex", startedAt: "2026-07-02T08:00:00.000Z", completedAt: "2026-07-02T08:10:00.000Z" }));
|
|
103
|
+
|
|
104
|
+
const results = await openResults(root);
|
|
105
|
+
const latest = results.latest();
|
|
106
|
+
expect(latest.snapshots.map((s) => s.experimentId)).toEqual(["mid/a", "mid/b"]);
|
|
107
|
+
expect(latest.snapshots[1].dir).toBe(tuesday);
|
|
108
|
+
|
|
109
|
+
expect(results.latest({ experiments: "mid/a" }).snapshots).toHaveLength(1);
|
|
110
|
+
expect(results.latest({ experiments: "mid/" }).snapshots).toHaveLength(2);
|
|
111
|
+
expect(results.latest({ experiments: ["mid/a", "mid/b"] }).snapshots).toHaveLength(2);
|
|
112
|
+
expect(results.latest({ experiments: "other" }).snapshots).toHaveLength(0);
|
|
113
|
+
expect(results.latest({ experiments: "mid/a" }).snapshots[0].experimentId).toBe("mid/a"); // 不误配 "mid/ab"
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("coverage:最新快照覆盖 < 已知并集时,missingEvalIds 列出缺的题;分母是 knownEvalIds", async () => {
|
|
117
|
+
const root = await makeRoot();
|
|
118
|
+
const mondayDir = await writeSnapshot(root, "midterm", "2026-07-01T08-00-00-000Z", meta({ experimentId: "midterm/bub-gpt-5.4", agent: "bub", startedAt: "2026-07-01T08:00:00.000Z", completedAt: "2026-07-01T08:10:00.000Z" }));
|
|
119
|
+
await writeResultFile(mondayDir, "algebra/q1/a1", record({ id: "algebra/q1", attempt: 1 }));
|
|
120
|
+
await writeResultFile(mondayDir, "algebra/q2/a1", record({ id: "algebra/q2", attempt: 1 }));
|
|
121
|
+
await writeResultFile(mondayDir, "algebra/q3/a1", record({ id: "algebra/q3", attempt: 1 }));
|
|
122
|
+
|
|
123
|
+
const fridayDir = await writeSnapshot(root, "midterm", "2026-07-05T08-00-00-000Z", meta({ experimentId: "midterm/bub-gpt-5.4", agent: "bub", startedAt: "2026-07-05T08:00:00.000Z", completedAt: "2026-07-05T08:10:00.000Z" }));
|
|
124
|
+
await writeResultFile(fridayDir, "algebra/q1/a1", record({ id: "algebra/q1", attempt: 1 }));
|
|
125
|
+
|
|
126
|
+
const latest = (await openResults(root)).latest();
|
|
127
|
+
expect(latest.snapshots).toHaveLength(1);
|
|
128
|
+
// partial-coverage / stale-snapshot 不再是 warnings —— 覆盖缺口是行级事实,物化在 coverage 上。
|
|
129
|
+
expect(latest.warnings.filter((w) => w.kind !== "unreadable-snapshot")).toHaveLength(0);
|
|
130
|
+
const coverage = latest.coverage.find((c) => c.experimentId === "midterm/bub-gpt-5.4")!;
|
|
131
|
+
expect(coverage.knownEvalIds).toEqual(["algebra/q1", "algebra/q2", "algebra/q3"]);
|
|
132
|
+
expect(coverage.missingEvalIds).toEqual(["algebra/q2", "algebra/q3"]);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("unfinished-snapshot:选中快照缺 completedAt", async () => {
|
|
136
|
+
const root = await makeRoot();
|
|
137
|
+
const dir = await writeSnapshot(root, "e", "2026-07-01T08-00-00-000Z", meta({ experimentId: "e", agent: "bub", startedAt: "2026-07-01T08:00:00.000Z" }));
|
|
138
|
+
|
|
139
|
+
const latest = (await openResults(root)).latest();
|
|
140
|
+
const warn = latest.warnings.find((w) => w.kind === "unfinished-snapshot")!;
|
|
141
|
+
expect(warn).toMatchObject({ experimentId: "e", startedAt: "2026-07-01T08:00:00.000Z", dir });
|
|
142
|
+
expect(warn.message).toContain("has no completedAt");
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("Selection.filter 只删不换:快照删减,幸存实验的 coverage / 警告保留、其余丢弃", async () => {
|
|
146
|
+
const root = await makeRoot();
|
|
147
|
+
const aDir = await writeSnapshot(root, "mid_a", "s1", meta({ experimentId: "mid/a", agent: "bub", startedAt: "2026-07-01T08:00:00.000Z", completedAt: "2026-07-01T08:10:00.000Z" }));
|
|
148
|
+
await writeResultFile(aDir, "q1/a1", record({ id: "q1", attempt: 1 }));
|
|
149
|
+
await writeResultFile(aDir, "q2/a1", record({ id: "q2", attempt: 1 }));
|
|
150
|
+
const bDir = await writeSnapshot(root, "mid_b", "s1", meta({ experimentId: "mid/b", agent: "codex", startedAt: "2026-07-01T08:00:00.000Z", completedAt: "2026-07-01T08:10:00.000Z" }));
|
|
151
|
+
await writeResultFile(bDir, "q1/a1", record({ id: "q1", attempt: 1 }));
|
|
152
|
+
await writeResultFile(bDir, "q2/a1", record({ id: "q2", attempt: 1 }));
|
|
153
|
+
|
|
154
|
+
// 两个实验的最新快照都只重跑了 q1(残缺,进 coverage.missingEvalIds)且都未收尾(进 warnings)。
|
|
155
|
+
const aDir2 = await writeSnapshot(root, "mid_a", "s2", meta({ experimentId: "mid/a", agent: "bub", startedAt: "2026-07-02T08:00:00.000Z" }));
|
|
156
|
+
await writeResultFile(aDir2, "q1/a1", record({ id: "q1", attempt: 1 }));
|
|
157
|
+
const bDir2 = await writeSnapshot(root, "mid_b", "s2", meta({ experimentId: "mid/b", agent: "codex", startedAt: "2026-07-02T08:00:00.000Z" }));
|
|
158
|
+
await writeResultFile(bDir2, "q1/a1", record({ id: "q1", attempt: 1 }));
|
|
159
|
+
|
|
160
|
+
const latest = (await openResults(root)).latest();
|
|
161
|
+
expect(latest.coverage.map((c) => c.experimentId).sort()).toEqual(["mid/a", "mid/b"]);
|
|
162
|
+
expect(latest.coverage.find((c) => c.experimentId === "mid/a")!.missingEvalIds).toEqual(["q2"]);
|
|
163
|
+
expect(latest.warnings.filter((w) => w.kind === "unfinished-snapshot")).toHaveLength(2);
|
|
164
|
+
|
|
165
|
+
const filtered = latest.filter((s) => s.experimentId !== "mid/b");
|
|
166
|
+
expect(filtered.snapshots.map((s) => s.experimentId)).toEqual(["mid/a"]);
|
|
167
|
+
expect(filtered.coverage.map((c) => c.experimentId)).toEqual(["mid/a"]);
|
|
168
|
+
expect(filtered.warnings.map((w) => ("experimentId" in w ? w.experimentId : undefined))).toEqual(["mid/a"]);
|
|
169
|
+
// 原 Selection 不被改动。
|
|
170
|
+
expect(latest.snapshots).toHaveLength(2);
|
|
171
|
+
expect(latest.coverage).toHaveLength(2);
|
|
172
|
+
expect(latest.warnings).toHaveLength(2);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it("current() 下同一 experiment 有两个贡献 Snapshot 时,filter 删除其中一个只删该来源的水位,不整实验全留或全删", async () => {
|
|
176
|
+
const root = await makeRoot();
|
|
177
|
+
// 周一:q1、q2 真实执行。
|
|
178
|
+
const mondayDir = await writeSnapshot(root, "multi_e", "2026-07-01T08-00-00-000Z", meta({ experimentId: "multi/e", agent: "bub", startedAt: "2026-07-01T08:00:00.000Z", completedAt: "2026-07-01T08:10:00.000Z" }));
|
|
179
|
+
await writeResultFile(mondayDir, "q1/a1", record({ id: "q1", attempt: 1 }));
|
|
180
|
+
await writeResultFile(mondayDir, "q2/a1", record({ id: "q2", attempt: 1 }));
|
|
181
|
+
// 周二:只重跑 q1;current() 从周一补齐 q2,两个真实快照都进 Scope.snapshots。
|
|
182
|
+
const tuesdayDir = await writeSnapshot(root, "multi_e", "2026-07-02T08-00-00-000Z", meta({ experimentId: "multi/e", agent: "bub", startedAt: "2026-07-02T08:00:00.000Z", completedAt: "2026-07-02T08:10:00.000Z" }));
|
|
183
|
+
await writeResultFile(tuesdayDir, "q1/a1", record({ id: "q1", attempt: 1 }));
|
|
184
|
+
|
|
185
|
+
const current = (await openResults(root)).current();
|
|
186
|
+
expect(current.snapshots.map((s) => s.startedAt).sort()).toEqual(["2026-07-01T08:00:00.000Z", "2026-07-02T08:00:00.000Z"]);
|
|
187
|
+
expect(current.attempts.map((a) => a.evalId).sort()).toEqual(["q1", "q2"]);
|
|
188
|
+
expect(current.coverage.find((c) => c.experimentId === "multi/e")!.missingEvalIds).toEqual([]);
|
|
189
|
+
|
|
190
|
+
// 删掉周一(q2 唯一来源),周二(q1 来源)保留:不是整实验全留或全删。
|
|
191
|
+
const filtered = current.filter((s) => s.startedAt !== "2026-07-01T08:00:00.000Z");
|
|
192
|
+
expect(filtered.snapshots.map((s) => s.startedAt)).toEqual(["2026-07-02T08:00:00.000Z"]);
|
|
193
|
+
expect(filtered.attempts.map((a) => a.evalId)).toEqual(["q1"]);
|
|
194
|
+
// knownEvalIds(分母)不变,只有被删来源独占贡献的 q2 转入 missingEvalIds。
|
|
195
|
+
const filteredCoverage = filtered.coverage.find((c) => c.experimentId === "multi/e")!;
|
|
196
|
+
expect(filteredCoverage.knownEvalIds).toEqual(["q1", "q2"]);
|
|
197
|
+
expect(filteredCoverage.missingEvalIds).toEqual(["q2"]);
|
|
198
|
+
// 原 Scope 不被改动。
|
|
199
|
+
expect(current.snapshots).toHaveLength(2);
|
|
200
|
+
expect(current.attempts).toHaveLength(2);
|
|
201
|
+
|
|
202
|
+
// 反过来删掉周二(q1 唯一来源),周一(q2 来源)保留。
|
|
203
|
+
const otherWay = current.filter((s) => s.startedAt !== "2026-07-02T08:00:00.000Z");
|
|
204
|
+
expect(otherWay.snapshots.map((s) => s.startedAt)).toEqual(["2026-07-01T08:00:00.000Z"]);
|
|
205
|
+
expect(otherWay.attempts.map((a) => a.evalId)).toEqual(["q2"]);
|
|
206
|
+
expect(otherWay.coverage.find((c) => c.experimentId === "multi/e")!.missingEvalIds).toEqual(["q1"]);
|
|
207
|
+
|
|
208
|
+
// attempt.snapshot 原样指回真实来源(不是过滤时重建的新对象)。
|
|
209
|
+
for (const attempt of filtered.attempts) {
|
|
210
|
+
expect(filtered.snapshots).toContain(attempt.snapshot);
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
// ───────────────────────── unreadable-snapshot 警告 ─────────────────────────
|
|
216
|
+
|
|
217
|
+
describe("results.latest() / results.current() · unreadable-snapshot", () => {
|
|
218
|
+
it("malformed 快照产生 warning 且其余快照照常计入;无关 JSON 不产生 warning", async () => {
|
|
219
|
+
const root = await makeRoot();
|
|
220
|
+
const okDir = await writeSnapshot(root, "ok-exp", "2026-07-04T08-00-00-000Z-oooo", meta({ experimentId: "ok", agent: "bub", startedAt: "2026-07-04T08:00:00.000Z", completedAt: "2026-07-04T08:10:00.000Z" }));
|
|
221
|
+
await writeResultFile(okDir, "q1/a1", record({ id: "q1", attempt: 1 }));
|
|
222
|
+
|
|
223
|
+
const badDir = join(root, "bad-exp", "2026-07-02T08-00-00-000Z-zzzz");
|
|
224
|
+
await mkdir(badDir, { recursive: true });
|
|
225
|
+
await writeFile(join(badDir, "snapshot.json"), "not json {", "utf-8");
|
|
226
|
+
|
|
227
|
+
const alienDir = join(root, "alien-exp", "2026-07-06T08-00-00-000Z-alien");
|
|
228
|
+
await mkdir(alienDir, { recursive: true });
|
|
229
|
+
await writeFile(join(alienDir, "summary.json"), JSON.stringify({ hello: 1 }), "utf-8");
|
|
230
|
+
|
|
231
|
+
const results = await openResults(root);
|
|
232
|
+
const latest = results.latest();
|
|
233
|
+
// 其余快照照常计入,不被坏落盘拖垮。
|
|
234
|
+
expect(latest.snapshots.map((s) => s.experimentId)).toEqual(["ok"]);
|
|
235
|
+
|
|
236
|
+
const unreadable = latest.warnings.filter((w) => w.kind === "unreadable-snapshot");
|
|
237
|
+
expect(unreadable).toHaveLength(1); // 无关 JSON(alien)不产生 warning
|
|
238
|
+
expect(unreadable[0]).toMatchObject({ kind: "unreadable-snapshot", dir: badDir, reason: "malformed" });
|
|
239
|
+
expect(unreadable[0].message).toContain(badDir);
|
|
240
|
+
expect(unreadable[0].message).toMatch(/inspect .*snapshot\.json/);
|
|
241
|
+
expect((unreadable[0] as { command?: string }).command).toBeUndefined();
|
|
242
|
+
|
|
243
|
+
// results.current() 同一份事实(show / view 报告槽走的是它)。
|
|
244
|
+
const current = results.current();
|
|
245
|
+
expect(current.warnings.filter((w) => w.kind === "unreadable-snapshot")).toHaveLength(1);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it("incompatible(schemaVersion 不兼容,niceeval producer)的 warning 带版本化 command", async () => {
|
|
249
|
+
const root = await makeRoot();
|
|
250
|
+
const oldDir = join(root, "old-exp", "2026-06-01T08-00-00-000Z");
|
|
251
|
+
await mkdir(oldDir, { recursive: true });
|
|
252
|
+
await writeFile(
|
|
253
|
+
join(oldDir, "snapshot.json"),
|
|
254
|
+
JSON.stringify({
|
|
255
|
+
format: RESULTS_FORMAT,
|
|
256
|
+
schemaVersion: RESULTS_SCHEMA_VERSION - 1,
|
|
257
|
+
producer: { name: "niceeval", version: "0.4.6" },
|
|
258
|
+
experimentId: "old",
|
|
259
|
+
agent: "bub",
|
|
260
|
+
startedAt: "2026-06-01T08:00:00.000Z",
|
|
261
|
+
}),
|
|
262
|
+
"utf-8",
|
|
263
|
+
);
|
|
264
|
+
|
|
265
|
+
const latest = (await openResults(root)).latest();
|
|
266
|
+
const warn = latest.warnings.find((w) => w.kind === "unreadable-snapshot")!;
|
|
267
|
+
expect(warn).toMatchObject({ kind: "unreadable-snapshot", dir: oldDir, reason: "incompatible-version" });
|
|
268
|
+
expect((warn as { command?: string }).command).toBe(`npx niceeval@0.4.6 show --results ${root}`);
|
|
269
|
+
expect(warn.message).toContain("npx niceeval@0.4.6 show --results");
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it("非实验作用域:Selection.filter 收窄后 unreadable-snapshot warning 仍在", async () => {
|
|
273
|
+
const root = await makeRoot();
|
|
274
|
+
const aDir = await writeSnapshot(root, "mid_a", "s1", meta({ experimentId: "mid/a", agent: "bub", startedAt: "2026-07-01T08:00:00.000Z", completedAt: "2026-07-01T08:10:00.000Z" }));
|
|
275
|
+
await writeResultFile(aDir, "q1/a1", record({ id: "q1", attempt: 1 }));
|
|
276
|
+
|
|
277
|
+
const badDir = join(root, "bad-exp", "2026-07-02T08-00-00-000Z-zzzz");
|
|
278
|
+
await mkdir(badDir, { recursive: true });
|
|
279
|
+
await writeFile(join(badDir, "snapshot.json"), "not json {", "utf-8");
|
|
280
|
+
|
|
281
|
+
const latest = (await openResults(root)).latest();
|
|
282
|
+
expect(latest.warnings.filter((w) => w.kind === "unreadable-snapshot")).toHaveLength(1);
|
|
283
|
+
|
|
284
|
+
// filter 只删快照,不删非实验作用域的警告 —— 即便过滤条件与该 warning 无关的实验都不复存在。
|
|
285
|
+
const filtered = latest.filter((s) => s.experimentId === "mid/a");
|
|
286
|
+
expect(filtered.snapshots).toHaveLength(1);
|
|
287
|
+
expect(filtered.warnings.filter((w) => w.kind === "unreadable-snapshot")).toHaveLength(1);
|
|
288
|
+
});
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
// ───────────────────────── ScopeWarning 联合成员恰为三种(回归锁死) ─────────────────────────
|
|
292
|
+
|
|
293
|
+
// 类型契约(编译期,随 pnpm typecheck):ScopeWarning 联合成员恰为三种——多一个 kind 而没同步
|
|
294
|
+
// 改这里,default 分支的 never 赋值编译不过;少一个则对应 case 编译不过。kind 全集不是运行时
|
|
295
|
+
// 行为,没有运行时断言可写(docs/engineering/testing/unit/README.md「类型契约」)。
|
|
296
|
+
function assertScopeWarningKindExhaustive(kind: import("./types.ts").ScopeWarning["kind"]): void {
|
|
297
|
+
switch (kind) {
|
|
298
|
+
case "unfinished-snapshot":
|
|
299
|
+
case "missing-startedAt":
|
|
300
|
+
case "unreadable-snapshot":
|
|
301
|
+
return;
|
|
302
|
+
default: {
|
|
303
|
+
const exhausted: never = kind;
|
|
304
|
+
void exhausted;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
void assertScopeWarningKindExhaustive;
|
|
309
|
+
|
|
310
|
+
// ───────────────────────── 时效:attempt.carried 与 fresh 口径 ─────────────────────────
|
|
311
|
+
|
|
312
|
+
describe("时效:carried 投影与 fresh 口径", () => {
|
|
313
|
+
it("latest({ fresh: true }):排除携带条目,被排除的题进 coverage.missingEvalIds", async () => {
|
|
314
|
+
const root = await makeRoot();
|
|
315
|
+
const monday = await writeSnapshot(root, "e", "2026-07-01T08-00-00-000Z", meta({ experimentId: "e", agent: "bub", startedAt: "2026-07-01T08:00:00.000Z", completedAt: "2026-07-01T08:10:00.000Z" }));
|
|
316
|
+
await writeResultFile(monday, "q1/a1", record({ id: "q1", attempt: 1 }));
|
|
317
|
+
|
|
318
|
+
// 周二:q1 携带合入(artifactBase 指回周一),q2 真实执行。
|
|
319
|
+
const tuesday = await writeSnapshot(root, "e", "2026-07-02T08-00-00-000Z", meta({ experimentId: "e", agent: "bub", startedAt: "2026-07-02T08:00:00.000Z", completedAt: "2026-07-02T08:10:00.000Z" }));
|
|
320
|
+
await writeResultFile(tuesday, "q1/a1", record({ id: "q1", attempt: 1, startedAt: "2026-07-01T08:00:00.000Z", artifactBase: "e/2026-07-01T08-00-00-000Z/q1/a1" }));
|
|
321
|
+
await writeResultFile(tuesday, "q2/a1", record({ id: "q2", attempt: 1 }));
|
|
322
|
+
|
|
323
|
+
const results = await openResults(root);
|
|
324
|
+
const notFresh = results.latest();
|
|
325
|
+
expect(notFresh.snapshots[0]!.evals.map((e) => e.id).sort()).toEqual(["q1", "q2"]);
|
|
326
|
+
expect(notFresh.coverage.find((c) => c.experimentId === "e")!.missingEvalIds).toEqual([]);
|
|
327
|
+
|
|
328
|
+
const fresh = results.latest({ fresh: true });
|
|
329
|
+
expect(fresh.attempts.map((a) => a.evalId)).toEqual(["q2"]); // q1 是携带条目,被排除
|
|
330
|
+
expect(fresh.coverage.find((c) => c.experimentId === "e")!.missingEvalIds).toEqual(["q1"]);
|
|
331
|
+
// 真实 Snapshot 原样保留:q1 仍在它自己的 evals 里,fresh 没有克隆/裁剪来源对象。
|
|
332
|
+
expect(fresh.snapshots[0]!.evals.map((e) => e.id).sort()).toEqual(["q1", "q2"]);
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
it("current({ fresh: true }):同时排除携带条目与跨快照拼入的历史执行,区分力 fixture ——「只排携带 / 只排旧快照 / 两者都排」三种候选算法各给出不同答案", async () => {
|
|
336
|
+
const root = await makeRoot();
|
|
337
|
+
// 周一:q1、q2 真实执行。
|
|
338
|
+
const monday = await writeSnapshot(root, "e", "2026-07-01T08-00-00-000Z", meta({ experimentId: "e", agent: "bub", startedAt: "2026-07-01T08:00:00.000Z", completedAt: "2026-07-01T08:10:00.000Z" }));
|
|
339
|
+
await writeResultFile(monday, "q1/a1", record({ id: "q1", attempt: 1 }));
|
|
340
|
+
await writeResultFile(monday, "q2/a1", record({ id: "q2", attempt: 1 }));
|
|
341
|
+
|
|
342
|
+
// 周二:q1 携带合入(仍是这次快照里"最新"的一份,但 carried=true);q3 真实执行;q2 本次没有
|
|
343
|
+
// 任何 attempt —— current() 从周一补齐它,补齐来的 q2 attempt 所属快照早于本实验在 Scope 中
|
|
344
|
+
// 最新快照(周二),是「跨快照拼入」的历史执行,即使它自己不是携带条目。
|
|
345
|
+
const tuesday = await writeSnapshot(root, "e", "2026-07-02T08-00-00-000Z", meta({ experimentId: "e", agent: "bub", startedAt: "2026-07-02T08:00:00.000Z", completedAt: "2026-07-02T08:10:00.000Z" }));
|
|
346
|
+
await writeResultFile(tuesday, "q1/a1", record({ id: "q1", attempt: 1, startedAt: "2026-07-01T08:00:00.000Z", artifactBase: "e/2026-07-01T08-00-00-000Z/q1/a1" }));
|
|
347
|
+
await writeResultFile(tuesday, "q3/a1", record({ id: "q3", attempt: 1 }));
|
|
348
|
+
|
|
349
|
+
const results = await openResults(root);
|
|
350
|
+
const notFresh = results.current();
|
|
351
|
+
expect(notFresh.attempts.map((a) => a.evalId).sort()).toEqual(["q1", "q2", "q3"]);
|
|
352
|
+
expect(notFresh.coverage.find((c) => c.experimentId === "e")!.missingEvalIds).toEqual([]);
|
|
353
|
+
// 两个真实来源都在场:q2 唯一来自周一,q1/q3 来自周二——不是一个合并对象。
|
|
354
|
+
expect(notFresh.snapshots.map((s) => s.startedAt).sort()).toEqual(["2026-07-01T08:00:00.000Z", "2026-07-02T08:00:00.000Z"]);
|
|
355
|
+
|
|
356
|
+
const fresh = results.current({ fresh: true });
|
|
357
|
+
// 只有 q3(本次快照真实执行、非携带)是新执行;q1(携带)与 q2(跨快照拼入)都被排除。
|
|
358
|
+
expect(fresh.attempts.map((a) => a.evalId)).toEqual(["q3"]);
|
|
359
|
+
expect(fresh.coverage.find((c) => c.experimentId === "e")!.missingEvalIds.sort()).toEqual(["q1", "q2"]);
|
|
360
|
+
// 周一(q2 唯一来源)不再贡献任何 fresh attempt,退出 snapshots;周二仍在,即便它自己的
|
|
361
|
+
// evals 里仍然原样带着被排除的 q1(真实 Snapshot 不被裁剪)。
|
|
362
|
+
expect(fresh.snapshots.map((s) => s.startedAt)).toEqual(["2026-07-02T08:00:00.000Z"]);
|
|
363
|
+
expect(fresh.snapshots[0]!.evals.map((e) => e.id).sort()).toEqual(["q1", "q3"]);
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
it("fresh 清空全部 attempt 后 Scope 只剩 coverage 事实;filter 不拿 snapshots 当 coverage 的存续代理", async () => {
|
|
367
|
+
const root = await makeRoot();
|
|
368
|
+
const monday = await writeSnapshot(root, "e", "2026-07-01T08-00-00-000Z", meta({ experimentId: "e", agent: "bub", startedAt: "2026-07-01T08:00:00.000Z", completedAt: "2026-07-01T08:10:00.000Z" }));
|
|
369
|
+
await writeResultFile(monday, "q1/a1", record({ id: "q1", attempt: 1 }));
|
|
370
|
+
// 周二唯一的条目是携带复印件:fresh 把它排除后,这个实验没有任何新执行。
|
|
371
|
+
const tuesday = await writeSnapshot(root, "e", "2026-07-02T08-00-00-000Z", meta({ experimentId: "e", agent: "bub", startedAt: "2026-07-02T08:00:00.000Z", completedAt: "2026-07-02T08:10:00.000Z" }));
|
|
372
|
+
await writeResultFile(tuesday, "q1/a1", record({ id: "q1", attempt: 1, startedAt: "2026-07-01T08:00:00.000Z", artifactBase: "e/2026-07-01T08-00-00-000Z/q1/a1" }));
|
|
373
|
+
|
|
374
|
+
const fresh = (await openResults(root)).current({ fresh: true });
|
|
375
|
+
expect(fresh.attempts).toEqual([]);
|
|
376
|
+
const coverage = fresh.coverage.find((c) => c.experimentId === "e")!;
|
|
377
|
+
expect(coverage.missingEvalIds).toEqual(["q1"]);
|
|
378
|
+
// coverage-only 实验(零 snapshot 贡献)经 filter 后覆盖缺口不静默消失——榜单占位行的上游事实。
|
|
379
|
+
expect(fresh.filter(() => true).coverage).toEqual(fresh.coverage);
|
|
380
|
+
});
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
// ───────────────────────── 身份键去重 ─────────────────────────
|
|
384
|
+
|
|
385
|
+
function fakeSnapshot(over: { experimentId: string; startedAt: string; dir: string }): Snapshot {
|
|
386
|
+
return {
|
|
387
|
+
agent: "bub",
|
|
388
|
+
producer: { name: "niceeval" },
|
|
389
|
+
schemaVersion: RESULTS_SCHEMA_VERSION,
|
|
390
|
+
evals: [],
|
|
391
|
+
attempts: [],
|
|
392
|
+
...over,
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function fakeAttempt(snapshot: Snapshot, result: EvalResult): AttemptHandle {
|
|
397
|
+
return {
|
|
398
|
+
evalId: result.id,
|
|
399
|
+
experimentId: snapshot.experimentId,
|
|
400
|
+
result,
|
|
401
|
+
ref: { snapshot: "x/y", attempt: `${result.id}/a${result.attempt}` },
|
|
402
|
+
snapshot,
|
|
403
|
+
carried: Boolean(result.artifactBase),
|
|
404
|
+
commands: async () => null,
|
|
405
|
+
events: async () => null,
|
|
406
|
+
trace: async () => null,
|
|
407
|
+
o11y: async () => null,
|
|
408
|
+
agentSetup: async () => null,
|
|
409
|
+
diff: async () => null,
|
|
410
|
+
sources: async () => null,
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
describe("dedupeAttempts", () => {
|
|
415
|
+
it("按 (experimentId, evalId, attempt, startedAt) 去重,保留最新快照里的那份;缺 startedAt 不去重并出 missing-startedAt", () => {
|
|
416
|
+
const monday = fakeSnapshot({ experimentId: "e", startedAt: "2026-07-01T08:00:00.000Z", dir: "/tmp/e/monday" });
|
|
417
|
+
const tuesday = fakeSnapshot({ experimentId: "e", startedAt: "2026-07-02T08:00:00.000Z", dir: "/tmp/e/tuesday" });
|
|
418
|
+
|
|
419
|
+
const a1 = fakeAttempt(monday, record({ id: "q1", attempt: 1, startedAt: "2026-07-01T08:01:00.000Z" }));
|
|
420
|
+
const a1Resumed = fakeAttempt(tuesday, record({ id: "q1", attempt: 1, startedAt: "2026-07-01T08:01:00.000Z" })); // resume 原样合入
|
|
421
|
+
const a2Mon = fakeAttempt(monday, record({ id: "q2", attempt: 1, startedAt: "2026-07-01T08:02:00.000Z" }));
|
|
422
|
+
const a2Tue = fakeAttempt(tuesday, record({ id: "q2", attempt: 1, startedAt: "2026-07-02T08:02:00.000Z" })); // 重跑,新 startedAt
|
|
423
|
+
const a3Mon = fakeAttempt(monday, record({ id: "q3", attempt: 1 })); // 缺 startedAt(携带条目缺锚的极端情况)
|
|
424
|
+
const a3Tue = fakeAttempt(tuesday, record({ id: "q3", attempt: 1 }));
|
|
425
|
+
|
|
426
|
+
const { attempts, warnings } = dedupeAttempts([a1, a2Mon, a3Mon, a1Resumed, a2Tue, a3Tue]);
|
|
427
|
+
expect(attempts).toHaveLength(5);
|
|
428
|
+
const q1 = attempts.filter((a) => a.evalId === "q1");
|
|
429
|
+
expect(q1).toHaveLength(1);
|
|
430
|
+
expect(q1[0].snapshot).toBe(tuesday); // 保留最新快照
|
|
431
|
+
expect(attempts.filter((a) => a.evalId === "q2")).toHaveLength(2);
|
|
432
|
+
expect(attempts.filter((a) => a.evalId === "q3")).toHaveLength(2);
|
|
433
|
+
|
|
434
|
+
expect(warnings).toHaveLength(2);
|
|
435
|
+
expect(warnings[0]).toMatchObject({ kind: "missing-startedAt", experimentId: "e", evalId: "q3" });
|
|
436
|
+
expect(warnings[0].message).toContain("has no startedAt");
|
|
437
|
+
});
|
|
438
|
+
});
|
package/src/results/select.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// 快照 Scope 与 attempt 去重(定稿见 docs/feature/
|
|
1
|
+
// 快照 Scope 与 attempt 去重(定稿见 docs/feature/record/library.md「选择快照」「官方现刻水位」「身份键与去重」)。
|
|
2
2
|
//
|
|
3
3
|
// 选择器长在集合上(results.latest() / results.current()),不是 DSL,只是最常用的两种口径。
|
|
4
4
|
// 选择器必须诚实:残缺、落后、未收尾都被算出来,以结构化 warnings 随 Scope 走 ——
|
|
@@ -42,7 +42,7 @@ export function selectLatest(
|
|
|
42
42
|
// fresh: true 只保留新执行的 attempt——在 latest() 口径下,选中集合永远只有这一份
|
|
43
43
|
// 快照,「所属快照早于该实验在 Scope 中最新快照」这条历史出身天生不成立(只有它自己),
|
|
44
44
|
// 唯一的历史出身是携带条目(attempt.carried)。只过滤显式物化的 attempts 集,不克隆/
|
|
45
|
-
// 改写来源 Snapshot 的 evals(见 docs/feature/
|
|
45
|
+
// 改写来源 Snapshot 的 evals(见 docs/feature/record/library.md「选择快照」)。
|
|
46
46
|
const picked = fresh ? raw.attempts.filter((a) => !a.carried) : raw.attempts;
|
|
47
47
|
attempts.push(...picked);
|
|
48
48
|
|
|
@@ -85,7 +85,7 @@ export interface ResultScope {
|
|
|
85
85
|
// ───────────────────────── 可比性配置 ─────────────────────────
|
|
86
86
|
|
|
87
87
|
/**
|
|
88
|
-
* current() 跨快照拼接的可比性前提所比较的字段集(docs/feature/
|
|
88
|
+
* current() 跨快照拼接的可比性前提所比较的字段集(docs/feature/record/library.md
|
|
89
89
|
* 「官方现刻水位」):会改变单题被测行为或判定的字段。runs / earlyExit / maxConcurrency /
|
|
90
90
|
* selectedEvalIds / evalFilterFingerprint / description 是编排与选题字段,不参与比较。
|
|
91
91
|
*/
|
|
@@ -134,7 +134,7 @@ export function deepEqualJson(a: unknown, b: unknown): boolean {
|
|
|
134
134
|
/**
|
|
135
135
|
* 一个快照实际选中的 eval id 全集:优先读落盘的 `experiment.selectedEvalIds`(niceeval 自己的
|
|
136
136
|
* 写入面必有此字段);第三方 harness 未实现该字段时退化为该快照实际写出的 `evals`,不把整份
|
|
137
|
-
* 来源排除在外(见 docs/feature/
|
|
137
|
+
* 来源排除在外(见 docs/feature/record/architecture.md「selectedEvalIds」)。
|
|
138
138
|
*/
|
|
139
139
|
export function selectedEvalIdsOf(snapshot: Snapshot): readonly string[] {
|
|
140
140
|
return snapshot.experiment?.selectedEvalIds ?? snapshot.evals.map((ev) => ev.id);
|
|
@@ -146,7 +146,7 @@ export function selectedEvalIdsOf(snapshot: Snapshot): readonly string[] {
|
|
|
146
146
|
* 快照」,带 eval 前缀的局部重跑会产出残缺快照;现刻水位承诺「不会因为一次局部重跑变残缺」,
|
|
147
147
|
* 所以在实验的历史快照上逐 eval 向更早的 run 补齐——但补齐只发生在 `Scope.attempts` 这份
|
|
148
148
|
* 物化选择上,真正贡献过至少一道题的来源 Snapshot 原样进 `Scope.snapshots`,不重建、不
|
|
149
|
-
* 合并成报告专用对象(见 docs/feature/
|
|
149
|
+
* 合并成报告专用对象(见 docs/feature/record/library.md「官方现刻水位」)。
|
|
150
150
|
*
|
|
151
151
|
* **可比性前提**:每个 experiment 以最新快照的可比性配置(agent / model / reasoningEffort /
|
|
152
152
|
* flags / budget / timeoutMs / sandbox)为基准,只有配置与基准深相等的历史快照才参与补齐;
|
|
@@ -419,7 +419,7 @@ export function filterExperiments(experiments: Experiment[], filter?: string | s
|
|
|
419
419
|
/**
|
|
420
420
|
* 时效标注(`↩` + 人话时距)的粒度选择:选粒度最大的单位,四舍五入。结构化形态是单源——
|
|
421
421
|
* entity-lists 渲染面的紧凑时距("3d")与曾经的 stale-snapshot message 用同一套阈值,
|
|
422
|
-
* 阈值不写两份(docs/feature/reports/
|
|
422
|
+
* 阈值不写两份(docs/feature/reports/components/entity-lists/README.md「时效标注」)。
|
|
423
423
|
*/
|
|
424
424
|
export function gapParts(fromIso: string, toIso: string): { n: number; unit: "second" | "minute" | "hour" | "day" } {
|
|
425
425
|
const ms = Math.max(0, Date.parse(toIso) - Date.parse(fromIso));
|
|
@@ -16,7 +16,7 @@ export interface SkippedVersionGroup {
|
|
|
16
16
|
/**
|
|
17
17
|
* 把 `skipped` 拆成「可按版本分组给出统一 npx 建议的」与「其余原样列出的」两部分。
|
|
18
18
|
* 只有 reason 为 `incompatible-version` 且 `producer.name === "niceeval"`、`producer.version`
|
|
19
|
-
* 存在时才分组——第三方 harness 如实报名字版本、不拼 npx(docs/feature/
|
|
19
|
+
* 存在时才分组——第三方 harness 如实报名字版本、不拼 npx(docs/feature/record/library.md 的裁决);
|
|
20
20
|
* 版本信息缺失时也没有可执行的统一建议,归入 `rest`。分组键是 `(producer.version, schemaVersion)`,
|
|
21
21
|
* 因为同一 producer.version 理论上 schemaVersion 恒定,但落盘可能来自更旧的 patch 版本,
|
|
22
22
|
* 拆开更保守。
|
package/src/results/truncate.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// 落盘大值截断(见 docs/feature/
|
|
1
|
+
// 落盘大值截断(见 docs/feature/record/architecture.md「大值截断」)。
|
|
2
2
|
// 运行时全量,落盘截断:落点唯一在 snap.writeAttempt(writer.ts)——不在 adapter、不在 OTLP
|
|
3
3
|
// 解析、不在事件归一化里做。适用范围:events.json 的事件字段与 trace.json 的 span 属性里的
|
|
4
4
|
// 任意字符串值。没有 flag、没有配置项;截断永远不影响判决(落盘是证据,不是评分输入)。
|
|
@@ -90,7 +90,7 @@ export function truncateSpans(spans: readonly TraceSpan[]): TraceSpan[] {
|
|
|
90
90
|
}
|
|
91
91
|
|
|
92
92
|
/** commands.json 落盘前的截断:每条证据的 stdout / stderr 逐值截断,`truncated[].path` 固定
|
|
93
|
-
* 是 "stdout" 或 "stderr"(见 docs/feature/
|
|
93
|
+
* 是 "stdout" 或 "stderr"(见 docs/feature/record/architecture.md「commandsjson」)。 */
|
|
94
94
|
export function truncateCommands(commands: readonly FailedCommandEvidence[]): FailedCommandEvidence[] {
|
|
95
95
|
return commands.map((cmd) => {
|
|
96
96
|
const out: Truncation[] = [];
|
package/src/results/types.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// results 域类型:openResults 的分层读取契约与 Selection(定稿见 docs/feature/
|
|
1
|
+
// results 域类型:openResults 的分层读取契约与 Selection(定稿见 docs/feature/record/library.md、docs/feature/record/architecture.md)。
|
|
2
2
|
//
|
|
3
3
|
// 结果数据类型(EvalResult / ExperimentRunInfo / StreamEvent / …)仍住在各自的域文件里,
|
|
4
4
|
// 这里只 import,不搬家 —— 「类型的家」迁移(facade 反向 re-export)是下一波,不在本次范围。
|
|
@@ -11,7 +11,7 @@ import type { FailedCommandEvidence, O11ySummary, StreamEvent, TraceSpan } from
|
|
|
11
11
|
import type { AgentSetupManifest, DiffData, SourceArtifact } from "../types.ts";
|
|
12
12
|
import type { AttemptLocator } from "./locator.ts";
|
|
13
13
|
|
|
14
|
-
/** attempt 级 artifact 的种类;文件名见 format.ts 的 artifactFileOf,布局见 docs/feature/
|
|
14
|
+
/** attempt 级 artifact 的种类;文件名见 format.ts 的 artifactFileOf,布局见 docs/feature/record/architecture.md。 */
|
|
15
15
|
export const ARTIFACT_KINDS = ["commands", "events", "trace", "o11y", "agentSetup", "diff", "sources"] as const;
|
|
16
16
|
export type ArtifactKind = (typeof ARTIFACT_KINDS)[number];
|
|
17
17
|
|
|
@@ -25,7 +25,7 @@ export interface Producer {
|
|
|
25
25
|
/**
|
|
26
26
|
* `snapshot.json` 的持久化契约:快照元数据 —— 身份、快照级字段与版本元数据,
|
|
27
27
|
* 不含任何逐 attempt 数据。快照开始时写入;收尾时补写 `completedAt`。
|
|
28
|
-
* 字段规则与版本判定见 docs/feature/
|
|
28
|
+
* 字段规则与版本判定见 docs/feature/record/architecture.md「snapshot.json」「版本与升级设计」。
|
|
29
29
|
*/
|
|
30
30
|
export interface SnapshotMeta {
|
|
31
31
|
/** 恒为 "niceeval.results";和 schemaVersion、producer 一起构成持久化契约,永不移动或改名。 */
|
|
@@ -50,7 +50,7 @@ export interface SnapshotMeta {
|
|
|
50
50
|
diagnostics?: DiagnosticRecord[];
|
|
51
51
|
/**
|
|
52
52
|
* experiment 作用域生命周期代码经 `ctx.fact()` 上报的运行事实;与 completedAt 同批在快照
|
|
53
|
-
* 封口补写。字段契约见 result.json 的 facts 小节(docs/feature/
|
|
53
|
+
* 封口补写。字段契约见 result.json 的 facts 小节(docs/feature/record/architecture.md#facts运行事实)。
|
|
54
54
|
*/
|
|
55
55
|
facts?: Record<string, string | number | boolean>;
|
|
56
56
|
/** 写入时刻该实验已知的 eval 并集 —— 残缺检测的分母随数据走(copySnapshots 自动补记,writer 可声明)。 */
|
|
@@ -98,7 +98,7 @@ export interface AttemptHandle {
|
|
|
98
98
|
/**
|
|
99
99
|
* 携带条目投影:true = fingerprint 未变、上一轮终态结果合入本快照(`result.artifactBase`
|
|
100
100
|
* 有值);false = 本快照那次运行真实执行。`startedAt` 为原执行时刻,不因携带而改写
|
|
101
|
-
* (时效语义见 docs/feature/
|
|
101
|
+
* (时效语义见 docs/feature/sample/library.md「时效:新执行与历史执行」)。
|
|
102
102
|
*/
|
|
103
103
|
carried: boolean;
|
|
104
104
|
/** 非零 Sandbox 命令的 stdout/stderr 证据(`commands.json`);没有失败命令时 null。 */
|
|
@@ -196,7 +196,7 @@ export interface Results {
|
|
|
196
196
|
* 每个实验取最新一次快照,返回 Scope(快照与挑选警告绑在一起走)。
|
|
197
197
|
* `experiments` 是 experiment id 前缀过滤(string | string[]),同 CLI 位置参数语义。
|
|
198
198
|
* `fresh: true` 只保留新执行的 attempt(排除携带条目),被排除的题按覆盖事实进入
|
|
199
|
-
* `coverage.missingEvalIds`(见 docs/feature/
|
|
199
|
+
* `coverage.missingEvalIds`(见 docs/feature/sample/library.md「时效:新执行与历史执行」)。
|
|
200
200
|
*/
|
|
201
201
|
latest(opts?: { experiments?: string | string[]; fresh?: boolean }): Scope;
|
|
202
202
|
/**
|
|
@@ -204,7 +204,7 @@ export interface Results {
|
|
|
204
204
|
* 跨历史拼出当前判定水位。可比性前提:每个 experiment 以最新快照的可比性配置
|
|
205
205
|
* (agent / model / reasoningEffort / flags / budget / timeoutMs / sandbox)为基准,
|
|
206
206
|
* 配置不一致的旧快照不贡献 attempt,缺口进 `coverage.missingEvalIds`
|
|
207
|
-
* (见 docs/feature/
|
|
207
|
+
* (见 docs/feature/record/library.md「官方现刻水位」)。`fresh: true` 同 `latest()`。
|
|
208
208
|
*/
|
|
209
209
|
current(opts?: { experiments?: string | string[]; fresh?: boolean }): Scope;
|
|
210
210
|
}
|
|
@@ -212,7 +212,7 @@ export interface Results {
|
|
|
212
212
|
/**
|
|
213
213
|
* 一个实验的覆盖事实:已知 eval 并集(分母)与当前口径下没有任何 attempt 的题。
|
|
214
214
|
* `missingEvalIds` 永远被算出来,不静默——渲染面把它转成榜单占位行
|
|
215
|
-
* (见 docs/feature/
|
|
215
|
+
* (见 docs/feature/sample/library.md「选择快照」「时效:新执行与历史执行」)。
|
|
216
216
|
*/
|
|
217
217
|
export interface ScopeCoverage {
|
|
218
218
|
experimentId: string;
|
|
@@ -250,7 +250,7 @@ export interface Scope {
|
|
|
250
250
|
/**
|
|
251
251
|
* 挑选警告:每种带 kind、可判断的结构化字段和渲染好的英文 message;能用一条命令直接推进的
|
|
252
252
|
* kind 同时带 `command`(已替换真实 id,复制即跑)。kind 是契约的一部分,全集与触发条件见
|
|
253
|
-
* docs/feature/
|
|
253
|
+
* docs/feature/record/library.md「警告 kind 全集」——三种都是**定位不到任何一行**的完整性
|
|
254
254
|
* 事实:覆盖缺口(行级事实,见 `ScopeCoverage`)与时效(`AttemptHandle.carried` 投影的行级
|
|
255
255
|
* 属性)不在这个联合里。
|
|
256
256
|
*/
|
package/src/results/writer.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// createResultsWriter:Results Format 的写入面(定稿见 docs/feature/
|
|
1
|
+
// createResultsWriter:Results Format 的写入面(定稿见 docs/feature/record/library.md「写:createResultsWriter」)。
|
|
2
2
|
//
|
|
3
3
|
// writer 与 reader 是同一组类型的两半,而且是字面的两半:reader 的 attempt.result 由
|
|
4
4
|
// 「snapshot() 声明的快照级字段(experimentId / agent / model / startedAt / experiment)+
|
|
@@ -83,7 +83,7 @@ export interface AttemptArtifacts {
|
|
|
83
83
|
agentSetup?: AgentSetupManifest;
|
|
84
84
|
diff?: DiffArtifact;
|
|
85
85
|
sources?: SourceArtifact[];
|
|
86
|
-
/** 非零 Sandbox 命令的 stdout/stderr 证据(见 docs/feature/
|
|
86
|
+
/** 非零 Sandbox 命令的 stdout/stderr 证据(见 docs/feature/record/architecture.md「commandsjson」)。 */
|
|
87
87
|
commands?: FailedCommandEvidence[];
|
|
88
88
|
}
|
|
89
89
|
|
|
@@ -351,7 +351,7 @@ async function writeAttemptFiles(
|
|
|
351
351
|
// 大值截断只发生在这里(序列化的那一刻):events 的事件字段、trace 的 span 属性与 commands
|
|
352
352
|
// 的 stdout/stderr 里的任意字符串值按 ARTIFACT_VALUE_MAX_BYTES 截断并留结构化 truncated
|
|
353
353
|
// 标记;运行时(断言 / o11y 派生)看到的始终是完整值。sources 与 diff 不截断(见
|
|
354
|
-
// docs/feature/
|
|
354
|
+
// docs/feature/record/architecture.md)。
|
|
355
355
|
if (hasCommands)
|
|
356
356
|
writes.push(
|
|
357
357
|
writeFile(join(attemptDir, artifactFileOf("commands")), JSON.stringify(truncateCommands(artifacts!.commands!)), "utf-8"),
|
|
@@ -456,7 +456,7 @@ async function createSnapshotDir(root: string, experimentId: string): Promise<st
|
|
|
456
456
|
|
|
457
457
|
|
|
458
458
|
|
|
459
|
-
/** 快照目录名的时间戳段:Date#toISOString 把 : 与 . 换成 -(与 docs/feature/
|
|
459
|
+
/** 快照目录名的时间戳段:Date#toISOString 把 : 与 . 换成 -(与 docs/feature/record/architecture.md 一致)。 */
|
|
460
460
|
function safeTimestamp(d: Date): string {
|
|
461
461
|
return d.toISOString().replace(/[:.]/g, "-");
|
|
462
462
|
}
|
|
@@ -3,11 +3,11 @@
|
|
|
3
3
|
// ctx.fact() 的作用域归属单测在本文件末尾单独一个 describe 块:sandbox hook / eval.setup /
|
|
4
4
|
// agent setup·send·teardown 上报的 fact 是否真的落进同一个 attempt 的 EvalResult.facts、
|
|
5
5
|
// 同 key 后写覆盖先写、非法 key / 非标量 value 是否完整报错(见
|
|
6
|
-
// docs/feature/
|
|
6
|
+
// docs/feature/record/architecture.md#facts运行事实)。
|
|
7
7
|
//
|
|
8
8
|
// 路径提升单测:agent.setup 写进沙箱 `__niceeval__/agent-setup.json` 的安装 manifest,
|
|
9
9
|
// runAttemptEffect 在 setup 之后把它读出来、原样挂到 EvalResult.agentSetup(见
|
|
10
|
-
// docs/feature/
|
|
10
|
+
// docs/feature/record/architecture.md「agent-setup.json」、src/agents/manifest.ts 的注释)。
|
|
11
11
|
// 沙箱是内存 fake(记文件,不起容器)——这里要验的是运行器自己「何时读、读到什么、读不到
|
|
12
12
|
// 怎么办」这段编排逻辑,不是 adapter 侧的 manifest 构造规则(那部分已在 agents/skills.test.ts
|
|
13
13
|
// 覆盖)。
|