niceeval 0.4.6 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (174) hide show
  1. package/docs-site/zh/concepts/adapter.mdx +6 -6
  2. package/docs-site/zh/concepts/experiment.mdx +7 -7
  3. package/docs-site/zh/concepts/overview.mdx +1 -1
  4. package/docs-site/zh/concepts/tier.mdx +6 -6
  5. package/docs-site/zh/guides/agent-feedback-loop.mdx +64 -21
  6. package/docs-site/zh/guides/connect-your-agent.mdx +7 -7
  7. package/docs-site/zh/guides/custom-reports.mdx +243 -0
  8. package/docs-site/zh/guides/experiments.mdx +3 -3
  9. package/docs-site/zh/guides/report-components.mdx +252 -0
  10. package/docs-site/zh/guides/reporters.mdx +1 -1
  11. package/docs-site/zh/guides/results-data.mdx +224 -0
  12. package/docs-site/zh/guides/runner.mdx +1 -1
  13. package/docs-site/zh/guides/sandbox-agent.mdx +2 -2
  14. package/docs-site/zh/guides/viewing-results.mdx +147 -77
  15. package/docs-site/zh/guides/write-experiment.mdx +9 -9
  16. package/docs-site/zh/guides/write-send.mdx +6 -6
  17. package/docs-site/zh/index.mdx +1 -1
  18. package/docs-site/zh/introduction.mdx +1 -1
  19. package/docs-site/zh/reference/cli.mdx +11 -3
  20. package/docs-site/zh/reference/define-agent.mdx +5 -5
  21. package/docs-site/zh/reference/define-config.mdx +1 -1
  22. package/docs-site/zh/reference/define-eval.mdx +3 -3
  23. package/package.json +3 -2
  24. package/src/agents/ai-sdk.test.ts +1 -1
  25. package/src/agents/ai-sdk.ts +2 -2
  26. package/src/agents/claude-code.ts +1 -1
  27. package/src/agents/codex.ts +2 -2
  28. package/src/agents/streaming.test.ts +1 -1
  29. package/src/agents/types.ts +4 -4
  30. package/src/agents/ui-message-stream.test.ts +1 -1
  31. package/src/cli.ts +120 -18
  32. package/src/context/context.test.ts +1 -1
  33. package/src/context/context.ts +3 -3
  34. package/src/context/session.ts +2 -2
  35. package/src/context/types.ts +2 -2
  36. package/src/i18n/en.ts +29 -7
  37. package/src/i18n/zh-CN.ts +24 -2
  38. package/src/report/aggregate.ts +79 -50
  39. package/src/report/components.tsx +190 -0
  40. package/src/report/compute.ts +149 -84
  41. package/src/report/default-report.tsx +222 -0
  42. package/src/report/dual-face.test.tsx +527 -0
  43. package/src/report/format.ts +29 -0
  44. package/src/report/index.ts +79 -24
  45. package/src/report/load.ts +67 -0
  46. package/src/report/metrics.ts +2 -1
  47. package/src/report/param.ts +18 -0
  48. package/src/report/primitives.tsx +117 -0
  49. package/src/report/react/CaseList.tsx +2 -2
  50. package/src/report/react/DeltaTable.tsx +2 -2
  51. package/src/report/react/MetricBars.tsx +109 -0
  52. package/src/report/react/MetricLine.tsx +200 -0
  53. package/src/report/react/MetricMatrix.tsx +1 -1
  54. package/src/report/react/MetricScatter.tsx +1 -1
  55. package/src/report/react/MetricTable.tsx +1 -1
  56. package/src/report/react/RunOverview.tsx +4 -4
  57. package/src/report/react/Scoreboard.tsx +3 -3
  58. package/src/report/react/cell.tsx +1 -1
  59. package/src/report/react/fixtures.ts +98 -36
  60. package/src/report/react/format.ts +3 -28
  61. package/src/report/react/index.tsx +41 -24
  62. package/src/report/react/render.test.tsx +8 -8
  63. package/src/report/react/styles.css +77 -0
  64. package/src/report/report.test.ts +248 -128
  65. package/src/report/report.ts +69 -0
  66. package/src/report/text/faces.ts +356 -0
  67. package/src/report/text/layout.ts +125 -0
  68. package/src/report/text/plot.ts +127 -0
  69. package/src/report/tree.ts +220 -0
  70. package/src/report/types.ts +75 -27
  71. package/src/report/web.ts +40 -0
  72. package/src/results/copy.ts +95 -41
  73. package/src/results/format.ts +12 -11
  74. package/src/results/index.ts +34 -17
  75. package/src/results/open.ts +201 -81
  76. package/src/results/results.test.ts +595 -191
  77. package/src/results/select.ts +107 -55
  78. package/src/results/types.ts +137 -45
  79. package/src/results/writer.ts +258 -0
  80. package/src/runner/attempt.ts +3 -3
  81. package/src/runner/fingerprint.ts +1 -1
  82. package/src/runner/reporters/artifacts.ts +38 -78
  83. package/src/runner/reporters/braintrust.test.ts +2 -2
  84. package/src/runner/reporters/braintrust.ts +3 -3
  85. package/src/runner/run.ts +1 -1
  86. package/src/runner/types.ts +18 -12
  87. package/src/show/compose.ts +215 -0
  88. package/src/show/index.ts +263 -0
  89. package/src/show/render.ts +456 -0
  90. package/src/show/show.test.ts +505 -0
  91. package/src/view/app/App.tsx +85 -24
  92. package/src/view/app/components/CostScoreChart.tsx +29 -10
  93. package/src/view/app/components/ExperimentTable.tsx +27 -9
  94. package/src/view/app/components/GroupSelector.tsx +1 -1
  95. package/src/view/app/i18n.ts +18 -3
  96. package/src/view/app/lib/attempt-route.test.ts +9 -9
  97. package/src/view/app/lib/attempt-route.ts +5 -5
  98. package/src/view/app/lib/outcome.ts +1 -3
  99. package/src/view/app/lib/rows.ts +95 -15
  100. package/src/view/app/main.tsx +18 -6
  101. package/src/view/app/pages/RunsPage.tsx +4 -7
  102. package/src/view/app/pages/TracesPage.tsx +3 -8
  103. package/src/view/app/types.ts +50 -6
  104. package/src/view/client-dist/app.css +1 -1
  105. package/src/view/client-dist/app.js +20 -20
  106. package/src/view/data.test.ts +254 -0
  107. package/src/view/data.ts +336 -0
  108. package/src/view/index.ts +64 -10
  109. package/src/view/server.ts +31 -9
  110. package/src/view/shared/types.ts +47 -41
  111. package/src/view/styles.css +5 -0
  112. package/src/view/template.html +1 -0
  113. package/src/view/view-report.test.ts +308 -0
  114. package/docs-site/.mintignore +0 -13
  115. package/docs-site/AGENTS.md +0 -46
  116. package/docs-site/concepts/adapter.mdx +0 -287
  117. package/docs-site/concepts/assert.mdx +0 -243
  118. package/docs-site/concepts/drive.mdx +0 -118
  119. package/docs-site/concepts/evals.mdx +0 -108
  120. package/docs-site/concepts/experiment.mdx +0 -37
  121. package/docs-site/concepts/hitl.mdx +0 -83
  122. package/docs-site/concepts/judge.mdx +0 -129
  123. package/docs-site/concepts/overview.mdx +0 -98
  124. package/docs-site/concepts/tier.mdx +0 -42
  125. package/docs-site/docs-ref/00-index.md +0 -23
  126. package/docs-site/docs-ref/01-page-types.md +0 -43
  127. package/docs-site/docs-ref/03-style-rules.md +0 -45
  128. package/docs-site/docs-ref/04-code-examples.md +0 -37
  129. package/docs-site/docs-ref/05-dx-failure-paths.md +0 -45
  130. package/docs-site/docs-ref/06-checklists.md +0 -53
  131. package/docs-site/docs.json +0 -256
  132. package/docs-site/example/ai-agent-application.mdx +0 -148
  133. package/docs-site/example/claude-code-codex-plugin.mdx +0 -162
  134. package/docs-site/example/claude-code-codex-skill.mdx +0 -147
  135. package/docs-site/example/showcase.mdx +0 -39
  136. package/docs-site/example/tier1-ai-sdk-v7.mdx +0 -136
  137. package/docs-site/example/tier1-claude-sdk.mdx +0 -119
  138. package/docs-site/example/tier1-codex-sdk.mdx +0 -111
  139. package/docs-site/example/tier1-langgraph.mdx +0 -119
  140. package/docs-site/example/tier1-pi-sdk.mdx +0 -119
  141. package/docs-site/favicon.svg +0 -5
  142. package/docs-site/github-diff.css +0 -135
  143. package/docs-site/github-diff.js +0 -11
  144. package/docs-site/guides/authoring.mdx +0 -129
  145. package/docs-site/guides/ci-integration.mdx +0 -97
  146. package/docs-site/guides/connect-otel.mdx +0 -209
  147. package/docs-site/guides/connect-your-agent.mdx +0 -221
  148. package/docs-site/guides/dataset-fanout.mdx +0 -98
  149. package/docs-site/guides/experiments.mdx +0 -71
  150. package/docs-site/guides/fixtures.mdx +0 -147
  151. package/docs-site/guides/reporters.mdx +0 -113
  152. package/docs-site/guides/runner.mdx +0 -79
  153. package/docs-site/guides/sandbox-agent.mdx +0 -138
  154. package/docs-site/guides/sandbox-backends.mdx +0 -64
  155. package/docs-site/guides/scoring-guide.mdx +0 -92
  156. package/docs-site/guides/viewing-results.mdx +0 -195
  157. package/docs-site/guides/write-experiment.mdx +0 -81
  158. package/docs-site/guides/write-send.mdx +0 -347
  159. package/docs-site/index.mdx +0 -181
  160. package/docs-site/introduction.mdx +0 -141
  161. package/docs-site/quickstart.mdx +0 -136
  162. package/docs-site/reference/builtin-agents.mdx +0 -161
  163. package/docs-site/reference/capabilities.mdx +0 -76
  164. package/docs-site/reference/cli.mdx +0 -120
  165. package/docs-site/reference/define-agent.mdx +0 -168
  166. package/docs-site/reference/define-config.mdx +0 -41
  167. package/docs-site/reference/define-eval.mdx +0 -160
  168. package/docs-site/reference/events.mdx +0 -131
  169. package/docs-site/reference/expect.mdx +0 -112
  170. package/docs-site/tracker.js +0 -8
  171. package/src/report/react/data.ts +0 -17
  172. package/src/view/aggregate.ts +0 -103
  173. package/src/view/loader.test.ts +0 -61
  174. package/src/view/loader.ts +0 -257
@@ -1,75 +1,115 @@
1
- // 快照选择器与 attempt 去重(设计见 docs/results-lib.md「选择快照」「身份键与去重」)。
1
+ // 快照选集与 attempt 去重(定稿见 docs/results-lib.md「选择快照」「身份键与去重」)。
2
2
  //
3
- // 选择器不是 DSL,只是最常用的那次筛选;更细的口径消费方自己 .filter()。
4
- // 选择器必须诚实:残缺(最新快照覆盖的 eval 少于历史并集)永远被算出来,以 warnings 返回,
5
- // 渲染与否在消费方,但缺口不静默。
3
+ // 选择器只有一个(latest),长在集合上;它不是 DSL,只是最常用的那次筛选。
4
+ // 选择器必须诚实:残缺、落后、合成键都被算出来,以结构化 warnings 随选集走 ——
5
+ // 渲染与否在消费方(message 是渲染好的英文句子),但缺口不静默。
6
6
 
7
7
  import { experimentKeyOf } from "./format.ts";
8
- import type { AttemptHandle, RunHandle, SnapshotHandle } from "./types.ts";
8
+ import type {
9
+ AttemptHandle,
10
+ DedupeWarning,
11
+ Experiment,
12
+ RunDir,
13
+ Selection,
14
+ SelectionWarning,
15
+ Snapshot,
16
+ } from "./types.ts";
9
17
 
10
- /**
11
- * 每个 experiment 取最新一次快照(按所属 run 的 startedAt,同刻按 run 目录名兜底)。
12
- * `experiments` 是 experiment id 前缀过滤,同 CLI 位置参数语义("compare" 匹配自身与
13
- * "compare/..." 子级;允许写成 "compare/",等价)。
14
- */
15
- export function latestPerExperiment(
16
- snapshots: SnapshotHandle[],
18
+ /** Results.latest() 的实现:每个实验取最新一次快照(= exp.snapshots[0]),生成挑选警告。 */
19
+ export function selectLatest(
20
+ experiments: Experiment[],
17
21
  opts?: { experiments?: string | string[] },
18
- ): { snapshots: SnapshotHandle[]; warnings: string[] } {
19
- const filtered = filterByExperiments(snapshots, opts?.experiments);
22
+ ): Selection {
23
+ const selected = filterByExperimentPrefix(experiments, opts?.experiments);
24
+ const snapshots = selected.map((exp) => exp.latest);
25
+ const warnings: SelectionWarning[] = [];
20
26
 
21
- const byExperiment = new Map<string, SnapshotHandle[]>();
22
- for (const snapshot of filtered) {
23
- const group = byExperiment.get(snapshot.experimentId);
24
- if (group) group.push(snapshot);
25
- else byExperiment.set(snapshot.experimentId, [snapshot]);
27
+ // stale 的基准:选集中最新的落盘(无阈值,如实触发;要阈值消费方按字段自比)
28
+ let latestStartedAt = "";
29
+ for (const snapshot of snapshots) {
30
+ if (snapshot.startedAt > latestStartedAt) latestStartedAt = snapshot.startedAt;
26
31
  }
27
32
 
28
- const picked: SnapshotHandle[] = [];
29
- const warnings: string[] = [];
30
- for (const [experimentId, group] of byExperiment) {
31
- let latest = group[0];
32
- for (const candidate of group) {
33
- if (isNewerRun(candidate.run, latest.run)) latest = candidate;
34
- }
35
- // 残缺检测:与该 experiment 全部历史快照的 evalId 并集对比。
33
+ for (const exp of selected) {
34
+ const snapshot = exp.latest;
35
+ // 残缺检测:分母 = 该实验已知 eval 并集(本地历史 各快照携带的 knownEvalIds)
36
36
  // 位置参数允许只重跑一道题,产出的「最新快照」可能只有一道题 —— 不能安静吞下。
37
- const historyIds = new Set<string>();
38
- for (const snapshot of group) for (const id of snapshot.evalIds) historyIds.add(id);
39
- if (latest.evalIds.length < historyIds.size) {
37
+ const covered = snapshot.evals.length;
38
+ const total = exp.evalIds.length;
39
+ if (covered < total) {
40
40
  // 合成键不是真实 experiment id,拼不出可执行的 `niceeval exp` 命令,提示退化成中性说法。
41
- const hint = latest.synthetic
42
- ? " Re-run the experiment for a full snapshot, or pick another via .filter()."
43
- : ` Re-run \`niceeval exp ${experimentId}\` for a full snapshot, or pick another via .filter().`;
44
- warnings.push(
45
- `warning: snapshot "${experimentId}" @ ${latest.startedAt} covers ${latest.evalIds.length} of ${historyIds.size} evals seen in history.\n` +
46
- hint,
47
- );
41
+ const hint = snapshot.synthetic
42
+ ? "re-run the experiment for a full snapshot"
43
+ : `re-run \`niceeval exp ${exp.id}\` for a full snapshot`;
44
+ warnings.push({
45
+ kind: "partial-coverage",
46
+ experimentId: exp.id,
47
+ covered,
48
+ total,
49
+ message: `snapshot covers ${covered} of ${total} evals seen in history; ${hint}`,
50
+ });
51
+ }
52
+ if (snapshot.startedAt < latestStartedAt) {
53
+ warnings.push({
54
+ kind: "stale-snapshot",
55
+ experimentId: exp.id,
56
+ startedAt: snapshot.startedAt,
57
+ latestStartedAt,
58
+ message: `snapshot "${exp.id}" (${snapshot.startedAt}) predates the latest run in this selection by ${humanizeGap(snapshot.startedAt, latestStartedAt)}`,
59
+ });
60
+ }
61
+ if (snapshot.synthetic) {
62
+ warnings.push({
63
+ kind: "synthetic-experiment-id",
64
+ experimentId: exp.id,
65
+ runDir: snapshot.runDir.dir,
66
+ message: `run "${snapshot.runDir.dir}" has results without experimentId; grouped as "${exp.id}" by agent/model`,
67
+ });
48
68
  }
49
- picked.push(latest);
50
69
  }
70
+ return makeSelection(snapshots, warnings);
71
+ }
51
72
 
52
- picked.sort((a, b) => a.experimentId.localeCompare(b.experimentId));
53
- return { snapshots: picked, warnings };
73
+ /**
74
+ * Selection 构造:filter 只删不换 —— 快照删减,warnings 修剪规则是
75
+ * 「experimentId 不在幸存快照中的丢弃,非实验作用域的保留」(为将来非 per-experiment 的 kind 留位置)。
76
+ */
77
+ export function makeSelection(snapshots: Snapshot[], warnings: SelectionWarning[]): Selection {
78
+ return {
79
+ snapshots,
80
+ warnings,
81
+ filter(predicate: (snapshot: Snapshot) => boolean): Selection {
82
+ const kept = snapshots.filter(predicate);
83
+ const survivors = new Set(kept.map((s) => s.experimentId));
84
+ const keptWarnings = warnings.filter((w) => {
85
+ const scope = (w as { experimentId?: unknown }).experimentId;
86
+ return typeof scope !== "string" || survivors.has(scope);
87
+ });
88
+ return makeSelection(kept, keptWarnings);
89
+ },
90
+ };
54
91
  }
55
92
 
56
93
  /**
57
94
  * 跨快照聚合前的身份键去重:(experimentId, evalId, attempt, startedAt)。
58
95
  * --resume 会把上一轮已通过的结果原样合入新 run 的 summary,同一 attempt 因此存在于多份落盘;
59
- * 重复时保留最新 run 里的那份(位置取首次出现处,顺序稳定)。
60
- * startedAt 缺失时宁可不去重也不误删,并记入 warnings。
96
+ * 重复时保留最新 run 目录里的那份(内容相同,取新 run 的副本让 ref 落在最新落盘上;
97
+ * 位置取首次出现处,顺序稳定)。startedAt 缺失时宁可不去重也不误删,记入 warnings。
61
98
  */
62
- export function dedupeAttempts(attempts: AttemptHandle[]): { attempts: AttemptHandle[]; warnings: string[] } {
99
+ export function dedupeAttempts(attempts: AttemptHandle[]): { attempts: AttemptHandle[]; warnings: DedupeWarning[] } {
63
100
  const deduped: AttemptHandle[] = [];
64
101
  const indexByKey = new Map<string, number>();
65
- const warnings: string[] = [];
102
+ const warnings: DedupeWarning[] = [];
66
103
 
67
104
  for (const attempt of attempts) {
68
105
  const r = attempt.result;
69
106
  if (!r.startedAt) {
70
- warnings.push(
71
- `warning: attempt "${r.id}" (attempt ${r.attempt}) in run "${attempt.run.dir}" has no startedAt; kept as-is without dedupe.`,
72
- );
107
+ warnings.push({
108
+ kind: "missing-startedAt",
109
+ experimentId: attempt.experimentId,
110
+ evalId: attempt.evalId,
111
+ message: `attempt ${r.attempt} of eval "${attempt.evalId}" in experiment "${attempt.experimentId}" has no startedAt; kept as-is without dedupe`,
112
+ });
73
113
  deduped.push(attempt);
74
114
  continue;
75
115
  }
@@ -78,7 +118,7 @@ export function dedupeAttempts(attempts: AttemptHandle[]): { attempts: AttemptHa
78
118
  if (existing === undefined) {
79
119
  indexByKey.set(key, deduped.length);
80
120
  deduped.push(attempt);
81
- } else if (isNewerRun(attempt.run, deduped[existing].run)) {
121
+ } else if (isNewerRunDir(attempt.runDir, deduped[existing].runDir)) {
82
122
  deduped[existing] = attempt;
83
123
  }
84
124
  }
@@ -86,16 +126,28 @@ export function dedupeAttempts(attempts: AttemptHandle[]): { attempts: AttemptHa
86
126
  }
87
127
 
88
128
  /** run 新旧比较:startedAt 优先,同刻按目录名(时间戳目录,字典序即时序)。 */
89
- export function isNewerRun(a: RunHandle, b: RunHandle): boolean {
129
+ export function isNewerRunDir(a: RunDir, b: RunDir): boolean {
90
130
  const byStart = a.summary.startedAt.localeCompare(b.summary.startedAt);
91
131
  if (byStart !== 0) return byStart > 0;
92
132
  return a.dir.localeCompare(b.dir) > 0;
93
133
  }
94
134
 
95
- function filterByExperiments(snapshots: SnapshotHandle[], experiments?: string | string[]): SnapshotHandle[] {
96
- if (experiments === undefined) return snapshots;
97
- const prefixes = (Array.isArray(experiments) ? experiments : [experiments])
98
- // 允许 "compare/" 这种带尾斜杠的写法,与 "compare" 等价。
99
- .map((p) => p.replace(/\/+$/, ""));
100
- return snapshots.filter((s) => prefixes.some((p) => s.experimentId === p || s.experimentId.startsWith(p + "/")));
135
+ function filterByExperimentPrefix(experiments: Experiment[], filter?: string | string[]): Experiment[] {
136
+ if (filter === undefined) return experiments;
137
+ // 允许 "compare/" 这种带尾斜杠的写法,与 "compare" 等价;分段匹配不误配 "compare2"。
138
+ const prefixes = (Array.isArray(filter) ? filter : [filter]).map((p) => p.replace(/\/+$/, ""));
139
+ return experiments.filter((exp) => prefixes.some((p) => exp.id === p || exp.id.startsWith(p + "/")));
140
+ }
141
+
142
+ /** stale 警告的人话时距:选粒度最大的单位,四舍五入。 */
143
+ function humanizeGap(fromIso: string, toIso: string): string {
144
+ const ms = Math.max(0, Date.parse(toIso) - Date.parse(fromIso));
145
+ const seconds = Math.round(ms / 1000);
146
+ if (seconds < 90) return `${seconds} second${seconds === 1 ? "" : "s"}`;
147
+ const minutes = Math.round(seconds / 60);
148
+ if (minutes < 90) return `${minutes} minute${minutes === 1 ? "" : "s"}`;
149
+ const hours = Math.round(minutes / 60);
150
+ if (hours < 36) return `${hours} hour${hours === 1 ? "" : "s"}`;
151
+ const days = Math.round(hours / 24);
152
+ return `${days} day${days === 1 ? "" : "s"}`;
101
153
  }
@@ -1,47 +1,56 @@
1
- // results 域类型:openResults 返回的类型化句柄(读取面契约,见 docs/results-lib.md)。
1
+ // results 域类型:openResults 的分层读取契约与选集(定稿见 docs/results-lib.md)。
2
2
  //
3
3
  // 结果数据类型(EvalResult / RunSummary / StreamEvent / …)仍住在各自的域文件里,
4
- // 这里只 import,不搬家 —— 「类型的家」迁移是 results-lib 设计里单独的一步,不在本次范围。
4
+ // 这里只 import,不搬家 —— 「类型的家」迁移(facade 反向 re-export)是下一波,不在本次范围。
5
+ //
6
+ // 命名约定:Experiment / Snapshot / Eval / RunDir 是纯数据,不带 Handle 后缀;
7
+ // 唯一叫 AttemptHandle 的是 attempt —— 它的方法真的会碰磁盘,后缀标记的就是这件事。
5
8
 
6
- import type { EvalResult, RunSummary } from "../runner/types.ts";
7
- import type { O11ySummary, StreamEvent, TraceSpan } from "../o11y/types.ts";
8
- import type { DiffData } from "../scoring/types.ts";
9
- import type { SourceArtifact } from "../shared/types.ts";
9
+ import type { EvalResult, RunSummary } from "../types.ts";
10
+ import type { O11ySummary, StreamEvent, TraceSpan } from "../types.ts";
11
+ import type { DiffData, SourceArtifact } from "../types.ts";
10
12
 
11
13
  /** attempt 级工件的种类;文件名恒为 `<kind>.json`,布局见 docs/results-format.md。 */
12
- export const ARTIFACT_KINDS = ["events", "sources", "trace", "o11y", "diff"] as const;
14
+ export const ARTIFACT_KINDS = ["events", "trace", "o11y", "diff", "sources"] as const;
13
15
  export type ArtifactKind = (typeof ARTIFACT_KINDS)[number];
14
16
 
15
17
  /**
16
18
  * 回到证据的引用:run 目录名(相对结果根目录)+ summary.results[] 下标。
17
- * Reports MetricCell.refs view 深链 `#/attempt/<run>/<result>` 用的是同一个身份。
19
+ * 字段名(run / result)是 view 深链 `#/attempt/<run>/<result>` 的持久化路由契约,不随句柄改名;
20
+ * Reports 的 MetricCell.refs 用的是同一个身份。
18
21
  */
19
22
  export interface AttemptRef {
20
23
  run: string;
21
24
  result: number;
22
25
  }
23
26
 
24
- /** 一个落盘 run 目录;忠实反映磁盘,不合并、不去重。 */
25
- export interface RunHandle {
27
+ /** 一个物理落盘 run 目录(低层忠实磁盘面);多数消费方走 experiments,不碰它。 */
28
+ export interface RunDir {
26
29
  /** run 目录的绝对路径(summary.json 所在目录)。 */
27
30
  dir: string;
28
31
  /** 与写入侧同一类型的 RunSummary(results[] 是瘦身条目)。 */
29
32
  summary: RunSummary;
30
- /** summary.results[] 逐条包一层懒加载句柄。 */
33
+ /** summary.results[] 逐条包一层懒加载句柄(按下标顺序)。 */
31
34
  attempts: AttemptHandle[];
32
35
  }
33
36
 
34
37
  /**
35
38
  * 单个 eval attempt:瘦身条目 + 重工件的懒加载方法。
36
- * 懒加载即存在性判断:工件缺失返回 null,不抛错;同一 handle 内读过一次即缓存。
39
+ * 懒加载即存在性判断:工件缺失返回 null,不抛错;同一 handle 内读过一次即记忆化。
40
+ * 工件定位按候选顺序回退:先本 run 的 artifactsDir,再 artifactBase 指向的原 run 目录
41
+ * (--resume 合入条目的工件留在原 run 里);原 run 被清理后如实返回 null。
37
42
  */
38
43
  export interface AttemptHandle {
39
- /** 所属物理 run;去重「保留最新 run 的那份」靠它比较新旧。 */
40
- run: RunHandle;
41
- /** attempt 的证据引用(run 目录名 + summary.results 下标);消费方不必自己反查下标。 */
42
- ref: AttemptRef;
44
+ /** 属于哪道题 —— 直达字段,不绕 result。 */
45
+ evalId: string;
46
+ /** 属于哪个实验;落盘缺 experimentId 时是 "<agent>/<model>" 合成键(所属快照 synthetic: true) */
47
+ experimentId: string;
43
48
  /** EvalResult 瘦身条目:判决、断言、用量、成本、experiment 元数据。 */
44
49
  result: EvalResult;
50
+ /** 证据引用,指条目所在的落盘(合入后的新 run);工件经候选回退仍可达。 */
51
+ ref: AttemptRef;
52
+ /** 条目所在的物理落盘;去重「保留最新 run 目录里的那份」靠它比较新旧。 */
53
+ runDir: RunDir;
45
54
  events(): Promise<StreamEvent[] | null>;
46
55
  trace(): Promise<TraceSpan[] | null>;
47
56
  o11y(): Promise<O11ySummary | null>;
@@ -49,50 +58,133 @@ export interface AttemptHandle {
49
58
  sources(): Promise<SourceArtifact[] | null>;
50
59
  }
51
60
 
61
+ /** 一道题在一个快照里的结果:id + 该题的全部 attempt(重试历史)。 */
62
+ export interface Eval {
63
+ id: string;
64
+ attempts: AttemptHandle[];
65
+ }
66
+
52
67
  /**
53
- * 快照 = 一个 experiment 在一个 run 里的那部分 attempt(experiment × run 切片)
54
- * 一个 run 目录可以装多个 experiment(`niceeval exp compare`),所以「每个 experiment
55
- * 最新一次」只能用快照粒度表达,不能用 run 粒度。
68
+ * 快照 = 单次跑的实验(experiment × run 切片)。一个 run 目录可以装多个 experiment
69
+ * (`niceeval exp compare`),所以「每个 experiment 最新一次」只能用快照粒度表达。
56
70
  */
57
- export interface SnapshotHandle {
58
- /** experiment id;结果里缺 experimentId 时以 "<agent>/<model>" 合成,并记入集合级 warnings。 */
71
+ export interface Snapshot {
72
+ /** 结果里缺 experimentId 时以 "<agent>/<model>" 合成键,并经 latest() 记入 warnings。 */
59
73
  experimentId: string;
60
- /** 所属物理 run。 */
61
- run: RunHandle;
62
- /** = run.summary.startedAt;快照新旧按它比较。 */
63
74
  startedAt: string;
64
- /** 本快照自己的 agent —— 不是 run 顶层那个「第一个配置」。 */
75
+ /** 本快照自己的 agent —— 不是落盘顶层那个「第一个配置」。 */
65
76
  agent: string;
66
77
  model?: string;
78
+ /** 谁写的这份结果(niceeval 或第三方 harness;legacy 结果可能缺失)。 */
79
+ producer?: RunSummary["producer"];
80
+ /** 结果格式版本(能读进来的恒为当前版本;不兼容的在 skipped),缺失按 1。 */
81
+ schemaVersion: number;
82
+ /** 每道题一项:{ id, attempts };残缺检测 / 逐题遍历从这里走。 */
83
+ evals: Eval[];
84
+ /** 全部 attempt 平铺(= evals 逐题展开),不关心题目边界的聚合消费用。 */
67
85
  attempts: AttemptHandle[];
68
- /** 覆盖的 eval 集合(去重,保持首次出现顺序),供选择器做残缺检测。 */
69
- evalIds: string[];
70
- /** experimentId 是合成键(结果里没有 experimentId)时为 true;选择器据此调整提示文案。 */
86
+ /** 所属物理落盘(低层面) */
87
+ runDir: RunDir;
88
+ /** experimentId 是合成键(结果里没有 experimentId)时为 true */
71
89
  synthetic?: boolean;
90
+ /** 写入时刻该实验已知的 eval 并集(可选);copySnapshots 自动补记,writer.snapshot() 也可声明。 */
91
+ knownEvalIds?: string[];
92
+ }
93
+
94
+ /** 一个实验的全部历史:同一 experiment id 的历次快照归在一起。 */
95
+ export interface Experiment {
96
+ id: string;
97
+ /** 历次快照,最新在前。 */
98
+ snapshots: Snapshot[];
99
+ /** 最新一次(= snapshots[0])。 */
100
+ latest: Snapshot;
101
+ /** 已知 eval 并集 = 本地历史 ∪ 各快照携带的 knownEvalIds —— 残缺检测的分母。 */
102
+ evalIds: string[];
72
103
  }
73
104
 
74
- /** 目录扫描里读不了、但必须让调用方知道的 run;无关 JSON 不记(静默忽略)。 */
75
- export interface SkippedResultsRun {
105
+ /** 目录扫描里读不了、但必须让调用方知道的落盘;无关 JSON 不记(静默忽略)。 */
106
+ export interface SkippedRun {
76
107
  /** run 目录的绝对路径。 */
77
108
  dir: string;
78
- /** summary.json 的绝对路径。 */
79
- path: string;
80
- reason: "incompatible-version" | "malformed";
81
- /** incompatible-version:那份报告声明的 schemaVersion */
109
+ /**
110
+ * incompatible-version:schemaVersion 与读取器不同(不解析、不迁移、不降级);
111
+ * malformed:summary.json 是坏数据;
112
+ * incomplete:有 attempt 工件、没有 summary.json —— run 中途 crash、writer 没走到 finish()
113
+ * summary 是收尾事实,reader 不读无 summary 的目录;已完成的工件留在盘上供手工排查。
114
+ */
115
+ reason: "incompatible-version" | "malformed" | "incomplete";
116
+ /** 那份结果声明的 schemaVersion(incomplete 没有 summary,自然缺失)。 */
82
117
  schemaVersion?: number;
83
- /** incompatible-version:写报告的 niceeval 版本,拼 `npx niceeval@<version> view` 提示用。 */
84
- producerVersion?: string;
118
+ /** 完整的 producer(name + version):只有 name === "niceeval" 才能拼 npx 提示,第三方如实报名字。 */
119
+ producer?: RunSummary["producer"];
85
120
  /** malformed:一句英文诊断。 */
86
121
  detail?: string;
87
122
  }
88
123
 
89
- /** openResults 的返回:runs 忠实磁盘;snapshots 只切片、不合并、不去重;skipped 不静默丢。 */
90
- export interface ResultsCollection {
91
- /** startedAt 降序(最新在前),同刻按目录名降序。 */
92
- runs: RunHandle[];
93
- /** 按 runs 顺序逐 run 切片;每个 experiment 在每个 run 里至多一个快照。 */
94
- snapshots: SnapshotHandle[];
95
- skipped: SkippedResultsRun[];
96
- /** 集合级警告(如 experimentId 缺失时的合成键);渲染与否在消费方,缺口永远被算出来。 */
97
- warnings: string[];
124
+ /** openResults 的返回:experiments 分层;skipped 不静默丢;runDirs 忠实磁盘(新→旧)。 */
125
+ export interface Results {
126
+ /** 每个实验一项,挂着自己的全部历史(id 字典序) */
127
+ experiments: Experiment[];
128
+ skipped: SkippedRun[];
129
+ /** 低层忠实磁盘面:物理落盘目录,新→旧;多数消费方不碰。 */
130
+ runDirs: RunDir[];
131
+ /**
132
+ * 每个实验取最新一次快照,返回 Selection(快照与挑选警告绑在一起走)。
133
+ * `experiments` 是 experiment id 前缀过滤(string | string[]),同 CLI 位置参数语义。
134
+ */
135
+ latest(opts?: { experiments?: string | string[] }): Selection;
136
+ }
137
+
138
+ /**
139
+ * 选集:选出的快照 + 挑选过程算出的警告。渲染与否在消费方,但缺口永远被算出来。
140
+ * 下游(Reports 计算函数、copySnapshots)收 `Selection | Snapshot[]`;
141
+ * 手工挑的裸数组没有挑选过程,自然没有 warnings 可带,也如实。
142
+ */
143
+ export interface Selection {
144
+ snapshots: Snapshot[];
145
+ warnings: SelectionWarning[];
146
+ /**
147
+ * 只删不换:返回新 Selection,快照删减,warnings 按规则修剪 ——
148
+ * experimentId 不在幸存快照中的丢弃,非实验作用域的保留。
149
+ * 「换成上一个完整快照」这类替换式重挑不给方法,回 exp.snapshots 自己挑。
150
+ */
151
+ filter(predicate: (snapshot: Snapshot) => boolean): Selection;
152
+ }
153
+
154
+ /**
155
+ * 挑选警告:每种带 kind、可判断的结构化字段和渲染好的英文 message。
156
+ * kind 是契约的一部分;全集与触发条件见 docs/results-lib.md「警告 kind 全集」。
157
+ */
158
+ export type SelectionWarning =
159
+ | {
160
+ /** 选中快照的覆盖 < 该实验已知 eval 并集(本地历史 ∪ knownEvalIds)。 */
161
+ kind: "partial-coverage";
162
+ experimentId: string;
163
+ covered: number;
164
+ total: number;
165
+ message: string;
166
+ }
167
+ | {
168
+ /** 该实验选中的快照早于选集中最新的落盘;无阈值,如实触发,要阈值消费方按字段自比。 */
169
+ kind: "stale-snapshot";
170
+ experimentId: string;
171
+ startedAt: string;
172
+ latestStartedAt: string;
173
+ message: string;
174
+ }
175
+ | {
176
+ /** 落盘缺 experimentId,以 "<agent>/<model>" 合成键(快照的 synthetic: true 同源)。 */
177
+ kind: "synthetic-experiment-id";
178
+ experimentId: string;
179
+ /** 该快照所在 run 目录的绝对路径。 */
180
+ runDir: string;
181
+ message: string;
182
+ };
183
+
184
+ /** dedupeAttempts 的警告:身份键缺 startedAt,宁可不去重也不误删。 */
185
+ export interface DedupeWarning {
186
+ kind: "missing-startedAt";
187
+ experimentId: string;
188
+ evalId: string;
189
+ message: string;
98
190
  }