niceeval 0.5.4 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (342) hide show
  1. package/INDEX.md +55 -0
  2. package/dist/agents/types.d.ts +317 -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/execution-tree.d.ts +103 -0
  9. package/dist/o11y/otlp/parse.d.ts +3 -0
  10. package/dist/o11y/otlp/receiver.d.ts +21 -0
  11. package/dist/o11y/otlp/select.d.ts +22 -0
  12. package/dist/o11y/otlp/turn-otel.d.ts +43 -0
  13. package/dist/o11y/types.d.ts +190 -0
  14. package/dist/report/aggregate.d.ts +57 -0
  15. package/dist/report/aggregate.js +212 -0
  16. package/dist/report/built-ins/cost-pass-rate-comparison.d.ts +1 -0
  17. package/dist/report/built-ins/cost-pass-rate-comparison.js +17 -0
  18. package/dist/report/built-ins/index.d.ts +1 -0
  19. package/dist/report/built-ins/index.js +2 -0
  20. package/dist/report/components.d.ts +178 -0
  21. package/dist/report/components.js +132 -0
  22. package/dist/report/compute.d.ts +106 -0
  23. package/dist/report/compute.js +618 -0
  24. package/dist/report/flag.d.ts +6 -0
  25. package/dist/report/flag.js +8 -0
  26. package/dist/report/format.d.ts +32 -0
  27. package/dist/report/format.js +126 -0
  28. package/dist/report/index.d.ts +19 -0
  29. package/dist/report/index.js +30 -0
  30. package/dist/report/load.d.ts +12 -0
  31. package/dist/report/load.js +56 -0
  32. package/dist/report/locale.d.ts +83 -0
  33. package/dist/report/locale.js +160 -0
  34. package/dist/report/metrics.d.ts +21 -0
  35. package/dist/report/metrics.js +115 -0
  36. package/dist/report/primitives.d.ts +64 -0
  37. package/dist/report/primitives.js +113 -0
  38. package/dist/report/react/AttemptList.d.ts +23 -0
  39. package/dist/report/react/AttemptList.js +20 -0
  40. package/dist/report/react/DeltaTable.d.ts +8 -0
  41. package/dist/report/react/DeltaTable.js +24 -0
  42. package/dist/report/react/EvalList.d.ts +10 -0
  43. package/dist/report/react/EvalList.js +0 -0
  44. package/dist/report/react/ExperimentList.d.ts +10 -0
  45. package/dist/report/react/ExperimentList.js +15 -0
  46. package/dist/report/react/GroupSummary.d.ts +8 -0
  47. package/dist/report/react/GroupSummary.js +8 -0
  48. package/dist/report/react/MetricBars.d.ts +10 -0
  49. package/dist/report/react/MetricBars.js +48 -0
  50. package/dist/report/react/MetricLine.d.ts +9 -0
  51. package/dist/report/react/MetricLine.js +68 -0
  52. package/dist/report/react/MetricMatrix.d.ts +10 -0
  53. package/dist/report/react/MetricMatrix.js +23 -0
  54. package/dist/report/react/MetricScatter.d.ts +9 -0
  55. package/dist/report/react/MetricScatter.js +100 -0
  56. package/dist/report/react/MetricTable.d.ts +11 -0
  57. package/dist/report/react/MetricTable.js +26 -0
  58. package/dist/report/react/RunOverview.d.ts +8 -0
  59. package/dist/report/react/RunOverview.js +12 -0
  60. package/dist/report/react/Scoreboard.d.ts +8 -0
  61. package/dist/report/react/Scoreboard.js +26 -0
  62. package/dist/report/react/cell.d.ts +9 -0
  63. package/dist/report/react/cell.js +9 -0
  64. package/dist/report/react/chart-math.d.ts +14 -0
  65. package/dist/report/react/chart-math.js +68 -0
  66. package/dist/report/react/colors.d.ts +14 -0
  67. package/dist/report/react/colors.js +45 -0
  68. package/dist/report/react/fixtures.d.ts +19 -0
  69. package/dist/report/react/fixtures.js +393 -0
  70. package/dist/report/react/format.d.ts +3 -0
  71. package/dist/report/react/format.js +7 -0
  72. package/dist/report/react/index.d.ts +6 -0
  73. package/dist/report/react/index.js +14 -0
  74. package/dist/report/report.d.ts +24 -0
  75. package/dist/report/report.js +49 -0
  76. package/dist/report/text/faces.d.ts +29 -0
  77. package/dist/report/text/faces.js +503 -0
  78. package/dist/report/text/layout.d.ts +75 -0
  79. package/dist/report/text/layout.js +173 -0
  80. package/dist/report/text/plot.d.ts +37 -0
  81. package/dist/report/text/plot.js +81 -0
  82. package/dist/report/text/table.d.ts +8 -0
  83. package/dist/report/text/table.js +86 -0
  84. package/dist/report/tree.d.ts +99 -0
  85. package/dist/report/tree.js +228 -0
  86. package/dist/report/types.d.ts +420 -0
  87. package/dist/report/types.js +4 -0
  88. package/dist/report/web.d.ts +15 -0
  89. package/dist/report/web.js +37 -0
  90. package/dist/results/annotated-source.d.ts +61 -0
  91. package/dist/results/attempt-evidence.d.ts +69 -0
  92. package/dist/results/attempt-source.d.ts +15 -0
  93. package/dist/results/locator.d.ts +75 -0
  94. package/dist/results/locator.js +0 -0
  95. package/dist/results/select.d.ts +45 -0
  96. package/dist/results/select.js +230 -0
  97. package/dist/results/source-hash.d.ts +9 -0
  98. package/dist/results/types.d.ts +204 -0
  99. package/dist/runner/eval-source.d.ts +22 -0
  100. package/dist/runner/fingerprint.d.ts +23 -0
  101. package/dist/runner/types.d.ts +332 -0
  102. package/dist/sandbox/docker-stream.d.ts +24 -0
  103. package/dist/sandbox/docker.d.ts +84 -0
  104. package/dist/sandbox/e2b.d.ts +31 -0
  105. package/dist/sandbox/errors.d.ts +4 -0
  106. package/dist/sandbox/local-files.d.ts +6 -0
  107. package/dist/sandbox/paths.d.ts +4 -0
  108. package/dist/sandbox/registry.d.ts +13 -0
  109. package/dist/sandbox/resolve.d.ts +40 -0
  110. package/dist/sandbox/retry.d.ts +10 -0
  111. package/dist/sandbox/shell.d.ts +11 -0
  112. package/dist/sandbox/source-files.d.ts +13 -0
  113. package/dist/sandbox/types.d.ts +203 -0
  114. package/dist/sandbox/vercel.d.ts +37 -0
  115. package/dist/scoring/types.d.ts +70 -0
  116. package/dist/shared/aggregate.d.ts +29 -0
  117. package/dist/shared/aggregate.js +55 -0
  118. package/dist/shared/types.d.ts +27 -0
  119. package/dist/shared/verdict.d.ts +27 -0
  120. package/dist/shared/verdict.js +34 -0
  121. package/dist/tty-line.d.ts +6 -0
  122. package/dist/types.d.ts +7 -0
  123. package/dist/util.d.ts +22 -0
  124. package/docs-site/zh/concepts/adapter.mdx +5 -5
  125. package/docs-site/zh/concepts/assert.mdx +8 -8
  126. package/docs-site/zh/concepts/evals.mdx +5 -5
  127. package/docs-site/zh/concepts/experiment.mdx +2 -2
  128. package/docs-site/zh/concepts/judge.mdx +9 -9
  129. package/docs-site/zh/concepts/overview.mdx +2 -2
  130. package/docs-site/zh/concepts/tier.mdx +6 -6
  131. package/docs-site/zh/example/claude-code-codex-plugin.mdx +9 -5
  132. package/docs-site/zh/example/claude-code-codex-skill.mdx +12 -7
  133. package/docs-site/zh/guides/agent-feedback-loop.mdx +235 -78
  134. package/docs-site/zh/guides/connect-otel.mdx +3 -3
  135. package/docs-site/zh/guides/connect-your-agent.mdx +2 -2
  136. package/docs-site/zh/guides/custom-reports.mdx +196 -36
  137. package/docs-site/zh/guides/experiments.mdx +7 -1
  138. package/docs-site/zh/guides/fixtures.mdx +1 -1
  139. package/docs-site/zh/guides/official-adapters.mdx +31 -9
  140. package/docs-site/zh/guides/publish-report.mdx +3 -3
  141. package/docs-site/zh/guides/report-components.mdx +194 -55
  142. package/docs-site/zh/guides/reporters.mdx +2 -2
  143. package/docs-site/zh/guides/results-data.mdx +35 -34
  144. package/docs-site/zh/guides/runner.mdx +2 -2
  145. package/docs-site/zh/guides/sandbox-agent.mdx +2 -2
  146. package/docs-site/zh/guides/{sandbox-backends.mdx → sandbox-providers.mdx} +11 -11
  147. package/docs-site/zh/guides/viewing-results.mdx +199 -79
  148. package/docs-site/zh/guides/write-experiment.mdx +9 -1
  149. package/docs-site/zh/guides/write-send.mdx +2 -2
  150. package/docs-site/zh/index.mdx +4 -3
  151. package/docs-site/zh/introduction.mdx +2 -1
  152. package/docs-site/zh/quickstart.mdx +5 -3
  153. package/docs-site/zh/reference/builtin-agents.mdx +38 -13
  154. package/docs-site/zh/reference/capabilities.mdx +1 -1
  155. package/docs-site/zh/reference/cli.mdx +36 -13
  156. package/docs-site/zh/reference/define-agent.mdx +12 -12
  157. package/docs-site/zh/reference/define-config.mdx +1 -1
  158. package/docs-site/zh/reference/define-eval.mdx +3 -3
  159. package/docs-site/zh/reference/events.mdx +7 -1
  160. package/docs-site/zh/reference/expect.mdx +3 -3
  161. package/package.json +10 -6
  162. package/src/agents/bub.ts +129 -42
  163. package/src/agents/claude-code.test.ts +152 -0
  164. package/src/agents/claude-code.ts +127 -13
  165. package/src/agents/codex.test.ts +192 -0
  166. package/src/agents/codex.ts +151 -12
  167. package/src/agents/index.ts +9 -3
  168. package/src/agents/manifest.ts +41 -0
  169. package/src/agents/plugin-config.test.ts +94 -0
  170. package/src/agents/shared.ts +33 -2
  171. package/src/agents/skills.test.ts +214 -0
  172. package/src/agents/skills.ts +304 -0
  173. package/src/agents/streaming.ts +5 -5
  174. package/src/agents/types.ts +54 -2
  175. package/src/cli.ts +74 -47
  176. package/src/context/context.test.ts +20 -2
  177. package/src/context/control-flow.ts +1 -1
  178. package/src/context/session.test.ts +92 -1
  179. package/src/context/session.ts +23 -1
  180. package/src/context/types.ts +4 -4
  181. package/src/define.test.ts +8 -8
  182. package/src/define.ts +11 -11
  183. package/src/expect/index.ts +5 -5
  184. package/src/i18n/en.ts +34 -15
  185. package/src/i18n/zh-CN.ts +37 -21
  186. package/src/index.ts +1 -1
  187. package/src/o11y/execution-tree.test.ts +452 -0
  188. package/src/o11y/execution-tree.ts +367 -0
  189. package/src/o11y/otlp/receiver.ts +1 -1
  190. package/src/o11y/otlp/select.ts +7 -3
  191. package/src/o11y/parsers/claude-code.test.ts +175 -0
  192. package/src/o11y/parsers/claude-code.ts +32 -0
  193. package/src/o11y/types.ts +6 -1
  194. package/src/report/aggregate.ts +26 -20
  195. package/src/report/built-in-user-parity.test.tsx +643 -0
  196. package/src/report/built-ins/cost-pass-rate-comparison.tsx +23 -0
  197. package/src/report/built-ins/index.ts +2 -0
  198. package/src/report/components.tsx +223 -50
  199. package/src/report/compute.ts +358 -53
  200. package/src/report/dual-render.test.tsx +1125 -0
  201. package/src/report/flag.ts +1 -1
  202. package/src/report/format.ts +50 -2
  203. package/src/report/index.ts +48 -14
  204. package/src/report/load.ts +1 -1
  205. package/src/report/locale.ts +206 -0
  206. package/src/report/metrics.ts +39 -15
  207. package/src/report/primitives.tsx +100 -2
  208. package/src/report/react/AttemptList.tsx +115 -0
  209. package/src/report/react/DeltaTable.tsx +9 -6
  210. package/src/report/react/EvalList.tsx +0 -0
  211. package/src/report/react/ExperimentList.tsx +108 -0
  212. package/src/report/react/GroupSummary.tsx +66 -0
  213. package/src/report/react/MetricBars.tsx +24 -13
  214. package/src/report/react/MetricLine.tsx +21 -21
  215. package/src/report/react/MetricMatrix.tsx +8 -4
  216. package/src/report/react/MetricScatter.tsx +136 -96
  217. package/src/report/react/MetricTable.tsx +115 -15
  218. package/src/report/react/RunOverview.tsx +48 -26
  219. package/src/report/react/Scoreboard.tsx +15 -7
  220. package/src/report/react/cell.tsx +18 -9
  221. package/src/{view/app/lib/chart.ts → report/react/chart-math.ts} +2 -6
  222. package/src/report/react/colors.ts +24 -15
  223. package/src/report/react/enhance.js +129 -0
  224. package/src/report/react/fixtures.ts +163 -34
  225. package/src/report/react/format.ts +1 -1
  226. package/src/report/react/index.tsx +22 -6
  227. package/src/report/react/render.test.tsx +164 -31
  228. package/src/report/react/styles.css +405 -213
  229. package/src/report/report.test.ts +403 -94
  230. package/src/report/report.ts +29 -11
  231. package/src/report/text/faces.ts +303 -95
  232. package/src/report/text/layout.ts +76 -11
  233. package/src/report/text/table.ts +98 -0
  234. package/src/report/tree.ts +103 -20
  235. package/src/report/types.ts +196 -25
  236. package/src/report/web.ts +46 -19
  237. package/src/results/annotated-source.test.ts +146 -0
  238. package/src/results/annotated-source.ts +128 -0
  239. package/src/results/attempt-evidence.test.ts +158 -0
  240. package/src/results/attempt-evidence.ts +158 -0
  241. package/src/results/attempt-source.ts +48 -0
  242. package/src/results/copy.ts +154 -139
  243. package/src/results/format.ts +54 -47
  244. package/src/results/host-equivalence.test.ts +527 -0
  245. package/src/results/index.ts +56 -14
  246. package/src/results/locator.test.ts +198 -0
  247. package/src/results/locator.ts +0 -0
  248. package/src/results/open.ts +392 -175
  249. package/src/results/results.test.ts +943 -494
  250. package/src/results/select.ts +129 -23
  251. package/src/results/skipped-notice.ts +0 -0
  252. package/src/results/source-hash.ts +28 -0
  253. package/src/results/types.ts +98 -62
  254. package/src/results/writer.ts +304 -176
  255. package/src/runner/attempt.test.ts +170 -0
  256. package/src/runner/attempt.ts +48 -13
  257. package/src/runner/discover.test.ts +65 -0
  258. package/src/runner/discover.ts +6 -2
  259. package/src/runner/eval-source.test.ts +121 -0
  260. package/src/runner/eval-source.ts +45 -0
  261. package/src/runner/fingerprint.ts +54 -1
  262. package/src/runner/remote-sandbox.ts +1 -1
  263. package/src/runner/report.test.ts +6 -8
  264. package/src/runner/report.ts +5 -4
  265. package/src/runner/reporters/artifacts.ts +27 -41
  266. package/src/runner/reporters/braintrust.test.ts +3 -3
  267. package/src/runner/reporters/braintrust.ts +1 -1
  268. package/src/runner/reporters/console.ts +6 -6
  269. package/src/runner/reporters/json.ts +4 -4
  270. package/src/runner/reporters/live.test.ts +56 -0
  271. package/src/runner/reporters/live.ts +36 -10
  272. package/src/runner/reporters/quiet.test.ts +66 -0
  273. package/src/runner/reporters/quiet.ts +49 -0
  274. package/src/runner/reporters/shared.ts +5 -5
  275. package/src/runner/reporters/table.ts +17 -17
  276. package/src/runner/run.ts +61 -102
  277. package/src/runner/types.ts +43 -32
  278. package/src/sandbox/checkpoint.ts +1 -1
  279. package/src/sandbox/docker.ts +15 -2
  280. package/src/sandbox/e2b.ts +11 -5
  281. package/src/sandbox/errors.ts +13 -0
  282. package/src/sandbox/index.ts +3 -3
  283. package/src/sandbox/local-files.ts +1 -1
  284. package/src/sandbox/registry.ts +6 -5
  285. package/src/sandbox/resolve.ts +38 -23
  286. package/src/sandbox/retry.test.ts +68 -0
  287. package/src/sandbox/retry.ts +44 -0
  288. package/src/sandbox/types.ts +24 -24
  289. package/src/sandbox/vercel.ts +17 -3
  290. package/src/scoring/collector.ts +3 -3
  291. package/src/scoring/judge.ts +2 -2
  292. package/src/scoring/scoped.ts +13 -1
  293. package/src/scoring/types.ts +7 -7
  294. package/src/scoring/verdict.ts +4 -4
  295. package/src/shared/aggregate.ts +23 -3
  296. package/src/shared/types.ts +2 -2
  297. package/src/shared/{outcome.ts → verdict.ts} +15 -15
  298. package/src/show/compose.ts +12 -132
  299. package/src/show/index.ts +125 -94
  300. package/src/show/render.ts +660 -160
  301. package/src/show/show.test.ts +472 -253
  302. package/src/source-loc.ts +1 -1
  303. package/src/tty-line.ts +32 -0
  304. package/src/types.ts +2 -2
  305. package/src/util.test.ts +21 -1
  306. package/src/util.ts +5 -1
  307. package/src/view/app/App.tsx +33 -198
  308. package/src/view/app/components/AttemptModal.tsx +6 -6
  309. package/src/view/app/components/CodeView.tsx +2 -2
  310. package/src/view/app/components/CopyControls.tsx +32 -36
  311. package/src/view/app/components/SkippedRunsBanner.tsx +140 -0
  312. package/src/view/app/components/ui/badge.tsx +1 -1
  313. package/src/view/app/components/ui/dialog.tsx +2 -2
  314. package/src/view/app/i18n.ts +40 -31
  315. package/src/view/app/lib/artifact-url.ts +1 -1
  316. package/src/view/app/lib/attempt-route.test.ts +37 -47
  317. package/src/view/app/lib/attempt-route.ts +26 -26
  318. package/src/view/app/lib/rows.ts +9 -112
  319. package/src/view/app/lib/verdict.ts +25 -0
  320. package/src/view/app/main.tsx +9 -12
  321. package/src/view/app/pages/RunsPage.tsx +4 -4
  322. package/src/view/app/pages/TracesPage.tsx +3 -3
  323. package/src/view/app/shared.ts +0 -1
  324. package/src/view/app/types.ts +6 -51
  325. package/src/view/artifact-serving.test.ts +136 -0
  326. package/src/view/client-dist/app.css +1 -1
  327. package/src/view/client-dist/app.js +21 -21
  328. package/src/view/data.test.ts +214 -143
  329. package/src/view/data.ts +166 -120
  330. package/src/view/index.ts +23 -11
  331. package/src/view/server.ts +52 -17
  332. package/src/view/shared/types.ts +22 -31
  333. package/src/view/styles.css +117 -327
  334. package/src/view/view-report.test.ts +146 -98
  335. package/src/report/default-report.tsx +0 -222
  336. package/src/report/dual-face.test.tsx +0 -527
  337. package/src/report/react/CaseList.tsx +0 -70
  338. package/src/view/app/components/CostScoreChart.tsx +0 -147
  339. package/src/view/app/components/ExperimentTable.tsx +0 -284
  340. package/src/view/app/components/GroupSelector.tsx +0 -61
  341. package/src/view/app/components/primitives.tsx +0 -43
  342. package/src/view/app/lib/outcome.ts +0 -82
@@ -1,38 +1,47 @@
1
- // createRunWriter:Results Format 的写入面(定稿见 docs/results-lib.md「写:createRunWriter」)。
1
+ // createResultsWriter:Results Format 的写入面(定稿见 docs/feature/results/library.md「写:createResultsWriter」)。
2
2
  //
3
3
  // writer 与 reader 是同一组类型的两半,而且是字面的两半:reader 的 attempt.result 由
4
- // 「snapshot() 声明的快照级字段(experiment / agent / model / startedAt)+ writeAttempt 第一参」
5
- // 拼成,快照级字段不在 attempt 参数类型里(AttemptEntry 的 Omit),不存在「谁的值为准」。
6
- // 布局知识(时间戳目录、attempt 路径清洗、大字段拆工件、has* 回填、空数据不落文件)全在这里;
7
- // src/runner/reporters/artifacts.ts 是本文件的薄壳。
4
+ // 「snapshot() 声明的快照级字段(experimentId / agent / model / startedAt / experiment)+
5
+ // writeAttempt 第一参」拼成,快照级字段不在 attempt 参数类型里(AttemptEntry 的 Omit),
6
+ // 不存在「谁的值为准」。布局知识(快照目录独占创建、attempt 路径清洗、大字段拆 artifact、
7
+ // has* 回填、空数据不落文件)全在这里;src/runner/reporters/artifacts.ts 是本文件的薄壳。
8
8
 
9
9
  import { mkdir, writeFile } from "node:fs/promises";
10
10
  import { join } from "node:path";
11
- import type { EvalResult, RunSummary, Usage } from "../types.ts";
12
- import type { LocalizedText } from "../types.ts";
11
+ import type { AgentSetupManifest, EvalResult, ExperimentRunInfo, LocalizedText } from "../types.ts";
13
12
  import type { DiffData, O11ySummary, SourceArtifact, StreamEvent, TraceSpan } from "../types.ts";
14
13
  import { RESULTS_FORMAT, RESULTS_SCHEMA_VERSION } from "../types.ts";
15
- import { attemptDirOf } from "./format.ts";
14
+ import { RESULT_FILE, SNAPSHOT_FILE, artifactFileOf, attemptDirOf, experimentDirOf } from "./format.ts";
15
+ import { encodeAttemptLocator } from "./locator.ts";
16
+ import { hashEvalSource, normalizeEvalSource } from "./source-hash.ts";
17
+ import type { Producer, SnapshotMeta } from "./types.ts";
16
18
 
17
- export interface RunWriterOptions {
19
+ export interface ResultsWriterOptions {
18
20
  /** 谁在写这份结果:niceeval 自己,或第三方 harness(name 如实写,别冒充 "niceeval")。 */
19
- producer: NonNullable<RunSummary["producer"]>;
21
+ producer: Producer;
20
22
  }
21
23
 
22
- /** 快照级元数据的家:一个 experiment 开一个;这些字段不塞进每条 attempt。 */
24
+ /** 快照级声明:一个 experiment 声明一次,这些字段不塞进每条 attempt。 */
23
25
  export interface SnapshotDeclaration {
24
- experiment: string;
26
+ experimentId: string;
25
27
  agent: string;
26
28
  model?: string;
27
29
  /** 必填:身份键与去重以它为锚,官方产出永不缺。 */
28
30
  startedAt: string;
31
+ /** 转换历史数据时如实交代收尾时刻;省略则 finish() 用当前时刻。 */
32
+ completedAt?: string;
33
+ /** 实验运行配置(flags / runs / earlyExit / sandbox / timeoutMs / budget),快照内全部 attempt 共享。 */
34
+ experiment?: ExperimentRunInfo;
29
35
  /** 该实验已知的 eval 并集(残缺检测的分母);转换只覆盖部分题目时如实交代全集。 */
30
36
  knownEvalIds?: string[];
37
+ /** 项目名(来自 config.name),透传给 `niceeval view` 顶部 hero 显示。 */
38
+ name?: LocalizedText;
31
39
  }
32
40
 
33
41
  /**
34
42
  * writeAttempt 的第一参 = attempt 级条目:reader 的 attempt.result 中,快照级字段
35
- * (agent / model / startedAt / experimentId)以外的全部;工件引用字段由 writer 回填。
43
+ * (experimentId / agent / model / startedAt / experiment)与引用字段(artifactBase / has*)
44
+ * 以外的全部;引用字段由 writer 按实际写入的 artifact 回填。
36
45
  */
37
46
  export type AttemptEntry = Omit<
38
47
  EvalResult,
@@ -40,219 +49,338 @@ export type AttemptEntry = Omit<
40
49
  | "model"
41
50
  | "startedAt"
42
51
  | "experimentId"
52
+ | "experiment"
43
53
  | "events"
44
54
  | "sources"
45
55
  | "o11y"
46
56
  | "trace"
57
+ | "agentSetup"
47
58
  | "diff"
48
59
  | "rawTranscript"
49
- | "artifactsDir"
50
60
  | "artifactBase"
51
- | "artifactAbsBase"
52
61
  | "hasTrace"
53
62
  | "hasEvents"
54
63
  | "hasSources"
55
64
  >;
56
65
 
57
- /** writeAttempt 的第二参:reader 懒加载能拿到的那几样工件,全部可选;缺哪样读取面就懒加载出 null。 */
66
+ /** writeAttempt 的第二参:reader 懒加载能拿到的那几样 artifact,全部可选;缺哪样读取面就懒加载出 null。 */
58
67
  export interface AttemptArtifacts {
59
68
  events?: StreamEvent[];
60
69
  trace?: TraceSpan[];
61
70
  o11y?: O11ySummary;
71
+ /** agent setup 的安装清单(沙箱型 coding agent 装了 Skill / plugin / MCP 才有)。 */
72
+ agentSetup?: AgentSetupManifest;
62
73
  diff?: DiffData;
63
74
  sources?: SourceArtifact[];
64
75
  }
65
76
 
66
77
  export interface SnapshotWriter {
67
- /** 增量落盘一条 attempt:拆工件文件、算 artifactsDir(含路径清洗)、回填 has* 引用;空数据不落文件。 */
78
+ /** 本快照的目录(绝对路径) */
79
+ readonly dir: string;
80
+ /** 增量落盘一条 attempt:拆 artifact 文件、回填 has* 引用、写 result.json;空数据不落文件。 */
68
81
  writeAttempt(entry: AttemptEntry, artifacts?: AttemptArtifacts): Promise<void>;
69
82
  }
70
83
 
71
- /** finish() 从已写 attempt 推导 summary;个别推不出的字段走这里覆盖,不让调用方手拼整份 summary。 */
72
- export interface FinishOverrides {
73
- name?: LocalizedText;
74
- agent?: string;
75
- model?: string;
76
- startedAt?: string;
77
- completedAt?: string;
78
- durationMs?: number;
79
- usage?: Usage;
80
- estimatedCostUSD?: number;
84
+ export interface ResultsWriter {
81
85
  /**
82
- * @internal runner 专用:携带条目(--resume 合入)与最终排序只有调度器知道,Artifacts 薄壳
83
- * 从这里交权威 results(可含内联大字段,writer 统一瘦身)。第三方转换不需要它。
86
+ * 建快照目录(独占创建,撞名换随机后缀重试)+ 立即写 snapshot.json(不含 completedAt)
87
+ * 同一 writer 内同 experimentId 重复声明 → 返回同一个 SnapshotWriter(懒建语义;
88
+ * knownEvalIds 取并集,completedAt / name 以最后一次声明为准,finish() 时才落盘)。
84
89
  */
85
- results?: EvalResult[];
90
+ snapshot(decl: SnapshotDeclaration): Promise<SnapshotWriter>;
91
+ /** 给每个已声明的快照补 completedAt(decl.completedAt ?? 当前时刻)与 name(参数优先,声明兜底)。 */
92
+ finish(opts?: { name?: LocalizedText }): Promise<void>;
93
+ /** @internal runner 薄壳入口:按 EvalResult 的 experimentId 懒建快照并落盘一条 attempt。 */
94
+ writeAttemptFor(result: EvalResult): Promise<void>;
95
+ /** @internal 已创建快照清单(CLI 收尾打印)。 */
96
+ snapshotDirs(): { experimentId: string; dir: string }[];
86
97
  }
87
98
 
88
- export interface RunWriter {
89
- /** 本次 run 的输出目录(root 下的时间戳目录,: 与 . 已替换)。 */
90
- readonly dir: string;
91
- snapshot(decl: SnapshotDeclaration): SnapshotWriter;
92
- /** 写出 summary.json 收尾;没走到这里的目录读取面归入 skipped("incomplete")。 */
93
- finish(overrides?: FinishOverrides): Promise<RunSummary>;
94
- /**
95
- * @internal runner Artifacts 薄壳的增量工件落盘入口:runner 的条目自带 agent / model /
96
- * experimentId / startedAt(且存在无 experiment 的普通 run),不经 snapshot() 声明。
97
- */
98
- writeAttemptArtifacts(result: EvalResult): Promise<void>;
99
+ interface SnapshotState {
100
+ /** 快照的权威 meta(不含 completedAt;knownEvalIds 随重复声明累加)。 */
101
+ meta: SnapshotMeta;
102
+ dir: string;
103
+ writer: SnapshotWriter;
104
+ declCompletedAt?: string;
105
+ declName?: LocalizedText;
99
106
  }
100
107
 
101
- export async function createRunWriter(root: string, opts: RunWriterOptions): Promise<RunWriter> {
102
- const createdAt = new Date();
103
- const dir = join(root, safeTimestamp(createdAt));
104
- await mkdir(dir, { recursive: true });
105
-
106
- const decls: SnapshotDeclaration[] = [];
107
- const entries: EvalResult[] = [];
108
+ /** 同步:不建目录、不碰磁盘。目录创建发生在第一次 snapshot() 调用里。 */
109
+ export function createResultsWriter(root: string, opts: ResultsWriterOptions): ResultsWriter {
110
+ const pending = new Map<string, Promise<SnapshotState>>();
111
+ const created: { experimentId: string; dir: string }[] = [];
108
112
  let finished = false;
109
113
 
110
- const writeArtifacts = async (target: EvalResult | AttemptArtifacts, dirFor: EvalResult): Promise<void> => {
111
- const attemptDest = join(dir, attemptDirOf(dirFor));
112
- await mkdir(attemptDest, { recursive: true });
113
- const writes: Promise<unknown>[] = [];
114
- if (target.events?.length) writes.push(writeFile(join(attemptDest, "events.json"), JSON.stringify(target.events), "utf-8"));
115
- if (target.sources?.length) writes.push(writeFile(join(attemptDest, "sources.json"), JSON.stringify(target.sources), "utf-8"));
116
- if (target.trace?.length) writes.push(writeFile(join(attemptDest, "trace.json"), JSON.stringify(target.trace), "utf-8"));
117
- if (target.o11y) writes.push(writeFile(join(attemptDest, "o11y.json"), JSON.stringify(target.o11y), "utf-8"));
118
- if (target.diff) writes.push(writeFile(join(attemptDest, "diff.json"), JSON.stringify(target.diff), "utf-8"));
119
- await Promise.all(writes);
120
- };
114
+ async function buildSnapshot(decl: SnapshotDeclaration): Promise<SnapshotState> {
115
+ const meta: SnapshotMeta = {
116
+ format: RESULTS_FORMAT,
117
+ schemaVersion: RESULTS_SCHEMA_VERSION,
118
+ producer: opts.producer,
119
+ experimentId: decl.experimentId,
120
+ // 运行配置不带 id:身份的家是顶层 experimentId,重复一份只会引出「以谁为准」。
121
+ ...(decl.experiment !== undefined ? { experiment: stripInfoId(decl.experiment) } : {}),
122
+ agent: decl.agent,
123
+ ...(decl.model !== undefined ? { model: decl.model } : {}),
124
+ startedAt: decl.startedAt,
125
+ ...(decl.knownEvalIds?.length ? { knownEvalIds: [...new Set(decl.knownEvalIds)] } : {}),
126
+ ...(decl.name !== undefined ? { name: decl.name } : {}),
127
+ };
128
+ const dir = await createSnapshotDir(root, decl.experimentId);
129
+ await writeFile(join(dir, SNAPSHOT_FILE), JSON.stringify(meta, null, 2), "utf-8");
130
+ created.push({ experimentId: decl.experimentId, dir });
131
+ // 快照级源码去重仓库:sha256 → 落盘 Promise,同一快照内并发/重复的 writeAttempt 共享同一次写入
132
+ // (Map 的 has/set 之间没有 await,JS 单线程语义下不会重复起两次写)。
133
+ const sourceStore = new Map<string, Promise<void>>();
134
+ const writer: SnapshotWriter = {
135
+ dir,
136
+ async writeAttempt(entry: AttemptEntry, artifacts?: AttemptArtifacts): Promise<void> {
137
+ await writeAttemptFiles(dir, { experimentId: meta.experimentId, startedAt: meta.startedAt }, entry, artifacts, sourceStore);
138
+ },
139
+ };
140
+ return { meta, dir, writer, declCompletedAt: decl.completedAt, declName: decl.name };
141
+ }
121
142
 
122
- return {
123
- dir,
143
+ async function snapshotImpl(decl: SnapshotDeclaration): Promise<SnapshotWriter> {
144
+ if (!decl.experimentId || !decl.agent || !decl.startedAt) {
145
+ throw new Error(
146
+ "writer.snapshot() requires experimentId, agent and startedAt. They are snapshot-level identity: declare them once here instead of on each attempt.",
147
+ );
148
+ }
149
+ const existing = pending.get(decl.experimentId);
150
+ const statePromise: Promise<SnapshotState> = existing
151
+ ? existing.then((state) => {
152
+ if (decl.knownEvalIds?.length) {
153
+ state.meta.knownEvalIds = [...new Set([...(state.meta.knownEvalIds ?? []), ...decl.knownEvalIds!])];
154
+ }
155
+ if (decl.completedAt !== undefined) state.declCompletedAt = decl.completedAt;
156
+ if (decl.name !== undefined) state.declName = decl.name;
157
+ return state;
158
+ })
159
+ : buildSnapshot(decl);
160
+ pending.set(decl.experimentId, statePromise);
161
+ const state = await statePromise;
162
+ return state.writer;
163
+ }
124
164
 
125
- snapshot(decl: SnapshotDeclaration): SnapshotWriter {
126
- if (!decl.experiment || !decl.agent || !decl.startedAt) {
127
- throw new Error("writer.snapshot() requires experiment, agent and startedAt. They are snapshot-level identity: declare them once here instead of on each attempt.");
128
- }
129
- decls.push(decl);
130
- return {
131
- async writeAttempt(entry: AttemptEntry, artifacts?: AttemptArtifacts): Promise<void> {
132
- // 快照级字段注入用「缺才补」:参数类型上它们不存在(Omit),运行时保守处理,
133
- // 不覆盖调用方带来的值,也不打乱既有键序。
134
- const full = { ...entry } as EvalResult;
135
- full.experimentId ??= decl.experiment;
136
- full.agent ??= decl.agent;
137
- if (full.model === undefined && decl.model !== undefined) full.model = decl.model;
138
- full.startedAt ??= decl.startedAt;
139
- await writeArtifacts(artifacts ?? {}, full);
140
- entries.push(
141
- slimEntry({
142
- ...full,
143
- events: artifacts?.events,
144
- sources: artifacts?.sources,
145
- trace: artifacts?.trace,
146
- o11y: artifacts?.o11y,
147
- diff: artifacts?.diff,
148
- }),
149
- );
150
- },
151
- };
152
- },
165
+ async function writeAttemptForImpl(result: EvalResult): Promise<void> {
166
+ if (!result.experimentId) {
167
+ throw new Error(
168
+ `writeAttemptFor() requires EvalResult.experimentId (results schemaVersion ${RESULTS_SCHEMA_VERSION} lays out one directory per experiment); eval "${result.id}" has none.`,
169
+ );
170
+ }
171
+ const snap = await snapshotImpl({
172
+ experimentId: result.experimentId,
173
+ agent: result.agent,
174
+ model: result.model,
175
+ // 快照 startedAt 以该实验首条落盘结果的 attempt 时刻为锚(首条 ≈ 实验开跑)。
176
+ startedAt: result.startedAt ?? new Date().toISOString(),
177
+ experiment: result.experiment,
178
+ });
179
+
180
+ if (result.artifactBase) {
181
+ // 携带条目(--resume 合入):本轮没有任何新数据,不写 artifact、不重算 has*,
182
+ // startedAt(身份锚)与 artifactBase 原样保留。locator 同理原样保留(在 ...rest 里,
183
+ // 没被解构掉)、从不重算——`result` 是上一轮 openResults() 读回的记录,原快照的
184
+ // startedAt 已经不在本轮快照里了,重算会用错的 snapshotStartedAt 算出不同的字符串,
185
+ // 让已经发布/引用过的 locator 失效。真缺失(没经过 openResults 的手工构造)时如实留空,
186
+ // 交给读取面按当前身份兜底算(见 open.ts 的 locator 回填),不在这里瞎猜。
187
+ const { agent, model, experimentId, experiment, events, sources, o11y, trace, agentSetup, diff, rawTranscript, ...rest } =
188
+ result;
189
+ void agent;
190
+ void model;
191
+ void experimentId;
192
+ void experiment;
193
+ void events;
194
+ void sources;
195
+ void o11y;
196
+ void trace;
197
+ void agentSetup;
198
+ void diff;
199
+ void rawTranscript;
200
+ const attemptDir = join(snap.dir, attemptDirOf(result));
201
+ await mkdir(attemptDir, { recursive: true });
202
+ await writeFile(join(attemptDir, RESULT_FILE), JSON.stringify(rest, null, 2), "utf-8");
203
+ return;
204
+ }
153
205
 
154
- async writeAttemptArtifacts(result: EvalResult): Promise<void> {
155
- await writeArtifacts(result, result);
206
+ const {
207
+ agent,
208
+ model,
209
+ startedAt,
210
+ experimentId,
211
+ experiment,
212
+ events,
213
+ sources,
214
+ o11y,
215
+ trace,
216
+ agentSetup,
217
+ diff,
218
+ rawTranscript,
219
+ artifactBase,
220
+ hasTrace,
221
+ hasEvents,
222
+ hasSources,
223
+ ...entry
224
+ } = result;
225
+ void agent;
226
+ void model;
227
+ void experimentId;
228
+ void experiment;
229
+ void artifactBase;
230
+ void hasTrace;
231
+ void hasEvents;
232
+ void hasSources;
233
+ // startedAt 是 attempt 级事实(每条各异,view 靠它显示「何时跑的」),原样落盘;
234
+ // 读取面只在记录缺失时才回退快照的 startedAt。
235
+ const record = { ...entry, ...(startedAt !== undefined ? { startedAt } : {}) };
236
+ await snap.writeAttempt(record as AttemptEntry, { events, sources, o11y, trace, agentSetup, diff });
237
+ }
238
+
239
+ return {
240
+ snapshot: snapshotImpl,
241
+ writeAttemptFor: writeAttemptForImpl,
242
+ snapshotDirs(): { experimentId: string; dir: string }[] {
243
+ return [...created];
156
244
  },
157
245
 
158
- async finish(overrides: FinishOverrides = {}): Promise<RunSummary> {
159
- if (finished) throw new Error(`finish() already called for run directory ${dir}.`);
246
+ async finish(finishOpts?: { name?: LocalizedText }): Promise<void> {
247
+ if (finished) throw new Error("writer.finish() was already called.");
160
248
  finished = true;
249
+ const states = await Promise.all([...pending.values()]);
250
+ await Promise.all(
251
+ states.map(async (state) => {
252
+ const completedAt = state.declCompletedAt ?? new Date().toISOString();
253
+ const name = finishOpts?.name ?? state.declName;
254
+ const finalMeta: SnapshotMeta = {
255
+ format: state.meta.format,
256
+ schemaVersion: state.meta.schemaVersion,
257
+ producer: state.meta.producer,
258
+ experimentId: state.meta.experimentId,
259
+ ...(state.meta.experiment !== undefined ? { experiment: state.meta.experiment } : {}),
260
+ agent: state.meta.agent,
261
+ ...(state.meta.model !== undefined ? { model: state.meta.model } : {}),
262
+ startedAt: state.meta.startedAt,
263
+ completedAt,
264
+ ...(state.meta.knownEvalIds?.length ? { knownEvalIds: state.meta.knownEvalIds } : {}),
265
+ ...(name !== undefined ? { name } : {}),
266
+ };
267
+ state.meta = finalMeta;
268
+ await writeFile(join(state.dir, SNAPSHOT_FILE), JSON.stringify(finalMeta, null, 2), "utf-8");
269
+ }),
270
+ );
271
+ },
272
+ };
273
+ }
161
274
 
162
- const results = overrides.results ? overrides.results.map(slimEntry) : entries;
163
- const counts = { passed: 0, failed: 0, skipped: 0, errored: 0 };
164
- let inTok = 0;
165
- let outTok = 0;
166
- let cost = 0;
167
- let duration = 0;
168
- for (const r of results) {
169
- counts[r.outcome] += 1;
170
- inTok += r.usage?.inputTokens ?? 0;
171
- outTok += r.usage?.outputTokens ?? 0;
172
- cost += r.estimatedCostUSD ?? 0;
173
- duration += r.durationMs ?? 0;
174
- }
275
+ /** 一条 attempt 的落盘:拆 artifact 文件、算 has*、写 result.json;空数据不落文件。 */
276
+ async function writeAttemptFiles(
277
+ snapDir: string,
278
+ snapshot: { experimentId: string; startedAt: string },
279
+ entry: AttemptEntry,
280
+ artifacts: AttemptArtifacts | undefined,
281
+ sourceStore: Map<string, Promise<void>>,
282
+ ): Promise<void> {
283
+ const attemptDir = join(snapDir, attemptDirOf(entry));
284
+ await mkdir(attemptDir, { recursive: true });
175
285
 
176
- const startedAt =
177
- overrides.startedAt ??
178
- decls.map((d) => d.startedAt).sort()[0] ??
179
- createdAt.toISOString();
180
- const agent = overrides.agent ?? decls[0]?.agent ?? results[0]?.agent ?? "";
181
- const model = overrides.model ?? decls[0]?.model;
182
- const estimatedCostUSD = overrides.estimatedCostUSD ?? (cost || undefined);
183
- const snapshotsMeta = buildSnapshotsMeta(decls, startedAt);
286
+ const hasEvents = !!(artifacts?.events && artifacts.events.length);
287
+ const hasSources = !!(artifacts?.sources && artifacts.sources.length);
288
+ const hasTrace = !!(artifacts?.trace && artifacts.trace.length);
184
289
 
185
- // 键序是持久化契约的一部分(与 runner 直写时代逐字节一致):format / schemaVersion /
186
- // producer 在最前,results outputDir 收尾,可选键缺席时不留 undefined 占位。
187
- const summary: RunSummary = {
188
- format: RESULTS_FORMAT,
189
- schemaVersion: RESULTS_SCHEMA_VERSION,
190
- producer: opts.producer,
191
- ...(overrides.name !== undefined ? { name: overrides.name } : {}),
192
- agent,
193
- ...(model !== undefined ? { model } : {}),
194
- startedAt,
195
- completedAt: overrides.completedAt ?? new Date().toISOString(),
196
- passed: counts.passed,
197
- failed: counts.failed,
198
- skipped: counts.skipped,
199
- errored: counts.errored,
200
- durationMs: overrides.durationMs ?? duration,
201
- usage: overrides.usage ?? { inputTokens: inTok, outputTokens: outTok },
202
- ...(estimatedCostUSD !== undefined ? { estimatedCostUSD } : {}),
203
- results,
204
- outputDir: dir,
205
- ...(snapshotsMeta ? { snapshots: snapshotsMeta } : {}),
206
- };
207
- await writeFile(join(dir, "summary.json"), JSON.stringify(summary, null, 2), "utf-8");
208
- return summary;
209
- },
210
- };
290
+ const writes: Promise<unknown>[] = [];
291
+ if (hasEvents) writes.push(writeFile(join(attemptDir, "events.json"), JSON.stringify(artifacts!.events), "utf-8"));
292
+ if (hasSources) writes.push(writeSourcesRef(snapDir, attemptDir, artifacts!.sources!, sourceStore));
293
+ if (hasTrace) writes.push(writeFile(join(attemptDir, "trace.json"), JSON.stringify(artifacts!.trace), "utf-8"));
294
+ if (artifacts?.o11y) writes.push(writeFile(join(attemptDir, "o11y.json"), JSON.stringify(artifacts.o11y), "utf-8"));
295
+ if (artifacts?.agentSetup) {
296
+ writes.push(
297
+ writeFile(join(attemptDir, artifactFileOf("agentSetup")), JSON.stringify(artifacts.agentSetup), "utf-8"),
298
+ );
299
+ }
300
+ if (artifacts?.diff) writes.push(writeFile(join(attemptDir, "diff.json"), JSON.stringify(artifacts.diff), "utf-8"));
301
+ await Promise.all(writes);
302
+
303
+ // locator:caller(如第三方 harness 直接调 SnapshotWriter.writeAttempt)已经带了就尊重,
304
+ // 否则按当前身份元组算一份 —— 这条路径只服务「非携带」的新写入,携带条目走
305
+ // writeAttemptForImpl artifactBase 分支,原样透传 result.locator,从不落到这里重算。
306
+ const locator =
307
+ entry.locator ??
308
+ encodeAttemptLocator({
309
+ experimentId: snapshot.experimentId,
310
+ snapshotStartedAt: snapshot.startedAt,
311
+ evalId: entry.id,
312
+ attempt: entry.attempt,
313
+ });
314
+
315
+ const record = { ...entry, locator, hasEvents, hasTrace, hasSources };
316
+ await writeFile(join(attemptDir, RESULT_FILE), JSON.stringify(record, null, 2), "utf-8");
211
317
  }
212
318
 
213
319
  /**
214
- * 瘦身条目:去掉内联大字段,回填 artifactsDir has* 引用。
215
- * 携带条目(--resume 合入,rest 上带着 artifactBase 指向原 run 的工件目录)原样保留:
216
- * 本轮没有任何新数据,不能重新推导出 false has*,更不能编出一个本轮没写过文件的 artifactsDir。
320
+ * sources 是唯一「两层」的 artifact:attempt 目录下只落一份小引用(`{path, sha256}[]`),
321
+ * 真正的源码内容按 sha256 去重存进快照根的 `sources/<sha256>.json`——同一快照内多个 attempt
322
+ * 引用同一份 eval 源码(同文件、同内容)只写一次盘。sourceStore 是这个快照专属的去重登记表
323
+ * (调用方按快照生命周期传入同一个 Map),覆盖并发与重复两种场景。
217
324
  */
218
- export function slimEntry(r: EvalResult): EvalResult {
219
- const { events, sources, o11y, trace, diff, rawTranscript, ...rest } = r;
220
- void rawTranscript;
221
- if (rest.artifactBase) return rest;
222
- return {
223
- ...rest,
224
- artifactsDir: attemptDirOf(r),
225
- hasTrace: !!(trace && trace.length),
226
- hasEvents: !!(events && events.length),
227
- hasSources: !!(sources && sources.length),
228
- };
325
+ async function writeSourcesRef(
326
+ snapDir: string,
327
+ attemptDir: string,
328
+ sources: SourceArtifact[],
329
+ sourceStore: Map<string, Promise<void>>,
330
+ ): Promise<void> {
331
+ const storeDir = join(snapDir, "sources");
332
+ const refs: { path: string; sha256: string }[] = [];
333
+ for (const src of sources) {
334
+ const sha256 = hashEvalSource(normalizeEvalSource(src.content));
335
+ refs.push({ path: src.path, sha256 });
336
+ if (!sourceStore.has(sha256)) {
337
+ sourceStore.set(
338
+ sha256,
339
+ (async () => {
340
+ await mkdir(storeDir, { recursive: true });
341
+ await writeFile(join(storeDir, `${sha256}.json`), JSON.stringify({ content: src.content }), "utf-8");
342
+ })(),
343
+ );
344
+ }
345
+ }
346
+ await Promise.all(refs.map((r) => sourceStore.get(r.sha256)!));
347
+ await writeFile(join(attemptDir, artifactFileOf("sources")), JSON.stringify(refs), "utf-8");
229
348
  }
230
349
 
231
- /** 快照级元数据落盘:startedAt 只在与顶层不同( run 多快照、时刻不同)时记;空 meta 不落字段。 */
232
- function buildSnapshotsMeta(
233
- decls: SnapshotDeclaration[],
234
- summaryStartedAt: string,
235
- ): RunSummary["snapshots"] | undefined {
236
- const meta: NonNullable<RunSummary["snapshots"]> = {};
237
- let any = false;
238
- for (const decl of decls) {
239
- const entry: { startedAt?: string; knownEvalIds?: string[] } = {};
240
- if (decl.startedAt !== summaryStartedAt) entry.startedAt = decl.startedAt;
241
- if (decl.knownEvalIds?.length) entry.knownEvalIds = [...decl.knownEvalIds];
242
- if (Object.keys(entry).length > 0) {
243
- // 同一 experiment 声明多次时后声明覆盖(knownEvalIds 取并集,覆盖事实只会变大不会缩水)。
244
- const existing = meta[decl.experiment];
245
- if (existing?.knownEvalIds && entry.knownEvalIds) {
246
- entry.knownEvalIds = [...new Set([...existing.knownEvalIds, ...entry.knownEvalIds])];
247
- }
248
- meta[decl.experiment] = { ...existing, ...entry };
249
- any = true;
350
+ /** 快照目录:独占创建(EEXIST 换随机后缀重试,≤5 ) */
351
+ async function createSnapshotDir(root: string, experimentId: string): Promise<string> {
352
+ const parent = join(root, experimentDirOf(experimentId));
353
+ await mkdir(parent, { recursive: true });
354
+ let lastError: unknown;
355
+ for (let i = 0; i < 5; i++) {
356
+ const dir = join(parent, `${safeTimestamp(new Date())}-${randomSuffix()}`);
357
+ try {
358
+ await mkdir(dir);
359
+ return dir;
360
+ } catch (e) {
361
+ if ((e as NodeJS.ErrnoException)?.code !== "EEXIST") throw e;
362
+ lastError = e;
250
363
  }
251
364
  }
252
- return any ? meta : undefined;
365
+ throw new Error(`Could not create a unique snapshot directory under "${parent}" after 5 attempts (${String(lastError)}).`);
253
366
  }
254
367
 
255
- /** run 目录名:Date#toISOString 把 : . 换成 -(与 docs/results-format.md 一致)。 */
256
- export function safeTimestamp(d: Date): string {
368
+ /** 运行配置落盘前剥掉 id:experimentId 的家在 snapshot.json 顶层。 */
369
+ function stripInfoId(info: ExperimentRunInfo): ExperimentRunInfo {
370
+ const { id, ...rest } = info;
371
+ void id;
372
+ return rest;
373
+ }
374
+
375
+ /** 快照目录名的时间戳段:Date#toISOString 把 : 与 . 换成 -(与 docs/feature/results/architecture.md 一致)。 */
376
+ function safeTimestamp(d: Date): string {
257
377
  return d.toISOString().replace(/[:.]/g, "-");
258
378
  }
379
+
380
+ /** 快照目录名的随机后缀:4 位 [a-z0-9]。 */
381
+ function randomSuffix(): string {
382
+ const chars = "abcdefghijklmnopqrstuvwxyz0123456789";
383
+ let out = "";
384
+ for (let i = 0; i < 4; i++) out += chars[Math.floor(Math.random() * chars.length)];
385
+ return out;
386
+ }