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
@@ -1,8 +1,8 @@
1
- // 快照 Selection 与 attempt 去重(定稿见 docs/feature/results/library.md「选择快照」「身份键与去重」)。
1
+ // 快照 Scope 与 attempt 去重(定稿见 docs/feature/results/library.md「选择快照」「官方现刻水位」「身份键与去重」)。
2
2
  //
3
- // 选择器只有一个(latest),长在集合上;它不是 DSL,只是最常用的那次筛选。
4
- // 选择器必须诚实:残缺、落后、未收尾都被算出来,以结构化 warnings 随 Selection 走 ——
5
- // 渲染与否在消费方(message 是渲染好的英文句子),但缺口不静默。
3
+ // 选择器长在集合上(results.latest() / results.current()),不是 DSL,只是最常用的两种口径。
4
+ // 选择器必须诚实:残缺、落后、未收尾都被算出来,以结构化 warnings 随 Scope 走 ——
5
+ // 渲染与否在消费方(message 是渲染好的英文句子,以下一步收尾),但缺口不静默。
6
6
 
7
7
  import type {
8
8
  AttemptHandle,
@@ -10,22 +10,23 @@ import type {
10
10
  Eval,
11
11
  Experiment,
12
12
  Results,
13
- Selection,
14
- SelectionWarning,
13
+ Scope,
14
+ ScopeWarning,
15
15
  Snapshot,
16
16
  } from "./types.ts";
17
+ import type { ExperimentRunInfo, JsonValue } from "../types.ts";
17
18
  import { evalPrefixPredicate } from "../shared/aggregate.ts";
18
19
 
19
20
  /** Results.latest() 的实现:每个实验取最新一次快照(= exp.snapshots[0]),生成挑选警告。 */
20
21
  export function selectLatest(
21
22
  experiments: Experiment[],
22
23
  opts?: { experiments?: string | string[] },
23
- ): Selection {
24
+ ): Scope {
24
25
  const selected = filterExperiments(experiments, opts?.experiments);
25
26
  const snapshots = selected.map((exp) => exp.latest);
26
- const warnings: SelectionWarning[] = [];
27
+ const warnings: ScopeWarning[] = [];
27
28
 
28
- // stale 的基准:Selection 中最新的落盘(无阈值,如实触发;要阈值消费方按字段自比)。
29
+ // stale 的基准:Scope 中最新的落盘(无阈值,如实触发;要阈值消费方按字段自比)。
29
30
  let latestStartedAt = "";
30
31
  for (const snapshot of snapshots) {
31
32
  if (snapshot.startedAt > latestStartedAt) latestStartedAt = snapshot.startedAt;
@@ -44,6 +45,7 @@ export function selectLatest(
44
45
  covered,
45
46
  total,
46
47
  message: `snapshot covers ${covered} of ${total} evals seen in history; re-run \`niceeval exp ${exp.id}\` for a full snapshot`,
48
+ command: `niceeval exp ${exp.id}`,
47
49
  });
48
50
  }
49
51
  if (snapshot.startedAt < latestStartedAt) {
@@ -52,7 +54,8 @@ export function selectLatest(
52
54
  experimentId: exp.id,
53
55
  startedAt: snapshot.startedAt,
54
56
  latestStartedAt,
55
- message: `snapshot "${exp.id}" (${snapshot.startedAt}) predates the latest run in this selection by ${humanizeGap(snapshot.startedAt, latestStartedAt)}`,
57
+ message: `snapshot "${exp.id}" (${snapshot.startedAt}) predates the latest run in this scope by ${humanizeGap(snapshot.startedAt, latestStartedAt)}; re-run \`niceeval exp ${exp.id}\` to align, or ignore if evals, agent and model are unchanged between the runs`,
58
+ command: `niceeval exp ${exp.id}`,
56
59
  });
57
60
  }
58
61
  if (!snapshot.completedAt) {
@@ -61,46 +64,101 @@ export function selectLatest(
61
64
  experimentId: exp.id,
62
65
  startedAt: snapshot.startedAt,
63
66
  dir: snapshot.dir,
64
- message: `snapshot "${exp.id}" (${snapshot.startedAt}) has no completedAt — the run was interrupted; results may be incomplete`,
67
+ message: `snapshot "${exp.id}" (${snapshot.startedAt}) has no completedAt — the run was interrupted; re-run \`niceeval exp ${exp.id}\` for a complete snapshot`,
68
+ command: `niceeval exp ${exp.id}`,
65
69
  });
66
70
  }
67
71
  }
68
- return makeSelection(snapshots, warnings);
72
+ return makeScope("latest-snapshots", snapshots, warnings);
69
73
  }
70
74
 
71
75
  /** selectCurrentResults 的范围输入:experiment id 前缀与 eval id 前缀,都可缺省。 */
72
76
  export interface ResultScope {
73
77
  /** experiment id 前缀(--experiment),分段匹配语义同 filterExperiments。 */
74
- experiment?: string;
75
- /** eval id 前缀(位置参数),收窄 Selection 覆盖的 eval;覆盖警告分母同步收窄到范围内。 */
78
+ experiment?: string | string[];
79
+ /** eval id 前缀(位置参数),收窄 Scope 覆盖的 eval;覆盖警告分母同步收窄到范围内。 */
76
80
  patterns?: string[];
77
81
  }
78
82
 
83
+ // ───────────────────────── 可比性配置 ─────────────────────────
84
+
85
+ /**
86
+ * current() 跨快照拼接的可比性前提所比较的字段集(docs/feature/results/library.md
87
+ * 「官方现刻水位」):会改变单题被测行为或判定的字段。runs / earlyExit / maxConcurrency /
88
+ * selectedEvalIds / evalFilterFingerprint / description 是编排与选题字段,不参与比较。
89
+ */
90
+ export interface ComparabilityConfig {
91
+ agent: string;
92
+ model?: string;
93
+ reasoningEffort?: string;
94
+ flags?: Record<string, JsonValue>;
95
+ budget?: number;
96
+ timeoutMs?: number;
97
+ sandbox?: ExperimentRunInfo["sandbox"];
98
+ }
99
+
100
+ /** 一个快照的可比性配置投影;pairsByFlag 与 experimentListData 复用同一字段集。 */
101
+ export function comparabilityConfigOf(snapshot: Snapshot): ComparabilityConfig {
102
+ const info = snapshot.experiment;
103
+ return {
104
+ agent: snapshot.agent,
105
+ ...(snapshot.model !== undefined ? { model: snapshot.model } : {}),
106
+ ...(info?.reasoningEffort !== undefined ? { reasoningEffort: info.reasoningEffort } : {}),
107
+ ...(info?.flags !== undefined ? { flags: info.flags } : {}),
108
+ ...(info?.budget !== undefined ? { budget: info.budget } : {}),
109
+ ...(info?.timeoutMs !== undefined ? { timeoutMs: info.timeoutMs } : {}),
110
+ ...(info?.sandbox !== undefined ? { sandbox: info.sandbox } : {}),
111
+ };
112
+ }
113
+
114
+ /** 可序列化值的深相等(对象键序无关;undefined 字段与缺席字段等价)。 */
115
+ export function deepEqualJson(a: unknown, b: unknown): boolean {
116
+ if (a === b) return true;
117
+ if (Array.isArray(a) || Array.isArray(b)) {
118
+ if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) return false;
119
+ return a.every((item, i) => deepEqualJson(item, b[i]));
120
+ }
121
+ if (typeof a === "object" && typeof b === "object" && a !== null && b !== null) {
122
+ const keysA = Object.keys(a).filter((k) => (a as Record<string, unknown>)[k] !== undefined);
123
+ const keysB = Object.keys(b).filter((k) => (b as Record<string, unknown>)[k] !== undefined);
124
+ if (keysA.length !== keysB.length) return false;
125
+ return keysA.every((k) =>
126
+ deepEqualJson((a as Record<string, unknown>)[k], (b as Record<string, unknown>)[k]),
127
+ );
128
+ }
129
+ return false;
130
+ }
131
+
79
132
  /**
80
- * 两个宿主(show / view)共用的现刻水位选择器:每个 experiment × eval 取时间上最新的那份
81
- * 判定,跨 run 合成。results.latest() 只挑「每实验最新快照」,带 eval 前缀的局部重跑会产出
82
- * 残缺快照;现刻水位承诺「不会因为一次局部重跑变残缺」,所以在实验的全部历史快照上逐 eval
83
- * 向更早的 run 补齐,再把合成好的 Selection 交给宿主注入报告槽——内置默认报告与 --report 吃
84
- * 同一份。
133
+ * 两个宿主(show / view)共用的现刻水位选择器:每个 experiment × eval 取「包含该 eval 的
134
+ * 最新快照」里的全部 attempt,跨 run 合成。results.latest() 只挑「每实验最新快照」,带 eval
135
+ * 前缀的局部重跑会产出残缺快照;现刻水位承诺「不会因为一次局部重跑变残缺」,所以在实验的
136
+ * 历史快照上逐 eval 向更早的 run 补齐,再把合成好的 Scope 交给宿主注入报告槽。
137
+ *
138
+ * **可比性前提**:每个 experiment 以最新快照的可比性配置(agent / model / reasoningEffort /
139
+ * flags / budget / timeoutMs / sandbox)为基准,只有配置与基准深相等的历史快照才参与补齐;
140
+ * 改过配置后只补跑部分 eval 时,旧配置快照覆盖的其余题不冒充新配置的水位,按 partial-coverage
141
+ * 如实告警。这保证 current() 产出的每个 experiment 只对应一套配置。
85
142
  *
86
143
  * 同一 eval 的全部 attempts 必须整批取自包含它的最新快照,不把历史快照的 attempts 平铺后
87
144
  * 按 eval 聚合——否则会把不同运行的重试混成一次虚构运行。合成快照的 dir/元数据只服务报告
88
145
  * 分组与来源展示,证据身份一律来自 attempt 自己的 ref。
89
- * 警告随 Selection 重算:partial-coverage 的分母 = 已知并集 ∩ 范围(范围收窄时分母同步收窄,
90
- * 不让范围外的缺口刷屏);stale / unfinished 与 results.latest() 同口径。
91
146
  */
92
- export function selectCurrentResults(results: Results, scope: ResultScope = {}): Selection {
147
+ export function selectCurrentResults(results: Results, scope: ResultScope = {}): Scope {
93
148
  const match =
94
149
  scope.patterns && scope.patterns.length > 0 ? evalPrefixPredicate(scope.patterns) : () => true;
95
150
  const experiments = filterExperiments(results.experiments, scope.experiment);
96
151
 
97
152
  const snapshots: Snapshot[] = [];
98
- const warnings: SelectionWarning[] = [];
153
+ const warnings: ScopeWarning[] = [];
99
154
 
100
155
  for (const exp of experiments) {
156
+ // 可比性基准 = 该实验最新快照的可比性配置;不一致的旧快照整份跳过,不贡献 attempt。
157
+ const baseline = comparabilityConfigOf(exp.latest);
101
158
  // 逐题取最新:快照按最新在前,首个出现即最新判定
102
159
  const taken = new Map<string, { ev: Eval; snapshot: Snapshot }>();
103
160
  for (const snapshot of exp.snapshots) {
161
+ if (!deepEqualJson(comparabilityConfigOf(snapshot), baseline)) continue;
104
162
  for (const ev of snapshot.evals) {
105
163
  if (!match(ev.id) || taken.has(ev.id)) continue;
106
164
  taken.set(ev.id, { ev, snapshot });
@@ -124,6 +182,8 @@ export function selectCurrentResults(results: Results, scope: ResultScope = {}):
124
182
  startedAt,
125
183
  agent: base.agent,
126
184
  ...(base.model !== undefined ? { model: base.model } : {}),
185
+ ...(base.experiment !== undefined ? { experiment: base.experiment } : {}),
186
+ ...(base.name !== undefined ? { name: base.name } : {}),
127
187
  producer: base.producer,
128
188
  schemaVersion: base.schemaVersion,
129
189
  evals,
@@ -133,8 +193,8 @@ export function selectCurrentResults(results: Results, scope: ResultScope = {}):
133
193
  ...(base.knownEvalIds ? { knownEvalIds: [...base.knownEvalIds] } : {}),
134
194
  });
135
195
 
136
- // 残缺检测:跨快照补齐后仍缺,只可能是历史上见过(或 knownEvalIds 声明过)
137
- // 却从未在可读落盘里出现的题 —— 分母收窄到范围内,不让范围外的缺口刷屏。
196
+ // 残缺检测:跨快照补齐后仍缺,来自「历史上见过却从未在可比配置的可读落盘里出现」的题
197
+ // (含改配置后未补跑的题)—— 分母收窄到范围内,不让范围外的缺口刷屏。
138
198
  const total = exp.evalIds.filter(match).length;
139
199
  if (evals.length < total) {
140
200
  warnings.push({
@@ -143,6 +203,7 @@ export function selectCurrentResults(results: Results, scope: ResultScope = {}):
143
203
  covered: evals.length,
144
204
  total,
145
205
  message: `verdicts cover ${evals.length} of ${total} evals seen in history; re-run \`niceeval exp ${exp.id}\` for a full snapshot`,
206
+ command: `niceeval exp ${exp.id}`,
146
207
  });
147
208
  }
148
209
  }
@@ -158,7 +219,8 @@ export function selectCurrentResults(results: Results, scope: ResultScope = {}):
158
219
  experimentId: snapshot.experimentId,
159
220
  startedAt: snapshot.startedAt,
160
221
  latestStartedAt,
161
- message: `verdicts for "${snapshot.experimentId}" were produced at ${snapshot.startedAt}, before the latest run in this selection (${latestStartedAt})`,
222
+ message: `verdicts for "${snapshot.experimentId}" were produced at ${snapshot.startedAt}, ${humanizeGap(snapshot.startedAt, latestStartedAt)} before the latest run in this scope; re-run \`niceeval exp ${snapshot.experimentId}\` to align, or ignore if evals, agent and model are unchanged between the runs`,
223
+ command: `niceeval exp ${snapshot.experimentId}`,
162
224
  });
163
225
  }
164
226
  if (snapshot.completedAt === undefined) {
@@ -167,37 +229,45 @@ export function selectCurrentResults(results: Results, scope: ResultScope = {}):
167
229
  experimentId: snapshot.experimentId,
168
230
  startedAt: snapshot.startedAt,
169
231
  dir: snapshot.dir,
170
- message: `snapshot "${snapshot.experimentId}" (${snapshot.startedAt}) is unfinished (the process was interrupted); completed attempts are read as-is, but the set may be incomplete`,
232
+ message: `snapshot "${snapshot.experimentId}" (${snapshot.startedAt}) is unfinished (the process was interrupted); completed attempts are read as-is, but the set may be incomplete — re-run \`niceeval exp ${snapshot.experimentId}\` for a complete snapshot`,
233
+ command: `niceeval exp ${snapshot.experimentId}`,
171
234
  });
172
235
  }
173
236
  }
174
237
 
175
- return makeSelection(snapshots, warnings);
238
+ return makeScope("current-evals", snapshots, warnings);
176
239
  }
177
240
 
178
241
  /**
179
- * Selection 构造:filter 只删不换 —— 快照删减,warnings 修剪规则是
180
- * 「experimentId 不在幸存快照中的丢弃,非实验作用域的保留」(为将来非 per-experiment 的 kind 留位置)。
242
+ * Scope 构造:attempts 按口径物化(快照 attempts 的平铺);filter 只删不换 —— 快照删减,
243
+ * attempts 随之同步修剪,warnings 修剪规则是「experimentId 不在幸存快照中的丢弃,
244
+ * 非实验作用域的保留」(为将来非 per-experiment 的 kind 留位置)。
181
245
  */
182
- export function makeSelection(snapshots: Snapshot[], warnings: SelectionWarning[]): Selection {
246
+ export function makeScope(
247
+ mode: Scope["mode"],
248
+ snapshots: Snapshot[],
249
+ warnings: ScopeWarning[],
250
+ ): Scope {
183
251
  return {
252
+ mode,
184
253
  snapshots,
254
+ attempts: snapshots.flatMap((s) => s.attempts),
185
255
  warnings,
186
- filter(predicate: (snapshot: Snapshot) => boolean): Selection {
256
+ filter(predicate: (snapshot: Snapshot) => boolean): Scope {
187
257
  const kept = snapshots.filter(predicate);
188
258
  const survivors = new Set(kept.map((s) => s.experimentId));
189
259
  const keptWarnings = warnings.filter((w) => {
190
260
  const scope = (w as { experimentId?: unknown }).experimentId;
191
261
  return typeof scope !== "string" || survivors.has(scope);
192
262
  });
193
- return makeSelection(kept, keptWarnings);
263
+ return makeScope(mode, kept, keptWarnings);
194
264
  },
195
265
  };
196
266
  }
197
267
 
198
268
  /**
199
269
  * 跨快照聚合前的身份键去重:(experimentId, evalId, attempt, startedAt)。
200
- * --resume 会把上一轮已通过的结果原样合入新快照,同一 attempt 因此存在于多份落盘;
270
+ * 携带合入会把上一轮已通过的结果原样合入新快照,同一 attempt 因此存在于多份落盘;
201
271
  * 重复时保留最新快照里的那份(内容相同,取新快照的副本让 ref 落在最新落盘上;
202
272
  * 位置取首次出现处,顺序稳定)。startedAt 缺失时宁可不去重也不误删,记入 warnings。
203
273
  */
@@ -0,0 +1,90 @@
1
+ // 落盘大值截断(见 docs/feature/results/architecture.md「大值截断」)。
2
+ // 运行时全量,落盘截断:落点唯一在 snap.writeAttempt(writer.ts)——不在 adapter、不在 OTLP
3
+ // 解析、不在事件归一化里做。适用范围:events.json 的事件字段与 trace.json 的 span 属性里的
4
+ // 任意字符串值。没有 flag、没有配置项;截断永远不影响判决(落盘是证据,不是评分输入)。
5
+
6
+ import type { StreamEvent, TraceSpan, Truncation } from "../types.ts";
7
+
8
+ /** 每个字符串值的落盘上限(UTF-8 字节)。 */
9
+ export const ARTIFACT_VALUE_MAX_BYTES = 256 * 1024;
10
+
11
+ const encoder = new TextEncoder();
12
+
13
+ /** 截断到 maxBytes(按 UTF-8 字符边界回退,不切断多字节字符),末尾追加人可读 marker。 */
14
+ function truncateString(value: string, originalBytes: number): string {
15
+ // 按字节预算切:先按「字节数 ≥ 字符数」粗切,再逐步回退到不超预算的字符边界。
16
+ let sliceLen = Math.min(value.length, ARTIFACT_VALUE_MAX_BYTES);
17
+ let head = value.slice(0, sliceLen);
18
+ while (encoder.encode(head).length > ARTIFACT_VALUE_MAX_BYTES && sliceLen > 0) {
19
+ // 超出量 ÷ 4(UTF-8 最长 4 字节)是安全的最小回退步长;循环兜底处理边界。
20
+ sliceLen -= Math.max(1, Math.ceil((encoder.encode(head).length - ARTIFACT_VALUE_MAX_BYTES) / 4));
21
+ head = value.slice(0, sliceLen);
22
+ }
23
+ const kept = encoder.encode(head).length;
24
+ return `${head}\n[niceeval] truncated ${originalBytes} → ${kept} bytes`;
25
+ }
26
+
27
+ /**
28
+ * 深度遍历一个 JSON 值,截断其中超限的字符串;返回(可能新建的)值与收集到的截断记录。
29
+ * `path` 用点分段(数组下标同样入段),事件里从字段名起、span 属性里从 attribute key 起。
30
+ */
31
+ function truncateJsonValue(value: unknown, path: string, out: Truncation[]): unknown {
32
+ if (typeof value === "string") {
33
+ const bytes = encoder.encode(value).length;
34
+ if (bytes <= ARTIFACT_VALUE_MAX_BYTES) return value;
35
+ out.push({ path, originalBytes: bytes });
36
+ return truncateString(value, bytes);
37
+ }
38
+ if (Array.isArray(value)) {
39
+ let changed = false;
40
+ const next = value.map((v, i) => {
41
+ const r = truncateJsonValue(v, `${path}.${i}`, out);
42
+ if (r !== v) changed = true;
43
+ return r;
44
+ });
45
+ return changed ? next : value;
46
+ }
47
+ if (value !== null && typeof value === "object") {
48
+ let changed = false;
49
+ const next: Record<string, unknown> = {};
50
+ for (const [k, v] of Object.entries(value)) {
51
+ const r = truncateJsonValue(v, path ? `${path}.${k}` : k, out);
52
+ if (r !== v) changed = true;
53
+ next[k] = r;
54
+ }
55
+ return changed ? next : value;
56
+ }
57
+ return value;
58
+ }
59
+
60
+ /** events.json 落盘前的截断:逐事件处理全部字符串字段,超限的加结构化 `truncated` 标记。 */
61
+ export function truncateEvents(events: readonly StreamEvent[]): StreamEvent[] {
62
+ return events.map((event) => {
63
+ const out: Truncation[] = [];
64
+ const next: Record<string, unknown> = {};
65
+ let changed = false;
66
+ for (const [k, v] of Object.entries(event)) {
67
+ if (k === "type" || k === "truncated") {
68
+ next[k] = v;
69
+ continue;
70
+ }
71
+ const r = truncateJsonValue(v, k, out);
72
+ if (r !== v) changed = true;
73
+ next[k] = r;
74
+ }
75
+ if (!changed) return event;
76
+ return { ...next, truncated: [...(event.truncated ?? []), ...out] } as StreamEvent;
77
+ });
78
+ }
79
+
80
+ /** trace.json 落盘前的截断:span 属性里的任意字符串值(name 一并覆盖——可携带动态内容)。 */
81
+ export function truncateSpans(spans: readonly TraceSpan[]): TraceSpan[] {
82
+ return spans.map((span) => {
83
+ const out: Truncation[] = [];
84
+ const attributes = span.attributes
85
+ ? (truncateJsonValue(span.attributes, "", out) as TraceSpan["attributes"])
86
+ : span.attributes;
87
+ if (out.length === 0) return span;
88
+ return { ...span, attributes, truncated: [...(span.truncated ?? []), ...out] };
89
+ });
90
+ }
@@ -44,6 +44,11 @@ export interface SnapshotMeta {
44
44
  completedAt?: string;
45
45
  /** 写入时刻该实验已知的 eval 并集 —— 残缺检测的分母随数据走(copySnapshots 自动补记,writer 可声明)。 */
46
46
  knownEvalIds?: string[];
47
+ /**
48
+ * 发布拷贝的自描述标记:copySnapshots 补记,消毒函数 → "applied"、redact: false → "none";
49
+ * 本地事实根没有此字段。只声明流程,不证明无秘密;view --out 据此分级防呆。
50
+ */
51
+ publish?: { redaction: "applied" | "none" };
47
52
  /** 项目名(来自 config.name),透传给 `niceeval view` 顶部 hero 显示。 */
48
53
  name?: LocalizedText;
49
54
  }
@@ -125,6 +130,8 @@ export interface Snapshot {
125
130
  dir: string;
126
131
  /** 写入时刻该实验已知的 eval 并集(可选);copySnapshots 自动补记,writer.snapshot() 也可声明。 */
127
132
  knownEvalIds?: string[];
133
+ /** 发布拷贝的自描述标记(见 SnapshotMeta.publish);本地事实根没有此字段。 */
134
+ publish?: { redaction: "applied" | "none" };
128
135
  }
129
136
 
130
137
  /** 一个实验的全部历史:同一 experiment id 的历次快照归在一起。 */
@@ -164,33 +171,49 @@ export interface Results {
164
171
  experiments: Experiment[];
165
172
  skipped: SkippedDir[];
166
173
  /**
167
- * 每个实验取最新一次快照,返回 Selection(快照与挑选警告绑在一起走)。
174
+ * 每个实验取最新一次快照,返回 Scope(快照与挑选警告绑在一起走)。
168
175
  * `experiments` 是 experiment id 前缀过滤(string | string[]),同 CLI 位置参数语义。
169
176
  */
170
- latest(opts?: { experiments?: string | string[] }): Selection;
177
+ latest(opts?: { experiments?: string | string[] }): Scope;
178
+ /**
179
+ * 官方现刻水位:每个 experiment × eval 取「包含该 eval 的最新快照」里的全部 attempt,
180
+ * 跨历史拼出当前判定水位。可比性前提:每个 experiment 以最新快照的可比性配置
181
+ * (agent / model / reasoningEffort / flags / budget / timeoutMs / sandbox)为基准,
182
+ * 配置不一致的旧快照不贡献 attempt,缺口走 partial-coverage
183
+ * (见 docs/feature/results/library.md「官方现刻水位」)。
184
+ */
185
+ current(opts?: { experiments?: string | string[] }): Scope;
171
186
  }
172
187
 
173
188
  /**
174
- * Selection:选出的快照 + 挑选过程算出的警告。渲染与否在消费方,但缺口永远被算出来。
175
- * 下游(Reports 计算函数、copySnapshots)收 `Selection | Snapshot[]`;
176
- * 手工挑的裸数组没有挑选过程,自然没有 warnings 可带,也如实。
189
+ * Scope(范围):选出的快照 + 口径 + 已按口径物化的 attempt 全集 + 挑选警告。
190
+ * 渲染与否在消费方,但缺口永远被算出来。下游(Reports 计算函数、copySnapshots)收
191
+ * `Scope | readonly Snapshot[]`;手工挑的裸数组没有挑选过程,自然没有 warnings 可带,也如实。
177
192
  */
178
- export interface Selection {
193
+ export interface Scope {
194
+ /** 这份 Scope 的口径,字面写在数据上。 */
195
+ mode: "latest-snapshots" | "current-evals";
179
196
  snapshots: Snapshot[];
180
- warnings: SelectionWarning[];
181
197
  /**
182
- * 只删不换:返回新 Selection,快照删减,warnings 按规则修剪 ——
183
- * experimentId 不在幸存快照中的丢弃,非实验作用域的保留。
198
+ * 按口径物化的 attempt 全集:消费 attempts 就自动正确,不需要自己 flatten snapshots,
199
+ * 也就不可能算错口径。官方计算函数同样只消费它。
200
+ */
201
+ attempts: AttemptHandle[];
202
+ warnings: ScopeWarning[];
203
+ /**
204
+ * 只删不换:返回新 Scope,快照删减,attempts 与 warnings 随之同步修剪 ——
205
+ * experimentId 不在幸存快照中的警告丢弃,非实验作用域的警告保留。
184
206
  * 「换成上一个完整快照」这类替换式重挑不给方法,回 exp.snapshots 自己挑。
185
207
  */
186
- filter(predicate: (snapshot: Snapshot) => boolean): Selection;
208
+ filter(predicate: (snapshot: Snapshot) => boolean): Scope;
187
209
  }
188
210
 
189
211
  /**
190
- * 挑选警告:每种带 kind、可判断的结构化字段和渲染好的英文 message;
191
- * kind 是契约的一部分,全集与触发条件见 docs/feature/results/library.md「警告 kind 全集」。
212
+ * 挑选警告:每种带 kind、可判断的结构化字段和渲染好的英文 message;能用一条命令直接推进的
213
+ * kind 同时带 `command`(已替换真实 id,复制即跑)。kind 是契约的一部分,全集与触发条件见
214
+ * docs/feature/results/library.md「警告 kind 全集」。
192
215
  */
193
- export type SelectionWarning =
216
+ export type ScopeWarning =
194
217
  | {
195
218
  /** 选中快照的覆盖 < 该实验已知 eval 并集(本地历史 ∪ knownEvalIds)。 */
196
219
  kind: "partial-coverage";
@@ -198,14 +221,18 @@ export type SelectionWarning =
198
221
  covered: number;
199
222
  total: number;
200
223
  message: string;
224
+ /** 一条可复制即跑的推进命令:`niceeval exp <experimentId>`。 */
225
+ command: string;
201
226
  }
202
227
  | {
203
- /** 该实验选中的快照早于 Selection 中最新的落盘;无阈值,如实触发,要阈值消费方按字段自比。 */
228
+ /** 该实验选中的快照早于 Scope 中最新的落盘;无阈值,如实触发,要阈值消费方按字段自比。 */
204
229
  kind: "stale-snapshot";
205
230
  experimentId: string;
206
231
  startedAt: string;
207
232
  latestStartedAt: string;
208
233
  message: string;
234
+ /** 一条可复制即跑的推进命令:`niceeval exp <experimentId>`。 */
235
+ command: string;
209
236
  }
210
237
  | {
211
238
  /** 选中快照缺 completedAt(进程中断,未收尾);已落盘 attempt 照常读出,警告提示集合可能不完整。 */
@@ -215,6 +242,8 @@ export type SelectionWarning =
215
242
  /** 该快照目录的绝对路径。 */
216
243
  dir: string;
217
244
  message: string;
245
+ /** 一条可复制即跑的推进命令:`niceeval exp <experimentId>`。 */
246
+ command: string;
218
247
  };
219
248
 
220
249
  /** dedupeAttempts 的警告:身份键缺 startedAt,宁可不去重也不误删。 */
@@ -9,16 +9,28 @@
9
9
  import { mkdir, writeFile } from "node:fs/promises";
10
10
  import { join } from "node:path";
11
11
  import type { AgentSetupManifest, EvalResult, ExperimentRunInfo, LocalizedText } from "../types.ts";
12
- import type { DiffData, O11ySummary, SourceArtifact, StreamEvent, TraceSpan } from "../types.ts";
12
+ import type { DiffArtifact, O11ySummary, SourceArtifact, StreamEvent, TraceSpan } from "../types.ts";
13
13
  import { RESULTS_FORMAT, RESULTS_SCHEMA_VERSION } from "../types.ts";
14
14
  import { RESULT_FILE, SNAPSHOT_FILE, artifactFileOf, attemptDirOf, experimentDirOf } from "./format.ts";
15
15
  import { encodeAttemptLocator } from "./locator.ts";
16
16
  import { hashEvalSource, normalizeEvalSource } from "./source-hash.ts";
17
+ import { truncateEvents, truncateSpans } from "./truncate.ts";
17
18
  import type { Producer, SnapshotMeta } from "./types.ts";
18
19
 
19
20
  export interface ResultsWriterOptions {
20
21
  /** 谁在写这份结果:niceeval 自己,或第三方 harness(name 如实写,别冒充 "niceeval")。 */
21
22
  producer: Producer;
23
+ /**
24
+ * 本次 invocation 的快照身份锚点(ISO 时间戳,即 runner 的 `RunShape.snapshotStartedAt`)。
25
+ * `writeAttemptFor()` 的隐式 snapshot 声明统一用它做 `startedAt`,不再按「该 experiment
26
+ * 第一条落盘 result 的 attempt startedAt」猜 —— 那个锚点依赖并发完成顺序,不确定;
27
+ * 同一 writer 处理多个 experiment 时也共享这同一个值(locator 身份还含 experimentId,
28
+ * 不会碰撞)。省略时退回旧行为:每次隐式声明按 `result.startedAt ?? now()` 各自取锚,
29
+ * 供未提供该值的直调场景使用(测试、结果转换脚本、第三方 harness 直接调
30
+ * `createResultsWriter()` 而不经过 niceeval 自己的 runner)。显式调用 `writer.snapshot()`
31
+ * 声明快照的调用方不受这个选项影响,必须自己传 `SnapshotDeclaration.startedAt`。
32
+ */
33
+ snapshotStartedAt?: string;
22
34
  }
23
35
 
24
36
  /** 快照级声明:一个 experiment 声明一次,这些字段不塞进每条 attempt。 */
@@ -70,7 +82,7 @@ export interface AttemptArtifacts {
70
82
  o11y?: O11ySummary;
71
83
  /** agent setup 的安装清单(沙箱型 coding agent 装了 Skill / plugin / MCP 才有)。 */
72
84
  agentSetup?: AgentSetupManifest;
73
- diff?: DiffData;
85
+ diff?: DiffArtifact;
74
86
  sources?: SourceArtifact[];
75
87
  }
76
88
 
@@ -118,7 +130,7 @@ export function createResultsWriter(root: string, opts: ResultsWriterOptions): R
118
130
  producer: opts.producer,
119
131
  experimentId: decl.experimentId,
120
132
  // 运行配置不带 id:身份的家是顶层 experimentId,重复一份只会引出「以谁为准」。
121
- ...(decl.experiment !== undefined ? { experiment: stripInfoId(decl.experiment) } : {}),
133
+ ...(decl.experiment !== undefined ? { experiment: (decl.experiment) } : {}),
122
134
  agent: decl.agent,
123
135
  ...(decl.model !== undefined ? { model: decl.model } : {}),
124
136
  startedAt: decl.startedAt,
@@ -172,8 +184,11 @@ export function createResultsWriter(root: string, opts: ResultsWriterOptions): R
172
184
  experimentId: result.experimentId,
173
185
  agent: result.agent,
174
186
  model: result.model,
175
- // 快照 startedAt 以该实验首条落盘结果的 attempt 时刻为锚(首条 ≈ 实验开跑)
176
- startedAt: result.startedAt ?? new Date().toISOString(),
187
+ // 快照 startedAt 优先用 writer 级的 invocation 锚点( ResultsWriterOptions.snapshotStartedAt)——
188
+ // niceeval 自己的 runner 恒会提供,多个 experiment 共享同一个值。省略时(第三方直调
189
+ // createResultsWriter() 未传该选项)退回旧行为:以本次调用这个 result 自己的 attempt
190
+ // startedAt 为锚,首条落盘的 result 决定了这个 experiment 快照的身份锚点。
191
+ startedAt: opts.snapshotStartedAt ?? result.startedAt ?? new Date().toISOString(),
177
192
  experiment: result.experiment,
178
193
  });
179
194
 
@@ -288,9 +303,14 @@ async function writeAttemptFiles(
288
303
  const hasTrace = !!(artifacts?.trace && artifacts.trace.length);
289
304
 
290
305
  const writes: Promise<unknown>[] = [];
291
- if (hasEvents) writes.push(writeFile(join(attemptDir, "events.json"), JSON.stringify(artifacts!.events), "utf-8"));
306
+ // 大值截断只发生在这里(序列化的那一刻):events 的事件字段与 trace span 属性里的任意
307
+ // 字符串值按 ARTIFACT_VALUE_MAX_BYTES 截断并留结构化 truncated 标记;运行时(断言 / o11y
308
+ // 派生)看到的始终是完整值。sources 与 diff 不截断(见 docs/feature/results/architecture.md)。
309
+ if (hasEvents)
310
+ writes.push(writeFile(join(attemptDir, "events.json"), JSON.stringify(truncateEvents(artifacts!.events!)), "utf-8"));
292
311
  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"));
312
+ if (hasTrace)
313
+ writes.push(writeFile(join(attemptDir, "trace.json"), JSON.stringify(truncateSpans(artifacts!.trace!)), "utf-8"));
294
314
  if (artifacts?.o11y) writes.push(writeFile(join(attemptDir, "o11y.json"), JSON.stringify(artifacts.o11y), "utf-8"));
295
315
  if (artifacts?.agentSetup) {
296
316
  writes.push(
@@ -303,6 +323,9 @@ async function writeAttemptFiles(
303
323
  // locator:caller(如第三方 harness 直接调 SnapshotWriter.writeAttempt)已经带了就尊重,
304
324
  // 否则按当前身份元组算一份 —— 这条路径只服务「非携带」的新写入,携带条目走
305
325
  // writeAttemptForImpl 的 artifactBase 分支,原样透传 result.locator,从不落到这里重算。
326
+ // niceeval 自己的 runner(src/runner/run.ts)在 fresh attempt 完成时已经把 locator 写进
327
+ // entry.locator(用的正是同一个 snapshot.startedAt),所以这条 encodeAttemptLocator 兜底
328
+ // 对 niceeval 自身运行永不触发,只在第三方 harness 未预先算好 locator 时才会走到。
306
329
  const locator =
307
330
  entry.locator ??
308
331
  encodeAttemptLocator({
@@ -365,12 +388,7 @@ async function createSnapshotDir(root: string, experimentId: string): Promise<st
365
388
  throw new Error(`Could not create a unique snapshot directory under "${parent}" after 5 attempts (${String(lastError)}).`);
366
389
  }
367
390
 
368
- /** 运行配置落盘前剥掉 id:experimentId 的家在 snapshot.json 顶层。 */
369
- function stripInfoId(info: ExperimentRunInfo): ExperimentRunInfo {
370
- const { id, ...rest } = info;
371
- void id;
372
- return rest;
373
- }
391
+
374
392
 
375
393
  /** 快照目录名的时间戳段:Date#toISOString 把 : 与 . 换成 -(与 docs/feature/results/architecture.md 一致)。 */
376
394
  function safeTimestamp(d: Date): string {