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
package/src/report/web.ts CHANGED
@@ -6,12 +6,20 @@ import * as React from "react";
6
6
  import type { ReactNode } from "react";
7
7
  import { renderToStaticMarkup } from "react-dom/server";
8
8
  import type { AttemptLocator } from "../results/locator.ts";
9
- import type { SelectionWarning } from "../results/types.ts";
10
- import { resolveReportTree, runWithWebContext, validateReportTree, type WebContext } from "./tree.ts";
9
+ import type { Scope } from "../results/types.ts";
10
+ import {
11
+ resolveReportTree,
12
+ runWithWebContext,
13
+ validateReportTree,
14
+ ResolveMemo,
15
+ type WebContext,
16
+ } from "./tree.ts";
11
17
  import { DEFAULT_REPORT_LOCALE, type ReportLocale } from "./locale.ts";
12
- import type { ReportContext, ReportDefinition } from "./report.ts";
18
+ import { buildReportMeta, pickReportPage, type ReportDefinition, type ReportHostContext } from "./report.ts";
13
19
 
14
20
  export interface StaticHtmlOptions {
21
+ /** 渲染哪一页;缺省第一页。未命中抛 ReportPageNotFoundError。 */
22
+ pageId?: string;
15
23
  /** 证据室深链;缺省用 view 的 attempt 路由 `#/attempt/@<locator>`(单段、不透明)。 */
16
24
  attemptHref?: (locator: AttemptLocator) => string;
17
25
  /** 官方组件 chrome 文案的 locale;默认 "en"。 */
@@ -19,12 +27,12 @@ export interface StaticHtmlOptions {
19
27
  }
20
28
 
21
29
  /**
22
- * 挑选警告的 HTML 形态:宿主级前置块,与 RunOverview 里的警告用同一套结构和类名
23
- * (`.nre nre-report-warnings` 外壳内一个 `ul.nre-warnings` + `li.nre-warning[data-kind]`,
24
- * 复用 styles.css 已有的 `.nre .nre-warnings` 样式)。经 renderToStaticMarkup 走 React,
25
- * message 文本自动转义,不裸拼 HTML。裸跑 / --report 都在报告顶上如实报残缺,不静默。
30
+ * 挑选警告的 HTML 形态:宿主级前置块(`.nre nre-report-warnings` 外壳内一个
31
+ * `ul.nre-warnings` + `li.nre-warning[data-kind]`,复用 styles.css 已有样式)。
32
+ * `command` 的警告把命令渲染为可复制块(`.nre-warning-command`);无 command
33
+ * 只显示 message,不硬造动作。经 renderToStaticMarkup React,文本自动转义。
26
34
  */
27
- function renderSelectionWarningsHtml(warnings: SelectionWarning[]): string {
35
+ function renderScopeWarningsHtml(scope: Scope): string {
28
36
  return renderToStaticMarkup(
29
37
  React.createElement(
30
38
  "div",
@@ -32,8 +40,15 @@ function renderSelectionWarningsHtml(warnings: SelectionWarning[]): string {
32
40
  React.createElement(
33
41
  "ul",
34
42
  { className: "nre-warnings" },
35
- warnings.map((w, i) =>
36
- React.createElement("li", { key: i, className: "nre-warning", "data-kind": w.kind }, w.message),
43
+ scope.warnings.map((w, i) =>
44
+ React.createElement(
45
+ "li",
46
+ { key: i, className: "nre-warning", "data-kind": w.kind },
47
+ w.message,
48
+ "command" in w && w.command
49
+ ? React.createElement("code", { className: "nre-warning-command", "data-nre-copy": w.command }, w.command)
50
+ : null,
51
+ ),
37
52
  ),
38
53
  ),
39
54
  ),
@@ -41,27 +56,55 @@ function renderSelectionWarningsHtml(warnings: SelectionWarning[]): string {
41
56
  }
42
57
 
43
58
  /**
44
- * build渲染前解析数据组件(唯一的 await 边界)→ 树校验(与 text 宿主同一遍)→ 静态渲染
45
- * web 面;Selection 有挑选警告时在报告顶部前置一块警告 HTML;报告树里的 RunOverview
46
- * 已经渲染同一条时不重复。
59
+ * web 宿主的装载语义:选页 resolve(组合展开 + spec 取数,唯一的 await 边界)→
60
+ * 树校验(与 text 宿主同一遍)→ 静态渲染 web 面;Scope 有挑选警告时在报告顶部前置
61
+ * 一块警告 HTML(宿主是 warning 的唯一呈现者,组件数据不复制 warning)。
47
62
  */
48
63
  export async function renderReportToStaticHtml(
49
64
  definition: ReportDefinition,
50
- ctx: ReportContext,
65
+ ctx: ReportHostContext,
51
66
  options?: StaticHtmlOptions,
52
67
  ): Promise<string> {
53
- const node = await definition.build(ctx);
54
- const resolved = await resolveReportTree(node);
68
+ const page = pickReportPage(definition, options?.pageId);
69
+ const meta = buildReportMeta(definition, ctx.scope, page.id);
70
+ const resolved = await resolveReportTree(page.content, {
71
+ scope: ctx.scope,
72
+ results: ctx.results,
73
+ report: meta,
74
+ memo: new ResolveMemo(),
75
+ });
55
76
  validateReportTree(resolved);
56
77
  const webCtx: WebContext = {
57
78
  attemptHref: options?.attemptHref ?? ((locator) => `#/attempt/${locator}`),
58
79
  locale: options?.locale ?? DEFAULT_REPORT_LOCALE,
59
80
  };
60
81
  const body = runWithWebContext(webCtx, () => renderToStaticMarkup(resolved as ReactNode));
61
- const missingWarnings = ctx.selection.warnings.filter((warning) => {
62
- const escapedMessage = renderToStaticMarkup(React.createElement(React.Fragment, null, warning.message));
63
- return !body.includes(escapedMessage);
82
+ const warnings = ctx.scope.warnings.length > 0 ? renderScopeWarningsHtml(ctx.scope) : "";
83
+ return warnings + body;
84
+ }
85
+
86
+ /**
87
+ * 渲染一页报告树的 web 面(宿主逐页调用;页选择归宿主):resolve → validate → 静态渲染。
88
+ * Scope 有挑选警告时在页顶前置警告块(带 command 的警告渲染为可复制命令)——宿主是
89
+ * warning 的唯一呈现者,组件数据不复制 warning。ctx.report 是宿主规范化后的声明。
90
+ */
91
+ export async function renderReportTreeToStaticHtml(
92
+ tree: import("./tree.ts").ReportNode,
93
+ ctx: { scope: Scope; results: import("../results/types.ts").Results; report: import("./report.ts").ReportMeta },
94
+ options?: { attemptHref?: (locator: AttemptLocator) => string; locale?: ReportLocale },
95
+ ): Promise<string> {
96
+ const resolved = await resolveReportTree(tree, {
97
+ scope: ctx.scope,
98
+ results: ctx.results,
99
+ report: ctx.report,
100
+ memo: new ResolveMemo(),
64
101
  });
65
- const warnings = missingWarnings.length > 0 ? renderSelectionWarningsHtml(missingWarnings) : "";
102
+ validateReportTree(resolved);
103
+ const webCtx: WebContext = {
104
+ attemptHref: options?.attemptHref ?? ((locator) => `#/attempt/${locator}`),
105
+ locale: options?.locale ?? DEFAULT_REPORT_LOCALE,
106
+ };
107
+ const body = runWithWebContext(webCtx, () => renderToStaticMarkup(resolved as ReactNode));
108
+ const warnings = ctx.scope.warnings.length > 0 ? renderScopeWarningsHtml(ctx.scope) : "";
66
109
  return warnings + body;
67
110
  }
@@ -1,10 +1,11 @@
1
+ // cases: docs/engineering/unit-tests/results/cases.md
1
2
  // buildAnnotatedEvalSource 的单测(定稿见 docs/concepts.md「标注 Eval 源码」)。
2
3
  // 覆盖:同一行多条断言、无 loc 断言进 unmapped、loc 指向别的文件/越界行也进 unmapped
3
4
  // (never silently dropped 的边界情况)、空断言数组、summary 计数、哈希与归一化行为。
4
5
 
5
6
  import { describe, expect, it } from "vitest";
6
- import type { AssertionResult } from "../types.ts";
7
- import { buildAnnotatedEvalSource } from "./annotated-source.ts";
7
+ import type { AssertionResult, PhaseTiming, StreamEvent } from "../types.ts";
8
+ import { buildAnnotatedEvalSource, deriveSendAnnotations, type SendAnnotation } from "./annotated-source.ts";
8
9
  import { hashEvalSource, normalizeEvalSource } from "./source-hash.ts";
9
10
 
10
11
  const SOURCE_PATH = "evals/weather.eval.ts";
@@ -13,9 +14,9 @@ function assertion(over: Partial<AssertionResult> & Pick<AssertionResult, "name"
13
14
  return {
14
15
  severity: "soft",
15
16
  score: 1,
16
- passed: true,
17
+ outcome: "passed" as const,
17
18
  ...over,
18
- };
19
+ } as AssertionResult;
19
20
  }
20
21
 
21
22
  describe("buildAnnotatedEvalSource", () => {
@@ -94,10 +95,10 @@ describe("buildAnnotatedEvalSource", () => {
94
95
  it("computes summary counts across passed/failed and gate/soft, mapped and unmapped alike", () => {
95
96
  const content = "line 1\nline 2\n";
96
97
  const assertions = [
97
- assertion({ name: "a", severity: "gate", passed: true, loc: { file: SOURCE_PATH, line: 1 } }),
98
- assertion({ name: "b", severity: "gate", passed: false, loc: { file: SOURCE_PATH, line: 1 } }),
99
- assertion({ name: "c", severity: "soft", passed: true, loc: { file: SOURCE_PATH, line: 2 } }),
100
- assertion({ name: "d", severity: "soft", passed: false }), // unmapped
98
+ assertion({ name: "a", severity: "gate", outcome: "passed" as const, loc: { file: SOURCE_PATH, line: 1 } }),
99
+ assertion({ name: "b", severity: "gate", outcome: "failed" as const, loc: { file: SOURCE_PATH, line: 1 } }),
100
+ assertion({ name: "c", severity: "soft", outcome: "passed" as const, loc: { file: SOURCE_PATH, line: 2 } }),
101
+ assertion({ name: "d", severity: "soft", outcome: "failed" as const }), // unmapped
101
102
  ];
102
103
 
103
104
  const model = buildAnnotatedEvalSource({ path: SOURCE_PATH, content }, assertions);
@@ -129,7 +130,7 @@ describe("buildAnnotatedEvalSource", () => {
129
130
 
130
131
  it("treats an empty source file as a single empty line", () => {
131
132
  const model = buildAnnotatedEvalSource({ path: SOURCE_PATH, content: "" }, []);
132
- expect(model.lines).toEqual([{ line: 1, text: "", assertions: [] }]);
133
+ expect(model.lines).toEqual([{ line: 1, text: "", assertions: [], sends: [] }]);
133
134
  });
134
135
 
135
136
  it("sourceSha256 matches hashEvalSource(normalizeEvalSource(content)) and is stable across CRLF/LF", () => {
@@ -143,4 +144,56 @@ describe("buildAnnotatedEvalSource", () => {
143
144
  expect(modelLf.sourceSha256).toBe(modelCrlf.sourceSha256);
144
145
  expect(modelLf.lines.map((l) => l.text)).toEqual(modelCrlf.lines.map((l) => l.text));
145
146
  });
147
+
148
+ it("send 标注按 loc 落到对应行,一行多轮逐轮保留;别的文件或越界行直接丢(全量面在 --execution)", () => {
149
+ const content = "await t.send('a');\nawait t.send('b');\n";
150
+ const sends: SendAnnotation[] = [
151
+ { label: "s1/t1", status: "completed", durationMs: 1200, loc: { file: SOURCE_PATH, line: 1, column: 9 } },
152
+ { label: "s1/t2", status: "failed", durationMs: 800, loc: { file: SOURCE_PATH, line: 1, column: 9 } },
153
+ { label: "s1/t3", status: "completed", loc: { file: "evals/other.eval.ts", line: 1 } },
154
+ { label: "s1/t4", status: "completed", loc: { file: SOURCE_PATH, line: 99 } },
155
+ ];
156
+ const model = buildAnnotatedEvalSource({ path: SOURCE_PATH, content }, [], sends);
157
+ expect(model.lines[0]!.sends.map((s) => s.label)).toEqual(["s1/t1", "s1/t2"]);
158
+ expect(model.lines[1]!.sends).toEqual([]);
159
+ });
160
+ });
161
+
162
+ describe("deriveSendAnnotations", () => {
163
+ it("第 i 条用户消息配第 i 个 turn 节点(与 --execution 分轮同一规则);无 loc 的轮不产出", () => {
164
+ const events: StreamEvent[] = [
165
+ { type: "message", role: "user", text: "first", loc: { file: SOURCE_PATH, line: 3, column: 5 } },
166
+ { type: "message", role: "assistant", text: "reply" },
167
+ { type: "message", role: "user", text: "second (no loc)" },
168
+ { type: "message", role: "user", text: "third", loc: { file: SOURCE_PATH, line: 9, column: 5 } },
169
+ ];
170
+ const phases: PhaseTiming[] = [{
171
+ name: "eval.run" as PhaseTiming["name"],
172
+ durationMs: 5000,
173
+ children: [
174
+ { id: "n1", kind: "command", label: "git", startOffsetMs: 0, durationMs: 10 },
175
+ { id: "n2", kind: "turn", label: "s1/t1", startOffsetMs: 10, durationMs: 1500 },
176
+ { id: "n3", kind: "turn", label: "s1/t2", startOffsetMs: 1510, durationMs: 900, failed: true },
177
+ { id: "n4", kind: "turn", label: "s1/t3", startOffsetMs: 2410, durationMs: 300 },
178
+ ],
179
+ }];
180
+
181
+ const sends = deriveSendAnnotations(events, phases);
182
+ expect(sends).toEqual([
183
+ { label: "s1/t1", status: "completed", durationMs: 1500, loc: { file: SOURCE_PATH, line: 3, column: 5 } },
184
+ // 第二条用户消息没有 loc → 不产出;第三条配第 3 个 turn 节点,不因跳过而错位
185
+ { label: "s1/t3", status: "completed", durationMs: 300, loc: { file: SOURCE_PATH, line: 9, column: 5 } },
186
+ ]);
187
+ });
188
+
189
+ it("时间树缺 turn 节点时回退 t<i> 标签、无墙钟;没有事件时为空", () => {
190
+ const events: StreamEvent[] = [
191
+ { type: "message", role: "user", text: "only", loc: { file: SOURCE_PATH, line: 1 } },
192
+ ];
193
+ expect(deriveSendAnnotations(events, undefined)).toEqual([
194
+ { label: "t1", status: "completed", loc: { file: SOURCE_PATH, line: 1 } },
195
+ ]);
196
+ expect(deriveSendAnnotations(null, undefined)).toEqual([]);
197
+ expect(deriveSendAnnotations([], undefined)).toEqual([]);
198
+ });
146
199
  });
@@ -6,16 +6,33 @@
6
6
  //
7
7
  // 泛化自 src/view/app/lib/transcript-data.tsx 的 indexAsserts()(Map<string, Assertion[]>
8
8
  // + noloc 兜底桶的先例)——同一套"按 loc 分桶、没有 loc 进兜底"的思路,换成源码行数组
9
- // (不是 Map)存放,外加 sourceSha256 / summary 计数。indexTurns() 那部分(events → 轮次)
10
- // ExecutionTree 的地盘,不在这个模型里。
9
+ // (不是 Map)存放,外加 sourceSha256 / summary 计数。轮次的完整展开(events → ExecutionTree)
10
+ // 仍是 ExecutionTree 的地盘;这个模型只把每轮的头行事实标回 send 调用行(见 SendAnnotation),
11
+ // 作为源码页指向 --execution 的跨面指针。
11
12
  //
12
13
  // 本模块不 import react/jsx,纯数据 + 纯函数,可以在任何 Node 语境(CLI 的 show、
13
14
  // view 的 server 端数据准备)调用。
14
15
 
15
- import type { AssertionResult } from "../types.ts";
16
+ import type { AssertionResult, PhaseTiming, SourceLoc, StreamEvent } from "../types.ts";
16
17
  import { hashEvalSource, normalizeEvalSource } from "./source-hash.ts";
17
18
 
18
- /** 一行源码 + 映射到这一行的全部断言(保持原始顺序;可以是空数组)。 */
19
+ /**
20
+ * 标回 `t.send(...)` 调用行的一轮 turn 头行事实(契约见 docs/feature/reports/show.md
21
+ * 「--eval:把断言放回源码」)。身份标签与 --execution / --timing / diff windows 同一套;
22
+ * 回复全文与轮内卡片不进这个模型——源码页只回答「这行代码对应哪一轮、这一轮成了没成」。
23
+ */
24
+ export interface SendAnnotation {
25
+ /** `s<session>/t<turn>`。 */
26
+ label: string;
27
+ /** 轮的终态;时间树只记 failed 位,waiting 需要事件流佐证时由派生方给。 */
28
+ status: "completed" | "failed" | "waiting";
29
+ /** 该轮墙钟;时间树缺这一轮的节点时省略。 */
30
+ durationMs?: number;
31
+ /** send 调用位置(用户消息事件的 loc)。 */
32
+ loc: SourceLoc;
33
+ }
34
+
35
+ /** 一行源码 + 映射到这一行的全部断言与 send 标注(保持原始顺序;可以是空数组)。 */
19
36
  export interface AnnotatedSourceLine {
20
37
  /** 1-indexed 行号,与 SourceLoc.line 同一坐标系。 */
21
38
  line: number;
@@ -28,6 +45,11 @@ export interface AnnotatedSourceLine {
28
45
  * 领域 model 的职责(与 indexAsserts() 的先例一致,它同样只分桶、不折叠)。
29
46
  */
30
47
  assertions: AssertionResult[];
48
+ /**
49
+ * 映射到这一行的 send 标注,按轮次顺序;循环里的 send 一行多轮。与断言的 never-drop
50
+ * 契约不同,定位不到行的轮不进任何兜底桶——轮次的全量面是 --execution,这里只是指针。
51
+ */
52
+ sends: SendAnnotation[];
31
53
  }
32
54
 
33
55
  export interface AnnotatedEvalSourceSummary {
@@ -67,6 +89,34 @@ export interface AnnotatedEvalSource {
67
89
  summary: AnnotatedEvalSourceSummary;
68
90
  }
69
91
 
92
+ /**
93
+ * 从标准事件流与阶段时间树派生 send 标注:第 i 条用户消息开第 i 轮(与 --execution 的
94
+ * 分轮边界同一条规则,见 show/render.ts::executionText),头行事实取 `eval.run` 下第 i 个
95
+ * turn 节点;用户消息没有 loc 的轮不产出标注。纯函数,无 IO。
96
+ */
97
+ export function deriveSendAnnotations(
98
+ events: readonly StreamEvent[] | null,
99
+ phases: readonly PhaseTiming[] | undefined,
100
+ ): SendAnnotation[] {
101
+ if (!events || events.length === 0) return [];
102
+ const turnNodes = (phases ?? []).flatMap((p) => p.children ?? []).filter((n) => n.kind === "turn");
103
+ const out: SendAnnotation[] = [];
104
+ let turnIndex = -1;
105
+ for (const event of events) {
106
+ if (event.type !== "message" || event.role !== "user") continue;
107
+ turnIndex += 1;
108
+ if (!event.loc) continue;
109
+ const turn = turnNodes[turnIndex];
110
+ out.push({
111
+ label: turn?.label ?? `t${turnIndex + 1}`,
112
+ status: turn?.failed ? "failed" : "completed",
113
+ ...(turn !== undefined ? { durationMs: turn.durationMs } : {}),
114
+ loc: event.loc,
115
+ });
116
+ }
117
+ return out;
118
+ }
119
+
70
120
  /**
71
121
  * 纯函数:给定一份源码文本(未归一化也可以,内部会 normalizeEvalSource)与一批断言,
72
122
  * 产出标注好的 AnnotatedEvalSource。幂等 / 无 IO —— 可以在渲染路径上安全调用。
@@ -77,13 +127,21 @@ export interface AnnotatedEvalSource {
77
127
  export function buildAnnotatedEvalSource(
78
128
  source: { path: string; content: string },
79
129
  assertions: readonly AssertionResult[],
130
+ sends: readonly SendAnnotation[] = [],
80
131
  ): AnnotatedEvalSource {
81
132
  const normalized = normalizeEvalSource(source.content);
82
133
  // 末尾的单个换行符不产出幻影空行(源码文件几乎总以换行符收尾);中间的空行原样保留为一行。
83
134
  const body = normalized.endsWith("\n") ? normalized.slice(0, -1) : normalized;
84
135
  const rawLines = body.length === 0 && normalized.length === 0 ? [""] : body.split("\n");
85
136
 
86
- const lines: AnnotatedSourceLine[] = rawLines.map((text, i) => ({ line: i + 1, text, assertions: [] }));
137
+ const lines: AnnotatedSourceLine[] = rawLines.map((text, i) => ({ line: i + 1, text, assertions: [], sends: [] }));
138
+
139
+ for (const send of sends) {
140
+ // 与断言同一条映射规则;不满足的轮直接丢(全量面在 --execution,见 SendAnnotation)。
141
+ if (send.loc.file === source.path && send.loc.line >= 1 && send.loc.line <= lines.length) {
142
+ lines[send.loc.line - 1]!.sends.push(send);
143
+ }
144
+ }
87
145
 
88
146
  const unmapped: AssertionResult[] = [];
89
147
  let mappedAssertions = 0;
@@ -93,7 +151,7 @@ export function buildAnnotatedEvalSource(
93
151
  let soft = 0;
94
152
 
95
153
  for (const assertion of assertions) {
96
- if (assertion.passed) passed++;
154
+ if (assertion.outcome === "passed") passed++;
97
155
  else failed++;
98
156
  if (assertion.severity === "gate") gate++;
99
157
  else soft++;
@@ -1,3 +1,4 @@
1
+ // cases: docs/engineering/unit-tests/results/cases.md
1
2
  // loadAttemptEvidence 单测(定稿契约见 plan/attempt-evidence-feedback-loop.md「中性数据准备」、
2
3
  // src/results/attempt-evidence.ts 的头注)。用真实 createResultsWriter → openResults 的读写链路
3
4
  // 落一份最小 fixture(不手写 JSON 文件,理由同 loadAnnotatedEvalSource 的既有端到端测试:
@@ -7,6 +8,7 @@ import { afterEach, describe, expect, it } from "vitest";
7
8
  import { mkdtemp, rm } from "node:fs/promises";
8
9
  import { tmpdir } from "node:os";
9
10
  import { join } from "node:path";
11
+ import type { DiffArtifact } from "../types.ts";
10
12
  import {
11
13
  createResultsWriter,
12
14
  loadAttemptEvidence,
@@ -34,7 +36,7 @@ const SOURCE_PATH = "evals/a.eval.ts";
34
36
  const SOURCE_CONTENT = 'import { defineEval } from "niceeval";\nexport default defineEval({\n test() {},\n});\n';
35
37
 
36
38
  const ASSERTIONS: EvalResult["assertions"] = [
37
- { name: "check-1", passed: true, severity: "gate", score: 1, loc: { file: SOURCE_PATH, line: 3 } },
39
+ { name: "check-1", outcome: "passed" as const, severity: "gate", score: 1, loc: { file: SOURCE_PATH, line: 3 } },
38
40
  ];
39
41
 
40
42
  const EVENTS: StreamEvent[] = [
@@ -47,8 +49,8 @@ const TRACE: TraceSpan[] = [
47
49
  { traceId: "t1", spanId: "s1", name: "tool.get_weather", startMs: 0, endMs: 10, attributes: { call_id: "c1" } },
48
50
  ];
49
51
 
50
- const NONEMPTY_DIFF: DiffData = { generatedFiles: { "a.txt": "hello" }, deletedFiles: [] };
51
- const EMPTY_DIFF: DiffData = { generatedFiles: {}, deletedFiles: [] };
52
+ const NONEMPTY_DIFF: DiffArtifact = [{ window: "s1/t1", changes: { "a.txt": { status: "added", after: "hello" } } }];
53
+ const EMPTY_DIFF: DiffArtifact = [{ window: "s1/t1", changes: {} }];
52
54
 
53
55
  /** 起一个 writer,写一条 attempt,finish,再从头 openResults 读回它的 AttemptHandle。 */
54
56
  async function seedAttempt(
@@ -68,11 +70,11 @@ async function seedAttempt(
68
70
  }
69
71
 
70
72
  describe("loadAttemptEvidence", () => {
71
- it("四个 capability 全部具备:eval / execution / timing / diff", async () => {
73
+ it("四个 capability 全部具备:source / execution / timing / diff", async () => {
72
74
  const root = await makeRoot();
73
75
  const attempt = await seedAttempt(
74
76
  root,
75
- { id: "weather/brooklyn", assertions: ASSERTIONS },
77
+ { id: "weather/brooklyn", assertions: ASSERTIONS, phases: [{ name: "eval.run", durationMs: 900 }] },
76
78
  { events: EVENTS, trace: TRACE, diff: NONEMPTY_DIFF, sources: [{ path: SOURCE_PATH, content: SOURCE_CONTENT }] },
77
79
  );
78
80
 
@@ -86,12 +88,12 @@ describe("loadAttemptEvidence", () => {
86
88
  const actionNode = evidence.execution!.nodes.find((n) => n.kind === "action");
87
89
  expect(actionNode).toBeDefined();
88
90
  expect((actionNode as { span?: TraceSpan }).span).toBeDefined();
89
- expect(evidence.diff).toEqual(NONEMPTY_DIFF);
91
+ expect(evidence.diff?.windows).toEqual(NONEMPTY_DIFF);
90
92
 
91
- expect(evidence.capabilities).toEqual({ eval: true, execution: true, timing: true, diff: true });
93
+ expect(evidence.capabilities).toEqual({ source: true, execution: true, timing: true, diff: true });
92
94
  });
93
95
 
94
- it("四个 capability 全部缺失:没有 source / events / trace / diff,不崩溃", async () => {
96
+ it("四个 capability 全部缺失:没有 sources / events / trace / diff,不崩溃", async () => {
95
97
  const root = await makeRoot();
96
98
  const attempt = await seedAttempt(root, { id: "weather/queens" });
97
99
 
@@ -100,10 +102,10 @@ describe("loadAttemptEvidence", () => {
100
102
  expect(evidence.evalSource).toBeNull();
101
103
  expect(evidence.execution).toBeNull();
102
104
  expect(evidence.diff).toBeNull();
103
- expect(evidence.capabilities).toEqual({ eval: false, execution: false, timing: false, diff: false });
105
+ expect(evidence.capabilities).toEqual({ source: false, execution: false, timing: false, diff: false });
104
106
  });
105
107
 
106
- it("有 events 没有 OTel spans:execution 为真、timing 为假", async () => {
108
+ it("有 events 没有 phases(旧 runner 产出):execution 为真、timing 为假", async () => {
107
109
  const root = await makeRoot();
108
110
  const attempt = await seedAttempt(root, { id: "weather/albany" }, { events: EVENTS });
109
111
 
@@ -122,7 +124,7 @@ describe("loadAttemptEvidence", () => {
122
124
 
123
125
  const evidence = await loadAttemptEvidence(attempt);
124
126
 
125
- expect(evidence.diff).toEqual(EMPTY_DIFF);
127
+ expect(evidence.diff?.windows).toEqual(EMPTY_DIFF);
126
128
  expect(evidence.capabilities.diff).toBe(false);
127
129
  });
128
130
 
@@ -12,7 +12,7 @@
12
12
  // "有什么可看"这件事产生同一个答案(没有),只是原因不同,原因留给 unavailable 文案区分,
13
13
  // 不留给 capability 位区分(capability 位只回答"要不要显示这个证据切面")。
14
14
  //
15
- // eval —— evalSource 非空 AND (至少有一行源码 或 至少有一条断言);理论上
15
+ // source —— evalSource 非空 AND (至少有一行源码 或 至少有一条断言);理论上
16
16
  // buildAnnotatedEvalSource 对空文件也会产出一行 ""(split("\n") 的自然结果),
17
17
  // 所以这条件实践中约等于"evalSource 非空",按文档口径原样写出两个子句,
18
18
  // 不依赖这个实现细节。
@@ -31,7 +31,7 @@ import { join } from "node:path";
31
31
  import type { AttemptHandle } from "./types.ts";
32
32
  import { type AttemptIdentity, type AttemptLocator, encodeAttemptLocator } from "./locator.ts";
33
33
  import { loadAnnotatedEvalSource } from "./attempt-source.ts";
34
- import type { AnnotatedEvalSource } from "./annotated-source.ts";
34
+ import { deriveSendAnnotations, type AnnotatedEvalSource } from "./annotated-source.ts";
35
35
  import { buildExecutionTree, type ExecutionTree } from "../o11y/execution-tree.ts";
36
36
  import type { DiffData, EvalResult } from "../types.ts";
37
37
 
@@ -59,11 +59,11 @@ export interface EvidencePaths {
59
59
  * 对应的证据切面,不自己重新判断"这个 attempt 有没有 X"。
60
60
  */
61
61
  export interface AttemptEvidenceCapabilities {
62
- /** evalSource 非空且有内容(源码行或断言二选一非空) */
63
- eval: boolean;
62
+ /** evalSource 非空且有内容(源码行或断言二选一非空);对应 CLI 的 `--source` 证据切面。 */
63
+ source: boolean;
64
64
  /** events 非空数组——有骨架可看。 */
65
65
  execution: boolean;
66
- /** execution 成立且这次运行接入过 OTel(ExecutionTree.timingAvailable) */
66
+ /** result.json phases(阶段计时)——`--timing` 时间树可渲染。 */
67
67
  timing: boolean;
68
68
  /** diff 非空且至少改动/删除了一个文件。 */
69
69
  diff: boolean;
@@ -79,13 +79,15 @@ export interface AttemptEvidence {
79
79
  * 的调用方直接读结构化字段(如按 experimentId 分组、按时间排序)。 */
80
80
  identity: AttemptIdentity;
81
81
  result: EvalResult;
82
- /** 运行时保存的 Eval 源码标注(`--eval` 切面);没有 sources() 就是 null,不伪造空文档。 */
82
+ /** 运行时保存的 Eval 源码标注(`--source` 切面);没有 sources() 就是 null,不伪造空文档。 */
83
83
  evalSource: AnnotatedEvalSource | null;
84
84
  /** 标准事件流 + OTel enrichment(`--execution` 切面);没有非空 events 就是 null,
85
85
  * 不产出一棵只剩 telemetry-only 节点、没有真实骨架的误导性树(见头注 execution 门槛)。 */
86
86
  execution: ExecutionTree | null;
87
87
  /** 被测 Agent 对 Sandbox 工作区的文件变化(`--diff` 切面);原样透传 attempt.diff()。 */
88
88
  diff: DiffData | null;
89
+ /** OTel spans(`--timing` 把 turn 节点下的 agent/model/tool spans 挂回时间树);没有 trace 就是 null。 */
90
+ trace: import("../types.ts").TraceSpan[] | null;
89
91
  artifactPaths: EvidencePaths;
90
92
  capabilities: AttemptEvidenceCapabilities;
91
93
  }
@@ -110,12 +112,17 @@ export async function loadAttemptEvidence(attempt: AttemptHandle): Promise<Attem
110
112
  };
111
113
  const locator = attempt.locator ?? encodeAttemptLocator(identity);
112
114
 
113
- const [evalSource, events, trace, diff] = await Promise.all([
114
- loadAnnotatedEvalSource(attempt),
115
+ const [events, trace, diff] = await Promise.all([
115
116
  attempt.events(),
116
117
  attempt.trace(),
117
118
  attempt.diff(),
118
119
  ]);
120
+ // send 标注要拿事件流里用户消息的 loc 与阶段时间树里的 turn 节点配对,所以在 events
121
+ // 就绪后再装配 evalSource;派生与分桶都是纯函数(annotated-source.ts),这里只接线。
122
+ const evalSource = await loadAnnotatedEvalSource(
123
+ attempt,
124
+ deriveSendAnnotations(events, attempt.result.phases),
125
+ );
119
126
 
120
127
  // execution 的 null 边界:没有非空事件骨架就是 null,即使 trace 里有 span(那些 span 在
121
128
  // buildExecutionTree 里只会全部落进 telemetry-only 桶,产出一棵没有骨架、只有遥测的树——
@@ -124,9 +131,8 @@ export async function loadAttemptEvidence(attempt: AttemptHandle): Promise<Attem
124
131
  const execution = hasEvents ? buildExecutionTree(events, trace ?? []) : null;
125
132
 
126
133
  const evalCapable = evalSource !== null && (evalSource.lines.length > 0 || evalSource.summary.totalAssertions > 0);
127
- // 与 show/render.ts::diffText 的既有空判定同一口径:generatedFiles 与 deletedFiles 都空
128
- // 才算"没有变化"。
129
- const diffCapable = diff !== null && (Object.keys(diff.generatedFiles).length > 0 || diff.deletedFiles.length > 0);
134
+ // 与 show/render.ts::diffText 同一口径:任一窗口触及过任一文件才算"有变化"。
135
+ const diffCapable = diff !== null && Object.keys(diff.files).length > 0;
130
136
 
131
137
  return {
132
138
  locator,
@@ -135,11 +141,12 @@ export async function loadAttemptEvidence(attempt: AttemptHandle): Promise<Attem
135
141
  evalSource,
136
142
  execution,
137
143
  diff,
144
+ trace,
138
145
  artifactPaths: { dir: attemptArtifactDir(attempt) },
139
146
  capabilities: {
140
- eval: evalCapable,
147
+ source: evalCapable,
141
148
  execution: execution !== null,
142
- timing: execution !== null && execution.timingAvailable,
149
+ timing: (attempt.result.phases?.length ?? 0) > 0,
143
150
  diff: diffCapable,
144
151
  },
145
152
  };
@@ -8,7 +8,7 @@
8
8
  // 或参照。
9
9
 
10
10
  import type { AttemptHandle } from "./types.ts";
11
- import { buildAnnotatedEvalSource, type AnnotatedEvalSource } from "./annotated-source.ts";
11
+ import { buildAnnotatedEvalSource, type AnnotatedEvalSource, type SendAnnotation } from "./annotated-source.ts";
12
12
 
13
13
  /**
14
14
  * 给定一个 attempt,取回它的 eval 源码(经 sources() 解引用,可能来自本快照或
@@ -22,7 +22,10 @@ import { buildAnnotatedEvalSource, type AnnotatedEvalSource } from "./annotated-
22
22
  * 只服务这条打通链路的验证目的,不是最终选择策略(后续阶段如需要为每份引用到的文件
23
23
  * 都出一份 AnnotatedEvalSource,在这基础上很容易扩成返回数组)。
24
24
  */
25
- export async function loadAnnotatedEvalSource(attempt: AttemptHandle): Promise<AnnotatedEvalSource | null> {
25
+ export async function loadAnnotatedEvalSource(
26
+ attempt: AttemptHandle,
27
+ sends: readonly SendAnnotation[] = [],
28
+ ): Promise<AnnotatedEvalSource | null> {
26
29
  const sources = await attempt.sources();
27
30
  if (!sources || sources.length === 0) return null;
28
31
 
@@ -44,5 +47,5 @@ export async function loadAnnotatedEvalSource(attempt: AttemptHandle): Promise<A
44
47
  }
45
48
  }
46
49
 
47
- return buildAnnotatedEvalSource(primary, assertions);
50
+ return buildAnnotatedEvalSource(primary, assertions, sends);
48
51
  }