niceeval 0.6.1 → 0.7.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 (378) hide show
  1. package/INDEX.md +23 -23
  2. package/README.zh.md +6 -6
  3. package/dist/agents/types.d.ts +69 -7
  4. package/dist/context/types.d.ts +32 -12
  5. package/dist/i18n/en.d.ts +54 -0
  6. package/dist/i18n/zh-CN.d.ts +57 -3
  7. package/dist/o11y/types.d.ts +16 -2
  8. package/dist/report/aggregate.d.ts +32 -24
  9. package/dist/report/aggregate.js +158 -50
  10. package/dist/report/built-in/index.d.ts +2 -0
  11. package/dist/report/built-in/index.js +8 -0
  12. package/dist/report/components.d.ts +93 -160
  13. package/dist/report/components.js +377 -114
  14. package/dist/report/compute.d.ts +87 -81
  15. package/dist/report/compute.js +597 -417
  16. package/dist/report/flag.d.ts +32 -6
  17. package/dist/report/flag.js +92 -4
  18. package/dist/report/format.d.ts +19 -11
  19. package/dist/report/format.js +30 -13
  20. package/dist/report/index.d.ts +16 -16
  21. package/dist/report/index.js +20 -21
  22. package/dist/report/load.js +3 -2
  23. package/dist/report/locale.d.ts +57 -33
  24. package/dist/report/locale.js +122 -56
  25. package/dist/report/metrics.d.ts +23 -4
  26. package/dist/report/metrics.js +110 -25
  27. package/dist/report/primitives.d.ts +48 -15
  28. package/dist/report/primitives.js +135 -26
  29. package/dist/report/react/AttemptList.d.ts +9 -7
  30. package/dist/report/react/AttemptList.js +17 -10
  31. package/dist/report/react/DeltaTable.js +19 -18
  32. package/dist/report/react/EvalList.d.ts +4 -4
  33. package/dist/report/react/EvalList.js +0 -0
  34. package/dist/report/react/ExperimentComparison.d.ts +10 -0
  35. package/dist/report/react/ExperimentComparison.js +12 -0
  36. package/dist/report/react/ExperimentList.d.ts +4 -3
  37. package/dist/report/react/ExperimentList.js +17 -18
  38. package/dist/report/react/MetricBars.js +5 -4
  39. package/dist/report/react/MetricLine.js +12 -5
  40. package/dist/report/react/MetricMatrix.js +1 -1
  41. package/dist/report/react/MetricScatter.js +59 -28
  42. package/dist/report/react/MetricTable.js +2 -12
  43. package/dist/report/react/ScopeSummary.d.ts +10 -0
  44. package/dist/report/react/ScopeSummary.js +17 -0
  45. package/dist/report/react/Scoreboard.js +6 -6
  46. package/dist/report/react/cell.js +2 -2
  47. package/dist/report/react/chart-math.d.ts +23 -6
  48. package/dist/report/react/chart-math.js +71 -19
  49. package/dist/report/react/fixtures.d.ts +5 -9
  50. package/dist/report/react/fixtures.js +110 -147
  51. package/dist/report/react/index.d.ts +15 -5
  52. package/dist/report/react/index.js +18 -7
  53. package/dist/report/report.d.ts +137 -16
  54. package/dist/report/report.js +259 -28
  55. package/dist/report/text/faces.d.ts +17 -19
  56. package/dist/report/text/faces.js +253 -184
  57. package/dist/report/text/plot.js +1 -1
  58. package/dist/report/text/table.js +38 -7
  59. package/dist/report/tree.d.ts +90 -40
  60. package/dist/report/tree.js +252 -94
  61. package/dist/report/types.d.ts +247 -284
  62. package/dist/report/types.js +4 -3
  63. package/dist/report/web.d.ts +21 -5
  64. package/dist/report/web.js +42 -16
  65. package/dist/results/select.d.ts +38 -16
  66. package/dist/results/select.js +73 -25
  67. package/dist/results/types.d.ts +49 -14
  68. package/dist/runner/feedback/sink.d.ts +110 -0
  69. package/dist/runner/types.d.ts +513 -22
  70. package/dist/sandbox/docker.d.ts +23 -2
  71. package/dist/sandbox/e2b.d.ts +15 -1
  72. package/dist/sandbox/errors.d.ts +30 -3
  73. package/dist/sandbox/io-retry.d.ts +17 -0
  74. package/dist/sandbox/registry.d.ts +2 -0
  75. package/dist/sandbox/resolve.d.ts +18 -5
  76. package/dist/sandbox/retry.d.ts +11 -1
  77. package/dist/sandbox/types.d.ts +39 -5
  78. package/dist/sandbox/vercel.d.ts +7 -1
  79. package/dist/scoring/coverage.d.ts +30 -0
  80. package/dist/scoring/display.d.ts +21 -0
  81. package/dist/scoring/display.js +120 -0
  82. package/dist/scoring/types.d.ts +103 -20
  83. package/dist/shared/aggregate.d.ts +4 -2
  84. package/dist/shared/aggregate.js +8 -7
  85. package/dist/shared/types.d.ts +28 -0
  86. package/dist/tty-line.d.ts +0 -4
  87. package/dist/util.d.ts +23 -0
  88. package/docs-site/zh/README.md +44 -0
  89. package/docs-site/zh/examples/ai-agent-application.mdx +63 -0
  90. package/docs-site/zh/examples/coding-agent-extensions.mdx +57 -0
  91. package/docs-site/zh/examples/index.mdx +50 -0
  92. package/docs-site/zh/{concepts → explanation}/adapter.mdx +31 -13
  93. package/docs-site/zh/{concepts → explanation}/assert.mdx +7 -7
  94. package/docs-site/zh/{concepts → explanation}/drive.mdx +8 -8
  95. package/docs-site/zh/{concepts → explanation}/evals.mdx +4 -4
  96. package/docs-site/zh/{concepts → explanation}/experiment.mdx +8 -8
  97. package/docs-site/zh/{concepts → explanation}/hitl.mdx +8 -8
  98. package/docs-site/zh/{concepts → explanation}/judge.mdx +5 -5
  99. package/docs-site/zh/{concepts → explanation}/overview.mdx +11 -11
  100. package/docs-site/zh/{guides → explanation}/runner.mdx +18 -8
  101. package/docs-site/zh/{concepts → explanation}/tier.mdx +6 -6
  102. package/docs-site/zh/{guides → how-to}/agent-feedback-loop.mdx +35 -33
  103. package/docs-site/zh/{guides → how-to}/authoring.mdx +35 -2
  104. package/docs-site/zh/{guides → how-to}/ci-integration.mdx +23 -12
  105. package/docs-site/zh/{guides → how-to}/connect-otel.mdx +6 -6
  106. package/docs-site/zh/{guides → how-to}/connect-your-agent.mdx +47 -21
  107. package/docs-site/zh/{guides → how-to}/custom-reports.mdx +34 -39
  108. package/docs-site/zh/{guides → how-to}/dataset-fanout.mdx +25 -3
  109. package/docs-site/zh/{guides → how-to}/experiments.mdx +12 -5
  110. package/docs-site/zh/how-to/publish-report.mdx +105 -0
  111. package/docs-site/zh/{guides → how-to}/reporters.mdx +2 -2
  112. package/docs-site/zh/{guides → how-to}/sandbox-agent.mdx +56 -7
  113. package/docs-site/zh/how-to/sandbox-providers.mdx +350 -0
  114. package/docs-site/zh/{guides → how-to}/scoring-guide.mdx +4 -4
  115. package/docs-site/zh/{guides → how-to}/viewing-results.mdx +82 -39
  116. package/docs-site/zh/{guides → how-to}/write-experiment.mdx +6 -4
  117. package/docs-site/zh/{guides → how-to}/write-send.mdx +30 -14
  118. package/docs-site/zh/index.mdx +24 -26
  119. package/docs-site/zh/introduction.mdx +8 -8
  120. package/docs-site/zh/reference/builtin-agents.mdx +32 -5
  121. package/docs-site/zh/reference/capabilities.mdx +8 -8
  122. package/docs-site/zh/reference/cli.mdx +40 -12
  123. package/docs-site/zh/reference/define-agent.mdx +58 -5
  124. package/docs-site/zh/reference/define-config.mdx +1 -1
  125. package/docs-site/zh/reference/define-eval.mdx +42 -9
  126. package/docs-site/zh/reference/events.mdx +3 -3
  127. package/docs-site/zh/reference/expect.mdx +26 -1
  128. package/docs-site/zh/{guides → reference}/official-adapters.mdx +32 -8
  129. package/docs-site/zh/{guides → reference}/report-components.mdx +45 -33
  130. package/docs-site/zh/{guides → reference}/results-data.mdx +21 -13
  131. package/docs-site/zh/troubleshooting/debug-sandbox.mdx +57 -0
  132. package/docs-site/zh/troubleshooting/debugging.mdx +212 -0
  133. package/docs-site/zh/{quickstart.mdx → tutorials/quickstart.mdx} +5 -17
  134. package/package.json +10 -2
  135. package/src/agents/ai-sdk-otel.test.ts +1 -0
  136. package/src/agents/ai-sdk.test.ts +3 -0
  137. package/src/agents/ai-sdk.ts +3 -0
  138. package/src/agents/bub-install-spec.test.ts +34 -0
  139. package/src/agents/bub-install-spec.ts +32 -0
  140. package/src/agents/bub.ts +31 -32
  141. package/src/agents/claude-code.test.ts +130 -9
  142. package/src/agents/claude-code.ts +76 -4
  143. package/src/agents/codex.test.ts +189 -40
  144. package/src/agents/codex.ts +155 -14
  145. package/src/agents/coding-cli-versions.test.ts +15 -0
  146. package/src/agents/coding-cli-versions.ts +3 -0
  147. package/src/agents/index.ts +13 -2
  148. package/src/agents/langgraph.test.ts +204 -0
  149. package/src/agents/langgraph.ts +495 -0
  150. package/src/agents/marketplace.ts +85 -0
  151. package/src/agents/native-config.test.ts +179 -0
  152. package/src/agents/native-config.ts +267 -0
  153. package/src/agents/openai-compat.test.ts +1 -0
  154. package/src/agents/openai-compat.ts +1 -1
  155. package/src/agents/openclaw.test.ts +31 -0
  156. package/src/agents/openclaw.ts +171 -0
  157. package/src/agents/plugin-config.test.ts +1 -0
  158. package/src/agents/sdk-streams.test.ts +79 -0
  159. package/src/agents/sdk-streams.ts +55 -10
  160. package/src/agents/skills.test.ts +1 -0
  161. package/src/agents/streaming.test.ts +3 -9
  162. package/src/agents/streaming.ts +2 -2
  163. package/src/agents/types.ts +71 -8
  164. package/src/agents/ui-message-stream.test.ts +3 -0
  165. package/src/cli.ts +446 -124
  166. package/src/context/context.test.ts +51 -12
  167. package/src/context/context.ts +162 -30
  168. package/src/context/session.test.ts +2 -1
  169. package/src/context/session.ts +115 -7
  170. package/src/context/types.ts +30 -12
  171. package/src/define.test.ts +13 -8
  172. package/src/define.ts +25 -4
  173. package/src/expect/index.ts +53 -23
  174. package/src/i18n/en.ts +81 -17
  175. package/src/i18n/zh-CN.ts +80 -17
  176. package/src/o11y/cost.test.ts +1 -0
  177. package/src/o11y/execution-tree.test.ts +1 -20
  178. package/src/o11y/otlp/mappers/claude-code.test.ts +1 -0
  179. package/src/o11y/otlp/parse.test.ts +1 -0
  180. package/src/o11y/otlp/turn-otel.test.ts +1 -0
  181. package/src/o11y/parsers/bub.test.ts +1 -0
  182. package/src/o11y/parsers/claude-code.test.ts +1 -34
  183. package/src/o11y/parsers/openclaw.test.ts +154 -0
  184. package/src/o11y/parsers/openclaw.ts +310 -0
  185. package/src/o11y/prices.json +746 -311
  186. package/src/o11y/tool-names.test.ts +1 -0
  187. package/src/o11y/types.ts +16 -2
  188. package/src/report/aggregate.ts +178 -61
  189. package/src/report/built-in/index.tsx +9 -0
  190. package/src/report/components.tsx +625 -279
  191. package/src/report/compute.ts +723 -491
  192. package/src/report/dual-render.test.tsx +741 -1024
  193. package/src/report/flag.ts +104 -12
  194. package/src/report/format.ts +32 -12
  195. package/src/report/index.ts +119 -46
  196. package/src/report/load.ts +3 -2
  197. package/src/report/locale.ts +136 -65
  198. package/src/report/metrics.ts +108 -25
  199. package/src/report/primitives.tsx +196 -45
  200. package/src/report/react/AttemptList.tsx +30 -43
  201. package/src/report/react/DeltaTable.tsx +63 -45
  202. package/src/report/react/EvalList.tsx +0 -0
  203. package/src/report/react/ExperimentComparison.tsx +73 -0
  204. package/src/report/react/ExperimentList.tsx +50 -32
  205. package/src/report/react/MetricBars.tsx +5 -4
  206. package/src/report/react/MetricLine.tsx +13 -8
  207. package/src/report/react/MetricMatrix.tsx +2 -2
  208. package/src/report/react/MetricScatter.tsx +86 -34
  209. package/src/report/react/MetricTable.tsx +4 -76
  210. package/src/report/react/ScopeSummary.tsx +86 -0
  211. package/src/report/react/Scoreboard.tsx +28 -10
  212. package/src/report/react/cell.tsx +2 -2
  213. package/src/report/react/chart-math.test.ts +85 -0
  214. package/src/report/react/chart-math.ts +101 -22
  215. package/src/report/react/enhance.js +89 -5
  216. package/src/report/react/fixtures.ts +114 -154
  217. package/src/report/react/index.tsx +24 -39
  218. package/src/report/react/render.test.tsx +138 -158
  219. package/src/report/react/styles.css +243 -82
  220. package/src/report/report.test.ts +779 -841
  221. package/src/report/report.ts +423 -41
  222. package/src/report/text/faces.ts +290 -193
  223. package/src/report/text/plot.ts +1 -1
  224. package/src/report/text/table.ts +44 -7
  225. package/src/report/tree.ts +362 -104
  226. package/src/report/types.ts +261 -271
  227. package/src/report/web.ts +63 -20
  228. package/src/results/annotated-source.test.ts +62 -9
  229. package/src/results/annotated-source.ts +64 -6
  230. package/src/results/attempt-evidence.test.ts +13 -11
  231. package/src/results/attempt-evidence.ts +20 -13
  232. package/src/results/attempt-source.ts +6 -3
  233. package/src/results/copy.ts +150 -60
  234. package/src/results/host-equivalence.test.ts +34 -20
  235. package/src/results/index.ts +12 -4
  236. package/src/results/locator.test.ts +1 -22
  237. package/src/results/open.ts +15 -5
  238. package/src/results/publish.ts +149 -0
  239. package/src/results/results.test.ts +89 -54
  240. package/src/results/select.ts +104 -34
  241. package/src/results/truncate.ts +90 -0
  242. package/src/results/types.ts +43 -14
  243. package/src/results/writer.ts +31 -13
  244. package/src/runner/attempt.test.ts +138 -7
  245. package/src/runner/attempt.ts +603 -104
  246. package/src/runner/discover.test.ts +47 -0
  247. package/src/runner/discover.ts +36 -2
  248. package/src/runner/eval-source.test.ts +1 -27
  249. package/src/runner/feedback/agent.test.ts +504 -0
  250. package/src/runner/feedback/agent.ts +409 -0
  251. package/src/runner/feedback/ci.test.ts +562 -0
  252. package/src/runner/feedback/ci.ts +401 -0
  253. package/src/runner/feedback/coordinator.test.ts +317 -0
  254. package/src/runner/feedback/coordinator.ts +397 -0
  255. package/src/runner/feedback/failure.ts +40 -0
  256. package/src/runner/feedback/human.test.ts +616 -0
  257. package/src/runner/feedback/human.ts +535 -0
  258. package/src/runner/feedback/index.ts +66 -0
  259. package/src/runner/feedback/io.ts +78 -0
  260. package/src/runner/feedback/profile.test.ts +50 -0
  261. package/src/runner/feedback/profile.ts +58 -0
  262. package/src/runner/feedback/reducer.test.ts +395 -0
  263. package/src/runner/feedback/reducer.ts +260 -0
  264. package/src/runner/feedback/renderer.ts +82 -0
  265. package/src/runner/feedback/sink.ts +203 -0
  266. package/src/runner/feedback/testing.ts +106 -0
  267. package/src/runner/ledger.test.ts +230 -0
  268. package/src/runner/ledger.ts +329 -0
  269. package/src/runner/report.test.ts +128 -3
  270. package/src/runner/report.ts +33 -9
  271. package/src/runner/reporters/artifacts.ts +8 -2
  272. package/src/runner/reporters/braintrust.test.ts +8 -7
  273. package/src/runner/reporters/braintrust.ts +9 -2
  274. package/src/runner/reporters/index.ts +2 -2
  275. package/src/runner/reporters/json.test.ts +162 -0
  276. package/src/runner/reporters/json.ts +35 -8
  277. package/src/runner/reporters/shared.ts +1 -5
  278. package/src/runner/run.test.ts +760 -3
  279. package/src/runner/run.ts +243 -37
  280. package/src/runner/sandbox-prep.ts +3 -42
  281. package/src/runner/timing.ts +158 -0
  282. package/src/runner/types.ts +518 -22
  283. package/src/sandbox/checkpoint.test.ts +55 -0
  284. package/src/sandbox/checkpoint.ts +29 -8
  285. package/src/sandbox/cli-commands.ts +407 -0
  286. package/src/sandbox/docker.ts +115 -16
  287. package/src/sandbox/e2b-agent-template.test.ts +56 -0
  288. package/src/sandbox/e2b-agent-template.ts +94 -0
  289. package/src/sandbox/e2b.ts +74 -9
  290. package/src/sandbox/errors.ts +111 -4
  291. package/src/sandbox/index.ts +2 -0
  292. package/src/sandbox/io-retry.test.ts +58 -0
  293. package/src/sandbox/io-retry.ts +45 -0
  294. package/src/sandbox/keep-registry.test.ts +86 -0
  295. package/src/sandbox/keep-registry.ts +142 -0
  296. package/src/sandbox/keep.ts +178 -0
  297. package/src/sandbox/paths.test.ts +1 -0
  298. package/src/sandbox/paths.ts +19 -8
  299. package/src/sandbox/registry.ts +20 -3
  300. package/src/sandbox/resolve.ts +76 -11
  301. package/src/sandbox/retry.test.ts +70 -0
  302. package/src/sandbox/retry.ts +46 -4
  303. package/src/sandbox/types.ts +44 -6
  304. package/src/sandbox/vercel.ts +43 -20
  305. package/src/scoring/collector.ts +60 -17
  306. package/src/scoring/coverage.ts +95 -0
  307. package/src/scoring/diff.ts +81 -0
  308. package/src/scoring/display.test.ts +121 -0
  309. package/src/scoring/display.ts +133 -0
  310. package/src/scoring/evidence.test.ts +189 -0
  311. package/src/scoring/judge.test.ts +142 -0
  312. package/src/scoring/judge.ts +15 -18
  313. package/src/scoring/scoped.ts +217 -50
  314. package/src/scoring/types.ts +117 -20
  315. package/src/scoring/verdict.ts +16 -4
  316. package/src/shared/aggregate.ts +8 -6
  317. package/src/shared/types.ts +31 -0
  318. package/src/show/compose.ts +50 -67
  319. package/src/show/index.ts +127 -56
  320. package/src/show/render.ts +662 -131
  321. package/src/show/report-host.test.ts +188 -0
  322. package/src/show/report-host.ts +375 -0
  323. package/src/show/show.test.ts +320 -54
  324. package/src/tty-line.ts +8 -26
  325. package/src/util.test.ts +1 -0
  326. package/src/util.ts +41 -0
  327. package/src/view/app/App.test.tsx +69 -0
  328. package/src/view/app/App.tsx +144 -48
  329. package/src/view/app/components/AttemptModal.tsx +423 -11
  330. package/src/view/app/components/CodeView.tsx +41 -14
  331. package/src/view/app/components/CopyControls.tsx +2 -2
  332. package/src/view/app/i18n.ts +37 -17
  333. package/src/view/app/lib/attempt-route.test.ts +1 -0
  334. package/src/view/app/lib/verdict.ts +7 -9
  335. package/src/view/app/main.tsx +13 -8
  336. package/src/view/app/pages/{RunsPage.tsx → AttemptsPage.tsx} +6 -6
  337. package/src/view/app/types.ts +4 -1
  338. package/src/view/artifact-serving.test.ts +2 -1
  339. package/src/view/client-dist/app.css +1 -1
  340. package/src/view/client-dist/app.js +17 -17
  341. package/src/view/data.test.ts +10 -3
  342. package/src/view/data.ts +155 -49
  343. package/src/view/index.ts +56 -41
  344. package/src/view/server.ts +37 -15
  345. package/src/view/shared/types.ts +34 -5
  346. package/src/view/styles.css +227 -0
  347. package/src/view/view-report.test.ts +167 -62
  348. package/dist/report/built-ins/experiment-comparison.d.ts +0 -1
  349. package/dist/report/built-ins/experiment-comparison.js +0 -13
  350. package/dist/report/built-ins/index.d.ts +0 -1
  351. package/dist/report/built-ins/index.js +0 -2
  352. package/dist/report/react/GroupSummary.d.ts +0 -8
  353. package/dist/report/react/GroupSummary.js +0 -8
  354. package/dist/report/react/RunOverview.d.ts +0 -8
  355. package/dist/report/react/RunOverview.js +0 -12
  356. package/docs-site/zh/example/ai-agent-application.mdx +0 -152
  357. package/docs-site/zh/example/claude-code-codex-plugin.mdx +0 -167
  358. package/docs-site/zh/example/claude-code-codex-skill.mdx +0 -152
  359. package/docs-site/zh/example/showcase.mdx +0 -39
  360. package/docs-site/zh/guides/publish-report.mdx +0 -91
  361. package/docs-site/zh/guides/sandbox-providers.mdx +0 -102
  362. package/src/report/built-in-user-parity.test.tsx +0 -640
  363. package/src/report/built-ins/experiment-comparison.tsx +0 -19
  364. package/src/report/built-ins/index.ts +0 -2
  365. package/src/report/react/GroupSummary.tsx +0 -66
  366. package/src/report/react/RunOverview.tsx +0 -109
  367. package/src/runner/reporters/console.ts +0 -70
  368. package/src/runner/reporters/live.test.ts +0 -56
  369. package/src/runner/reporters/live.ts +0 -247
  370. package/src/runner/reporters/quiet.test.ts +0 -66
  371. package/src/runner/reporters/quiet.ts +0 -49
  372. package/src/runner/reporters/table.ts +0 -277
  373. /package/docs-site/zh/{example/tier1-ai-sdk-v7.mdx → examples/integrations/ai-sdk-v7.mdx} +0 -0
  374. /package/docs-site/zh/{example/tier1-claude-sdk.mdx → examples/integrations/claude-sdk.mdx} +0 -0
  375. /package/docs-site/zh/{example/tier1-codex-sdk.mdx → examples/integrations/codex-sdk.mdx} +0 -0
  376. /package/docs-site/zh/{example/tier1-langgraph.mdx → examples/integrations/langgraph.mdx} +0 -0
  377. /package/docs-site/zh/{example/tier1-pi-sdk.mdx → examples/integrations/pi-sdk.mdx} +0 -0
  378. /package/docs-site/zh/{guides → how-to}/fixtures.mdx +0 -0
@@ -0,0 +1,149 @@
1
+ // 发布消毒与发布预算(见 docs/feature/results/library.md「复制与瘦身:copySnapshots」)。
2
+ // 结果数据分两个等级:.niceeval/ 是本地事实根(未消毒);任何要离开本机的拷贝是发布拷贝,
3
+ // 只能经 copySnapshots 这一条管线产出,消毒在这里做且没有隐式默认(redact 必填)。
4
+ //
5
+ // redact 逐值消毒,范围由 schema 的自由文本标注决定:redactor 只对自由文本字段调用——
6
+ // 格式、判定、身份、路径、哈希这类结构字段永不经过它,发布根不会因 redact 变得不可读或
7
+ // 引用断裂。自由文本清单在下面的 FREE_TEXT 标注单点维护(与 AttemptList.redact 同一口径)。
8
+
9
+ import type { EvalResult, StreamEvent, TraceSpan } from "../types.ts";
10
+
11
+ /** 发布前整文件预检的单文件上限(50 MiB,为 GitHub 100 MB 硬限保留余量);不是可调旋钮。 */
12
+ export const PUBLISH_FILE_MAX_BYTES = 50 * 1024 * 1024;
13
+
14
+ export type Redactor = (text: string) => string;
15
+
16
+ /**
17
+ * 结构字段键名(取值是格式、判定、身份、路径、哈希——redactor 永不触碰):
18
+ * 事件的 type/callId/status/role/tool/skill/requestId/optionId、span 的 ids/kind、
19
+ * 断言的 name/severity/outcome、错误的 code/phase、locator/artifactBase/fingerprint、
20
+ * 源码路径与 sha256、provider 名、时间戳等。新增字符串字段先判断体裁再决定进不进这张表。
21
+ */
22
+ const STRUCTURAL_KEYS = new Set([
23
+ "type",
24
+ "callId",
25
+ "status",
26
+ "role",
27
+ "tool",
28
+ "requestId",
29
+ "optionId",
30
+ "id",
31
+ "traceId",
32
+ "spanId",
33
+ "parentSpanId",
34
+ "kind",
35
+ "format",
36
+ "verdict",
37
+ "severity",
38
+ "outcome",
39
+ "artifactBase",
40
+ "locator",
41
+ "fingerprint",
42
+ "sha256",
43
+ "path",
44
+ "file",
45
+ "provider",
46
+ "sandboxId",
47
+ "code",
48
+ "phase",
49
+ "level",
50
+ "agent",
51
+ "model",
52
+ "experimentId",
53
+ "startedAt",
54
+ "completedAt",
55
+ "schemaVersion",
56
+ "evalFilterFingerprint",
57
+ "reasoningEffort",
58
+ "skill",
59
+ "window",
60
+ "net",
61
+ "loc",
62
+ "dedupeKey",
63
+ ]);
64
+
65
+ /** 深度遍历 JSON 值:字符串按「键名是否结构字段」决定过不过 redactor;结构键下整棵子树跳过。 */
66
+ export function redactJsonValue(value: unknown, redact: Redactor, key?: string): unknown {
67
+ if (typeof value === "string") {
68
+ if (key !== undefined && STRUCTURAL_KEYS.has(key)) return value;
69
+ return redact(value);
70
+ }
71
+ if (Array.isArray(value)) return value.map((v) => redactJsonValue(v, redact, key));
72
+ if (value !== null && typeof value === "object") {
73
+ const out: Record<string, unknown> = {};
74
+ for (const [k, v] of Object.entries(value)) {
75
+ if (STRUCTURAL_KEYS.has(k) && typeof v === "string") {
76
+ out[k] = v;
77
+ continue;
78
+ }
79
+ out[k] = redactJsonValue(v, redact, k);
80
+ }
81
+ return out;
82
+ }
83
+ return value;
84
+ }
85
+
86
+ /** events.json 的发布消毒:消息与工具入出参是自由文本,type/callId/status 等结构字段不动。 */
87
+ export function redactEvents(events: StreamEvent[], redact: Redactor): StreamEvent[] {
88
+ return redactJsonValue(events, redact) as StreamEvent[];
89
+ }
90
+
91
+ /** trace.json 的发布消毒:属性值与可携带动态内容的 span name 过 redactor,ids/kind 不动。 */
92
+ export function redactSpans(spans: TraceSpan[], redact: Redactor): TraceSpan[] {
93
+ return spans.map((span) => ({
94
+ ...span,
95
+ name: redact(span.name),
96
+ ...(span.attributes !== undefined
97
+ ? { attributes: redactJsonValue(span.attributes, redact) as TraceSpan["attributes"] }
98
+ : {}),
99
+ }));
100
+ }
101
+
102
+ /** result.json 的发布消毒:断言 detail/evidence/expected/received、error/diagnostic 的
103
+ * message/cause/stack、skipReason、description 是自由文本;判定与身份字段不动。 */
104
+ export function redactResultRecord(record: Record<string, unknown>, redact: Redactor): Record<string, unknown> {
105
+ const r = record as Partial<EvalResult> & Record<string, unknown>;
106
+ const out: Record<string, unknown> = { ...record };
107
+ if (typeof r.description === "string") out.description = redact(r.description);
108
+ if (typeof r.skipReason === "string") out.skipReason = redact(r.skipReason);
109
+ if (Array.isArray(r.assertions)) {
110
+ out.assertions = r.assertions.map((a) => ({
111
+ ...a,
112
+ ...(a.detail !== undefined ? { detail: redact(a.detail) } : {}),
113
+ ...(a.outcome !== "unavailable" && a.evidence !== undefined ? { evidence: redact(a.evidence) } : {}),
114
+ ...(a.outcome !== "unavailable" && a.expected !== undefined ? { expected: redact(a.expected) } : {}),
115
+ ...(a.outcome !== "unavailable" && a.received !== undefined ? { received: redact(a.received) } : {}),
116
+ }));
117
+ }
118
+ if (r.error !== undefined) {
119
+ out.error = {
120
+ ...r.error,
121
+ message: redact(r.error.message),
122
+ ...(r.error.stack !== undefined ? { stack: redact(r.error.stack) } : {}),
123
+ ...(r.error.cause !== undefined ? { cause: { ...r.error.cause, message: redact(r.error.cause.message) } } : {}),
124
+ };
125
+ }
126
+ if (Array.isArray(r.diagnostics)) {
127
+ out.diagnostics = r.diagnostics.map((d) => ({
128
+ ...d,
129
+ message: redact(d.message),
130
+ ...(d.data !== undefined ? { data: redactJsonValue(d.data, redact) as typeof d.data } : {}),
131
+ }));
132
+ }
133
+ if (r.experiment !== undefined) {
134
+ out.experiment = redactExperimentInfo(r.experiment as unknown as Record<string, unknown>, redact);
135
+ }
136
+ return out;
137
+ }
138
+
139
+ /** ExperimentRunInfo 的发布消毒:description、flags 与 sandbox params 的字符串值。 */
140
+ export function redactExperimentInfo(info: Record<string, unknown>, redact: Redactor): Record<string, unknown> {
141
+ const out: Record<string, unknown> = { ...info };
142
+ if (typeof info.description === "string") out.description = redact(info.description);
143
+ if (info.flags !== undefined) out.flags = redactJsonValue(info.flags, redact);
144
+ const sandbox = info.sandbox as { provider: string; params?: Record<string, unknown> } | undefined;
145
+ if (sandbox?.params !== undefined) {
146
+ out.sandbox = { ...sandbox, params: redactJsonValue(sandbox.params, redact) };
147
+ }
148
+ return out;
149
+ }
@@ -1,3 +1,4 @@
1
+ // cases: docs/engineering/unit-tests/results/cases.md
1
2
  // niceeval/results 的单测:临时目录里构造最小 snapshot.json / result.json / artifact fixture,
2
3
  // 覆盖定稿契约(docs/feature/results/library.md、docs/feature/results/architecture.md):分层读取(含快照级字段注入)、
3
4
  // 懒加载与 artifactBase 回退、skipped 三种原因、unfinished-snapshot、latest() 三种警告、
@@ -342,7 +343,7 @@ describe("results.latest() · Selection", () => {
342
343
  const stale = latest.warnings.filter((w) => w.kind === "stale-snapshot");
343
344
  expect(stale).toHaveLength(1);
344
345
  expect(stale[0]).toMatchObject({ experimentId: "mid/a", startedAt: "2026-07-01T08:00:00.000Z", latestStartedAt: "2026-07-05T08:00:00.000Z" });
345
- expect(stale[0].message).toContain("predates the latest run in this selection by 4 days");
346
+ expect(stale[0].message).toContain("predates the latest run in this scope by 4 days");
346
347
  });
347
348
 
348
349
  it("unfinished-snapshot:选中快照缺 completedAt", async () => {
@@ -464,7 +465,7 @@ describe("createResultsWriter", () => {
464
465
  await snapA.writeAttempt({ id: "q2", verdict: "failed", attempt: 1, durationMs: 50, assertions: [] });
465
466
 
466
467
  const snapB = await writer.snapshot({ experimentId: "compare/b", agent: "codex", startedAt: "2026-07-02T09:00:00.000Z" });
467
- await snapB.writeAttempt({ id: "q1", verdict: "passed", attempt: 1, durationMs: 80, assertions: [] }, { diff: { generatedFiles: { "a.txt": "1" }, deletedFiles: [] } });
468
+ await snapB.writeAttempt({ id: "q1", verdict: "passed", attempt: 1, durationMs: 80, assertions: [] }, { diff: [{ window: "s1/t1", changes: { "a.txt": { status: "added", after: "1" } } }] });
468
469
 
469
470
  await writer.finish();
470
471
  expect(writer.snapshotDirs().map((s) => s.experimentId).sort()).toEqual(["compare/a", "compare/b"]);
@@ -506,7 +507,9 @@ describe("createResultsWriter", () => {
506
507
 
507
508
  const b = results.experiments[1].latest;
508
509
  expect(b.model).toBeUndefined();
509
- expect(await b.attempts[0].diff()).toEqual({ generatedFiles: { "a.txt": "1" }, deletedFiles: [] });
510
+ const bDiff = await b.attempts[0].diff();
511
+ expect(bDiff?.windows).toEqual([{ window: "s1/t1", changes: { "a.txt": { status: "added", after: "1" } } }]);
512
+ expect(bDiff?.get("a.txt")).toBe("1");
510
513
 
511
514
  const partial = results.latest().warnings.find((w) => w.kind === "partial-coverage")!;
512
515
  expect(partial).toMatchObject({ experimentId: "compare/a", covered: 2, total: 3 });
@@ -549,7 +552,7 @@ describe("createResultsWriter", () => {
549
552
  expect(await q2.agentSetup()).toBeNull();
550
553
 
551
554
  const dest = join(await makeRoot(), "published");
552
- await copySnapshots(results.latest(), dest, { artifacts: ["agentSetup"] });
555
+ await copySnapshots(results.latest(), dest, { ...{ artifacts: ["agentSetup"] }, redact: false });
553
556
  const copied = join(dest, "skill-ab_claude-effect", basename(snap.dir), "q1", "a1", "agent-setup.json");
554
557
  expect(JSON.parse(await readFile(copied, "utf-8"))).toEqual(manifest);
555
558
  });
@@ -590,27 +593,32 @@ describe("createResultsWriter", () => {
590
593
 
591
594
  it("快照目录独占创建:撞名换随机后缀重试直到成功(EEXIST 不会覆盖已有目录)", async () => {
592
595
  const root = await makeRoot();
593
- vi.useFakeTimers();
594
- vi.setSystemTime(new Date("2026-07-01T08:00:00.000Z"));
596
+ // 受控随机序列:writer B 面对同一身份时重置序列,首次尝试重放 writer A 消耗过的
597
+ // 随机值,必然撞上 A 的目录;重试消耗序列后续值,得到不同后缀。测试不假设一个
598
+ // 后缀消耗几次 random,也不假设数值到后缀字符的映射。
595
599
  let call = 0;
596
600
  const randomSpy = vi.spyOn(Math, "random").mockImplementation(() => {
597
601
  call += 1;
598
- return call <= 4 ? 0 : 0.03; // 第一次 randomSuffix() 产出 "aaaa",第二次产出 "bbbb"
602
+ return ((call * 37) % 977) / 977;
599
603
  });
600
604
  try {
601
- const collidingDir = join(root, "e", "2026-07-01T08-00-00-000Z-aaaa");
602
- await mkdir(collidingDir, { recursive: true });
603
-
604
- const writer = createResultsWriter(root, { producer: { name: "x" } });
605
- const snap = await writer.snapshot({ experimentId: "e", agent: "bub", startedAt: "2026-07-01T08:00:00.000Z" });
606
- expect(snap.dir).not.toBe(collidingDir);
607
- expect(snap.dir.endsWith("-bbbb")).toBe(true);
608
- expect(await exists(join(snap.dir, "snapshot.json"))).toBe(true);
609
- // 被占用的目录没有被写入 snapshot.json —— 独占创建不会覆盖已有内容。
610
- expect(await exists(join(collidingDir, "snapshot.json"))).toBe(false);
605
+ const identity = { experimentId: "e", agent: "bub", startedAt: "2026-07-01T08:00:00.000Z" } as const;
606
+ const writerA = createResultsWriter(root, { producer: { name: "x" } });
607
+ const snapA = await writerA.snapshot(identity);
608
+ const canary = join(snapA.dir, "canary.txt");
609
+ await writeFile(canary, "occupied", "utf8");
610
+
611
+ call = 0; // 重放同一序列,让 writer B 的首次尝试与 snapA 同名
612
+ const writerB = createResultsWriter(root, { producer: { name: "x" } });
613
+ const snapB = await writerB.snapshot(identity);
614
+
615
+ expect(snapB.dir).not.toBe(snapA.dir);
616
+ expect(await exists(join(snapB.dir, "snapshot.json"))).toBe(true);
617
+ // 被占用的目录内容原样保留 —— 独占创建不会覆盖已有内容。
618
+ expect(await exists(canary)).toBe(true);
619
+ expect(await exists(join(snapB.dir, "canary.txt"))).toBe(false);
611
620
  } finally {
612
621
  randomSpy.mockRestore();
613
- vi.useRealTimers();
614
622
  }
615
623
  });
616
624
 
@@ -621,7 +629,7 @@ describe("createResultsWriter", () => {
621
629
  const fresh: EvalResult = {
622
630
  id: "algebra/q1",
623
631
  experimentId: "compare/bub",
624
- experiment: { id: "compare/bub", flags: { style: "concise" } },
632
+ experiment: { flags: { style: "concise" }, runs: 1, earlyExit: true, selectedEvalIds: ["algebra/q1"] },
625
633
  agent: "bub",
626
634
  model: "gpt-5.4",
627
635
  verdict: "passed",
@@ -636,7 +644,7 @@ describe("createResultsWriter", () => {
636
644
  sources: [{ path: "evals/a.ts", content: "x" }],
637
645
  trace: [{ name: "turn", kind: "turn" } as never],
638
646
  o11y: { toolCalls: 2 } as never,
639
- diff: { generatedFiles: { "a.txt": "1" }, deletedFiles: [] },
647
+ diff: [{ window: "s1/t1", changes: { "a.txt": { status: "added", after: "1" } } }],
640
648
  rawTranscript: "raw",
641
649
  };
642
650
  const carried: EvalResult = {
@@ -714,7 +722,7 @@ describe("copySnapshots", () => {
714
722
  await writeResultFile(monday, "q1/a1", record({ id: "q1", attempt: 1, hasEvents: true, hasTrace: true }));
715
723
  await writeArtifactFile(monday, "q1/a1", "events.json", [{ n: 1 }]);
716
724
  await writeArtifactFile(monday, "q1/a1", "trace.json", [{ name: "turn" }]);
717
- await writeArtifactFile(monday, "q1/a1", "diff.json", { generatedFiles: {}, deletedFiles: [] });
725
+ await writeArtifactFile(monday, "q1/a1", "diff.json", [{ window: "s1/t1", changes: {} }]);
718
726
  await writeResultFile(monday, "q2/a1", record({ id: "q2", attempt: 1 }));
719
727
 
720
728
  // 周五只重跑了 q1:最新快照残缺。
@@ -729,7 +737,7 @@ describe("copySnapshots", () => {
729
737
 
730
738
  const results = await openResults(root);
731
739
  const dest = join(await makeRoot(), "site/data/run");
732
- const copied = await copySnapshots(results.latest(), dest, { artifacts: ["events"] });
740
+ const copied = await copySnapshots(results.latest(), dest, { ...{ artifacts: ["events"] }, redact: false });
733
741
 
734
742
  expect(copied.warnings).toHaveLength(0);
735
743
  expect(copied.dir).toBe(dest);
@@ -767,15 +775,15 @@ describe("copySnapshots", () => {
767
775
 
768
776
  const occupied = await makeRoot();
769
777
  await writeFile(join(occupied, "existing.txt"), "x", "utf-8");
770
- await expect(copySnapshots(results.latest(), occupied)).rejects.toThrow(/not empty/);
778
+ await expect(copySnapshots(results.latest(), occupied, { redact: false })).rejects.toThrow(/not empty/);
771
779
 
772
- await expect(copySnapshots(results.latest(), join(await makeRoot(), "out"), { artifacts: ["evnets" as never] })).rejects.toThrow(/Unknown artifact kind/);
780
+ await expect(copySnapshots(results.latest(), join(await makeRoot(), "out"), { ...{ artifacts: ["evnets" as never] }, redact: false })).rejects.toThrow(/Unknown artifact kind/);
773
781
 
774
- await expect(copySnapshots([], join(await makeRoot(), "out"))).rejects.toThrow(/no snapshots/);
782
+ await expect(copySnapshots([], join(await makeRoot(), "out"), { redact: false })).rejects.toThrow(/no snapshots/);
775
783
 
776
784
  // 手工传入同一 experiment 的两个快照(未走 latest 去重):只带最新,记 warning。
777
785
  const dest2 = join(await makeRoot(), "run2");
778
- const collided = await copySnapshots(results.experiments[0].snapshots, dest2);
786
+ const collided = await copySnapshots(results.experiments[0].snapshots, dest2, { redact: false });
779
787
  expect(collided.warnings).toHaveLength(1);
780
788
  expect(collided.warnings[0]).toMatch(/multiple snapshots selected/);
781
789
  const destDirs = await readdir(join(dest2, "e"));
@@ -968,8 +976,9 @@ describe("AttemptLocator · 落盘 / 读取 / 携带 / 撞车", () => {
968
976
  const handMadeResults: Results = {
969
977
  experiments: [{ id: "e", snapshots: [snapshot], latest: snapshot, evalIds: ["q1"] }],
970
978
  skipped: [],
971
- // filter() 本测试不调用,用不到,给个占位实现即可满足 Selection 接口。
972
- latest: () => ({ snapshots: [snapshot], warnings: [], filter: () => { throw new Error("not implemented"); } }),
979
+ // filter() 本测试不调用,用不到,给个占位实现即可满足 Scope 接口。
980
+ latest: () => ({ mode: "latest-snapshots" as const, snapshots: [snapshot], attempts: snapshot.attempts, warnings: [], filter: () => { throw new Error("not implemented"); } }),
981
+ current: () => { throw new Error("not implemented"); },
973
982
  };
974
983
  // 这份 locator 语法合法、甚至真的对应 handMadeResults 里那个 attempt 的身份,
975
984
  // 但 handMadeResults 没经过 openResults(),locatorIndexByResults 里查不到它 —— 空索引,not-found。
@@ -996,48 +1005,74 @@ describe("AttemptLocator · 落盘 / 读取 / 携带 / 撞车", () => {
996
1005
  const locator1 = a1!.locator!;
997
1006
 
998
1007
  const dest = join(await makeRoot(), "published");
999
- await copySnapshots(results.latest(), dest, { artifacts: [] });
1008
+ await copySnapshots(results.latest(), dest, { ...{ artifacts: [] }, redact: false });
1000
1009
 
1001
1010
  const destResults = await openResults(dest);
1002
1011
  expect(resolveLocator(destResults, locator0).result.attempt).toBe(0);
1003
1012
  expect(resolveLocator(destResults, locator1).result.attempt).toBe(1);
1004
1013
  });
1005
- });
1006
-
1007
- // ───────────────────────── sources 去重仓库 ─────────────────────────
1008
1014
 
1009
- describe("sources · 快照级去重仓库", () => {
1010
- it("两个 attempt 共享字节相同的 eval 源码:去重仓库只落一份 blob(文件数 = 1)", async () => {
1015
+ it("createResultsWriter({ snapshotStartedAt }):writeAttemptFor() 的隐式声明统一用这个锚点,不再按各 result 自己的 attempt startedAt 分别猜", async () => {
1011
1016
  const root = await makeRoot();
1012
- const writer = createResultsWriter(root, { producer: { name: "niceeval", version: "1.0.0" } });
1013
- const snap = await writer.snapshot({ experimentId: "e", agent: "bub", startedAt: "2026-07-01T08:00:00.000Z" });
1014
- const content = "export default { test() {} };\n";
1015
- await snap.writeAttempt({ id: "q1", verdict: "passed", attempt: 0, durationMs: 1, assertions: [] }, { sources: [{ path: "evals/shared.eval.ts", content }] });
1016
- await snap.writeAttempt({ id: "q2", verdict: "passed", attempt: 0, durationMs: 1, assertions: [] }, { sources: [{ path: "evals/shared.eval.ts", content }] });
1017
+ const snapshotStartedAt = "2026-07-10T00:00:00.000Z";
1018
+ const writer = createResultsWriter(root, {
1019
+ producer: { name: "niceeval", version: "1.0.0" },
1020
+ snapshotStartedAt,
1021
+ });
1022
+
1023
+ // 两个不同 experiment,各自的 result.startedAt 与 writer 级锚点都不同 —— 如果还在按
1024
+ // 「该 experiment 首条落盘结果的 attempt 时刻」猜,两份 snapshot.json 的 startedAt 会
1025
+ // 分别变成各自 result 的 attempt 时刻,而不是这里统一传入的 snapshotStartedAt。
1026
+ await writer.writeAttemptFor({
1027
+ id: "q1", experimentId: "e1", agent: "bub", verdict: "passed", attempt: 0,
1028
+ startedAt: "2020-01-01T00:00:00.000Z", durationMs: 1, assertions: [],
1029
+ });
1030
+ await writer.writeAttemptFor({
1031
+ id: "q1", experimentId: "e2", agent: "bub", verdict: "passed", attempt: 0,
1032
+ startedAt: "2021-06-15T00:00:00.000Z", durationMs: 1, assertions: [],
1033
+ });
1017
1034
  await writer.finish();
1018
1035
 
1019
- const storeFiles = await readdir(join(snap.dir, "sources"));
1020
- expect(storeFiles).toHaveLength(1);
1036
+ const results = await openResults(root);
1037
+ const expE1 = results.experiments.find((e) => e.id === "e1")!;
1038
+ const expE2 = results.experiments.find((e) => e.id === "e2")!;
1039
+ expect(expE1.latest.startedAt).toBe(snapshotStartedAt);
1040
+ expect(expE2.latest.startedAt).toBe(snapshotStartedAt); // 两个 experiment 共用同一个锚点,不碰撞
1041
+
1042
+ // attempt 级 startedAt(墙钟事实)依然各自独立保留,没有被锚点覆盖 ——
1043
+ // 快照身份锚点与 attempt 墙钟事实是两回事,继续分别保存。
1044
+ const attempt1 = expE1.latest.evals[0]!.attempts[0]!;
1045
+ const attempt2 = expE2.latest.evals[0]!.attempts[0]!;
1046
+ expect(attempt1.result.startedAt).toBe("2020-01-01T00:00:00.000Z");
1047
+ expect(attempt2.result.startedAt).toBe("2021-06-15T00:00:00.000Z");
1048
+
1049
+ // locator 由统一锚点 + experimentId 派生,两边不因为共享锚点而碰撞。
1050
+ expect(attempt1.locator).not.toBe(attempt2.locator);
1051
+ expect(attempt1.locator).toBe(
1052
+ encodeAttemptLocator({ experimentId: "e1", snapshotStartedAt, evalId: "q1", attempt: 0 }),
1053
+ );
1054
+ expect(attempt2.locator).toBe(
1055
+ encodeAttemptLocator({ experimentId: "e2", snapshotStartedAt, evalId: "q1", attempt: 0 }),
1056
+ );
1021
1057
  });
1022
1058
 
1023
- it("两个 attempt eval 源码内容不同:各自落一份 blob(文件数 = 2)", async () => {
1059
+ it("createResultsWriter() 省略 snapshotStartedAt 时保留旧的按首条 result.startedAt 猜测行为(第三方直调兼容)", async () => {
1024
1060
  const root = await makeRoot();
1025
1061
  const writer = createResultsWriter(root, { producer: { name: "niceeval", version: "1.0.0" } });
1026
- const snap = await writer.snapshot({ experimentId: "e", agent: "bub", startedAt: "2026-07-01T08:00:00.000Z" });
1027
- await snap.writeAttempt(
1028
- { id: "q1", verdict: "passed", attempt: 0, durationMs: 1, assertions: [] },
1029
- { sources: [{ path: "evals/a.eval.ts", content: "export default { test() {} };\n" }] },
1030
- );
1031
- await snap.writeAttempt(
1032
- { id: "q2", verdict: "passed", attempt: 0, durationMs: 1, assertions: [] },
1033
- { sources: [{ path: "evals/b.eval.ts", content: "export default { test() { /* different */ } };\n" }] },
1034
- );
1062
+ await writer.writeAttemptFor({
1063
+ id: "q1", experimentId: "e", agent: "bub", verdict: "passed", attempt: 0,
1064
+ startedAt: "2020-03-03T00:00:00.000Z", durationMs: 1, assertions: [],
1065
+ });
1035
1066
  await writer.finish();
1036
1067
 
1037
- const storeFiles = await readdir(join(snap.dir, "sources"));
1038
- expect(storeFiles).toHaveLength(2);
1068
+ const results = await openResults(root);
1069
+ expect(results.experiments[0]!.latest.startedAt).toBe("2020-03-03T00:00:00.000Z");
1039
1070
  });
1071
+ });
1040
1072
 
1073
+ // ───────────────────────── sources 去重仓库 ─────────────────────────
1074
+
1075
+ describe("sources · 快照级去重仓库", () => {
1041
1076
  it("经真实 --resume carry 流程(writeAttemptFor 的 artifactBase 分支)携带的 attempt,其 sources() 引用在新快照里依然能解到原快照内容", async () => {
1042
1077
  const root = await makeRoot();
1043
1078
  const content = "export default { test() {} };\n";
@@ -1157,7 +1192,7 @@ describe("sources · 快照级去重仓库", () => {
1157
1192
 
1158
1193
  const results = await openResults(root);
1159
1194
  const dest = join(await makeRoot(), "published");
1160
- await copySnapshots(results.latest(), dest, { artifacts: ["sources"] });
1195
+ await copySnapshots(results.latest(), dest, { ...{ artifacts: ["sources"] }, redact: false });
1161
1196
 
1162
1197
  const destSnapDir = join(dest, "e", basename(snap.dir));
1163
1198
  const destStoreFiles = await readdir(join(destSnapDir, "sources"));