niceeval 0.5.4 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (337) hide show
  1. package/INDEX.md +55 -0
  2. package/dist/agents/types.d.ts +321 -0
  3. package/dist/context/types.d.ts +335 -0
  4. package/dist/i18n/core.d.ts +11 -0
  5. package/dist/i18n/en.d.ts +181 -0
  6. package/dist/i18n/index.d.ts +5 -0
  7. package/dist/i18n/zh-CN.d.ts +183 -0
  8. package/dist/o11y/otlp/parse.d.ts +3 -0
  9. package/dist/o11y/otlp/receiver.d.ts +21 -0
  10. package/dist/o11y/otlp/turn-otel.d.ts +43 -0
  11. package/dist/o11y/types.d.ts +190 -0
  12. package/dist/report/aggregate.d.ts +57 -0
  13. package/dist/report/aggregate.js +212 -0
  14. package/dist/report/built-ins/experiment-comparison.d.ts +1 -0
  15. package/dist/report/built-ins/experiment-comparison.js +13 -0
  16. package/dist/report/built-ins/index.d.ts +1 -0
  17. package/dist/report/built-ins/index.js +2 -0
  18. package/dist/report/components.d.ts +180 -0
  19. package/dist/report/components.js +132 -0
  20. package/dist/report/compute.d.ts +106 -0
  21. package/dist/report/compute.js +600 -0
  22. package/dist/report/flag.d.ts +6 -0
  23. package/dist/report/flag.js +8 -0
  24. package/dist/report/format.d.ts +26 -0
  25. package/dist/report/format.js +109 -0
  26. package/dist/report/index.d.ts +19 -0
  27. package/dist/report/index.js +30 -0
  28. package/dist/report/load.d.ts +12 -0
  29. package/dist/report/load.js +56 -0
  30. package/dist/report/locale.d.ts +94 -0
  31. package/dist/report/locale.js +183 -0
  32. package/dist/report/metrics.d.ts +21 -0
  33. package/dist/report/metrics.js +115 -0
  34. package/dist/report/primitives.d.ts +64 -0
  35. package/dist/report/primitives.js +113 -0
  36. package/dist/report/react/AttemptList.d.ts +23 -0
  37. package/dist/report/react/AttemptList.js +19 -0
  38. package/dist/report/react/DeltaTable.d.ts +8 -0
  39. package/dist/report/react/DeltaTable.js +24 -0
  40. package/dist/report/react/EvalList.d.ts +10 -0
  41. package/dist/report/react/EvalList.js +0 -0
  42. package/dist/report/react/ExperimentList.d.ts +11 -0
  43. package/dist/report/react/ExperimentList.js +67 -0
  44. package/dist/report/react/GroupSummary.d.ts +8 -0
  45. package/dist/report/react/GroupSummary.js +8 -0
  46. package/dist/report/react/MetricBars.d.ts +10 -0
  47. package/dist/report/react/MetricBars.js +48 -0
  48. package/dist/report/react/MetricLine.d.ts +9 -0
  49. package/dist/report/react/MetricLine.js +68 -0
  50. package/dist/report/react/MetricMatrix.d.ts +10 -0
  51. package/dist/report/react/MetricMatrix.js +23 -0
  52. package/dist/report/react/MetricScatter.d.ts +9 -0
  53. package/dist/report/react/MetricScatter.js +96 -0
  54. package/dist/report/react/MetricTable.d.ts +11 -0
  55. package/dist/report/react/MetricTable.js +26 -0
  56. package/dist/report/react/RunOverview.d.ts +8 -0
  57. package/dist/report/react/RunOverview.js +12 -0
  58. package/dist/report/react/Scoreboard.d.ts +8 -0
  59. package/dist/report/react/Scoreboard.js +26 -0
  60. package/dist/report/react/cell.d.ts +9 -0
  61. package/dist/report/react/cell.js +9 -0
  62. package/dist/report/react/chart-math.d.ts +14 -0
  63. package/dist/report/react/chart-math.js +68 -0
  64. package/dist/report/react/colors.d.ts +14 -0
  65. package/dist/report/react/colors.js +45 -0
  66. package/dist/report/react/fixtures.d.ts +19 -0
  67. package/dist/report/react/fixtures.js +398 -0
  68. package/dist/report/react/format.d.ts +3 -0
  69. package/dist/report/react/format.js +7 -0
  70. package/dist/report/react/index.d.ts +6 -0
  71. package/dist/report/react/index.js +14 -0
  72. package/dist/report/report.d.ts +24 -0
  73. package/dist/report/report.js +49 -0
  74. package/dist/report/text/faces.d.ts +29 -0
  75. package/dist/report/text/faces.js +541 -0
  76. package/dist/report/text/layout.d.ts +75 -0
  77. package/dist/report/text/layout.js +173 -0
  78. package/dist/report/text/plot.d.ts +37 -0
  79. package/dist/report/text/plot.js +81 -0
  80. package/dist/report/text/table.d.ts +8 -0
  81. package/dist/report/text/table.js +86 -0
  82. package/dist/report/tree.d.ts +99 -0
  83. package/dist/report/tree.js +228 -0
  84. package/dist/report/types.d.ts +408 -0
  85. package/dist/report/types.js +4 -0
  86. package/dist/report/web.d.ts +15 -0
  87. package/dist/report/web.js +37 -0
  88. package/dist/results/locator.d.ts +75 -0
  89. package/dist/results/locator.js +0 -0
  90. package/dist/results/select.d.ts +45 -0
  91. package/dist/results/select.js +230 -0
  92. package/dist/results/source-hash.d.ts +9 -0
  93. package/dist/results/types.d.ts +204 -0
  94. package/dist/runner/eval-source.d.ts +22 -0
  95. package/dist/runner/fingerprint.d.ts +23 -0
  96. package/dist/runner/types.d.ts +332 -0
  97. package/dist/sandbox/docker-stream.d.ts +24 -0
  98. package/dist/sandbox/docker.d.ts +84 -0
  99. package/dist/sandbox/e2b.d.ts +31 -0
  100. package/dist/sandbox/errors.d.ts +4 -0
  101. package/dist/sandbox/local-files.d.ts +6 -0
  102. package/dist/sandbox/paths.d.ts +4 -0
  103. package/dist/sandbox/registry.d.ts +13 -0
  104. package/dist/sandbox/resolve.d.ts +40 -0
  105. package/dist/sandbox/retry.d.ts +10 -0
  106. package/dist/sandbox/shell.d.ts +11 -0
  107. package/dist/sandbox/source-files.d.ts +13 -0
  108. package/dist/sandbox/types.d.ts +203 -0
  109. package/dist/sandbox/vercel.d.ts +37 -0
  110. package/dist/scoring/types.d.ts +70 -0
  111. package/dist/shared/aggregate.d.ts +29 -0
  112. package/dist/shared/aggregate.js +55 -0
  113. package/dist/shared/types.d.ts +27 -0
  114. package/dist/shared/verdict.d.ts +27 -0
  115. package/dist/shared/verdict.js +34 -0
  116. package/dist/tty-line.d.ts +6 -0
  117. package/dist/types.d.ts +7 -0
  118. package/dist/util.d.ts +22 -0
  119. package/docs-site/zh/concepts/adapter.mdx +7 -7
  120. package/docs-site/zh/concepts/assert.mdx +19 -18
  121. package/docs-site/zh/concepts/evals.mdx +12 -11
  122. package/docs-site/zh/concepts/experiment.mdx +2 -2
  123. package/docs-site/zh/concepts/judge.mdx +9 -9
  124. package/docs-site/zh/concepts/overview.mdx +3 -3
  125. package/docs-site/zh/concepts/tier.mdx +6 -6
  126. package/docs-site/zh/example/claude-code-codex-plugin.mdx +9 -5
  127. package/docs-site/zh/example/claude-code-codex-skill.mdx +12 -7
  128. package/docs-site/zh/guides/agent-feedback-loop.mdx +237 -78
  129. package/docs-site/zh/guides/connect-otel.mdx +3 -3
  130. package/docs-site/zh/guides/connect-your-agent.mdx +2 -2
  131. package/docs-site/zh/guides/custom-reports.mdx +196 -36
  132. package/docs-site/zh/guides/experiments.mdx +7 -1
  133. package/docs-site/zh/guides/fixtures.mdx +4 -2
  134. package/docs-site/zh/guides/official-adapters.mdx +31 -9
  135. package/docs-site/zh/guides/publish-report.mdx +4 -4
  136. package/docs-site/zh/guides/report-components.mdx +194 -55
  137. package/docs-site/zh/guides/reporters.mdx +2 -2
  138. package/docs-site/zh/guides/results-data.mdx +61 -34
  139. package/docs-site/zh/guides/runner.mdx +2 -2
  140. package/docs-site/zh/guides/sandbox-agent.mdx +3 -2
  141. package/docs-site/zh/guides/{sandbox-backends.mdx → sandbox-providers.mdx} +12 -12
  142. package/docs-site/zh/guides/viewing-results.mdx +200 -79
  143. package/docs-site/zh/guides/write-experiment.mdx +9 -1
  144. package/docs-site/zh/guides/write-send.mdx +4 -3
  145. package/docs-site/zh/index.mdx +4 -3
  146. package/docs-site/zh/introduction.mdx +2 -1
  147. package/docs-site/zh/quickstart.mdx +5 -3
  148. package/docs-site/zh/reference/builtin-agents.mdx +38 -13
  149. package/docs-site/zh/reference/capabilities.mdx +1 -1
  150. package/docs-site/zh/reference/cli.mdx +36 -13
  151. package/docs-site/zh/reference/define-agent.mdx +15 -13
  152. package/docs-site/zh/reference/define-config.mdx +1 -1
  153. package/docs-site/zh/reference/define-eval.mdx +3 -3
  154. package/docs-site/zh/reference/events.mdx +9 -3
  155. package/docs-site/zh/reference/expect.mdx +13 -8
  156. package/package.json +10 -6
  157. package/src/agents/bub.ts +129 -42
  158. package/src/agents/claude-code.test.ts +152 -0
  159. package/src/agents/claude-code.ts +127 -13
  160. package/src/agents/codex.test.ts +192 -0
  161. package/src/agents/codex.ts +151 -12
  162. package/src/agents/index.ts +9 -3
  163. package/src/agents/manifest.ts +41 -0
  164. package/src/agents/plugin-config.test.ts +94 -0
  165. package/src/agents/shared.ts +33 -2
  166. package/src/agents/skills.test.ts +214 -0
  167. package/src/agents/skills.ts +304 -0
  168. package/src/agents/streaming.ts +5 -5
  169. package/src/agents/types.ts +59 -3
  170. package/src/cli.ts +74 -47
  171. package/src/context/context.test.ts +20 -2
  172. package/src/context/control-flow.ts +1 -1
  173. package/src/context/session.test.ts +92 -1
  174. package/src/context/session.ts +23 -1
  175. package/src/context/types.ts +4 -4
  176. package/src/define.test.ts +8 -8
  177. package/src/define.ts +11 -11
  178. package/src/expect/index.ts +5 -5
  179. package/src/i18n/en.ts +33 -15
  180. package/src/i18n/zh-CN.ts +37 -21
  181. package/src/index.ts +1 -1
  182. package/src/o11y/execution-tree.test.ts +452 -0
  183. package/src/o11y/execution-tree.ts +367 -0
  184. package/src/o11y/otlp/receiver.ts +1 -1
  185. package/src/o11y/otlp/select.ts +7 -3
  186. package/src/o11y/parsers/claude-code.test.ts +175 -0
  187. package/src/o11y/parsers/claude-code.ts +32 -0
  188. package/src/o11y/types.ts +6 -1
  189. package/src/report/aggregate.ts +26 -20
  190. package/src/report/built-in-user-parity.test.tsx +640 -0
  191. package/src/report/built-ins/experiment-comparison.tsx +19 -0
  192. package/src/report/built-ins/index.ts +2 -0
  193. package/src/report/components.tsx +225 -50
  194. package/src/report/compute.ts +335 -54
  195. package/src/report/dual-render.test.tsx +1129 -0
  196. package/src/report/flag.ts +1 -1
  197. package/src/report/format.ts +35 -2
  198. package/src/report/index.ts +47 -14
  199. package/src/report/load.ts +1 -1
  200. package/src/report/locale.ts +229 -0
  201. package/src/report/metrics.ts +39 -15
  202. package/src/report/primitives.tsx +100 -2
  203. package/src/report/react/AttemptList.tsx +113 -0
  204. package/src/report/react/DeltaTable.tsx +9 -6
  205. package/src/report/react/EvalList.tsx +0 -0
  206. package/src/report/react/ExperimentList.tsx +220 -0
  207. package/src/report/react/GroupSummary.tsx +66 -0
  208. package/src/report/react/MetricBars.tsx +24 -13
  209. package/src/report/react/MetricLine.tsx +21 -21
  210. package/src/report/react/MetricMatrix.tsx +8 -4
  211. package/src/report/react/MetricScatter.tsx +126 -96
  212. package/src/report/react/MetricTable.tsx +115 -15
  213. package/src/report/react/RunOverview.tsx +48 -26
  214. package/src/report/react/Scoreboard.tsx +15 -7
  215. package/src/report/react/cell.tsx +18 -9
  216. package/src/{view/app/lib/chart.ts → report/react/chart-math.ts} +2 -6
  217. package/src/report/react/colors.ts +24 -15
  218. package/src/report/react/enhance.js +168 -0
  219. package/src/report/react/fixtures.ts +169 -34
  220. package/src/report/react/format.ts +1 -1
  221. package/src/report/react/index.tsx +21 -6
  222. package/src/report/react/render.test.tsx +180 -31
  223. package/src/report/react/styles.css +432 -213
  224. package/src/report/report.test.ts +398 -94
  225. package/src/report/report.ts +29 -11
  226. package/src/report/text/faces.ts +344 -96
  227. package/src/report/text/layout.ts +76 -11
  228. package/src/report/text/table.ts +98 -0
  229. package/src/report/tree.ts +103 -20
  230. package/src/report/types.ts +184 -25
  231. package/src/report/web.ts +46 -19
  232. package/src/results/annotated-source.test.ts +146 -0
  233. package/src/results/annotated-source.ts +128 -0
  234. package/src/results/attempt-evidence.test.ts +158 -0
  235. package/src/results/attempt-evidence.ts +158 -0
  236. package/src/results/attempt-source.ts +48 -0
  237. package/src/results/copy.ts +154 -139
  238. package/src/results/format.ts +54 -47
  239. package/src/results/host-equivalence.test.ts +527 -0
  240. package/src/results/index.ts +56 -14
  241. package/src/results/locator.test.ts +198 -0
  242. package/src/results/locator.ts +0 -0
  243. package/src/results/open.ts +392 -175
  244. package/src/results/results.test.ts +943 -494
  245. package/src/results/select.ts +129 -23
  246. package/src/results/skipped-notice.ts +0 -0
  247. package/src/results/source-hash.ts +28 -0
  248. package/src/results/types.ts +98 -62
  249. package/src/results/writer.ts +304 -176
  250. package/src/runner/attempt.test.ts +170 -0
  251. package/src/runner/attempt.ts +48 -13
  252. package/src/runner/discover.test.ts +65 -0
  253. package/src/runner/discover.ts +6 -2
  254. package/src/runner/eval-source.test.ts +121 -0
  255. package/src/runner/eval-source.ts +45 -0
  256. package/src/runner/fingerprint.ts +54 -1
  257. package/src/runner/remote-sandbox.ts +1 -1
  258. package/src/runner/report.test.ts +6 -8
  259. package/src/runner/report.ts +5 -4
  260. package/src/runner/reporters/artifacts.ts +27 -41
  261. package/src/runner/reporters/braintrust.test.ts +3 -3
  262. package/src/runner/reporters/braintrust.ts +1 -1
  263. package/src/runner/reporters/console.ts +6 -6
  264. package/src/runner/reporters/json.ts +4 -4
  265. package/src/runner/reporters/live.test.ts +56 -0
  266. package/src/runner/reporters/live.ts +36 -10
  267. package/src/runner/reporters/quiet.test.ts +66 -0
  268. package/src/runner/reporters/quiet.ts +49 -0
  269. package/src/runner/reporters/shared.ts +5 -5
  270. package/src/runner/reporters/table.ts +17 -17
  271. package/src/runner/run.ts +61 -102
  272. package/src/runner/types.ts +43 -32
  273. package/src/sandbox/checkpoint.ts +1 -1
  274. package/src/sandbox/docker.ts +15 -2
  275. package/src/sandbox/e2b.ts +11 -5
  276. package/src/sandbox/errors.ts +13 -0
  277. package/src/sandbox/index.ts +3 -3
  278. package/src/sandbox/local-files.ts +1 -1
  279. package/src/sandbox/registry.ts +6 -5
  280. package/src/sandbox/resolve.ts +38 -23
  281. package/src/sandbox/retry.test.ts +68 -0
  282. package/src/sandbox/retry.ts +44 -0
  283. package/src/sandbox/types.ts +24 -24
  284. package/src/sandbox/vercel.ts +17 -3
  285. package/src/scoring/collector.ts +3 -3
  286. package/src/scoring/judge.ts +2 -2
  287. package/src/scoring/scoped.ts +13 -1
  288. package/src/scoring/types.ts +7 -7
  289. package/src/scoring/verdict.ts +4 -4
  290. package/src/shared/aggregate.ts +23 -3
  291. package/src/shared/types.ts +2 -2
  292. package/src/shared/{outcome.ts → verdict.ts} +15 -15
  293. package/src/show/compose.ts +12 -132
  294. package/src/show/index.ts +119 -95
  295. package/src/show/render.ts +462 -159
  296. package/src/show/show.test.ts +471 -253
  297. package/src/source-loc.ts +1 -1
  298. package/src/tty-line.ts +32 -0
  299. package/src/types.ts +2 -2
  300. package/src/util.test.ts +21 -1
  301. package/src/util.ts +5 -1
  302. package/src/view/app/App.tsx +33 -198
  303. package/src/view/app/components/AttemptModal.tsx +6 -6
  304. package/src/view/app/components/CodeView.tsx +2 -2
  305. package/src/view/app/components/CopyControls.tsx +32 -36
  306. package/src/view/app/components/SkippedRunsBanner.tsx +140 -0
  307. package/src/view/app/components/ui/badge.tsx +1 -1
  308. package/src/view/app/components/ui/dialog.tsx +2 -2
  309. package/src/view/app/i18n.ts +40 -31
  310. package/src/view/app/lib/artifact-url.ts +1 -1
  311. package/src/view/app/lib/attempt-route.test.ts +37 -47
  312. package/src/view/app/lib/attempt-route.ts +26 -26
  313. package/src/view/app/lib/rows.ts +9 -112
  314. package/src/view/app/lib/verdict.ts +25 -0
  315. package/src/view/app/main.tsx +9 -12
  316. package/src/view/app/pages/RunsPage.tsx +4 -4
  317. package/src/view/app/pages/TracesPage.tsx +3 -3
  318. package/src/view/app/shared.ts +0 -1
  319. package/src/view/app/types.ts +6 -51
  320. package/src/view/artifact-serving.test.ts +136 -0
  321. package/src/view/client-dist/app.css +1 -1
  322. package/src/view/client-dist/app.js +21 -21
  323. package/src/view/data.test.ts +214 -143
  324. package/src/view/data.ts +166 -120
  325. package/src/view/index.ts +23 -11
  326. package/src/view/server.ts +52 -17
  327. package/src/view/shared/types.ts +22 -31
  328. package/src/view/styles.css +117 -327
  329. package/src/view/view-report.test.ts +146 -98
  330. package/src/report/default-report.tsx +0 -222
  331. package/src/report/dual-face.test.tsx +0 -527
  332. package/src/report/react/CaseList.tsx +0 -70
  333. package/src/view/app/components/CostScoreChart.tsx +0 -147
  334. package/src/view/app/components/ExperimentTable.tsx +0 -284
  335. package/src/view/app/components/GroupSelector.tsx +0 -61
  336. package/src/view/app/components/primitives.tsx +0 -43
  337. package/src/view/app/lib/outcome.ts +0 -82
@@ -1,40 +1,39 @@
1
- // copySnapshots:把选中快照按格式感知地复制到另一个目录(定稿见 docs/results-lib.md「复制与瘦身」)。
1
+ // copySnapshots:把选中快照按格式感知地复制到另一个目录(定稿见 docs/feature/results/library.md「复制与瘦身」)。
2
2
  //
3
- // 发布场景的原语:只带指定工件、只带选中的 attempt,布局知识不外泄。
4
- // 工件复制忠实于源(copyFile 原字节,不重新序列化、不消毒);summary.json 按选中条目重建,
5
- // 版本元数据保留,榜单计数按选中条目重算 —— 产物是一个标准 run 目录,openResults /
3
+ // 发布场景的原语:只带指定 artifact、只带选中快照的全部 attempt,布局知识不外泄。
4
+ // artifact 复制忠实于源(copyFile 原字节,不重新序列化、不消毒);snapshot.json / result.json
5
+ // 按选中条目重建,版本元数据保留。产物是一个标准结果根目录(同布局),openResults /
6
6
  // `niceeval view` 直接能读。唯一随行补记的是挑选时的覆盖事实:每个复制出的快照带上
7
7
  // knownEvalIds(复制时刻该实验已知的 eval 并集),发布目录上重新 openResults().latest(),
8
8
  // 残缺警告被同一套机制重新算出来,不靠发布者转述。
9
9
 
10
10
  import { copyFile, mkdir, readdir, stat, writeFile } from "node:fs/promises";
11
- import { dirname, join, resolve } from "node:path";
12
- import type { EvalResult, RunSummary, Usage } from "../types.ts";
13
- import { RESULTS_FORMAT, RESULTS_SCHEMA_VERSION } from "../types.ts";
14
- import { artifactFileOf, attemptDirOf } from "./format.ts";
11
+ import { basename, dirname, join, resolve } from "node:path";
12
+ import type { EvalResult } from "../types.ts";
13
+ import { RESULTS_FORMAT } from "../types.ts";
14
+ import { RESULT_FILE, SNAPSHOT_FILE, artifactFileOf, experimentDirOf } from "./format.ts";
15
15
  import { experimentOfSnapshot } from "./open.ts";
16
- import { isNewerRunDir } from "./select.ts";
17
- import type { ArtifactKind, AttemptHandle, RunDir, Selection, Snapshot } from "./types.ts";
16
+ import { isNewerSnapshot } from "./select.ts";
17
+ import { hashEvalSource, normalizeEvalSource } from "./source-hash.ts";
18
+ import type { ArtifactKind, AttemptHandle, Selection, Snapshot, SnapshotMeta } from "./types.ts";
18
19
  import { ARTIFACT_KINDS } from "./types.ts";
19
20
 
20
21
  export interface CopySnapshotsOptions {
21
- /** 要带上的工件种类;省略 = 全部五类。diff 可达百 MBo11y 查看器不读,发布时常见地不带。 */
22
+ /** 要带上的 artifact 种类;省略 = 全部六类。diff 可达百 MB,发布时常见地不带;o11y 只有几 KB,报告用 turns 这类 artifact 档指标(见 docs/feature/reports/library.md「内置指标」)时记得带上。 */
22
23
  artifacts?: ArtifactKind[];
23
24
  }
24
25
 
25
26
  export interface CopySnapshotsResult {
26
- /** 目标 run 目录的绝对路径(summary.json 所在目录)。 */
27
+ /** 目标结果根目录的绝对路径。 */
27
28
  dir: string;
28
- /** 重建后的 summary(与写入磁盘的一致) */
29
- summary: RunSummary;
30
- /** 复制过程中的警告(如多个 attempt 落到同一目录);不静默。 */
29
+ /** 复制过程中的警告(如同一实验选中多个快照);不静默。 */
31
30
  warnings: string[];
32
31
  }
33
32
 
34
33
  /**
35
- * 把选中快照的全部 attempt 复制成 `destDir` 下的一个标准 run 目录。
36
- * 输入收 Selection 或手工挑的 Snapshot[](与 Reports 计算函数同一输入约定);
37
- * 快照可以来自不同物理 run(latest() 的典型输出)。目标目录非空即报错,不静默覆盖、不合并。
34
+ * 把选中快照复制成 `destDir` 下的一个标准结果根目录(`<experiment-dir>/<源快照目录名>/`,
35
+ * 快照目录名原样保留,身份不变)。输入收 Selection 或手工挑的 Snapshot[]
36
+ * ( Reports 计算函数同一输入约定)。目标目录非空即报错,不静默覆盖、不合并。
38
37
  */
39
38
  export async function copySnapshots(
40
39
  selection: Selection | Snapshot[],
@@ -58,45 +57,112 @@ export async function copySnapshots(
58
57
  const dest = resolve(destDir);
59
58
  await assertEmptyDestination(dest);
60
59
 
61
- // 第一趟:按目标工件目录选出胜者(最新 run 的那份),顺序取首次出现处 —— 两趟避免
62
- // 「旧的先落盘、新的再去清理覆盖」的中间态。
63
- const winners = new Map<string, AttemptHandle>();
64
- const order: string[] = [];
60
+ // 同一 experiment 选中多个快照 只带最新的那个,记 warning(无胜者逻辑:一旦落到单快照,
61
+ // 快照内 evalId+attempt 天然唯一)。
62
+ const byExperiment = new Map<string, Snapshot>();
65
63
  const warnings: string[] = [];
66
64
  for (const snapshot of selected) {
67
- for (const attempt of snapshot.attempts) {
68
- const relDir = attemptDirOf(attempt.result);
69
- const existing = winners.get(relDir);
70
- if (!existing) {
71
- winners.set(relDir, attempt);
72
- order.push(relDir);
73
- continue;
74
- }
75
- warnings.push(
76
- `warning: multiple attempts map to "${relDir}" in the copied run; kept the one from the newest run. Dedupe attempts or re-select snapshots before copySnapshots to avoid this.`,
77
- );
78
- if (isNewerRunDir(attempt.runDir, existing.runDir)) winners.set(relDir, attempt);
65
+ const existing = byExperiment.get(snapshot.experimentId);
66
+ if (!existing) {
67
+ byExperiment.set(snapshot.experimentId, snapshot);
68
+ continue;
79
69
  }
70
+ warnings.push(
71
+ `warning: multiple snapshots selected for experiment "${snapshot.experimentId}"; kept the newest one, dropped the rest. Dedupe with Selection.filter() or pick a single snapshot per experiment before copySnapshots to avoid this.`,
72
+ );
73
+ if (isNewerSnapshot(snapshot, existing)) byExperiment.set(snapshot.experimentId, snapshot);
80
74
  }
81
75
 
82
- // 第二趟:复制工件 + 生成瘦身条目(has* 按目标目录里真的有什么重算)。
83
76
  await mkdir(dest, { recursive: true });
84
- const results: EvalResult[] = [];
85
- for (const relDir of order) {
86
- const attempt = winners.get(relDir)!;
87
- const files = await findArtifactFiles(attempt, kinds);
88
- if (files.length > 0) {
89
- const attemptDest = join(dest, relDir);
90
- await mkdir(attemptDest, { recursive: true });
91
- await Promise.all(files.map(({ kind, source }) => copyFile(source, join(attemptDest, artifactFileOf(kind)))));
92
- }
93
- const copied = new Set(files.map((f) => f.kind));
94
- results.push(slimForCopy(attempt.result, relDir, copied));
77
+ for (const snapshot of byExperiment.values()) {
78
+ await copyOneSnapshot(snapshot, selected, dest, kinds);
79
+ }
80
+
81
+ return { dir: dest, warnings };
82
+ }
83
+
84
+ async function copyOneSnapshot(snapshot: Snapshot, selected: Snapshot[], destRoot: string, kinds: ArtifactKind[]): Promise<void> {
85
+ const destSnapDir = join(destRoot, experimentDirOf(snapshot.experimentId), basename(snapshot.dir));
86
+ await mkdir(destSnapDir, { recursive: true });
87
+
88
+ // sources 的去重仓库(sources/<sha256>.json)是快照级的:同一份源码被多个 attempt 引用时,
89
+ // 复制到目的地也只应该有一份——这个 Set 记录本快照已经落过盘的 hash,整快照的 attempt 共享。
90
+ const copiedSourceHashes = new Set<string>();
91
+ for (const attempt of snapshot.attempts) {
92
+ await copyOneAttempt(attempt, destSnapDir, kinds, copiedSourceHashes);
95
93
  }
96
94
 
97
- const summary = rebuildSummary(selected, results, dest);
98
- await writeFile(join(dest, "summary.json"), JSON.stringify(summary, null, 2), "utf-8");
99
- return { dir: dest, summary, warnings };
95
+ const knownEvalIds = experimentOfSnapshot(snapshot)?.evalIds ?? fallbackUnion(selected, snapshot.experimentId);
96
+ const meta: SnapshotMeta = {
97
+ format: RESULTS_FORMAT,
98
+ schemaVersion: snapshot.schemaVersion,
99
+ producer: snapshot.producer,
100
+ experimentId: snapshot.experimentId,
101
+ ...(snapshot.experiment !== undefined ? { experiment: snapshot.experiment } : {}),
102
+ agent: snapshot.agent,
103
+ ...(snapshot.model !== undefined ? { model: snapshot.model } : {}),
104
+ startedAt: snapshot.startedAt,
105
+ ...(snapshot.completedAt !== undefined ? { completedAt: snapshot.completedAt } : {}),
106
+ ...(knownEvalIds.length ? { knownEvalIds } : {}),
107
+ ...(snapshot.name !== undefined ? { name: snapshot.name } : {}),
108
+ };
109
+ await writeFile(join(destSnapDir, SNAPSHOT_FILE), JSON.stringify(meta, null, 2), "utf-8");
110
+ }
111
+
112
+ async function copyOneAttempt(
113
+ attempt: AttemptHandle,
114
+ destSnapDir: string,
115
+ kinds: ArtifactKind[],
116
+ copiedSourceHashes: Set<string>,
117
+ ): Promise<void> {
118
+ const destAttemptDir = join(destSnapDir, attempt.ref.attempt);
119
+ await mkdir(destAttemptDir, { recursive: true });
120
+
121
+ // sources 是唯一「两层」的 artifact(attempt 级引用 + 快照级去重仓库),不能像其它四类那样
122
+ // 单文件 copyFile 原字节完事——原字节只是引用,不带内容。走读取面已经会解引用+回退的
123
+ // attempt.sources() 拿到完整内容,再在目的地按内容哈希重新去重落盘,天然吸收 artifactBase
124
+ // 回退链(携带条目复制后,原快照可能不在目的地里,必须此刻把内容落到自己脚下)。
125
+ const genericKinds = kinds.filter((k) => k !== "sources");
126
+ const files = await findArtifactFiles(attempt, genericKinds);
127
+ await Promise.all(files.map(({ kind, source }) => copyFile(source, join(destAttemptDir, artifactFileOf(kind)))));
128
+ const copied = new Set(files.map((f) => f.kind));
129
+
130
+ if (kinds.includes("sources")) {
131
+ const wroteSources = await copySources(attempt, destSnapDir, destAttemptDir, copiedSourceHashes);
132
+ if (wroteSources) copied.add("sources");
133
+ }
134
+
135
+ const record = slimForCopy(attempt.result, copied);
136
+ await writeFile(join(destAttemptDir, RESULT_FILE), JSON.stringify(record, null, 2), "utf-8");
137
+ }
138
+
139
+ /**
140
+ * sources 的复制:经 attempt.sources() 拿到已解引用的完整内容(null / 空数组 = 没有,不写任何
141
+ * 文件,与其它 artifact「空数据不落文件」的约定一致),按内容 sha256 写进目的快照的
142
+ * `sources/<sha256>.json`(同一快照内已经写过的 hash 不重写),attempt 目录下落一份引用。
143
+ */
144
+ async function copySources(
145
+ attempt: AttemptHandle,
146
+ destSnapDir: string,
147
+ destAttemptDir: string,
148
+ copiedSourceHashes: Set<string>,
149
+ ): Promise<boolean> {
150
+ const sources = await attempt.sources();
151
+ if (!sources || sources.length === 0) return false;
152
+
153
+ const destStoreDir = join(destSnapDir, "sources");
154
+ const refs: { path: string; sha256: string }[] = [];
155
+ for (const src of sources) {
156
+ const sha256 = hashEvalSource(normalizeEvalSource(src.content));
157
+ refs.push({ path: src.path, sha256 });
158
+ if (!copiedSourceHashes.has(sha256)) {
159
+ await mkdir(destStoreDir, { recursive: true });
160
+ await writeFile(join(destStoreDir, `${sha256}.json`), JSON.stringify({ content: src.content }), "utf-8");
161
+ copiedSourceHashes.add(sha256);
162
+ }
163
+ }
164
+ await writeFile(join(destAttemptDir, artifactFileOf("sources")), JSON.stringify(refs), "utf-8");
165
+ return true;
100
166
  }
101
167
 
102
168
  /** 目标目录非空即报错:盘上不该出现「我没写的东西被动过」的惊讶;发布脚本要幂等就自己先清目录。 */
@@ -117,14 +183,15 @@ async function assertEmptyDestination(dest: string): Promise<void> {
117
183
  }
118
184
  }
119
185
 
120
- /** 源工件定位:与读取面同一候选顺序(本 run 的 artifactsDir 优先,artifactBase 回退)。 */
186
+ /** 源 artifact 定位:与读取面同一候选顺序(本 attempt 目录优先,artifactBase 回退)。 */
121
187
  async function findArtifactFiles(
122
188
  attempt: AttemptHandle,
123
189
  kinds: ArtifactKind[],
124
190
  ): Promise<{ kind: ArtifactKind; source: string }[]> {
125
- const candidates: string[] = [];
126
- if (attempt.result.artifactsDir) candidates.push(join(attempt.runDir.dir, attempt.result.artifactsDir));
127
- if (attempt.result.artifactBase) candidates.push(join(dirname(attempt.runDir.dir), attempt.result.artifactBase));
191
+ const candidates: string[] = [join(attempt.snapshot.dir, attempt.ref.attempt)];
192
+ if (attempt.result.artifactBase) {
193
+ candidates.push(resolve(dirname(dirname(attempt.snapshot.dir)), attempt.result.artifactBase));
194
+ }
128
195
 
129
196
  const found: { kind: ArtifactKind; source: string }[] = [];
130
197
  for (const kind of kinds) {
@@ -136,88 +203,61 @@ async function findArtifactFiles(
136
203
  break;
137
204
  }
138
205
  } catch {
139
- // 缺文件跳过:某类数据为空本来就不生成对应 JSON(docs/results-format.md)。
206
+ // 缺文件跳过:某类数据为空本来就不生成对应 JSON(docs/feature/results/architecture.md)。
140
207
  }
141
208
  }
142
209
  }
143
210
  return found;
144
211
  }
145
212
 
146
- /** 瘦身条目:去掉内联大字段与 view 注入的路径字段,artifactsDir / has* 按目标目录重算。 */
147
- function slimForCopy(r: EvalResult, relDir: string, copied: Set<ArtifactKind>): EvalResult {
148
- const { events, sources, o11y, trace, diff, rawTranscript, artifactBase, artifactAbsBase, ...rest } = r;
213
+ /**
214
+ * 重建 attempt 记录:去掉快照级字段(agent/model/experimentId/experiment,目标 snapshot.json
215
+ * 已经带了)与 artifactBase(artifact 已本地化,不再需要回退指针);has* 按目标目录实际复制到的
216
+ * 种类重算。startedAt 是 attempt 级事实(身份键与「何时跑的」都靠它),原样保留。
217
+ */
218
+ function slimForCopy(r: EvalResult, copied: Set<ArtifactKind>): Record<string, unknown> {
219
+ const {
220
+ agent,
221
+ model,
222
+ experimentId,
223
+ experiment,
224
+ events,
225
+ sources,
226
+ o11y,
227
+ trace,
228
+ agentSetup,
229
+ diff,
230
+ rawTranscript,
231
+ artifactBase,
232
+ hasTrace,
233
+ hasEvents,
234
+ hasSources,
235
+ ...rest
236
+ } = r;
237
+ void agent;
238
+ void model;
239
+ void experimentId;
240
+ void experiment;
149
241
  void events;
150
242
  void sources;
151
243
  void o11y;
152
244
  void trace;
245
+ void agentSetup;
153
246
  void diff;
154
247
  void rawTranscript;
155
248
  void artifactBase;
156
- void artifactAbsBase;
249
+ void hasTrace;
250
+ void hasEvents;
251
+ void hasSources;
157
252
  return {
158
253
  ...rest,
159
- artifactsDir: relDir,
160
254
  hasEvents: copied.has("events"),
161
255
  hasTrace: copied.has("trace"),
162
256
  hasSources: copied.has("sources"),
163
257
  };
164
258
  }
165
259
 
166
- function rebuildSummary(selected: Snapshot[], results: EvalResult[], dest: string): RunSummary {
167
- // 版本元数据与 run 级元数据取最新源 run 的(producer 缺失的 legacy 源保持缺失,不冒充)。
168
- const sourceRuns = uniqueRuns(selected);
169
- let newest = sourceRuns[0];
170
- for (const run of sourceRuns) if (isNewerRunDir(run, newest)) newest = run;
171
-
172
- const counts = { passed: 0, failed: 0, skipped: 0, errored: 0 };
173
- for (const r of results) counts[r.outcome] += 1;
174
-
175
- return {
176
- format: RESULTS_FORMAT,
177
- schemaVersion: RESULTS_SCHEMA_VERSION,
178
- ...(newest.summary.producer ? { producer: newest.summary.producer } : {}),
179
- ...(newest.summary.name !== undefined ? { name: newest.summary.name } : {}),
180
- // 顶层 agent/model 沿用 runner 的「第一个配置」姿势;快照各自的 agent 在条目里。
181
- agent: selected[0].agent,
182
- ...(selected[0].model !== undefined ? { model: selected[0].model } : {}),
183
- startedAt: newest.summary.startedAt,
184
- completedAt: newest.summary.completedAt,
185
- ...counts,
186
- // 跨 run 拼装无法还原墙钟时长,这里是选中 attempt 的耗时合计。
187
- durationMs: results.reduce((total, r) => total + (r.durationMs ?? 0), 0),
188
- ...usageAndCost(results),
189
- results,
190
- outputDir: dest,
191
- snapshots: snapshotsMeta(selected, newest.summary.startedAt),
192
- };
193
- }
194
-
195
- /**
196
- * 快照级元数据补记:startedAt 保留各快照自己的时刻(它们可能来自不同源 run),
197
- * knownEvalIds = 复制时刻该实验已知的 eval 并集(openResults 产出的快照从所属实验取,
198
- * 手工构造的快照退回「同 id 输入快照的覆盖 ∪ 携带值」)—— 残缺检测的分母随数据走。
199
- */
200
- function snapshotsMeta(selected: Snapshot[], summaryStartedAt: string): NonNullable<RunSummary["snapshots"]> {
201
- // 同一 experiment 被选了多个快照(未 dedupe)时取最新的那个,与工件胜者规则一致。
202
- const bySnapshotId = new Map<string, Snapshot>();
203
- for (const snapshot of selected) {
204
- const existing = bySnapshotId.get(snapshot.experimentId);
205
- if (!existing || isNewerRunDir(snapshot.runDir, existing.runDir)) {
206
- bySnapshotId.set(snapshot.experimentId, snapshot);
207
- }
208
- }
209
-
210
- const meta: NonNullable<RunSummary["snapshots"]> = {};
211
- for (const [experimentId, snapshot] of bySnapshotId) {
212
- const union = experimentOfSnapshot(snapshot)?.evalIds ?? fallbackUnion(selected, experimentId);
213
- meta[experimentId] = {
214
- ...(snapshot.startedAt !== summaryStartedAt ? { startedAt: snapshot.startedAt } : {}),
215
- knownEvalIds: union,
216
- };
217
- }
218
- return meta;
219
- }
220
-
260
+ /** experimentOfSnapshot 查不到归属(手工构造的 Snapshot[])时的兜底:同 id 输入快照的覆盖 携带值。 */
221
261
  function fallbackUnion(selected: Snapshot[], experimentId: string): string[] {
222
262
  const ids = new Set<string>();
223
263
  for (const snapshot of selected) {
@@ -227,28 +267,3 @@ function fallbackUnion(selected: Snapshot[], experimentId: string): string[] {
227
267
  }
228
268
  return [...ids].sort();
229
269
  }
230
-
231
- /** usage / 成本按选中条目重算;口径同 runner 的 summarize(没有任何 usage 就不写)。 */
232
- function usageAndCost(results: EvalResult[]): Pick<RunSummary, "usage" | "estimatedCostUSD"> {
233
- const usages = results.map((r) => r.usage).filter((u): u is Usage => u !== undefined);
234
- const cost = results.reduce((total, r) => total + (r.estimatedCostUSD ?? 0), 0);
235
- if (usages.length === 0) return { estimatedCostUSD: cost || undefined };
236
- const usage: Usage = { inputTokens: 0, outputTokens: 0 };
237
- for (const u of usages) {
238
- usage.inputTokens += u.inputTokens;
239
- usage.outputTokens += u.outputTokens;
240
- for (const key of ["cacheReadTokens", "cacheWriteTokens", "requests", "costUSD"] as const) {
241
- const value = u[key];
242
- if (value !== undefined) usage[key] = (usage[key] ?? 0) + value;
243
- }
244
- }
245
- return { usage, estimatedCostUSD: cost || undefined };
246
- }
247
-
248
- function uniqueRuns(snapshots: Snapshot[]): RunDir[] {
249
- const runs: RunDir[] = [];
250
- for (const snapshot of snapshots) {
251
- if (!runs.includes(snapshot.runDir)) runs.push(snapshot.runDir);
252
- }
253
- return runs;
254
- }
@@ -1,76 +1,83 @@
1
- // Results Format 的布局与版本知识,规则见 docs/results-format.md。
1
+ // Results Format 的布局与版本知识,规则见 docs/feature/results/architecture.md。
2
2
  //
3
- // 按 docs/results-lib.md,这份知识只住在本库:写入面(writer.ts / copy.ts)与
3
+ // 按 docs/feature/results/library.md,这份知识只住在本库:写入面(writer.ts / copy.ts)与
4
4
  // 读取面(open.ts)共用这里的目录规则与版本判定;src/runner/reporters/artifacts.ts
5
5
  // 是 writer 的薄壳,view(src/view/data.ts)经 openResults 消费,不自带布局知识。
6
6
 
7
- import type { EvalResult, RunSummary } from "../types.ts";
7
+ import type { EvalResult } from "../types.ts";
8
8
  import { RESULTS_FORMAT, RESULTS_SCHEMA_VERSION } from "../types.ts";
9
- import type { ArtifactKind } from "./types.ts";
9
+ import type { ArtifactKind, SnapshotMeta } from "./types.ts";
10
10
 
11
- /** attempt 工件子目录(相对 run 根):<evalId>/<agent>/<model>[/<experimentId>]/a<attempt>。 */
12
- export function attemptDirOf(r: Pick<EvalResult, "id" | "agent" | "model" | "attempt" | "experimentId">): string {
13
- const safe = (s: string) => s.replace(/[^\w.@-]/g, "_");
14
- // evalId 里的 / 保留作目录层级,其余危险字符替换。
11
+ /** attempt 目录名:结果记录文件恒为 result.json,attempt 级判决权威落点。 */
12
+ export const RESULT_FILE = "result.json";
13
+ /** 快照元数据文件名。 */
14
+ export const SNAPSHOT_FILE = "snapshot.json";
15
+
16
+ /** attempt 目录(相对快照根):`<evalId>/a<attempt>`;evalId 里的 / 保留作目录层级,其余危险字符替换。 */
17
+ export function attemptDirOf(r: Pick<EvalResult, "id" | "attempt">): string {
15
18
  const id = r.id.replace(/[^\w./@-]/g, "_");
16
- // experiment 段:两个实验可以同 agent 同 model、只差实验参数,少这一段它们的工件会互相
17
- // 覆盖;experimentId 里的 / 不作层级(整段压成 _),整个实验一格。
18
- const exp = r.experimentId ? `/${safe(r.experimentId)}` : "";
19
- return `${id}/${safe(r.agent)}/${safe(r.model ?? "default")}${exp}/a${r.attempt}`;
19
+ return `${id}/a${r.attempt}`;
20
+ }
21
+
22
+ /** 实验目录名:experimentId 里的 / 与其它非 [\w.@-] 字符替换成 _。 */
23
+ export function experimentDirOf(experimentId: string): string {
24
+ return experimentId.replace(/[^\w.@-]/g, "_");
20
25
  }
21
26
 
22
- /** 工件文件名:种类即文件名。 */
27
+ /**
28
+ * artifact 文件名。种类名是 TS 侧的驼峰(`agentSetup`),文件名是磁盘侧的 kebab
29
+ * (`agent-setup.json`)——两边各自守自己的惯例,映射表是唯一的翻译点。
30
+ */
31
+ const ARTIFACT_FILES: Record<ArtifactKind, string> = {
32
+ events: "events.json",
33
+ trace: "trace.json",
34
+ o11y: "o11y.json",
35
+ agentSetup: "agent-setup.json",
36
+ diff: "diff.json",
37
+ sources: "sources.json",
38
+ };
39
+
23
40
  export function artifactFileOf(kind: ArtifactKind): string {
24
- return `${kind}.json`;
41
+ return ARTIFACT_FILES[kind];
25
42
  }
26
43
 
27
- /** summary.json 的版本判定结果;openResults 按它分流 ok / skipped / 静默忽略。 */
28
- export type SummaryClassification =
29
- | { kind: "ok"; summary: RunSummary }
30
- | { kind: "incompatible"; schemaVersion: number; producer?: RunSummary["producer"] }
44
+ /** snapshot.json 的版本判定结果;openResults 按它分流 ok / skipped / 静默忽略。 */
45
+ export type SnapshotClassification =
46
+ | { kind: "ok"; meta: SnapshotMeta }
47
+ | { kind: "incompatible"; schemaVersion: number; producer?: SnapshotMeta["producer"] }
31
48
  | { kind: "malformed"; detail: string }
32
49
  | { kind: "not-a-report" };
33
50
 
34
51
  /**
35
- * 版本判定与最小形状校验(docs/results-format.md「版本不匹配时的读取行为」):
36
- * - 带 format 信封:format 不是 niceeval.results → 无关 JSON;schemaVersion 与当前不同
37
- * 不兼容(不解析、不迁移、不降级),缺 schemaVersion 的早期信封按 1 处理。
38
- * - 无信封:results[] startedAt 同时具备才按 legacy 照读(未知字段忽略);
39
- * 只沾到一个键的多半是别家工具的 summary.json,按规范当无关 JSON 忽略,不进 skipped 制造噪音。
52
+ * 版本判定与最小形状校验(docs/feature/results/architecture.md「版本不匹配时的读取行为」):
53
+ * - 带 format 信封:format 不是 niceeval.results → 无关 JSON;schemaVersion 非数字 malformed;
54
+ * schemaVersion 与当前不同 → 不兼容(不解析、不迁移、不降级),带 schemaVersion 与完整 producer;
55
+ * schemaVersion 相同时校验 experimentId / agent / startedAt 均为 string,不满足 → malformed。
56
+ * - 无信封:v1 的 run 级 summary.json(results[] 是数组且 startedAt string)按 incompatible
57
+ * (schemaVersion 1)处理 —— 这是版本识别,不是迁移;不满足启发式的当无关 JSON 忽略。
58
+ * - v2/v3 的 summary.json 带 format + schemaVersion(≠ 4),自然落进 incompatible 档。
40
59
  */
41
- export function classifySummary(raw: unknown): SummaryClassification {
60
+ export function classifySnapshot(raw: unknown): SnapshotClassification {
42
61
  if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
43
62
  return { kind: "malformed", detail: "not a JSON object" };
44
63
  }
45
- const data = raw as Partial<RunSummary>;
64
+ const data = raw as Partial<SnapshotMeta> & { results?: unknown };
46
65
  if (data.format !== undefined && data.format !== RESULTS_FORMAT) return { kind: "not-a-report" };
47
66
  if (data.format === RESULTS_FORMAT) {
48
- const version = data.schemaVersion ?? 1;
49
- if (typeof version !== "number") return { kind: "malformed", detail: "schemaVersion is not a number" };
50
- if (version !== RESULTS_SCHEMA_VERSION) {
67
+ if (typeof data.schemaVersion !== "number") return { kind: "malformed", detail: "schemaVersion is not a number" };
68
+ if (data.schemaVersion !== RESULTS_SCHEMA_VERSION) {
51
69
  // skipped 必须带完整 producer(name + version):npx 提示只对 name === "niceeval" 成立,
52
70
  // 第三方 harness 的落盘只给裸版本号的话,消费方连做对这个分支的信息都没有。
53
- return { kind: "incompatible", schemaVersion: version, producer: data.producer };
71
+ return { kind: "incompatible", schemaVersion: data.schemaVersion, producer: data.producer };
54
72
  }
55
- if (!Array.isArray(data.results) || typeof data.startedAt !== "string") {
56
- return { kind: "malformed", detail: "missing results[] or startedAt" };
73
+ if (typeof data.experimentId !== "string" || typeof data.agent !== "string" || typeof data.startedAt !== "string") {
74
+ return { kind: "malformed", detail: "missing experimentId, agent or startedAt" };
57
75
  }
58
- return { kind: "ok", summary: data as RunSummary };
76
+ return { kind: "ok", meta: data as SnapshotMeta };
59
77
  }
60
- if (!Array.isArray(data.results) || typeof data.startedAt !== "string") {
61
- return { kind: "not-a-report" };
78
+ // 无信封:引入版本信封之前的存量报告(v1 run summary.json)
79
+ if (Array.isArray(data.results) && typeof (data as { startedAt?: unknown }).startedAt === "string") {
80
+ return { kind: "incompatible", schemaVersion: 1 };
62
81
  }
63
- return { kind: "ok", summary: data as RunSummary };
64
- }
65
-
66
- /**
67
- * 快照/去重共用的 experiment 身份键:experimentId 缺失时以 "<agent>/<model>" 合成
68
- * (无 model 用 "default",与工件目录命名同一口径),synthesized 供调用方决定要不要出 warning。
69
- */
70
- export function experimentKeyOf(r: Pick<EvalResult, "experimentId" | "agent" | "model">): {
71
- id: string;
72
- synthesized: boolean;
73
- } {
74
- if (r.experimentId) return { id: r.experimentId, synthesized: false };
75
- return { id: `${r.agent}/${r.model ?? "default"}`, synthesized: true };
82
+ return { kind: "not-a-report" };
76
83
  }